Instantly share code, notes, and snippets.
Last active
May 10, 2018 16:20
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save laughinghan/7e74a225f042d21ef107e57e6d95efeb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function () { | |
var global$1 = (typeof global !== "undefined" ? global : | |
typeof self !== "undefined" ? self : | |
typeof window !== "undefined" ? window : {}); | |
// shim for using process in browser | |
// based off https://github.com/defunctzombie/node-process/blob/master/browser.js | |
function defaultSetTimout() { | |
throw new Error('setTimeout has not been defined'); | |
} | |
function defaultClearTimeout () { | |
throw new Error('clearTimeout has not been defined'); | |
} | |
var cachedSetTimeout = defaultSetTimout; | |
var cachedClearTimeout = defaultClearTimeout; | |
if (typeof global$1.setTimeout === 'function') { | |
cachedSetTimeout = setTimeout; | |
} | |
if (typeof global$1.clearTimeout === 'function') { | |
cachedClearTimeout = clearTimeout; | |
} | |
function runTimeout(fun) { | |
if (cachedSetTimeout === setTimeout) { | |
//normal enviroments in sane situations | |
return setTimeout(fun, 0); | |
} | |
// if setTimeout wasn't available but was latter defined | |
if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { | |
cachedSetTimeout = setTimeout; | |
return setTimeout(fun, 0); | |
} | |
try { | |
// when when somebody has screwed with setTimeout but no I.E. maddness | |
return cachedSetTimeout(fun, 0); | |
} catch(e){ | |
try { | |
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally | |
return cachedSetTimeout.call(null, fun, 0); | |
} catch(e){ | |
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error | |
return cachedSetTimeout.call(this, fun, 0); | |
} | |
} | |
} | |
function runClearTimeout(marker) { | |
if (cachedClearTimeout === clearTimeout) { | |
//normal enviroments in sane situations | |
return clearTimeout(marker); | |
} | |
// if clearTimeout wasn't available but was latter defined | |
if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { | |
cachedClearTimeout = clearTimeout; | |
return clearTimeout(marker); | |
} | |
try { | |
// when when somebody has screwed with setTimeout but no I.E. maddness | |
return cachedClearTimeout(marker); | |
} catch (e){ | |
try { | |
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally | |
return cachedClearTimeout.call(null, marker); | |
} catch (e){ | |
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. | |
// Some versions of I.E. have different rules for clearTimeout vs setTimeout | |
return cachedClearTimeout.call(this, marker); | |
} | |
} | |
} | |
var queue = []; | |
var draining = false; | |
var currentQueue; | |
var queueIndex = -1; | |
function cleanUpNextTick() { | |
if (!draining || !currentQueue) { | |
return; | |
} | |
draining = false; | |
if (currentQueue.length) { | |
queue = currentQueue.concat(queue); | |
} else { | |
queueIndex = -1; | |
} | |
if (queue.length) { | |
drainQueue(); | |
} | |
} | |
function drainQueue() { | |
if (draining) { | |
return; | |
} | |
var timeout = runTimeout(cleanUpNextTick); | |
draining = true; | |
var len = queue.length; | |
while(len) { | |
currentQueue = queue; | |
queue = []; | |
while (++queueIndex < len) { | |
if (currentQueue) { | |
currentQueue[queueIndex].run(); | |
} | |
} | |
queueIndex = -1; | |
len = queue.length; | |
} | |
currentQueue = null; | |
draining = false; | |
runClearTimeout(timeout); | |
} | |
function nextTick(fun) { | |
var args = new Array(arguments.length - 1); | |
if (arguments.length > 1) { | |
for (var i = 1; i < arguments.length; i++) { | |
args[i - 1] = arguments[i]; | |
} | |
} | |
queue.push(new Item(fun, args)); | |
if (queue.length === 1 && !draining) { | |
runTimeout(drainQueue); | |
} | |
} | |
// v8 likes predictible objects | |
function Item(fun, array) { | |
this.fun = fun; | |
this.array = array; | |
} | |
Item.prototype.run = function () { | |
this.fun.apply(null, this.array); | |
}; | |
// from https://github.com/kumavis/browser-process-hrtime/blob/master/index.js | |
var performance = global$1.performance || {}; | |
var performanceNow = | |
performance.now || | |
performance.mozNow || | |
performance.msNow || | |
performance.oNow || | |
performance.webkitNow || | |
function(){ return (new Date()).getTime() }; | |
var domain; | |
// This constructor is used to store event handlers. Instantiating this is | |
// faster than explicitly calling `Object.create(null)` to get a "clean" empty | |
// object (tested with v8 v4.9). | |
function EventHandlers() {} | |
EventHandlers.prototype = Object.create(null); | |
function EventEmitter() { | |
EventEmitter.init.call(this); | |
} | |
// nodejs oddity | |
// require('events') === require('events').EventEmitter | |
EventEmitter.EventEmitter = EventEmitter; | |
EventEmitter.usingDomains = false; | |
EventEmitter.prototype.domain = undefined; | |
EventEmitter.prototype._events = undefined; | |
EventEmitter.prototype._maxListeners = undefined; | |
// By default EventEmitters will print a warning if more than 10 listeners are | |
// added to it. This is a useful default which helps finding memory leaks. | |
EventEmitter.defaultMaxListeners = 10; | |
EventEmitter.init = function() { | |
this.domain = null; | |
if (EventEmitter.usingDomains) { | |
// if there is an active domain, then attach to it. | |
if (domain.active && !(this instanceof domain.Domain)) { | |
this.domain = domain.active; | |
} | |
} | |
if (!this._events || this._events === Object.getPrototypeOf(this)._events) { | |
this._events = new EventHandlers(); | |
this._eventsCount = 0; | |
} | |
this._maxListeners = this._maxListeners || undefined; | |
}; | |
// Obviously not all Emitters should be limited to 10. This function allows | |
// that to be increased. Set to zero for unlimited. | |
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { | |
if (typeof n !== 'number' || n < 0 || isNaN(n)) | |
throw new TypeError('"n" argument must be a positive number'); | |
this._maxListeners = n; | |
return this; | |
}; | |
function $getMaxListeners(that) { | |
if (that._maxListeners === undefined) | |
return EventEmitter.defaultMaxListeners; | |
return that._maxListeners; | |
} | |
EventEmitter.prototype.getMaxListeners = function getMaxListeners() { | |
return $getMaxListeners(this); | |
}; | |
// These standalone emit* functions are used to optimize calling of event | |
// handlers for fast cases because emit() itself often has a variable number of | |
// arguments and can be deoptimized because of that. These functions always have | |
// the same number of arguments and thus do not get deoptimized, so the code | |
// inside them can execute faster. | |
function emitNone(handler, isFn, self) { | |
if (isFn) | |
handler.call(self); | |
else { | |
var len = handler.length; | |
var listeners = arrayClone(handler, len); | |
for (var i = 0; i < len; ++i) | |
listeners[i].call(self); | |
} | |
} | |
function emitOne(handler, isFn, self, arg1) { | |
if (isFn) | |
handler.call(self, arg1); | |
else { | |
var len = handler.length; | |
var listeners = arrayClone(handler, len); | |
for (var i = 0; i < len; ++i) | |
listeners[i].call(self, arg1); | |
} | |
} | |
function emitTwo(handler, isFn, self, arg1, arg2) { | |
if (isFn) | |
handler.call(self, arg1, arg2); | |
else { | |
var len = handler.length; | |
var listeners = arrayClone(handler, len); | |
for (var i = 0; i < len; ++i) | |
listeners[i].call(self, arg1, arg2); | |
} | |
} | |
function emitThree(handler, isFn, self, arg1, arg2, arg3) { | |
if (isFn) | |
handler.call(self, arg1, arg2, arg3); | |
else { | |
var len = handler.length; | |
var listeners = arrayClone(handler, len); | |
for (var i = 0; i < len; ++i) | |
listeners[i].call(self, arg1, arg2, arg3); | |
} | |
} | |
function emitMany(handler, isFn, self, args) { | |
if (isFn) | |
handler.apply(self, args); | |
else { | |
var len = handler.length; | |
var listeners = arrayClone(handler, len); | |
for (var i = 0; i < len; ++i) | |
listeners[i].apply(self, args); | |
} | |
} | |
EventEmitter.prototype.emit = function emit(type) { | |
var er, handler, len, args, i, events, domain; | |
var needDomainExit = false; | |
var doError = (type === 'error'); | |
events = this._events; | |
if (events) | |
doError = (doError && events.error == null); | |
else if (!doError) | |
return false; | |
domain = this.domain; | |
// If there is no 'error' event listener then throw. | |
if (doError) { | |
er = arguments[1]; | |
if (domain) { | |
if (!er) | |
er = new Error('Uncaught, unspecified "error" event'); | |
er.domainEmitter = this; | |
er.domain = domain; | |
er.domainThrown = false; | |
domain.emit('error', er); | |
} else if (er instanceof Error) { | |
throw er; // Unhandled 'error' event | |
} else { | |
// At least give some kind of context to the user | |
var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); | |
err.context = er; | |
throw err; | |
} | |
return false; | |
} | |
handler = events[type]; | |
if (!handler) | |
return false; | |
var isFn = typeof handler === 'function'; | |
len = arguments.length; | |
switch (len) { | |
// fast cases | |
case 1: | |
emitNone(handler, isFn, this); | |
break; | |
case 2: | |
emitOne(handler, isFn, this, arguments[1]); | |
break; | |
case 3: | |
emitTwo(handler, isFn, this, arguments[1], arguments[2]); | |
break; | |
case 4: | |
emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); | |
break; | |
// slower | |
default: | |
args = new Array(len - 1); | |
for (i = 1; i < len; i++) | |
args[i - 1] = arguments[i]; | |
emitMany(handler, isFn, this, args); | |
} | |
if (needDomainExit) | |
domain.exit(); | |
return true; | |
}; | |
function _addListener(target, type, listener, prepend) { | |
var m; | |
var events; | |
var existing; | |
if (typeof listener !== 'function') | |
throw new TypeError('"listener" argument must be a function'); | |
events = target._events; | |
if (!events) { | |
events = target._events = new EventHandlers(); | |
target._eventsCount = 0; | |
} else { | |
// To avoid recursion in the case that type === "newListener"! Before | |
// adding it to the listeners, first emit "newListener". | |
if (events.newListener) { | |
target.emit('newListener', type, | |
listener.listener ? listener.listener : listener); | |
// Re-assign `events` because a newListener handler could have caused the | |
// this._events to be assigned to a new object | |
events = target._events; | |
} | |
existing = events[type]; | |
} | |
if (!existing) { | |
// Optimize the case of one listener. Don't need the extra array object. | |
existing = events[type] = listener; | |
++target._eventsCount; | |
} else { | |
if (typeof existing === 'function') { | |
// Adding the second element, need to change to array. | |
existing = events[type] = prepend ? [listener, existing] : | |
[existing, listener]; | |
} else { | |
// If we've already got an array, just append. | |
if (prepend) { | |
existing.unshift(listener); | |
} else { | |
existing.push(listener); | |
} | |
} | |
// Check for listener leak | |
if (!existing.warned) { | |
m = $getMaxListeners(target); | |
if (m && m > 0 && existing.length > m) { | |
existing.warned = true; | |
var w = new Error('Possible EventEmitter memory leak detected. ' + | |
existing.length + ' ' + type + ' listeners added. ' + | |
'Use emitter.setMaxListeners() to increase limit'); | |
w.name = 'MaxListenersExceededWarning'; | |
w.emitter = target; | |
w.type = type; | |
w.count = existing.length; | |
emitWarning(w); | |
} | |
} | |
} | |
return target; | |
} | |
function emitWarning(e) { | |
typeof console.warn === 'function' ? console.warn(e) : console.log(e); | |
} | |
EventEmitter.prototype.addListener = function addListener(type, listener) { | |
return _addListener(this, type, listener, false); | |
}; | |
EventEmitter.prototype.on = EventEmitter.prototype.addListener; | |
EventEmitter.prototype.prependListener = | |
function prependListener(type, listener) { | |
return _addListener(this, type, listener, true); | |
}; | |
function _onceWrap(target, type, listener) { | |
var fired = false; | |
function g() { | |
target.removeListener(type, g); | |
if (!fired) { | |
fired = true; | |
listener.apply(target, arguments); | |
} | |
} | |
g.listener = listener; | |
return g; | |
} | |
EventEmitter.prototype.once = function once(type, listener) { | |
if (typeof listener !== 'function') | |
throw new TypeError('"listener" argument must be a function'); | |
this.on(type, _onceWrap(this, type, listener)); | |
return this; | |
}; | |
EventEmitter.prototype.prependOnceListener = | |
function prependOnceListener(type, listener) { | |
if (typeof listener !== 'function') | |
throw new TypeError('"listener" argument must be a function'); | |
this.prependListener(type, _onceWrap(this, type, listener)); | |
return this; | |
}; | |
// emits a 'removeListener' event iff the listener was removed | |
EventEmitter.prototype.removeListener = | |
function removeListener(type, listener) { | |
var list, events, position, i, originalListener; | |
if (typeof listener !== 'function') | |
throw new TypeError('"listener" argument must be a function'); | |
events = this._events; | |
if (!events) | |
return this; | |
list = events[type]; | |
if (!list) | |
return this; | |
if (list === listener || (list.listener && list.listener === listener)) { | |
if (--this._eventsCount === 0) | |
this._events = new EventHandlers(); | |
else { | |
delete events[type]; | |
if (events.removeListener) | |
this.emit('removeListener', type, list.listener || listener); | |
} | |
} else if (typeof list !== 'function') { | |
position = -1; | |
for (i = list.length; i-- > 0;) { | |
if (list[i] === listener || | |
(list[i].listener && list[i].listener === listener)) { | |
originalListener = list[i].listener; | |
position = i; | |
break; | |
} | |
} | |
if (position < 0) | |
return this; | |
if (list.length === 1) { | |
list[0] = undefined; | |
if (--this._eventsCount === 0) { | |
this._events = new EventHandlers(); | |
return this; | |
} else { | |
delete events[type]; | |
} | |
} else { | |
spliceOne(list, position); | |
} | |
if (events.removeListener) | |
this.emit('removeListener', type, originalListener || listener); | |
} | |
return this; | |
}; | |
EventEmitter.prototype.removeAllListeners = | |
function removeAllListeners(type) { | |
var listeners, events; | |
events = this._events; | |
if (!events) | |
return this; | |
// not listening for removeListener, no need to emit | |
if (!events.removeListener) { | |
if (arguments.length === 0) { | |
this._events = new EventHandlers(); | |
this._eventsCount = 0; | |
} else if (events[type]) { | |
if (--this._eventsCount === 0) | |
this._events = new EventHandlers(); | |
else | |
delete events[type]; | |
} | |
return this; | |
} | |
// emit removeListener for all listeners on all events | |
if (arguments.length === 0) { | |
var keys = Object.keys(events); | |
for (var i = 0, key; i < keys.length; ++i) { | |
key = keys[i]; | |
if (key === 'removeListener') continue; | |
this.removeAllListeners(key); | |
} | |
this.removeAllListeners('removeListener'); | |
this._events = new EventHandlers(); | |
this._eventsCount = 0; | |
return this; | |
} | |
listeners = events[type]; | |
if (typeof listeners === 'function') { | |
this.removeListener(type, listeners); | |
} else if (listeners) { | |
// LIFO order | |
do { | |
this.removeListener(type, listeners[listeners.length - 1]); | |
} while (listeners[0]); | |
} | |
return this; | |
}; | |
EventEmitter.prototype.listeners = function listeners(type) { | |
var evlistener; | |
var ret; | |
var events = this._events; | |
if (!events) | |
ret = []; | |
else { | |
evlistener = events[type]; | |
if (!evlistener) | |
ret = []; | |
else if (typeof evlistener === 'function') | |
ret = [evlistener.listener || evlistener]; | |
else | |
ret = unwrapListeners(evlistener); | |
} | |
return ret; | |
}; | |
EventEmitter.listenerCount = function(emitter, type) { | |
if (typeof emitter.listenerCount === 'function') { | |
return emitter.listenerCount(type); | |
} else { | |
return listenerCount.call(emitter, type); | |
} | |
}; | |
EventEmitter.prototype.listenerCount = listenerCount; | |
function listenerCount(type) { | |
var events = this._events; | |
if (events) { | |
var evlistener = events[type]; | |
if (typeof evlistener === 'function') { | |
return 1; | |
} else if (evlistener) { | |
return evlistener.length; | |
} | |
} | |
return 0; | |
} | |
EventEmitter.prototype.eventNames = function eventNames() { | |
return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; | |
}; | |
// About 1.5x faster than the two-arg version of Array#splice(). | |
function spliceOne(list, index) { | |
for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) | |
list[i] = list[k]; | |
list.pop(); | |
} | |
function arrayClone(arr, i) { | |
var copy = new Array(i); | |
while (i--) | |
copy[i] = arr[i]; | |
return copy; | |
} | |
function unwrapListeners(arr) { | |
var ret = new Array(arr.length); | |
for (var i = 0; i < ret.length; ++i) { | |
ret[i] = arr[i].listener || arr[i]; | |
} | |
return ret; | |
} | |
var events = /*#__PURE__*/Object.freeze({ | |
default: EventEmitter, | |
EventEmitter: EventEmitter | |
}); | |
var require$$0 = ( events && EventEmitter ) || events; | |
var EventEmitter$1 = require$$0.EventEmitter; | |
var EventEmitter_1 = EventEmitter$1; | |
var mixin_1 = mixin; | |
function mixin(Constructor) { | |
for (var key in EventEmitter$1.prototype) { | |
Constructor.prototype[key] = EventEmitter$1.prototype[key]; | |
} | |
} | |
var emitter = { | |
EventEmitter: EventEmitter_1, | |
mixin: mixin_1 | |
}; | |
function commonjsRequire () { | |
throw new Error('Dynamic requires are not currently supported by rollup-plugin-commonjs'); | |
} | |
function createCommonjsModule(fn, module) { | |
return module = { exports: {} }, fn(module, module.exports), module.exports; | |
} | |
var makeError_1 = createCommonjsModule(function (module, exports) { | |
// =================================================================== | |
var construct = typeof Reflect !== 'undefined' ? Reflect.construct : undefined; | |
var defineProperty = Object.defineProperty; | |
// ------------------------------------------------------------------- | |
var captureStackTrace = Error.captureStackTrace; | |
if (captureStackTrace === undefined) { | |
captureStackTrace = function captureStackTrace (error) { | |
var container = new Error(); | |
defineProperty(error, 'stack', { | |
configurable: true, | |
get: function getStack () { | |
var stack = container.stack; | |
// Replace property with value for faster future accesses. | |
defineProperty(this, 'stack', { | |
configurable: true, | |
value: stack, | |
writable: true | |
}); | |
return stack | |
}, | |
set: function setStack (stack) { | |
defineProperty(error, 'stack', { | |
configurable: true, | |
value: stack, | |
writable: true | |
}); | |
} | |
}); | |
}; | |
} | |
// ------------------------------------------------------------------- | |
function BaseError (message) { | |
if (message !== undefined) { | |
defineProperty(this, 'message', { | |
configurable: true, | |
value: message, | |
writable: true | |
}); | |
} | |
var cname = this.constructor.name; | |
if ( | |
cname !== undefined && | |
cname !== this.name | |
) { | |
defineProperty(this, 'name', { | |
configurable: true, | |
value: cname, | |
writable: true | |
}); | |
} | |
captureStackTrace(this, this.constructor); | |
} | |
BaseError.prototype = Object.create(Error.prototype, { | |
// See: https://github.com/JsCommunity/make-error/issues/4 | |
constructor: { | |
configurable: true, | |
value: BaseError, | |
writable: true | |
} | |
}); | |
// ------------------------------------------------------------------- | |
// Sets the name of a function if possible (depends of the JS engine). | |
var setFunctionName = (function () { | |
function setFunctionName (fn, name) { | |
return defineProperty(fn, 'name', { | |
configurable: true, | |
value: name | |
}) | |
} | |
try { | |
var f = function () {}; | |
setFunctionName(f, 'foo'); | |
if (f.name === 'foo') { | |
return setFunctionName | |
} | |
} catch (_) {} | |
})(); | |
// ------------------------------------------------------------------- | |
function makeError (constructor, super_) { | |
if (super_ == null || super_ === Error) { | |
super_ = BaseError; | |
} else if (typeof super_ !== 'function') { | |
throw new TypeError('super_ should be a function') | |
} | |
var name; | |
if (typeof constructor === 'string') { | |
name = constructor; | |
constructor = construct !== undefined | |
? function () { return construct(super_, arguments, this.constructor) } | |
: function () { super_.apply(this, arguments); }; | |
// If the name can be set, do it once and for all. | |
if (setFunctionName !== undefined) { | |
setFunctionName(constructor, name); | |
name = undefined; | |
} | |
} else if (typeof constructor !== 'function') { | |
throw new TypeError('constructor should be either a string or a function') | |
} | |
// Also register the super constructor also as `constructor.super_` just | |
// like Node's `util.inherits()`. | |
constructor.super_ = constructor['super'] = super_; | |
var properties = { | |
constructor: { | |
configurable: true, | |
value: constructor, | |
writable: true | |
} | |
}; | |
// If the name could not be set on the constructor, set it on the | |
// prototype. | |
if (name !== undefined) { | |
properties.name = { | |
configurable: true, | |
value: name, | |
writable: true | |
}; | |
} | |
constructor.prototype = Object.create(super_.prototype, properties); | |
return constructor | |
} | |
exports = module.exports = makeError; | |
exports.BaseError = BaseError; | |
}); | |
var makeError_2 = makeError_1.BaseError; | |
function ShareDBError(code, message) { | |
ShareDBError.super.call(this, message); | |
this.code = code; | |
} | |
makeError_1(ShareDBError); | |
var error = ShareDBError; | |
// These methods let you build a transform function from a transformComponent | |
// function for OT types like JSON0 in which operations are lists of components | |
// and transforming them requires N^2 work. I find it kind of nasty that I need | |
// this, but I'm not really sure what a better solution is. Maybe I should do | |
// this automatically to types that don't have a compose function defined. | |
// Add transform and transformX functions for an OT type which has | |
// transformComponent defined. transformComponent(destination array, | |
// component, other component, side) | |
var bootstrapTransform_1 = bootstrapTransform; | |
function bootstrapTransform(type, transformComponent, checkValidOp, append) { | |
var transformComponentX = function(left, right, destLeft, destRight) { | |
transformComponent(destLeft, left, right, 'left'); | |
transformComponent(destRight, right, left, 'right'); | |
}; | |
var transformX = type.transformX = function(leftOp, rightOp) { | |
checkValidOp(leftOp); | |
checkValidOp(rightOp); | |
var newRightOp = []; | |
for (var i = 0; i < rightOp.length; i++) { | |
var rightComponent = rightOp[i]; | |
// Generate newLeftOp by composing leftOp by rightComponent | |
var newLeftOp = []; | |
var k = 0; | |
while (k < leftOp.length) { | |
var nextC = []; | |
transformComponentX(leftOp[k], rightComponent, newLeftOp, nextC); | |
k++; | |
if (nextC.length === 1) { | |
rightComponent = nextC[0]; | |
} else if (nextC.length === 0) { | |
for (var j = k; j < leftOp.length; j++) { | |
append(newLeftOp, leftOp[j]); | |
} | |
rightComponent = null; | |
break; | |
} else { | |
// Recurse. | |
var pair = transformX(leftOp.slice(k), nextC); | |
for (var l = 0; l < pair[0].length; l++) { | |
append(newLeftOp, pair[0][l]); | |
} | |
for (var r = 0; r < pair[1].length; r++) { | |
append(newRightOp, pair[1][r]); | |
} | |
rightComponent = null; | |
break; | |
} | |
} | |
if (rightComponent != null) { | |
append(newRightOp, rightComponent); | |
} | |
leftOp = newLeftOp; | |
} | |
return [leftOp, newRightOp]; | |
}; | |
// Transforms op with specified type ('left' or 'right') by otherOp. | |
type.transform = function(op, otherOp, type) { | |
if (!(type === 'left' || type === 'right')) | |
throw new Error("type must be 'left' or 'right'"); | |
if (otherOp.length === 0) return op; | |
if (op.length === 1 && otherOp.length === 1) | |
return transformComponent([], op[0], otherOp[0], type); | |
if (type === 'left') | |
return transformX(op, otherOp)[0]; | |
else | |
return transformX(otherOp, op)[1]; | |
}; | |
} | |
var text0 = createCommonjsModule(function (module) { | |
// DEPRECATED! | |
// | |
// This type works, but is not exported. Its included here because the JSON0 | |
// embedded string operations use this library. | |
// A simple text implementation | |
// | |
// Operations are lists of components. Each component either inserts or deletes | |
// at a specified position in the document. | |
// | |
// Components are either: | |
// {i:'str', p:100}: Insert 'str' at position 100 in the document | |
// {d:'str', p:100}: Delete 'str' at position 100 in the document | |
// | |
// Components in an operation are executed sequentially, so the position of components | |
// assumes previous components have already executed. | |
// | |
// Eg: This op: | |
// [{i:'abc', p:0}] | |
// is equivalent to this op: | |
// [{i:'a', p:0}, {i:'b', p:1}, {i:'c', p:2}] | |
var text = module.exports = { | |
name: 'text0', | |
uri: 'http://sharejs.org/types/textv0', | |
create: function(initial) { | |
if ((initial != null) && typeof initial !== 'string') { | |
throw new Error('Initial data must be a string'); | |
} | |
return initial || ''; | |
} | |
}; | |
/** Insert s2 into s1 at pos. */ | |
var strInject = function(s1, pos, s2) { | |
return s1.slice(0, pos) + s2 + s1.slice(pos); | |
}; | |
/** Check that an operation component is valid. Throws if its invalid. */ | |
var checkValidComponent = function(c) { | |
if (typeof c.p !== 'number') | |
throw new Error('component missing position field'); | |
if ((typeof c.i === 'string') === (typeof c.d === 'string')) | |
throw new Error('component needs an i or d field'); | |
if (c.p < 0) | |
throw new Error('position cannot be negative'); | |
}; | |
/** Check that an operation is valid */ | |
var checkValidOp = function(op) { | |
for (var i = 0; i < op.length; i++) { | |
checkValidComponent(op[i]); | |
} | |
}; | |
/** Apply op to snapshot */ | |
text.apply = function(snapshot, op) { | |
var deleted; | |
checkValidOp(op); | |
for (var i = 0; i < op.length; i++) { | |
var component = op[i]; | |
if (component.i != null) { | |
snapshot = strInject(snapshot, component.p, component.i); | |
} else { | |
deleted = snapshot.slice(component.p, component.p + component.d.length); | |
if (component.d !== deleted) | |
throw new Error("Delete component '" + component.d + "' does not match deleted text '" + deleted + "'"); | |
snapshot = snapshot.slice(0, component.p) + snapshot.slice(component.p + component.d.length); | |
} | |
} | |
return snapshot; | |
}; | |
/** | |
* Append a component to the end of newOp. Exported for use by the random op | |
* generator and the JSON0 type. | |
*/ | |
var append = text._append = function(newOp, c) { | |
if (c.i === '' || c.d === '') return; | |
if (newOp.length === 0) { | |
newOp.push(c); | |
} else { | |
var last = newOp[newOp.length - 1]; | |
if (last.i != null && c.i != null && last.p <= c.p && c.p <= last.p + last.i.length) { | |
// Compose the insert into the previous insert | |
newOp[newOp.length - 1] = {i:strInject(last.i, c.p - last.p, c.i), p:last.p}; | |
} else if (last.d != null && c.d != null && c.p <= last.p && last.p <= c.p + c.d.length) { | |
// Compose the deletes together | |
newOp[newOp.length - 1] = {d:strInject(c.d, last.p - c.p, last.d), p:c.p}; | |
} else { | |
newOp.push(c); | |
} | |
} | |
}; | |
/** Compose op1 and op2 together */ | |
text.compose = function(op1, op2) { | |
checkValidOp(op1); | |
checkValidOp(op2); | |
var newOp = op1.slice(); | |
for (var i = 0; i < op2.length; i++) { | |
append(newOp, op2[i]); | |
} | |
return newOp; | |
}; | |
/** Clean up an op */ | |
text.normalize = function(op) { | |
var newOp = []; | |
// Normalize should allow ops which are a single (unwrapped) component: | |
// {i:'asdf', p:23}. | |
// There's no good way to test if something is an array: | |
// http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/ | |
// so this is probably the least bad solution. | |
if (op.i != null || op.p != null) op = [op]; | |
for (var i = 0; i < op.length; i++) { | |
var c = op[i]; | |
if (c.p == null) c.p = 0; | |
append(newOp, c); | |
} | |
return newOp; | |
}; | |
// This helper method transforms a position by an op component. | |
// | |
// If c is an insert, insertAfter specifies whether the transform | |
// is pushed after the insert (true) or before it (false). | |
// | |
// insertAfter is optional for deletes. | |
var transformPosition = function(pos, c, insertAfter) { | |
// This will get collapsed into a giant ternary by uglify. | |
if (c.i != null) { | |
if (c.p < pos || (c.p === pos && insertAfter)) { | |
return pos + c.i.length; | |
} else { | |
return pos; | |
} | |
} else { | |
// I think this could also be written as: Math.min(c.p, Math.min(c.p - | |
// otherC.p, otherC.d.length)) but I think its harder to read that way, and | |
// it compiles using ternary operators anyway so its no slower written like | |
// this. | |
if (pos <= c.p) { | |
return pos; | |
} else if (pos <= c.p + c.d.length) { | |
return c.p; | |
} else { | |
return pos - c.d.length; | |
} | |
} | |
}; | |
// Helper method to transform a cursor position as a result of an op. | |
// | |
// Like transformPosition above, if c is an insert, insertAfter specifies | |
// whether the cursor position is pushed after an insert (true) or before it | |
// (false). | |
text.transformCursor = function(position, op, side) { | |
var insertAfter = side === 'right'; | |
for (var i = 0; i < op.length; i++) { | |
position = transformPosition(position, op[i], insertAfter); | |
} | |
return position; | |
}; | |
// Transform an op component by another op component. Asymmetric. | |
// The result will be appended to destination. | |
// | |
// exported for use in JSON type | |
var transformComponent = text._tc = function(dest, c, otherC, side) { | |
//var cIntersect, intersectEnd, intersectStart, newC, otherIntersect, s; | |
checkValidComponent(c); | |
checkValidComponent(otherC); | |
if (c.i != null) { | |
// Insert. | |
append(dest, {i:c.i, p:transformPosition(c.p, otherC, side === 'right')}); | |
} else { | |
// Delete | |
if (otherC.i != null) { | |
// Delete vs insert | |
var s = c.d; | |
if (c.p < otherC.p) { | |
append(dest, {d:s.slice(0, otherC.p - c.p), p:c.p}); | |
s = s.slice(otherC.p - c.p); | |
} | |
if (s !== '') | |
append(dest, {d: s, p: c.p + otherC.i.length}); | |
} else { | |
// Delete vs delete | |
if (c.p >= otherC.p + otherC.d.length) | |
append(dest, {d: c.d, p: c.p - otherC.d.length}); | |
else if (c.p + c.d.length <= otherC.p) | |
append(dest, c); | |
else { | |
// They overlap somewhere. | |
var newC = {d: '', p: c.p}; | |
if (c.p < otherC.p) | |
newC.d = c.d.slice(0, otherC.p - c.p); | |
if (c.p + c.d.length > otherC.p + otherC.d.length) | |
newC.d += c.d.slice(otherC.p + otherC.d.length - c.p); | |
// This is entirely optional - I'm just checking the deleted text in | |
// the two ops matches | |
var intersectStart = Math.max(c.p, otherC.p); | |
var intersectEnd = Math.min(c.p + c.d.length, otherC.p + otherC.d.length); | |
var cIntersect = c.d.slice(intersectStart - c.p, intersectEnd - c.p); | |
var otherIntersect = otherC.d.slice(intersectStart - otherC.p, intersectEnd - otherC.p); | |
if (cIntersect !== otherIntersect) | |
throw new Error('Delete ops delete different text in the same region of the document'); | |
if (newC.d !== '') { | |
newC.p = transformPosition(newC.p, otherC); | |
append(dest, newC); | |
} | |
} | |
} | |
} | |
return dest; | |
}; | |
var invertComponent = function(c) { | |
return (c.i != null) ? {d:c.i, p:c.p} : {i:c.d, p:c.p}; | |
}; | |
// No need to use append for invert, because the components won't be able to | |
// cancel one another. | |
text.invert = function(op) { | |
// Shallow copy & reverse that sucka. | |
op = op.slice().reverse(); | |
for (var i = 0; i < op.length; i++) { | |
op[i] = invertComponent(op[i]); | |
} | |
return op; | |
}; | |
bootstrapTransform_1(text, transformComponent, checkValidOp, append); | |
}); | |
var text0_1 = text0.name; | |
var text0_2 = text0.uri; | |
var text0_3 = text0.create; | |
/* | |
This is the implementation of the JSON OT type. | |
Spec is here: https://github.com/josephg/ShareJS/wiki/JSON-Operations | |
Note: This is being made obsolete. It will soon be replaced by the JSON2 type. | |
*/ | |
/** | |
* UTILITY FUNCTIONS | |
*/ | |
/** | |
* Checks if the passed object is an Array instance. Can't use Array.isArray | |
* yet because its not supported on IE8. | |
* | |
* @param obj | |
* @returns {boolean} | |
*/ | |
var isArray = function(obj) { | |
return Object.prototype.toString.call(obj) == '[object Array]'; | |
}; | |
/** | |
* Checks if the passed object is an Object instance. | |
* No function call (fast) version | |
* | |
* @param obj | |
* @returns {boolean} | |
*/ | |
var isObject = function(obj) { | |
return (!!obj) && (obj.constructor === Object); | |
}; | |
/** | |
* Clones the passed object using JSON serialization (which is slow). | |
* | |
* hax, copied from test/types/json. Apparently this is still the fastest way | |
* to deep clone an object, assuming we have browser support for JSON. @see | |
* http://jsperf.com/cloning-an-object/12 | |
*/ | |
var clone = function(o) { | |
return JSON.parse(JSON.stringify(o)); | |
}; | |
/** | |
* JSON OT Type | |
* @type {*} | |
*/ | |
var json = { | |
name: 'json0', | |
uri: 'http://sharejs.org/types/JSONv0' | |
}; | |
// You can register another OT type as a subtype in a JSON document using | |
// the following function. This allows another type to handle certain | |
// operations instead of the builtin JSON type. | |
var subtypes = {}; | |
json.registerSubtype = function(subtype) { | |
subtypes[subtype.name] = subtype; | |
}; | |
json.create = function(data) { | |
// Null instead of undefined if you don't pass an argument. | |
return data === undefined ? null : clone(data); | |
}; | |
json.invertComponent = function(c) { | |
var c_ = {p: c.p}; | |
// handle subtype ops | |
if (c.t && subtypes[c.t]) { | |
c_.t = c.t; | |
c_.o = subtypes[c.t].invert(c.o); | |
} | |
if (c.si !== void 0) c_.sd = c.si; | |
if (c.sd !== void 0) c_.si = c.sd; | |
if (c.oi !== void 0) c_.od = c.oi; | |
if (c.od !== void 0) c_.oi = c.od; | |
if (c.li !== void 0) c_.ld = c.li; | |
if (c.ld !== void 0) c_.li = c.ld; | |
if (c.na !== void 0) c_.na = -c.na; | |
if (c.lm !== void 0) { | |
c_.lm = c.p[c.p.length-1]; | |
c_.p = c.p.slice(0,c.p.length-1).concat([c.lm]); | |
} | |
return c_; | |
}; | |
json.invert = function(op) { | |
var op_ = op.slice().reverse(); | |
var iop = []; | |
for (var i = 0; i < op_.length; i++) { | |
iop.push(json.invertComponent(op_[i])); | |
} | |
return iop; | |
}; | |
json.checkValidOp = function(op) { | |
for (var i = 0; i < op.length; i++) { | |
if (!isArray(op[i].p)) throw new Error('Missing path'); | |
} | |
}; | |
json.checkList = function(elem) { | |
if (!isArray(elem)) | |
throw new Error('Referenced element not a list'); | |
}; | |
json.checkObj = function(elem) { | |
if (!isObject(elem)) { | |
throw new Error("Referenced element not an object (it was " + JSON.stringify(elem) + ")"); | |
} | |
}; | |
// helper functions to convert old string ops to and from subtype ops | |
function convertFromText(c) { | |
c.t = 'text0'; | |
var o = {p: c.p.pop()}; | |
if (c.si != null) o.i = c.si; | |
if (c.sd != null) o.d = c.sd; | |
c.o = [o]; | |
} | |
function convertToText(c) { | |
c.p.push(c.o[0].p); | |
if (c.o[0].i != null) c.si = c.o[0].i; | |
if (c.o[0].d != null) c.sd = c.o[0].d; | |
delete c.t; | |
delete c.o; | |
} | |
json.apply = function(snapshot, op) { | |
json.checkValidOp(op); | |
op = clone(op); | |
var container = { | |
data: snapshot | |
}; | |
for (var i = 0; i < op.length; i++) { | |
var c = op[i]; | |
// convert old string ops to use subtype for backwards compatibility | |
if (c.si != null || c.sd != null) | |
convertFromText(c); | |
var parent = null; | |
var elem = container; | |
var key = 'data'; | |
for (var j = 0; j < c.p.length; j++) { | |
var p = c.p[j]; | |
parent = elem; | |
elem = elem[key]; | |
key = p; | |
if (parent == null) | |
throw new Error('Path invalid'); | |
} | |
// handle subtype ops | |
if (c.t && c.o !== void 0 && subtypes[c.t]) { | |
elem[key] = subtypes[c.t].apply(elem[key], c.o); | |
// Number add | |
} else if (c.na !== void 0) { | |
if (typeof elem[key] != 'number') | |
throw new Error('Referenced element not a number'); | |
elem[key] += c.na; | |
} | |
// List replace | |
else if (c.li !== void 0 && c.ld !== void 0) { | |
json.checkList(elem); | |
// Should check the list element matches c.ld | |
elem[key] = c.li; | |
} | |
// List insert | |
else if (c.li !== void 0) { | |
json.checkList(elem); | |
elem.splice(key,0, c.li); | |
} | |
// List delete | |
else if (c.ld !== void 0) { | |
json.checkList(elem); | |
// Should check the list element matches c.ld here too. | |
elem.splice(key,1); | |
} | |
// List move | |
else if (c.lm !== void 0) { | |
json.checkList(elem); | |
if (c.lm != key) { | |
var e = elem[key]; | |
// Remove it... | |
elem.splice(key,1); | |
// And insert it back. | |
elem.splice(c.lm,0,e); | |
} | |
} | |
// Object insert / replace | |
else if (c.oi !== void 0) { | |
json.checkObj(elem); | |
// Should check that elem[key] == c.od | |
elem[key] = c.oi; | |
} | |
// Object delete | |
else if (c.od !== void 0) { | |
json.checkObj(elem); | |
// Should check that elem[key] == c.od | |
delete elem[key]; | |
} | |
else { | |
throw new Error('invalid / missing instruction in op'); | |
} | |
} | |
return container.data; | |
}; | |
// Helper to break an operation up into a bunch of small ops. | |
json.shatter = function(op) { | |
var results = []; | |
for (var i = 0; i < op.length; i++) { | |
results.push([op[i]]); | |
} | |
return results; | |
}; | |
// Helper for incrementally applying an operation to a snapshot. Calls yield | |
// after each op component has been applied. | |
json.incrementalApply = function(snapshot, op, _yield) { | |
for (var i = 0; i < op.length; i++) { | |
var smallOp = [op[i]]; | |
snapshot = json.apply(snapshot, smallOp); | |
// I'd just call this yield, but thats a reserved keyword. Bah! | |
_yield(smallOp, snapshot); | |
} | |
return snapshot; | |
}; | |
// Checks if two paths, p1 and p2 match. | |
var pathMatches = json.pathMatches = function(p1, p2, ignoreLast) { | |
if (p1.length != p2.length) | |
return false; | |
for (var i = 0; i < p1.length; i++) { | |
if (p1[i] !== p2[i] && (!ignoreLast || i !== p1.length - 1)) | |
return false; | |
} | |
return true; | |
}; | |
json.append = function(dest,c) { | |
c = clone(c); | |
if (dest.length === 0) { | |
dest.push(c); | |
return; | |
} | |
var last = dest[dest.length - 1]; | |
// convert old string ops to use subtype for backwards compatibility | |
if ((c.si != null || c.sd != null) && (last.si != null || last.sd != null)) { | |
convertFromText(c); | |
convertFromText(last); | |
} | |
if (pathMatches(c.p, last.p)) { | |
// handle subtype ops | |
if (c.t && last.t && c.t === last.t && subtypes[c.t]) { | |
last.o = subtypes[c.t].compose(last.o, c.o); | |
// convert back to old string ops | |
if (c.si != null || c.sd != null) { | |
var p = c.p; | |
for (var i = 0; i < last.o.length - 1; i++) { | |
c.o = [last.o.pop()]; | |
c.p = p.slice(); | |
convertToText(c); | |
dest.push(c); | |
} | |
convertToText(last); | |
} | |
} else if (last.na != null && c.na != null) { | |
dest[dest.length - 1] = {p: last.p, na: last.na + c.na}; | |
} else if (last.li !== undefined && c.li === undefined && c.ld === last.li) { | |
// insert immediately followed by delete becomes a noop. | |
if (last.ld !== undefined) { | |
// leave the delete part of the replace | |
delete last.li; | |
} else { | |
dest.pop(); | |
} | |
} else if (last.od !== undefined && last.oi === undefined && c.oi !== undefined && c.od === undefined) { | |
last.oi = c.oi; | |
} else if (last.oi !== undefined && c.od !== undefined) { | |
// The last path component inserted something that the new component deletes (or replaces). | |
// Just merge them. | |
if (c.oi !== undefined) { | |
last.oi = c.oi; | |
} else if (last.od !== undefined) { | |
delete last.oi; | |
} else { | |
// An insert directly followed by a delete turns into a no-op and can be removed. | |
dest.pop(); | |
} | |
} else if (c.lm !== undefined && c.p[c.p.length - 1] === c.lm) { | |
// don't do anything | |
} else { | |
dest.push(c); | |
} | |
} else { | |
// convert string ops back | |
if ((c.si != null || c.sd != null) && (last.si != null || last.sd != null)) { | |
convertToText(c); | |
convertToText(last); | |
} | |
dest.push(c); | |
} | |
}; | |
json.compose = function(op1,op2) { | |
json.checkValidOp(op1); | |
json.checkValidOp(op2); | |
var newOp = clone(op1); | |
for (var i = 0; i < op2.length; i++) { | |
json.append(newOp,op2[i]); | |
} | |
return newOp; | |
}; | |
json.normalize = function(op) { | |
var newOp = []; | |
op = isArray(op) ? op : [op]; | |
for (var i = 0; i < op.length; i++) { | |
var c = op[i]; | |
if (c.p == null) c.p = []; | |
json.append(newOp,c); | |
} | |
return newOp; | |
}; | |
// Returns the common length of the paths of ops a and b | |
json.commonLengthForOps = function(a, b) { | |
var alen = a.p.length; | |
var blen = b.p.length; | |
if (a.na != null || a.t) | |
alen++; | |
if (b.na != null || b.t) | |
blen++; | |
if (alen === 0) return -1; | |
if (blen === 0) return null; | |
alen--; | |
blen--; | |
for (var i = 0; i < alen; i++) { | |
var p = a.p[i]; | |
if (i >= blen || p !== b.p[i]) | |
return null; | |
} | |
return alen; | |
}; | |
// Returns true if an op can affect the given path | |
json.canOpAffectPath = function(op, path) { | |
return json.commonLengthForOps({p:path}, op) != null; | |
}; | |
// transform c so it applies to a document with otherC applied. | |
json.transformComponent = function(dest, c, otherC, type) { | |
c = clone(c); | |
var common = json.commonLengthForOps(otherC, c); | |
var common2 = json.commonLengthForOps(c, otherC); | |
var cplength = c.p.length; | |
var otherCplength = otherC.p.length; | |
if (c.na != null || c.t) | |
cplength++; | |
if (otherC.na != null || otherC.t) | |
otherCplength++; | |
// if c is deleting something, and that thing is changed by otherC, we need to | |
// update c to reflect that change for invertibility. | |
if (common2 != null && otherCplength > cplength && c.p[common2] == otherC.p[common2]) { | |
if (c.ld !== void 0) { | |
var oc = clone(otherC); | |
oc.p = oc.p.slice(cplength); | |
c.ld = json.apply(clone(c.ld),[oc]); | |
} else if (c.od !== void 0) { | |
var oc = clone(otherC); | |
oc.p = oc.p.slice(cplength); | |
c.od = json.apply(clone(c.od),[oc]); | |
} | |
} | |
if (common != null) { | |
var commonOperand = cplength == otherCplength; | |
// backward compatibility for old string ops | |
var oc = otherC; | |
if ((c.si != null || c.sd != null) && (otherC.si != null || otherC.sd != null)) { | |
convertFromText(c); | |
oc = clone(otherC); | |
convertFromText(oc); | |
} | |
// handle subtype ops | |
if (oc.t && subtypes[oc.t]) { | |
if (c.t && c.t === oc.t) { | |
var res = subtypes[c.t].transform(c.o, oc.o, type); | |
// convert back to old string ops | |
if (c.si != null || c.sd != null) { | |
var p = c.p; | |
for (var i = 0; i < res.length; i++) { | |
c.o = [res[i]]; | |
c.p = p.slice(); | |
convertToText(c); | |
json.append(dest, c); | |
} | |
} else if (!isArray(res) || res.length > 0) { | |
c.o = res; | |
json.append(dest, c); | |
} | |
return dest; | |
} | |
} | |
// transform based on otherC | |
else if (otherC.na !== void 0) { | |
// this case is handled below | |
} else if (otherC.li !== void 0 && otherC.ld !== void 0) { | |
if (otherC.p[common] === c.p[common]) { | |
// noop | |
if (!commonOperand) { | |
return dest; | |
} else if (c.ld !== void 0) { | |
// we're trying to delete the same element, -> noop | |
if (c.li !== void 0 && type === 'left') { | |
// we're both replacing one element with another. only one can survive | |
c.ld = clone(otherC.li); | |
} else { | |
return dest; | |
} | |
} | |
} | |
} else if (otherC.li !== void 0) { | |
if (c.li !== void 0 && c.ld === undefined && commonOperand && c.p[common] === otherC.p[common]) { | |
// in li vs. li, left wins. | |
if (type === 'right') | |
c.p[common]++; | |
} else if (otherC.p[common] <= c.p[common]) { | |
c.p[common]++; | |
} | |
if (c.lm !== void 0) { | |
if (commonOperand) { | |
// otherC edits the same list we edit | |
if (otherC.p[common] <= c.lm) | |
c.lm++; | |
// changing c.from is handled above. | |
} | |
} | |
} else if (otherC.ld !== void 0) { | |
if (c.lm !== void 0) { | |
if (commonOperand) { | |
if (otherC.p[common] === c.p[common]) { | |
// they deleted the thing we're trying to move | |
return dest; | |
} | |
// otherC edits the same list we edit | |
var p = otherC.p[common]; | |
var from = c.p[common]; | |
var to = c.lm; | |
if (p < to || (p === to && from < to)) | |
c.lm--; | |
} | |
} | |
if (otherC.p[common] < c.p[common]) { | |
c.p[common]--; | |
} else if (otherC.p[common] === c.p[common]) { | |
if (otherCplength < cplength) { | |
// we're below the deleted element, so -> noop | |
return dest; | |
} else if (c.ld !== void 0) { | |
if (c.li !== void 0) { | |
// we're replacing, they're deleting. we become an insert. | |
delete c.ld; | |
} else { | |
// we're trying to delete the same element, -> noop | |
return dest; | |
} | |
} | |
} | |
} else if (otherC.lm !== void 0) { | |
if (c.lm !== void 0 && cplength === otherCplength) { | |
// lm vs lm, here we go! | |
var from = c.p[common]; | |
var to = c.lm; | |
var otherFrom = otherC.p[common]; | |
var otherTo = otherC.lm; | |
if (otherFrom !== otherTo) { | |
// if otherFrom == otherTo, we don't need to change our op. | |
// where did my thing go? | |
if (from === otherFrom) { | |
// they moved it! tie break. | |
if (type === 'left') { | |
c.p[common] = otherTo; | |
if (from === to) // ugh | |
c.lm = otherTo; | |
} else { | |
return dest; | |
} | |
} else { | |
// they moved around it | |
if (from > otherFrom) c.p[common]--; | |
if (from > otherTo) c.p[common]++; | |
else if (from === otherTo) { | |
if (otherFrom > otherTo) { | |
c.p[common]++; | |
if (from === to) // ugh, again | |
c.lm++; | |
} | |
} | |
// step 2: where am i going to put it? | |
if (to > otherFrom) { | |
c.lm--; | |
} else if (to === otherFrom) { | |
if (to > from) | |
c.lm--; | |
} | |
if (to > otherTo) { | |
c.lm++; | |
} else if (to === otherTo) { | |
// if we're both moving in the same direction, tie break | |
if ((otherTo > otherFrom && to > from) || | |
(otherTo < otherFrom && to < from)) { | |
if (type === 'right') c.lm++; | |
} else { | |
if (to > from) c.lm++; | |
else if (to === otherFrom) c.lm--; | |
} | |
} | |
} | |
} | |
} else if (c.li !== void 0 && c.ld === undefined && commonOperand) { | |
// li | |
var from = otherC.p[common]; | |
var to = otherC.lm; | |
p = c.p[common]; | |
if (p > from) c.p[common]--; | |
if (p > to) c.p[common]++; | |
} else { | |
// ld, ld+li, si, sd, na, oi, od, oi+od, any li on an element beneath | |
// the lm | |
// | |
// i.e. things care about where their item is after the move. | |
var from = otherC.p[common]; | |
var to = otherC.lm; | |
p = c.p[common]; | |
if (p === from) { | |
c.p[common] = to; | |
} else { | |
if (p > from) c.p[common]--; | |
if (p > to) c.p[common]++; | |
else if (p === to && from > to) c.p[common]++; | |
} | |
} | |
} | |
else if (otherC.oi !== void 0 && otherC.od !== void 0) { | |
if (c.p[common] === otherC.p[common]) { | |
if (c.oi !== void 0 && commonOperand) { | |
// we inserted where someone else replaced | |
if (type === 'right') { | |
// left wins | |
return dest; | |
} else { | |
// we win, make our op replace what they inserted | |
c.od = otherC.oi; | |
} | |
} else { | |
// -> noop if the other component is deleting the same object (or any parent) | |
return dest; | |
} | |
} | |
} else if (otherC.oi !== void 0) { | |
if (c.oi !== void 0 && c.p[common] === otherC.p[common]) { | |
// left wins if we try to insert at the same place | |
if (type === 'left') { | |
json.append(dest,{p: c.p, od:otherC.oi}); | |
} else { | |
return dest; | |
} | |
} | |
} else if (otherC.od !== void 0) { | |
if (c.p[common] == otherC.p[common]) { | |
if (!commonOperand) | |
return dest; | |
if (c.oi !== void 0) { | |
delete c.od; | |
} else { | |
return dest; | |
} | |
} | |
} | |
} | |
json.append(dest,c); | |
return dest; | |
}; | |
bootstrapTransform_1(json, json.transformComponent, json.checkValidOp, json.append); | |
/** | |
* Register a subtype for string operations, using the text0 type. | |
*/ | |
json.registerSubtype(text0); | |
var json0 = json; | |
// Only the JSON type is exported, because the text type is deprecated | |
// otherwise. (If you want to use it somewhere, you're welcome to pull it out | |
// into a separate module that json0 can depend on). | |
var lib = { | |
type: json0 | |
}; | |
var types = createCommonjsModule(function (module, exports) { | |
exports.defaultType = lib.type; | |
exports.map = {}; | |
exports.register = function(type) { | |
if (type.name) exports.map[type.name] = type; | |
if (type.uri) exports.map[type.uri] = type; | |
}; | |
exports.register(exports.defaultType); | |
}); | |
var types_1 = types.defaultType; | |
var types_2 = types.map; | |
var types_3 = types.register; | |
/** | |
* A Doc is a client's view on a sharejs document. | |
* | |
* It is is uniquely identified by its `id` and `collection`. Documents | |
* should not be created directly. Create them with connection.get() | |
* | |
* | |
* Subscriptions | |
* ------------- | |
* | |
* We can subscribe a document to stay in sync with the server. | |
* doc.subscribe(function(error) { | |
* doc.subscribed // = true | |
* }) | |
* The server now sends us all changes concerning this document and these are | |
* applied to our data. If the subscription was successful the initial | |
* data and version sent by the server are loaded into the document. | |
* | |
* To stop listening to the changes we call `doc.unsubscribe()`. | |
* | |
* If we just want to load the data but not stay up-to-date, we call | |
* doc.fetch(function(error) { | |
* doc.data // sent by server | |
* }) | |
* | |
* | |
* Events | |
* ------ | |
* | |
* You can use doc.on(eventName, callback) to subscribe to the following events: | |
* - `before op (op, source)` Fired before a partial operation is applied to the data. | |
* It may be used to read the old data just before applying an operation | |
* - `op (op, source)` Fired after every partial operation with this operation as the | |
* first argument | |
* - `create (source)` The document was created. That means its type was | |
* set and it has some initial data. | |
* - `del (data, source)` Fired after the document is deleted, that is | |
* the data is null. It is passed the data before delteion as an | |
* arguments | |
* - `load ()` Fired when a new snapshot is ingested from a fetch, subscribe, or query | |
*/ | |
var doc = Doc; | |
function Doc(connection, collection, id) { | |
emitter.EventEmitter.call(this); | |
this.connection = connection; | |
this.collection = collection; | |
this.id = id; | |
this.version = null; | |
this.type = null; | |
this.data = undefined; | |
// Array of callbacks or nulls as placeholders | |
this.inflightFetch = []; | |
this.inflightSubscribe = []; | |
this.inflightUnsubscribe = []; | |
this.pendingFetch = []; | |
// Whether we think we are subscribed on the server. Synchronously set to | |
// false on calls to unsubscribe and disconnect. Should never be true when | |
// this.wantSubscribe is false | |
this.subscribed = false; | |
// Whether to re-establish the subscription on reconnect | |
this.wantSubscribe = false; | |
// The op that is currently roundtripping to the server, or null. | |
// | |
// When the connection reconnects, the inflight op is resubmitted. | |
// | |
// This has the same format as an entry in pendingOps | |
this.inflightOp = null; | |
// All ops that are waiting for the server to acknowledge this.inflightOp | |
// This used to just be a single operation, but creates & deletes can't be | |
// composed with regular operations. | |
// | |
// This is a list of {[create:{...}], [del:true], [op:...], callbacks:[...]} | |
this.pendingOps = []; | |
// The OT type of this document. An uncreated document has type `null` | |
this.type = null; | |
// The applyStack enables us to track any ops submitted while we are | |
// applying an op incrementally. This value is an array when we are | |
// performing an incremental apply and null otherwise. When it is an array, | |
// all submitted ops should be pushed onto it. The `_otApply` method will | |
// reset it back to null when all incremental apply loops are complete. | |
this.applyStack = null; | |
// Disable the default behavior of composing submitted ops. This is read at | |
// the time of op submit, so it may be toggled on before submitting a | |
// specifc op and toggled off afterward | |
this.preventCompose = false; | |
} | |
emitter.mixin(Doc); | |
Doc.prototype.destroy = function(callback) { | |
var doc = this; | |
doc.whenNothingPending(function() { | |
doc.connection._destroyDoc(doc); | |
if (doc.wantSubscribe) { | |
return doc.unsubscribe(callback); | |
} | |
if (callback) callback(); | |
}); | |
}; | |
// ****** Manipulating the document data, version and type. | |
// Set the document's type, and associated properties. Most of the logic in | |
// this function exists to update the document based on any added & removed API | |
// methods. | |
// | |
// @param newType OT type provided by the ottypes library or its name or uri | |
Doc.prototype._setType = function(newType) { | |
if (typeof newType === 'string') { | |
newType = types.map[newType]; | |
} | |
if (newType) { | |
this.type = newType; | |
} else if (newType === null) { | |
this.type = newType; | |
// If we removed the type from the object, also remove its data | |
this.data = undefined; | |
} else { | |
var err = new error(4008, 'Missing type ' + newType); | |
return this.emit('error', err); | |
} | |
}; | |
// Ingest snapshot data. This data must include a version, snapshot and type. | |
// This is used both to ingest data that was exported with a webpage and data | |
// that was received from the server during a fetch. | |
// | |
// @param snapshot.v version | |
// @param snapshot.data | |
// @param snapshot.type | |
// @param callback | |
Doc.prototype.ingestSnapshot = function(snapshot, callback) { | |
if (!snapshot) return callback && callback(); | |
if (typeof snapshot.v !== 'number') { | |
var err = new error(5008, 'Missing version in ingested snapshot. ' + this.collection + '.' + this.id); | |
if (callback) return callback(err); | |
return this.emit('error', err); | |
} | |
// If the doc is already created or there are ops pending, we cannot use the | |
// ingested snapshot and need ops in order to update the document | |
if (this.type || this.hasWritePending()) { | |
// The version should only be null on a created document when it was | |
// created locally without fetching | |
if (this.version == null) { | |
if (this.hasWritePending()) { | |
// If we have pending ops and we get a snapshot for a locally created | |
// document, we have to wait for the pending ops to complete, because | |
// we don't know what version to fetch ops from. It is possible that | |
// the snapshot came from our local op, but it is also possible that | |
// the doc was created remotely (which would conflict and be an error) | |
return callback && this.once('no write pending', callback); | |
} | |
// Otherwise, we've encounted an error state | |
var err = new error(5009, 'Cannot ingest snapshot in doc with null version. ' + this.collection + '.' + this.id); | |
if (callback) return callback(err); | |
return this.emit('error', err); | |
} | |
// If we got a snapshot for a version further along than the document is | |
// currently, issue a fetch to get the latest ops and catch us up | |
if (snapshot.v > this.version) return this.fetch(callback); | |
return callback && callback(); | |
} | |
// Ignore the snapshot if we are already at a newer version. Under no | |
// circumstance should we ever set the current version backward | |
if (this.version > snapshot.v) return callback && callback(); | |
this.version = snapshot.v; | |
var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; | |
this._setType(type); | |
this.data = (this.type && this.type.deserialize) ? | |
this.type.deserialize(snapshot.data) : | |
snapshot.data; | |
this.emit('load'); | |
callback && callback(); | |
}; | |
Doc.prototype.whenNothingPending = function(callback) { | |
if (this.hasPending()) { | |
this.once('nothing pending', callback); | |
return; | |
} | |
callback(); | |
}; | |
Doc.prototype.hasPending = function() { | |
return !!( | |
this.inflightOp || | |
this.pendingOps.length || | |
this.inflightFetch.length || | |
this.inflightSubscribe.length || | |
this.inflightUnsubscribe.length || | |
this.pendingFetch.length | |
); | |
}; | |
Doc.prototype.hasWritePending = function() { | |
return !!(this.inflightOp || this.pendingOps.length); | |
}; | |
Doc.prototype._emitNothingPending = function() { | |
if (this.hasWritePending()) return; | |
this.emit('no write pending'); | |
if (this.hasPending()) return; | |
this.emit('nothing pending'); | |
}; | |
// **** Helpers for network messages | |
Doc.prototype._emitResponseError = function(err, callback) { | |
if (callback) { | |
callback(err); | |
this._emitNothingPending(); | |
return; | |
} | |
this._emitNothingPending(); | |
this.emit('error', err); | |
}; | |
Doc.prototype._handleFetch = function(err, snapshot) { | |
var callback = this.inflightFetch.shift(); | |
if (err) return this._emitResponseError(err, callback); | |
this.ingestSnapshot(snapshot, callback); | |
this._emitNothingPending(); | |
}; | |
Doc.prototype._handleSubscribe = function(err, snapshot) { | |
var callback = this.inflightSubscribe.shift(); | |
if (err) return this._emitResponseError(err, callback); | |
// Indicate we are subscribed only if the client still wants to be. In the | |
// time since calling subscribe and receiving a response from the server, | |
// unsubscribe could have been called and we might already be unsubscribed | |
// but not have received the response. Also, because requests from the | |
// client are not serialized and may take different async time to process, | |
// it is possible that we could hear responses back in a different order | |
// from the order originally sent | |
if (this.wantSubscribe) this.subscribed = true; | |
this.ingestSnapshot(snapshot, callback); | |
this._emitNothingPending(); | |
}; | |
Doc.prototype._handleUnsubscribe = function(err) { | |
var callback = this.inflightUnsubscribe.shift(); | |
if (err) return this._emitResponseError(err, callback); | |
if (callback) callback(); | |
this._emitNothingPending(); | |
}; | |
Doc.prototype._handleOp = function(err, message) { | |
if (err) { | |
if (this.inflightOp) { | |
// The server has rejected submission of the current operation. If we get | |
// an error code 4002 "Op submit rejected", this was done intentionally | |
// and we should roll back but not return an error to the user. | |
if (err.code === 4002) err = null; | |
return this._rollback(err); | |
} | |
return this.emit('error', err); | |
} | |
if (this.inflightOp && | |
message.src === this.inflightOp.src && | |
message.seq === this.inflightOp.seq) { | |
// The op has already been applied locally. Just update the version | |
// and pending state appropriately | |
this._opAcknowledged(message); | |
return; | |
} | |
if (this.version == null || message.v > this.version) { | |
// This will happen in normal operation if we become subscribed to a | |
// new document via a query. It can also happen if we get an op for | |
// a future version beyond the version we are expecting next. This | |
// could happen if the server doesn't publish an op for whatever reason | |
// or because of a race condition. In any case, we can send a fetch | |
// command to catch back up. | |
// | |
// Fetch only sends a new fetch command if no fetches are inflight, which | |
// will act as a natural debouncing so we don't send multiple fetch | |
// requests for many ops received at once. | |
this.fetch(); | |
return; | |
} | |
if (message.v < this.version) { | |
// We can safely ignore the old (duplicate) operation. | |
return; | |
} | |
if (this.inflightOp) { | |
var transformErr = transformX(this.inflightOp, message); | |
if (transformErr) return this._hardRollback(transformErr); | |
} | |
for (var i = 0; i < this.pendingOps.length; i++) { | |
var transformErr = transformX(this.pendingOps[i], message); | |
if (transformErr) return this._hardRollback(transformErr); | |
} | |
this.version++; | |
this._otApply(message, false); | |
return; | |
}; | |
// Called whenever (you guessed it!) the connection state changes. This will | |
// happen when we get disconnected & reconnect. | |
Doc.prototype._onConnectionStateChanged = function() { | |
if (this.connection.canSend) { | |
this.flush(); | |
this._resubscribe(); | |
} else { | |
if (this.inflightOp) { | |
this.pendingOps.unshift(this.inflightOp); | |
this.inflightOp = null; | |
} | |
this.subscribed = false; | |
if (this.inflightFetch.length || this.inflightSubscribe.length) { | |
this.pendingFetch = this.pendingFetch.concat(this.inflightFetch, this.inflightSubscribe); | |
this.inflightFetch.length = 0; | |
this.inflightSubscribe.length = 0; | |
} | |
if (this.inflightUnsubscribe.length) { | |
var callbacks = this.inflightUnsubscribe; | |
this.inflightUnsubscribe = []; | |
callEach(callbacks); | |
} | |
} | |
}; | |
Doc.prototype._resubscribe = function() { | |
var callbacks = this.pendingFetch; | |
this.pendingFetch = []; | |
if (this.wantSubscribe) { | |
if (callbacks.length) { | |
this.subscribe(function(err) { | |
callEach(callbacks, err); | |
}); | |
return; | |
} | |
this.subscribe(); | |
return; | |
} | |
if (callbacks.length) { | |
this.fetch(function(err) { | |
callEach(callbacks, err); | |
}); | |
} | |
}; | |
// Request the current document snapshot or ops that bring us up to date | |
Doc.prototype.fetch = function(callback) { | |
if (this.connection.canSend) { | |
var isDuplicate = this.connection.sendFetch(this); | |
pushActionCallback(this.inflightFetch, isDuplicate, callback); | |
return; | |
} | |
this.pendingFetch.push(callback); | |
}; | |
// Fetch the initial document and keep receiving updates | |
Doc.prototype.subscribe = function(callback) { | |
this.wantSubscribe = true; | |
if (this.connection.canSend) { | |
var isDuplicate = this.connection.sendSubscribe(this); | |
pushActionCallback(this.inflightSubscribe, isDuplicate, callback); | |
return; | |
} | |
this.pendingFetch.push(callback); | |
}; | |
// Unsubscribe. The data will stay around in local memory, but we'll stop | |
// receiving updates | |
Doc.prototype.unsubscribe = function(callback) { | |
this.wantSubscribe = false; | |
// The subscribed state should be conservative in indicating when we are | |
// subscribed on the server. We'll actually be unsubscribed some time | |
// between sending the message and hearing back, but we cannot know exactly | |
// when. Thus, immediately mark us as not subscribed | |
this.subscribed = false; | |
if (this.connection.canSend) { | |
var isDuplicate = this.connection.sendUnsubscribe(this); | |
pushActionCallback(this.inflightUnsubscribe, isDuplicate, callback); | |
return; | |
} | |
if (callback) nextTick(callback); | |
}; | |
function pushActionCallback(inflight, isDuplicate, callback) { | |
if (isDuplicate) { | |
var lastCallback = inflight.pop(); | |
inflight.push(function(err) { | |
lastCallback && lastCallback(err); | |
callback && callback(err); | |
}); | |
} else { | |
inflight.push(callback); | |
} | |
} | |
// Operations // | |
// Send the next pending op to the server, if we can. | |
// | |
// Only one operation can be in-flight at a time. If an operation is already on | |
// its way, or we're not currently connected, this method does nothing. | |
Doc.prototype.flush = function() { | |
// Ignore if we can't send or we are already sending an op | |
if (!this.connection.canSend || this.inflightOp) return; | |
// Send first pending op unless paused | |
if (!this.paused && this.pendingOps.length) { | |
this._sendOp(); | |
} | |
}; | |
// Helper function to set op to contain a no-op. | |
function setNoOp(op) { | |
delete op.op; | |
delete op.create; | |
delete op.del; | |
} | |
// Transform server op data by a client op, and vice versa. Ops are edited in place. | |
function transformX(client, server) { | |
// Order of statements in this function matters. Be especially careful if | |
// refactoring this function | |
// A client delete op should dominate if both the server and the client | |
// delete the document. Thus, any ops following the client delete (such as a | |
// subsequent create) will be maintained, since the server op is transformed | |
// to a no-op | |
if (client.del) return setNoOp(server); | |
if (server.del) { | |
return new error(4017, 'Document was deleted'); | |
} | |
if (server.create) { | |
return new error(4018, 'Document alredy created'); | |
} | |
// Ignore no-op coming from server | |
if (!server.op) return; | |
// I believe that this should not occur, but check just in case | |
if (client.create) { | |
return new error(4018, 'Document already created'); | |
} | |
// They both edited the document. This is the normal case for this function - | |
// as in, most of the time we'll end up down here. | |
// | |
// You should be wondering why I'm using client.type instead of this.type. | |
// The reason is, if we get ops at an old version of the document, this.type | |
// might be undefined or a totally different type. By pinning the type to the | |
// op data, we make sure the right type has its transform function called. | |
if (client.type.transformX) { | |
var result = client.type.transformX(client.op, server.op); | |
client.op = result[0]; | |
server.op = result[1]; | |
} else { | |
var clientOp = client.type.transform(client.op, server.op, 'left'); | |
var serverOp = client.type.transform(server.op, client.op, 'right'); | |
client.op = clientOp; | |
server.op = serverOp; | |
} | |
} | |
/** | |
* Applies the operation to the snapshot | |
* | |
* If the operation is create or delete it emits `create` or `del`. Then the | |
* operation is applied to the snapshot and `op` and `after op` are emitted. | |
* If the type supports incremental updates and `this.incremental` is true we | |
* fire `op` after every small operation. | |
* | |
* This is the only function to fire the above mentioned events. | |
* | |
* @private | |
*/ | |
Doc.prototype._otApply = function(op, source) { | |
if (op.op) { | |
if (!this.type) { | |
var err = new error(4015, 'Cannot apply op to uncreated document. ' + this.collection + '.' + this.id); | |
return this.emit('error', err); | |
} | |
// Iteratively apply multi-component remote operations and rollback ops | |
// (source === false) for the default JSON0 OT type. It could use | |
// type.shatter(), but since this code is so specific to use cases for the | |
// JSON0 type and ShareDB explicitly bundles the default type, we might as | |
// well write it this way and save needing to iterate through the op | |
// components twice. | |
// | |
// Ideally, we would not need this extra complexity. However, it is | |
// helpful for implementing bindings that update DOM nodes and other | |
// stateful objects by translating op events directly into corresponding | |
// mutations. Such bindings are most easily written as responding to | |
// individual op components one at a time in order, and it is important | |
// that the snapshot only include updates from the particular op component | |
// at the time of emission. Eliminating this would require rethinking how | |
// such external bindings are implemented. | |
if (!source && this.type === types.defaultType && op.op.length > 1) { | |
if (!this.applyStack) this.applyStack = []; | |
var stackLength = this.applyStack.length; | |
for (var i = 0; i < op.op.length; i++) { | |
var component = op.op[i]; | |
var componentOp = {op: [component]}; | |
// Transform componentOp against any ops that have been submitted | |
// sychronously inside of an op event handler since we began apply of | |
// our operation | |
for (var j = stackLength; j < this.applyStack.length; j++) { | |
var transformErr = transformX(this.applyStack[j], componentOp); | |
if (transformErr) return this._hardRollback(transformErr); | |
} | |
// Apply the individual op component | |
this.emit('before op', componentOp.op, source); | |
this.data = this.type.apply(this.data, componentOp.op); | |
this.emit('op', componentOp.op, source); | |
} | |
// Pop whatever was submitted since we started applying this op | |
this._popApplyStack(stackLength); | |
return; | |
} | |
// The 'before op' event enables clients to pull any necessary data out of | |
// the snapshot before it gets changed | |
this.emit('before op', op.op, source); | |
// Apply the operation to the local data, mutating it in place | |
this.data = this.type.apply(this.data, op.op); | |
// Emit an 'op' event once the local data includes the changes from the | |
// op. For locally submitted ops, this will be synchronously with | |
// submission and before the server or other clients have received the op. | |
// For ops from other clients, this will be after the op has been | |
// committed to the database and published | |
this.emit('op', op.op, source); | |
return; | |
} | |
if (op.create) { | |
this._setType(op.create.type); | |
this.data = (this.type.deserialize) ? | |
(this.type.createDeserialized) ? | |
this.type.createDeserialized(op.create.data) : | |
this.type.deserialize(this.type.create(op.create.data)) : | |
this.type.create(op.create.data); | |
this.emit('create', source); | |
return; | |
} | |
if (op.del) { | |
var oldData = this.data; | |
this._setType(null); | |
this.emit('del', oldData, source); | |
return; | |
} | |
}; | |
// ***** Sending operations | |
// Actually send op to the server. | |
Doc.prototype._sendOp = function() { | |
// Wait until we have a src id from the server | |
var src = this.connection.id; | |
if (!src) return; | |
// When there is no inflightOp, send the first item in pendingOps. If | |
// there is inflightOp, try sending it again | |
if (!this.inflightOp) { | |
// Send first pending op | |
this.inflightOp = this.pendingOps.shift(); | |
} | |
var op = this.inflightOp; | |
if (!op) { | |
var err = new error(5010, 'No op to send on call to _sendOp'); | |
return this.emit('error', err); | |
} | |
// Track data for retrying ops | |
op.sentAt = Date.now(); | |
op.retries = (op.retries == null) ? 0 : op.retries + 1; | |
// The src + seq number is a unique ID representing this operation. This tuple | |
// is used on the server to detect when ops have been sent multiple times and | |
// on the client to match acknowledgement of an op back to the inflightOp. | |
// Note that the src could be different from this.connection.id after a | |
// reconnect, since an op may still be pending after the reconnection and | |
// this.connection.id will change. In case an op is sent multiple times, we | |
// also need to be careful not to override the original seq value. | |
if (op.seq == null) op.seq = this.connection.seq++; | |
this.connection.sendOp(this, op); | |
// src isn't needed on the first try, since the server session will have the | |
// same id, but it must be set on the inflightOp in case it is sent again | |
// after a reconnect and the connection's id has changed by then | |
if (op.src == null) op.src = src; | |
}; | |
// Queues the operation for submission to the server and applies it locally. | |
// | |
// Internal method called to do the actual work for submit(), create() and del(). | |
// @private | |
// | |
// @param op | |
// @param [op.op] | |
// @param [op.del] | |
// @param [op.create] | |
// @param [callback] called when operation is submitted | |
Doc.prototype._submit = function(op, source, callback) { | |
// Locally submitted ops must always have a truthy source | |
if (!source) source = true; | |
// The op contains either op, create, delete, or none of the above (a no-op). | |
if (op.op) { | |
if (!this.type) { | |
var err = new error(4015, 'Cannot submit op. Document has not been created. ' + this.collection + '.' + this.id); | |
if (callback) return callback(err); | |
return this.emit('error', err); | |
} | |
// Try to normalize the op. This removes trailing skip:0's and things like that. | |
if (this.type.normalize) op.op = this.type.normalize(op.op); | |
} | |
this._pushOp(op, callback); | |
this._otApply(op, source); | |
// The call to flush is delayed so if submit() is called multiple times | |
// synchronously, all the ops are combined before being sent to the server. | |
var doc = this; | |
nextTick(function() { | |
doc.flush(); | |
}); | |
}; | |
Doc.prototype._pushOp = function(op, callback) { | |
if (this.applyStack) { | |
// If we are in the process of incrementally applying an operation, don't | |
// compose the op and push it onto the applyStack so it can be transformed | |
// against other components from the op or ops being applied | |
this.applyStack.push(op); | |
} else { | |
// If the type supports composes, try to compose the operation onto the | |
// end of the last pending operation. | |
var composed = this._tryCompose(op); | |
if (composed) { | |
composed.callbacks.push(callback); | |
return; | |
} | |
} | |
// Push on to the pendingOps queue of ops to submit if we didn't compose | |
op.type = this.type; | |
op.callbacks = [callback]; | |
this.pendingOps.push(op); | |
}; | |
Doc.prototype._popApplyStack = function(to) { | |
if (to > 0) { | |
this.applyStack.length = to; | |
return; | |
} | |
// Once we have completed the outermost apply loop, reset to null and no | |
// longer add ops to the applyStack as they are submitted | |
var op = this.applyStack[0]; | |
this.applyStack = null; | |
if (!op) return; | |
// Compose the ops added since the beginning of the apply stack, since we | |
// had to skip compose when they were originally pushed | |
var i = this.pendingOps.indexOf(op); | |
if (i === -1) return; | |
var ops = this.pendingOps.splice(i); | |
for (var i = 0; i < ops.length; i++) { | |
var op = ops[i]; | |
var composed = this._tryCompose(op); | |
if (composed) { | |
composed.callbacks = composed.callbacks.concat(op.callbacks); | |
} else { | |
this.pendingOps.push(op); | |
} | |
} | |
}; | |
// Try to compose a submitted op into the last pending op. Returns the | |
// composed op if it succeeds, undefined otherwise | |
Doc.prototype._tryCompose = function(op) { | |
if (this.preventCompose) return; | |
// We can only compose into the last pending op. Inflight ops have already | |
// been sent to the server, so we can't modify them | |
var last = this.pendingOps[this.pendingOps.length - 1]; | |
if (!last) return; | |
// Compose an op into a create by applying it. This effectively makes the op | |
// invisible, as if the document were created including the op originally | |
if (last.create && op.op) { | |
last.create.data = this.type.apply(last.create.data, op.op); | |
return last; | |
} | |
// Compose two ops into a single op if supported by the type. Types that | |
// support compose must be able to compose any two ops together | |
if (last.op && op.op && this.type.compose) { | |
last.op = this.type.compose(last.op, op.op); | |
return last; | |
} | |
}; | |
// *** Client OT entrypoints. | |
// Submit an operation to the document. | |
// | |
// @param operation handled by the OT type | |
// @param options {source: ...} | |
// @param [callback] called after operation submitted | |
// | |
// @fires before op, op, after op | |
Doc.prototype.submitOp = function(component, options, callback) { | |
if (typeof options === 'function') { | |
callback = options; | |
options = null; | |
} | |
var op = {op: component}; | |
var source = options && options.source; | |
this._submit(op, source, callback); | |
}; | |
// Create the document, which in ShareJS semantics means to set its type. Every | |
// object implicitly exists in the database but has no data and no type. Create | |
// sets the type of the object and can optionally set some initial data on the | |
// object, depending on the type. | |
// | |
// @param data initial | |
// @param type OT type | |
// @param options {source: ...} | |
// @param callback called when operation submitted | |
Doc.prototype.create = function(data, type, options, callback) { | |
if (typeof type === 'function') { | |
callback = type; | |
options = null; | |
type = null; | |
} else if (typeof options === 'function') { | |
callback = options; | |
options = null; | |
} | |
if (!type) { | |
type = types.defaultType.uri; | |
} | |
if (this.type) { | |
var err = new error(4016, 'Document already exists'); | |
if (callback) return callback(err); | |
return this.emit('error', err); | |
} | |
var op = {create: {type: type, data: data}}; | |
var source = options && options.source; | |
this._submit(op, source, callback); | |
}; | |
// Delete the document. This creates and submits a delete operation to the | |
// server. Deleting resets the object's type to null and deletes its data. The | |
// document still exists, and still has the version it used to have before you | |
// deleted it (well, old version +1). | |
// | |
// @param options {source: ...} | |
// @param callback called when operation submitted | |
Doc.prototype.del = function(options, callback) { | |
if (typeof options === 'function') { | |
callback = options; | |
options = null; | |
} | |
if (!this.type) { | |
var err = new error(4015, 'Document does not exist'); | |
if (callback) return callback(err); | |
return this.emit('error', err); | |
} | |
var op = {del: true}; | |
var source = options && options.source; | |
this._submit(op, source, callback); | |
}; | |
// Stops the document from sending any operations to the server. | |
Doc.prototype.pause = function() { | |
this.paused = true; | |
}; | |
// Continue sending operations to the server | |
Doc.prototype.resume = function() { | |
this.paused = false; | |
this.flush(); | |
}; | |
// *** Receiving operations | |
// This is called when the server acknowledges an operation from the client. | |
Doc.prototype._opAcknowledged = function(message) { | |
if (this.inflightOp.create) { | |
this.version = message.v; | |
} else if (message.v !== this.version) { | |
// We should already be at the same version, because the server should | |
// have sent all the ops that have happened before acknowledging our op | |
console.warn('Invalid version from server. Expected: ' + this.version + ' Received: ' + message.v, message); | |
// Fetching should get us back to a working document state | |
return this.fetch(); | |
} | |
// The op was committed successfully. Increment the version number | |
this.version++; | |
this._clearInflightOp(); | |
}; | |
Doc.prototype._rollback = function(err) { | |
// The server has rejected submission of the current operation. Invert by | |
// just the inflight op if possible. If not possible to invert, cancel all | |
// pending ops and fetch the latest from the server to get us back into a | |
// working state, then call back | |
var op = this.inflightOp; | |
if (op.op && op.type.invert) { | |
op.op = op.type.invert(op.op); | |
// Transform the undo operation by any pending ops. | |
for (var i = 0; i < this.pendingOps.length; i++) { | |
var transformErr = transformX(this.pendingOps[i], op); | |
if (transformErr) return this._hardRollback(transformErr); | |
} | |
// ... and apply it locally, reverting the changes. | |
// | |
// This operation is applied to look like it comes from a remote source. | |
// I'm still not 100% sure about this functionality, because its really a | |
// local op. Basically, the problem is that if the client's op is rejected | |
// by the server, the editor window should update to reflect the undo. | |
this._otApply(op, false); | |
this._clearInflightOp(err); | |
return; | |
} | |
this._hardRollback(err); | |
}; | |
Doc.prototype._hardRollback = function(err) { | |
// Cancel all pending ops and reset if we can't invert | |
var op = this.inflightOp; | |
var pending = this.pendingOps; | |
this._setType(null); | |
this.version = null; | |
this.inflightOp = null; | |
this.pendingOps = []; | |
// Fetch the latest from the server to get us back into a working state | |
var doc = this; | |
this.fetch(function() { | |
var called = op && callEach(op.callbacks, err); | |
for (var i = 0; i < pending.length; i++) { | |
callEach(pending[i].callbacks, err); | |
} | |
if (err && !called) return doc.emit('error', err); | |
}); | |
}; | |
Doc.prototype._clearInflightOp = function(err) { | |
var called = callEach(this.inflightOp.callbacks, err); | |
this.inflightOp = null; | |
this.flush(); | |
this._emitNothingPending(); | |
if (err && !called) return this.emit('error', err); | |
}; | |
function callEach(callbacks, err) { | |
var called = false; | |
for (var i = 0; i < callbacks.length; i++) { | |
var callback = callbacks[i]; | |
if (callback) { | |
callback(err); | |
called = true; | |
} | |
} | |
return called; | |
} | |
// Queries are live requests to the database for particular sets of fields. | |
// | |
// The server actively tells the client when there's new data that matches | |
// a set of conditions. | |
var query = Query; | |
function Query(action, connection, id, collection, query, options, callback) { | |
emitter.EventEmitter.call(this); | |
// 'qf' or 'qs' | |
this.action = action; | |
this.connection = connection; | |
this.id = id; | |
this.collection = collection; | |
// The query itself. For mongo, this should look something like {"data.x":5} | |
this.query = query; | |
// A list of resulting documents. These are actual documents, complete with | |
// data and all the rest. It is possible to pass in an initial results set, | |
// so that a query can be serialized and then re-established | |
this.results = null; | |
if (options && options.results) { | |
this.results = options.results; | |
delete options.results; | |
} | |
this.extra = undefined; | |
// Options to pass through with the query | |
this.options = options; | |
this.callback = callback; | |
this.ready = false; | |
this.sent = false; | |
} | |
emitter.mixin(Query); | |
Query.prototype.hasPending = function() { | |
return !this.ready; | |
}; | |
// Helper for subscribe & fetch, since they share the same message format. | |
// | |
// This function actually issues the query. | |
Query.prototype.send = function() { | |
if (!this.connection.canSend) return; | |
var message = { | |
a: this.action, | |
id: this.id, | |
c: this.collection, | |
q: this.query | |
}; | |
if (this.options) { | |
message.o = this.options; | |
} | |
if (this.results) { | |
// Collect the version of all the documents in the current result set so we | |
// don't need to be sent their snapshots again. | |
var results = []; | |
for (var i = 0; i < this.results.length; i++) { | |
var doc = this.results[i]; | |
results.push([doc.id, doc.version]); | |
} | |
message.r = results; | |
} | |
this.connection.send(message); | |
this.sent = true; | |
}; | |
// Destroy the query object. Any subsequent messages for the query will be | |
// ignored by the connection. | |
Query.prototype.destroy = function(callback) { | |
if (this.connection.canSend && this.action === 'qs') { | |
this.connection.send({a: 'qu', id: this.id}); | |
} | |
this.connection._destroyQuery(this); | |
// There is a callback for consistency, but we don't actually wait for the | |
// server's unsubscribe message currently | |
if (callback) nextTick(callback); | |
}; | |
Query.prototype._onConnectionStateChanged = function() { | |
if (this.connection.canSend && !this.sent) { | |
this.send(); | |
} else { | |
this.sent = false; | |
} | |
}; | |
Query.prototype._handleFetch = function(err, data, extra) { | |
// Once a fetch query gets its data, it is destroyed. | |
this.connection._destroyQuery(this); | |
this._handleResponse(err, data, extra); | |
}; | |
Query.prototype._handleSubscribe = function(err, data, extra) { | |
this._handleResponse(err, data, extra); | |
}; | |
Query.prototype._handleResponse = function(err, data, extra) { | |
var callback = this.callback; | |
this.callback = null; | |
if (err) return this._finishResponse(err, callback); | |
if (!data) return this._finishResponse(null, callback); | |
var query = this; | |
var wait = 1; | |
var finish = function(err) { | |
if (err) return query._finishResponse(err, callback); | |
if (--wait) return; | |
query._finishResponse(null, callback); | |
}; | |
if (Array.isArray(data)) { | |
wait += data.length; | |
this.results = this._ingestSnapshots(data, finish); | |
this.extra = extra; | |
} else { | |
for (var id in data) { | |
wait++; | |
var snapshot = data[id]; | |
var doc = this.connection.get(snapshot.c || this.collection, id); | |
doc.ingestSnapshot(snapshot, finish); | |
} | |
} | |
finish(); | |
}; | |
Query.prototype._ingestSnapshots = function(snapshots, finish) { | |
var results = []; | |
for (var i = 0; i < snapshots.length; i++) { | |
var snapshot = snapshots[i]; | |
var doc = this.connection.get(snapshot.c || this.collection, snapshot.d); | |
doc.ingestSnapshot(snapshot, finish); | |
results.push(doc); | |
} | |
return results; | |
}; | |
Query.prototype._finishResponse = function(err, callback) { | |
this.emit('ready'); | |
this.ready = true; | |
if (err) { | |
this.connection._destroyQuery(this); | |
if (callback) return callback(err); | |
return this.emit('error', err); | |
} | |
if (callback) callback(null, this.results, this.extra); | |
}; | |
Query.prototype._handleError = function(err) { | |
this.emit('error', err); | |
}; | |
Query.prototype._handleDiff = function(diff) { | |
// We need to go through the list twice. First, we'll ingest all the new | |
// documents. After that we'll emit events and actually update our list. | |
// This avoids race conditions around setting documents to be subscribed & | |
// unsubscribing documents in event callbacks. | |
for (var i = 0; i < diff.length; i++) { | |
var d = diff[i]; | |
if (d.type === 'insert') d.values = this._ingestSnapshots(d.values); | |
} | |
for (var i = 0; i < diff.length; i++) { | |
var d = diff[i]; | |
switch (d.type) { | |
case 'insert': | |
var newDocs = d.values; | |
Array.prototype.splice.apply(this.results, [d.index, 0].concat(newDocs)); | |
this.emit('insert', newDocs, d.index); | |
break; | |
case 'remove': | |
var howMany = d.howMany || 1; | |
var removed = this.results.splice(d.index, howMany); | |
this.emit('remove', removed, d.index); | |
break; | |
case 'move': | |
var howMany = d.howMany || 1; | |
var docs = this.results.splice(d.from, howMany); | |
Array.prototype.splice.apply(this.results, [d.to, 0].concat(docs)); | |
this.emit('move', docs, d.from, d.to); | |
break; | |
} | |
} | |
this.emit('changed', this.results); | |
}; | |
Query.prototype._handleExtra = function(extra) { | |
this.extra = extra; | |
this.emit('extra', extra); | |
}; | |
var doNothing_1 = doNothing; | |
function doNothing() {} | |
var hasKeys = function(object) { | |
for (var key in object) return true; | |
return false; | |
}; | |
var util = { | |
doNothing: doNothing_1, | |
hasKeys: hasKeys | |
}; | |
/** | |
* Handles communication with the sharejs server and provides queries and | |
* documents. | |
* | |
* We create a connection with a socket object | |
* connection = new sharejs.Connection(sockset) | |
* The socket may be any object handling the websocket protocol. See the | |
* documentation of bindToSocket() for details. We then wait for the connection | |
* to connect | |
* connection.on('connected', ...) | |
* and are finally able to work with shared documents | |
* connection.get('food', 'steak') // Doc | |
* | |
* @param socket @see bindToSocket | |
*/ | |
var connection = Connection; | |
function Connection(socket) { | |
emitter.EventEmitter.call(this); | |
// Map of collection -> id -> doc object for created documents. | |
// (created documents MUST BE UNIQUE) | |
this.collections = {}; | |
// Each query is created with an id that the server uses when it sends us | |
// info about the query (updates, etc) | |
this.nextQueryId = 1; | |
// Map from query ID -> query object. | |
this.queries = {}; | |
// A unique message number for the given id | |
this.seq = 1; | |
// Equals agent.clientId on the server | |
this.id = null; | |
// This direct reference from connection to agent is not used internal to | |
// ShareDB, but it is handy for server-side only user code that may cache | |
// state on the agent and read it in middleware | |
this.agent = null; | |
this.debug = false; | |
this.bindToSocket(socket); | |
} | |
emitter.mixin(Connection); | |
/** | |
* Use socket to communicate with server | |
* | |
* Socket is an object that can handle the websocket protocol. This method | |
* installs the onopen, onclose, onmessage and onerror handlers on the socket to | |
* handle communication and sends messages by calling socket.send(message). The | |
* sockets `readyState` property is used to determine the initaial state. | |
* | |
* @param socket Handles the websocket protocol | |
* @param socket.readyState | |
* @param socket.close | |
* @param socket.send | |
* @param socket.onopen | |
* @param socket.onclose | |
* @param socket.onmessage | |
* @param socket.onerror | |
*/ | |
Connection.prototype.bindToSocket = function(socket) { | |
if (this.socket) { | |
this.socket.close(); | |
this.socket.onmessage = null; | |
this.socket.onopen = null; | |
this.socket.onerror = null; | |
this.socket.onclose = null; | |
} | |
this.socket = socket; | |
// State of the connection. The correspoding events are emmited when this changes | |
// | |
// - 'connecting' The connection is still being established, or we are still | |
// waiting on the server to send us the initialization message | |
// - 'connected' The connection is open and we have connected to a server | |
// and recieved the initialization message | |
// - 'disconnected' Connection is closed, but it will reconnect automatically | |
// - 'closed' The connection was closed by the client, and will not reconnect | |
// - 'stopped' The connection was closed by the server, and will not reconnect | |
this.state = (socket.readyState === 0 || socket.readyState === 1) ? 'connecting' : 'disconnected'; | |
// This is a helper variable the document uses to see whether we're | |
// currently in a 'live' state. It is true if and only if we're connected | |
this.canSend = false; | |
var connection = this; | |
socket.onmessage = function(event) { | |
try { | |
var data = (typeof event.data === 'string') ? | |
JSON.parse(event.data) : event.data; | |
} catch (err) { | |
console.warn('Failed to parse message', event); | |
return; | |
} | |
if (connection.debug) console.log('RECV', JSON.stringify(data)); | |
var request = {data: data}; | |
connection.emit('receive', request); | |
if (!request.data) return; | |
try { | |
connection.handleMessage(request.data); | |
} catch (err) { | |
nextTick(function() { | |
connection.emit('error', err); | |
}); | |
} | |
}; | |
socket.onopen = function() { | |
connection._setState('connecting'); | |
}; | |
socket.onerror = function(err) { | |
// This isn't the same as a regular error, because it will happen normally | |
// from time to time. Your connection should probably automatically | |
// reconnect anyway, but that should be triggered off onclose not onerror. | |
// (onclose happens when onerror gets called anyway). | |
connection.emit('connection error', err); | |
}; | |
socket.onclose = function(reason) { | |
// node-browserchannel reason values: | |
// 'Closed' - The socket was manually closed by calling socket.close() | |
// 'Stopped by server' - The server sent the stop message to tell the client not to try connecting | |
// 'Request failed' - Server didn't respond to request (temporary, usually offline) | |
// 'Unknown session ID' - Server session for client is missing (temporary, will immediately reestablish) | |
if (reason === 'closed' || reason === 'Closed') { | |
connection._setState('closed', reason); | |
} else if (reason === 'stopped' || reason === 'Stopped by server') { | |
connection._setState('stopped', reason); | |
} else { | |
connection._setState('disconnected', reason); | |
} | |
}; | |
}; | |
/** | |
* @param {object} message | |
* @param {String} message.a action | |
*/ | |
Connection.prototype.handleMessage = function(message) { | |
var err = null; | |
if (message.error) { | |
// wrap in Error object so can be passed through event emitters | |
err = new Error(message.error.message); | |
err.code = message.error.code; | |
// Add the message data to the error object for more context | |
err.data = message; | |
delete message.error; | |
} | |
// Switch on the message action. Most messages are for documents and are | |
// handled in the doc class. | |
switch (message.a) { | |
case 'init': | |
// Client initialization packet | |
if (message.protocol !== 1) { | |
err = new error(4019, 'Invalid protocol version'); | |
return this.emit('error', err); | |
} | |
if (types.map[message.type] !== types.defaultType) { | |
err = new error(4020, 'Invalid default type'); | |
return this.emit('error', err); | |
} | |
if (typeof message.id !== 'string') { | |
err = new error(4021, 'Invalid client id'); | |
return this.emit('error', err); | |
} | |
this.id = message.id; | |
this._setState('connected'); | |
return; | |
case 'qf': | |
var query$$1 = this.queries[message.id]; | |
if (query$$1) query$$1._handleFetch(err, message.data, message.extra); | |
return; | |
case 'qs': | |
var query$$1 = this.queries[message.id]; | |
if (query$$1) query$$1._handleSubscribe(err, message.data, message.extra); | |
return; | |
case 'qu': | |
// Queries are removed immediately on calls to destroy, so we ignore | |
// replies to query unsubscribes. Perhaps there should be a callback for | |
// destroy, but this is currently unimplemented | |
return; | |
case 'q': | |
// Query message. Pass this to the appropriate query object. | |
var query$$1 = this.queries[message.id]; | |
if (!query$$1) return; | |
if (err) return query$$1._handleError(err); | |
if (message.diff) query$$1._handleDiff(message.diff); | |
if (message.hasOwnProperty('extra')) query$$1._handleExtra(message.extra); | |
return; | |
case 'bf': | |
return this._handleBulkMessage(message, '_handleFetch'); | |
case 'bs': | |
return this._handleBulkMessage(message, '_handleSubscribe'); | |
case 'bu': | |
return this._handleBulkMessage(message, '_handleUnsubscribe'); | |
case 'f': | |
var doc$$1 = this.getExisting(message.c, message.d); | |
if (doc$$1) doc$$1._handleFetch(err, message.data); | |
return; | |
case 's': | |
var doc$$1 = this.getExisting(message.c, message.d); | |
if (doc$$1) doc$$1._handleSubscribe(err, message.data); | |
return; | |
case 'u': | |
var doc$$1 = this.getExisting(message.c, message.d); | |
if (doc$$1) doc$$1._handleUnsubscribe(err); | |
return; | |
case 'op': | |
var doc$$1 = this.getExisting(message.c, message.d); | |
if (doc$$1) doc$$1._handleOp(err, message); | |
return; | |
default: | |
console.warn('Ignoring unrecognized message', message); | |
} | |
}; | |
Connection.prototype._handleBulkMessage = function(message, method) { | |
if (message.data) { | |
for (var id in message.data) { | |
var doc$$1 = this.getExisting(message.c, id); | |
if (doc$$1) doc$$1[method](message.error, message.data[id]); | |
} | |
} else if (Array.isArray(message.b)) { | |
for (var i = 0; i < message.b.length; i++) { | |
var id = message.b[i]; | |
var doc$$1 = this.getExisting(message.c, id); | |
if (doc$$1) doc$$1[method](message.error); | |
} | |
} else if (message.b) { | |
for (var id in message.b) { | |
var doc$$1 = this.getExisting(message.c, id); | |
if (doc$$1) doc$$1[method](message.error); | |
} | |
} else { | |
console.error('Invalid bulk message', message); | |
} | |
}; | |
Connection.prototype._reset = function() { | |
this.seq = 1; | |
this.id = null; | |
this.agent = null; | |
}; | |
// Set the connection's state. The connection is basically a state machine. | |
Connection.prototype._setState = function(newState, reason) { | |
if (this.state === newState) return; | |
// I made a state diagram. The only invalid transitions are getting to | |
// 'connecting' from anywhere other than 'disconnected' and getting to | |
// 'connected' from anywhere other than 'connecting'. | |
if ( | |
(newState === 'connecting' && this.state !== 'disconnected' && this.state !== 'stopped' && this.state !== 'closed') || | |
(newState === 'connected' && this.state !== 'connecting') | |
) { | |
var err = new error(5007, 'Cannot transition directly from ' + this.state + ' to ' + newState); | |
return this.emit('error', err); | |
} | |
this.state = newState; | |
this.canSend = (newState === 'connected'); | |
if (newState === 'disconnected' || newState === 'stopped' || newState === 'closed') this._reset(); | |
// Group subscribes together to help server make more efficient calls | |
this.startBulk(); | |
// Emit the event to all queries | |
for (var id in this.queries) { | |
var query$$1 = this.queries[id]; | |
query$$1._onConnectionStateChanged(); | |
} | |
// Emit the event to all documents | |
for (var collection in this.collections) { | |
var docs = this.collections[collection]; | |
for (var id in docs) { | |
docs[id]._onConnectionStateChanged(); | |
} | |
} | |
this.endBulk(); | |
this.emit(newState, reason); | |
this.emit('state', newState, reason); | |
}; | |
Connection.prototype.startBulk = function() { | |
if (!this.bulk) this.bulk = {}; | |
}; | |
Connection.prototype.endBulk = function() { | |
if (this.bulk) { | |
for (var collection in this.bulk) { | |
var actions = this.bulk[collection]; | |
this._sendBulk('f', collection, actions.f); | |
this._sendBulk('s', collection, actions.s); | |
this._sendBulk('u', collection, actions.u); | |
} | |
} | |
this.bulk = null; | |
}; | |
Connection.prototype._sendBulk = function(action, collection, values) { | |
if (!values) return; | |
var ids = []; | |
var versions$$1 = {}; | |
var versionsCount = 0; | |
var versionId; | |
for (var id in values) { | |
var value = values[id]; | |
if (value == null) { | |
ids.push(id); | |
} else { | |
versions$$1[id] = value; | |
versionId = id; | |
versionsCount++; | |
} | |
} | |
if (ids.length === 1) { | |
var id = ids[0]; | |
this.send({a: action, c: collection, d: id}); | |
} else if (ids.length) { | |
this.send({a: 'b' + action, c: collection, b: ids}); | |
} | |
if (versionsCount === 1) { | |
var version$$1 = versions$$1[versionId]; | |
this.send({a: action, c: collection, d: versionId, v: version$$1}); | |
} else if (versionsCount) { | |
this.send({a: 'b' + action, c: collection, b: versions$$1}); | |
} | |
}; | |
Connection.prototype._sendAction = function(action, doc$$1, version$$1) { | |
// Ensure the doc is registered so that it receives the reply message | |
this._addDoc(doc$$1); | |
if (this.bulk) { | |
// Bulk subscribe | |
var actions = this.bulk[doc$$1.collection] || (this.bulk[doc$$1.collection] = {}); | |
var versions$$1 = actions[action] || (actions[action] = {}); | |
var isDuplicate = versions$$1.hasOwnProperty(doc$$1.id); | |
versions$$1[doc$$1.id] = version$$1; | |
return isDuplicate; | |
} else { | |
// Send single doc subscribe message | |
var message = {a: action, c: doc$$1.collection, d: doc$$1.id, v: version$$1}; | |
this.send(message); | |
} | |
}; | |
Connection.prototype.sendFetch = function(doc$$1) { | |
return this._sendAction('f', doc$$1, doc$$1.version); | |
}; | |
Connection.prototype.sendSubscribe = function(doc$$1) { | |
return this._sendAction('s', doc$$1, doc$$1.version); | |
}; | |
Connection.prototype.sendUnsubscribe = function(doc$$1) { | |
return this._sendAction('u', doc$$1); | |
}; | |
Connection.prototype.sendOp = function(doc$$1, op) { | |
// Ensure the doc is registered so that it receives the reply message | |
this._addDoc(doc$$1); | |
var message = { | |
a: 'op', | |
c: doc$$1.collection, | |
d: doc$$1.id, | |
v: doc$$1.version, | |
src: op.src, | |
seq: op.seq | |
}; | |
if (op.op) message.op = op.op; | |
if (op.create) message.create = op.create; | |
if (op.del) message.del = op.del; | |
this.send(message); | |
}; | |
/** | |
* Sends a message down the socket | |
*/ | |
Connection.prototype.send = function(message) { | |
if (this.debug) console.log('SEND', JSON.stringify(message)); | |
this.emit('send', message); | |
this.socket.send(JSON.stringify(message)); | |
}; | |
/** | |
* Closes the socket and emits 'closed' | |
*/ | |
Connection.prototype.close = function() { | |
this.socket.close(); | |
}; | |
Connection.prototype.getExisting = function(collection, id) { | |
if (this.collections[collection]) return this.collections[collection][id]; | |
}; | |
/** | |
* Get or create a document. | |
* | |
* @param collection | |
* @param id | |
* @return {Doc} | |
*/ | |
Connection.prototype.get = function(collection, id) { | |
var docs = this.collections[collection] || | |
(this.collections[collection] = {}); | |
var doc$$1 = docs[id]; | |
if (!doc$$1) { | |
doc$$1 = docs[id] = new doc(this, collection, id); | |
this.emit('doc', doc$$1); | |
} | |
return doc$$1; | |
}; | |
/** | |
* Remove document from this.collections | |
* | |
* @private | |
*/ | |
Connection.prototype._destroyDoc = function(doc$$1) { | |
var docs = this.collections[doc$$1.collection]; | |
if (!docs) return; | |
delete docs[doc$$1.id]; | |
// Delete the collection container if its empty. This could be a source of | |
// memory leaks if you slowly make a billion collections, which you probably | |
// won't do anyway, but whatever. | |
if (!util.hasKeys(docs)) { | |
delete this.collections[doc$$1.collection]; | |
} | |
}; | |
Connection.prototype._addDoc = function(doc$$1) { | |
var docs = this.collections[doc$$1.collection]; | |
if (!docs) { | |
docs = this.collections[doc$$1.collection] = {}; | |
} | |
if (docs[doc$$1.id] !== doc$$1) { | |
docs[doc$$1.id] = doc$$1; | |
} | |
}; | |
// Helper for createFetchQuery and createSubscribeQuery, below. | |
Connection.prototype._createQuery = function(action, collection, q, options, callback) { | |
var id = this.nextQueryId++; | |
var query$$1 = new query(action, this, id, collection, q, options, callback); | |
this.queries[id] = query$$1; | |
query$$1.send(); | |
return query$$1; | |
}; | |
// Internal function. Use query.destroy() to remove queries. | |
Connection.prototype._destroyQuery = function(query$$1) { | |
delete this.queries[query$$1.id]; | |
}; | |
// The query options object can contain the following fields: | |
// | |
// db: Name of the db for the query. You can attach extraDbs to ShareDB and | |
// pick which one the query should hit using this parameter. | |
// Create a fetch query. Fetch queries are only issued once, returning the | |
// results directly into the callback. | |
// | |
// The callback should have the signature function(error, results, extra) | |
// where results is a list of Doc objects. | |
Connection.prototype.createFetchQuery = function(collection, q, options, callback) { | |
return this._createQuery('qf', collection, q, options, callback); | |
}; | |
// Create a subscribe query. Subscribe queries return with the initial data | |
// through the callback, then update themselves whenever the query result set | |
// changes via their own event emitter. | |
// | |
// If present, the callback should have the signature function(error, results, extra) | |
// where results is a list of Doc objects. | |
Connection.prototype.createSubscribeQuery = function(collection, q, options, callback) { | |
return this._createQuery('qs', collection, q, options, callback); | |
}; | |
Connection.prototype.hasPending = function() { | |
return !!( | |
this._firstDoc(hasPending) || | |
this._firstQuery(hasPending) | |
); | |
}; | |
function hasPending(object) { | |
return object.hasPending(); | |
} | |
Connection.prototype.hasWritePending = function() { | |
return !!this._firstDoc(hasWritePending); | |
}; | |
function hasWritePending(object) { | |
return object.hasWritePending(); | |
} | |
Connection.prototype.whenNothingPending = function(callback) { | |
var doc$$1 = this._firstDoc(hasPending); | |
if (doc$$1) { | |
// If a document is found with a pending operation, wait for it to emit | |
// that nothing is pending anymore, and then recheck all documents again. | |
// We have to recheck all documents, just in case another mutation has | |
// been made in the meantime as a result of an event callback | |
doc$$1.once('nothing pending', this._nothingPendingRetry(callback)); | |
return; | |
} | |
var query$$1 = this._firstQuery(hasPending); | |
if (query$$1) { | |
query$$1.once('ready', this._nothingPendingRetry(callback)); | |
return; | |
} | |
// Call back when no pending operations | |
nextTick(callback); | |
}; | |
Connection.prototype._nothingPendingRetry = function(callback) { | |
var connection = this; | |
return function() { | |
nextTick(function() { | |
connection.whenNothingPending(callback); | |
}); | |
}; | |
}; | |
Connection.prototype._firstDoc = function(fn) { | |
for (var collection in this.collections) { | |
var docs = this.collections[collection]; | |
for (var id in docs) { | |
var doc$$1 = docs[id]; | |
if (fn(doc$$1)) { | |
return doc$$1; | |
} | |
} | |
} | |
}; | |
Connection.prototype._firstQuery = function(fn) { | |
for (var id in this.queries) { | |
var query$$1 = this.queries[id]; | |
if (fn(query$$1)) { | |
return query$$1; | |
} | |
} | |
}; | |
var Connection$1 = connection; | |
var Doc$1 = doc; | |
var Error$1 = error; | |
var Query$1 = query; | |
var types$1 = types; | |
var client = { | |
Connection: Connection$1, | |
Doc: Doc$1, | |
Error: Error$1, | |
Query: Query$1, | |
types: types$1 | |
}; | |
// This is a really simple OT type. Its not compiled with the web client, but it could be. | |
// | |
// Its mostly included for demonstration purposes and its used in the meta unit tests. | |
// | |
// This defines a really simple text OT type which only allows inserts. (No deletes). | |
// | |
// Ops look like: | |
// {position:#, text:"asdf"} | |
// | |
// Document snapshots look like: | |
// {str:string} | |
var simple = { | |
// The name of the OT type. The type itself is exposed to ottypes[type.name] and ottypes[type.uri]. | |
// The name can be used instead of the actual type in all API methods in ShareJS. | |
name: 'simple', | |
// Canonical name. | |
uri: 'http://sharejs.org/types/simple', | |
// Create a new document snapshot. Initial data can be passed in. | |
create: function(initial) { | |
if (initial == null) | |
initial = ''; | |
return {str: initial}; | |
}, | |
// Apply the given op to the document snapshot. Returns the new snapshot. | |
apply: function(snapshot, op) { | |
if (op.position < 0 || op.position > snapshot.str.length) | |
throw new Error('Invalid position'); | |
var str = snapshot.str; | |
str = str.slice(0, op.position) + op.text + str.slice(op.position); | |
return {str: str}; | |
}, | |
// Transform op1 by op2. Returns transformed version of op1. | |
// Sym describes the symmetry of the operation. Its either 'left' or 'right' | |
// depending on whether the op being transformed comes from the client or the | |
// server. | |
transform: function(op1, op2, sym) { | |
var pos = op1.position; | |
if (op2.position < pos || (op2.position === pos && sym === 'left')) { | |
pos += op2.text.length; | |
} | |
return {position: pos, text: op1.text}; | |
} | |
}; | |
var text = createCommonjsModule(function (module, exports) { | |
/* Text OT! | |
* | |
* This is an OT implementation for text. It is the standard implementation of | |
* text used by ShareJS. | |
* | |
* This type is composable but non-invertable. Its similar to ShareJS's old | |
* text-composable type, but its not invertable and its very similar to the | |
* text-tp2 implementation but it doesn't support tombstones or purging. | |
* | |
* Ops are lists of components which iterate over the document. | |
* Components are either: | |
* A number N: Skip N characters in the original document | |
* "str" : Insert "str" at the current position in the document | |
* {d:N} : Delete N characters at the current position in the document | |
* | |
* Eg: [3, 'hi', 5, {d:8}] | |
* | |
* The operation does not have to skip the last characters in the document. | |
* | |
* Snapshots are strings. | |
* | |
* Cursors are either a single number (which is the cursor position) or a pair of | |
* [anchor, focus] (aka [start, end]). Be aware that end can be before start. | |
*/ | |
/** @module text */ | |
exports.name = 'text'; | |
exports.uri = 'http://sharejs.org/types/textv1'; | |
/** Create a new text snapshot. | |
* | |
* @param {string} initial - initial snapshot data. Optional. Defaults to ''. | |
*/ | |
exports.create = function(initial) { | |
if ((initial != null) && typeof initial !== 'string') { | |
throw Error('Initial data must be a string'); | |
} | |
return initial || ''; | |
}; | |
var isArray = Array.isArray || function(obj) { | |
return Object.prototype.toString.call(obj) === "[object Array]"; | |
}; | |
/** Check the operation is valid. Throws if not valid. */ | |
var checkOp = function(op) { | |
if (!isArray(op)) throw Error('Op must be an array of components'); | |
var last = null; | |
for (var i = 0; i < op.length; i++) { | |
var c = op[i]; | |
switch (typeof c) { | |
case 'object': | |
// The only valid objects are {d:X} for +ive values of X. | |
if (!(typeof c.d === 'number' && c.d > 0)) throw Error('Object components must be deletes of size > 0'); | |
break; | |
case 'string': | |
// Strings are inserts. | |
if (!(c.length > 0)) throw Error('Inserts cannot be empty'); | |
break; | |
case 'number': | |
// Numbers must be skips. They have to be +ive numbers. | |
if (!(c > 0)) throw Error('Skip components must be >0'); | |
if (typeof last === 'number') throw Error('Adjacent skip components should be combined'); | |
break; | |
} | |
last = c; | |
} | |
if (typeof last === 'number') throw Error('Op has a trailing skip'); | |
}; | |
/** Make a function that appends to the given operation. */ | |
var makeAppend = function(op) { | |
return function(component) { | |
if (!component || component.d === 0) { | |
// The component is a no-op. Ignore! | |
} else if (op.length === 0) { | |
return op.push(component); | |
} else if (typeof component === typeof op[op.length - 1]) { | |
if (typeof component === 'object') { | |
return op[op.length - 1].d += component.d; | |
} else { | |
return op[op.length - 1] += component; | |
} | |
} else { | |
return op.push(component); | |
} | |
}; | |
}; | |
/** Makes and returns utility functions take and peek. */ | |
var makeTake = function(op) { | |
// The index of the next component to take | |
var idx = 0; | |
// The offset into the component | |
var offset = 0; | |
// Take up to length n from the front of op. If n is -1, take the entire next | |
// op component. If indivisableField == 'd', delete components won't be separated. | |
// If indivisableField == 'i', insert components won't be separated. | |
var take = function(n, indivisableField) { | |
// We're at the end of the operation. The op has skips, forever. Infinity | |
// might make more sense than null here. | |
if (idx === op.length) | |
return n === -1 ? null : n; | |
var part; | |
var c = op[idx]; | |
if (typeof c === 'number') { | |
// Skip | |
if (n === -1 || c - offset <= n) { | |
part = c - offset; | |
++idx; | |
offset = 0; | |
return part; | |
} else { | |
offset += n; | |
return n; | |
} | |
} else if (typeof c === 'string') { | |
// Insert | |
if (n === -1 || indivisableField === 'i' || c.length - offset <= n) { | |
part = c.slice(offset); | |
++idx; | |
offset = 0; | |
return part; | |
} else { | |
part = c.slice(offset, offset + n); | |
offset += n; | |
return part; | |
} | |
} else { | |
// Delete | |
if (n === -1 || indivisableField === 'd' || c.d - offset <= n) { | |
part = {d: c.d - offset}; | |
++idx; | |
offset = 0; | |
return part; | |
} else { | |
offset += n; | |
return {d: n}; | |
} | |
} | |
}; | |
// Peek at the next op that will be returned. | |
var peekType = function() { return op[idx]; }; | |
return [take, peekType]; | |
}; | |
/** Get the length of a component */ | |
var componentLength = function(c) { | |
// Uglify will compress this down into a ternary | |
if (typeof c === 'number') { | |
return c; | |
} else { | |
return c.length || c.d; | |
} | |
}; | |
/** Trim any excess skips from the end of an operation. | |
* | |
* There should only be at most one, because the operation was made with append. | |
*/ | |
var trim = function(op) { | |
if (op.length > 0 && typeof op[op.length - 1] === 'number') { | |
op.pop(); | |
} | |
return op; | |
}; | |
exports.normalize = function(op) { | |
var newOp = []; | |
var append = makeAppend(newOp); | |
for (var i = 0; i < op.length; i++) { | |
append(op[i]); | |
} | |
return trim(newOp); | |
}; | |
/** Apply an operation to a document snapshot */ | |
exports.apply = function(str, op) { | |
if (typeof str !== 'string') { | |
throw Error('Snapshot should be a string'); | |
} | |
checkOp(op); | |
// We'll gather the new document here and join at the end. | |
var newDoc = []; | |
for (var i = 0; i < op.length; i++) { | |
var component = op[i]; | |
switch (typeof component) { | |
case 'number': | |
if (component > str.length) throw Error('The op is too long for this document'); | |
newDoc.push(str.slice(0, component)); | |
// This might be slow for big strings. Consider storing the offset in | |
// str instead of rewriting it each time. | |
str = str.slice(component); | |
break; | |
case 'string': | |
newDoc.push(component); | |
break; | |
case 'object': | |
str = str.slice(component.d); | |
break; | |
} | |
} | |
return newDoc.join('') + str; | |
}; | |
/** Transform op by otherOp. | |
* | |
* @param op - The operation to transform | |
* @param otherOp - Operation to transform it by | |
* @param side - Either 'left' or 'right' | |
*/ | |
exports.transform = function(op, otherOp, side) { | |
if (side != 'left' && side != 'right') throw Error("side (" + side + ") must be 'left' or 'right'"); | |
checkOp(op); | |
checkOp(otherOp); | |
var newOp = []; | |
var append = makeAppend(newOp); | |
var _fns = makeTake(op); | |
var take = _fns[0], | |
peek = _fns[1]; | |
for (var i = 0; i < otherOp.length; i++) { | |
var component = otherOp[i]; | |
var length, chunk; | |
switch (typeof component) { | |
case 'number': // Skip | |
length = component; | |
while (length > 0) { | |
chunk = take(length, 'i'); | |
append(chunk); | |
if (typeof chunk !== 'string') { | |
length -= componentLength(chunk); | |
} | |
} | |
break; | |
case 'string': // Insert | |
if (side === 'left') { | |
// The left insert should go first. | |
if (typeof peek() === 'string') { | |
append(take(-1)); | |
} | |
} | |
// Otherwise skip the inserted text. | |
append(component.length); | |
break; | |
case 'object': // Delete | |
length = component.d; | |
while (length > 0) { | |
chunk = take(length, 'i'); | |
switch (typeof chunk) { | |
case 'number': | |
length -= chunk; | |
break; | |
case 'string': | |
append(chunk); | |
break; | |
case 'object': | |
// The delete is unnecessary now - the text has already been deleted. | |
length -= chunk.d; | |
} | |
} | |
break; | |
} | |
} | |
// Append any extra data in op1. | |
while ((component = take(-1))) | |
append(component); | |
return trim(newOp); | |
}; | |
/** Compose op1 and op2 together and return the result */ | |
exports.compose = function(op1, op2) { | |
checkOp(op1); | |
checkOp(op2); | |
var result = []; | |
var append = makeAppend(result); | |
var take = makeTake(op1)[0]; | |
for (var i = 0; i < op2.length; i++) { | |
var component = op2[i]; | |
var length, chunk; | |
switch (typeof component) { | |
case 'number': // Skip | |
length = component; | |
while (length > 0) { | |
chunk = take(length, 'd'); | |
append(chunk); | |
if (typeof chunk !== 'object') { | |
length -= componentLength(chunk); | |
} | |
} | |
break; | |
case 'string': // Insert | |
append(component); | |
break; | |
case 'object': // Delete | |
length = component.d; | |
while (length > 0) { | |
chunk = take(length, 'd'); | |
switch (typeof chunk) { | |
case 'number': | |
append({d: chunk}); | |
length -= chunk; | |
break; | |
case 'string': | |
length -= chunk.length; | |
break; | |
case 'object': | |
append(chunk); | |
} | |
} | |
break; | |
} | |
} | |
while ((component = take(-1))) | |
append(component); | |
return trim(result); | |
}; | |
var transformPosition = function(cursor, op) { | |
var pos = 0; | |
for (var i = 0; i < op.length; i++) { | |
var c = op[i]; | |
if (cursor <= pos) break; | |
// I could actually use the op_iter stuff above - but I think its simpler | |
// like this. | |
switch (typeof c) { | |
case 'number': | |
if (cursor <= pos + c) | |
return cursor; | |
pos += c; | |
break; | |
case 'string': | |
pos += c.length; | |
cursor += c.length; | |
break; | |
case 'object': | |
cursor -= Math.min(c.d, cursor - pos); | |
break; | |
} | |
} | |
return cursor; | |
}; | |
exports.transformSelection = function(selection, op, isOwnOp) { | |
var pos = 0; | |
if (isOwnOp) { | |
// Just track the position. We'll teleport the cursor to the end anyway. | |
// This works because text ops don't have any trailing skips at the end - so the last | |
// component is the last thing. | |
for (var i = 0; i < op.length; i++) { | |
var c = op[i]; | |
switch (typeof c) { | |
case 'number': | |
pos += c; | |
break; | |
case 'string': | |
pos += c.length; | |
break; | |
// Just eat deletes. | |
} | |
} | |
return pos; | |
} else { | |
return typeof selection === 'number' ? | |
transformPosition(selection, op) : [transformPosition(selection[0], op), transformPosition(selection[1], op)]; | |
} | |
}; | |
// DEPRECATED | |
exports.transformCursor = exports.transformSelection; | |
exports.selectionEq = function(c1, c2) { | |
if (c1[0] != null && c1[0] === c1[1]) c1 = c1[0]; | |
if (c2[0] != null && c2[0] === c2[1]) c2 = c2[0]; | |
return c1 === c2 || (c1[0] != null && c2[0] != null && c1[0] === c2[0] && c1[1] == c2[1]); | |
}; | |
}); | |
var text_1 = text.name; | |
var text_2 = text.uri; | |
var text_3 = text.create; | |
var text_4 = text.normalize; | |
var text_5 = text.apply; | |
var text_6 = text.transform; | |
var text_7 = text.compose; | |
var text_8 = text.transformSelection; | |
var text_9 = text.transformCursor; | |
var text_10 = text.selectionEq; | |
var textTp2 = createCommonjsModule(function (module) { | |
// A TP2 implementation of text, following this spec: | |
// http://code.google.com/p/lightwave/source/browse/trunk/experimental/ot/README | |
// | |
// A document is made up of a string and a set of tombstones inserted throughout | |
// the string. For example, 'some ', (2 tombstones), 'string'. | |
// | |
// This is encoded in a document as ['some ', (2 tombstones), 'string'] | |
// (It should be encoded as {s:'some string', t:[5, -2, 6]} because thats | |
// faster in JS, but its not.) | |
// | |
// Ops are lists of components which iterate over the whole document. (I might | |
// change this at some point, but a version thats less strict is backwards | |
// compatible.) | |
// | |
// Components are either: | |
// N: Skip N characters in the original document | |
// {i:'str'}: Insert 'str' at the current position in the document | |
// {i:N}: Insert N tombstones at the current position in the document | |
// {d:N}: Delete (tombstone) N characters at the current position in the document | |
// | |
// Eg: [3, {i:'hi'}, 5, {d:8}] | |
// | |
// Snapshots are lists with characters and tombstones. Characters are stored in strings | |
// and adjacent tombstones are flattened into numbers. | |
// | |
// Eg, the document: 'Hello .....world' ('.' denotes tombstoned (deleted) characters) | |
// would be represented by a document snapshot of ['Hello ', 5, 'world'] | |
//var append, appendDoc, componentLength, makeTake, takeDoc, transformer; | |
var type = module.exports = { | |
name: 'text-tp2', | |
tp2: true, | |
uri: 'http://sharejs.org/types/text-tp2v1', | |
create: function(initial) { | |
if (initial == null) { | |
initial = ''; | |
} else { | |
if (typeof initial != 'string') throw new Error('Initial data must be a string'); | |
} | |
return { | |
charLength: initial.length, | |
totalLength: initial.length, | |
data: initial.length ? [initial] : [] | |
}; | |
}, | |
serialize: function(doc) { | |
if (!doc.data) { | |
throw new Error('invalid doc snapshot'); | |
} | |
return doc.data; | |
}, | |
deserialize: function(data) { | |
var doc = type.create(); | |
doc.data = data; | |
for (var i = 0; i < data.length; i++) { | |
var component = data[i]; | |
if (typeof component === 'string') { | |
doc.charLength += component.length; | |
doc.totalLength += component.length; | |
} else { | |
doc.totalLength += component; | |
} | |
} | |
return doc; | |
} | |
}; | |
var isArray = Array.isArray || function(obj) { | |
return Object.prototype.toString.call(obj) == '[object Array]'; | |
}; | |
var checkOp = function(op) { | |
if (!isArray(op)) throw new Error('Op must be an array of components'); | |
var last = null; | |
for (var i = 0; i < op.length; i++) { | |
var c = op[i]; | |
if (typeof c == 'object') { | |
// The component is an insert or a delete. | |
if (c.i !== undefined) { // Insert. | |
if (!((typeof c.i === 'string' && c.i.length > 0) // String inserts | |
|| (typeof c.i === 'number' && c.i > 0))) // Tombstone inserts | |
throw new Error('Inserts must insert a string or a +ive number'); | |
} else if (c.d !== undefined) { // Delete | |
if (!(typeof c.d === 'number' && c.d > 0)) | |
throw new Error('Deletes must be a +ive number'); | |
} else throw new Error('Operation component must define .i or .d'); | |
} else { | |
// The component must be a skip. | |
if (typeof c != 'number') throw new Error('Op components must be objects or numbers'); | |
if (c <= 0) throw new Error('Skip components must be a positive number'); | |
if (typeof last === 'number') throw new Error('Adjacent skip components should be combined'); | |
} | |
last = c; | |
} | |
}; | |
// Take the next part from the specified position in a document snapshot. | |
// position = {index, offset}. It will be updated. | |
var takeDoc = type._takeDoc = function(doc, position, maxlength, tombsIndivisible) { | |
if (position.index >= doc.data.length) | |
throw new Error('Operation goes past the end of the document'); | |
var part = doc.data[position.index]; | |
// This can be written as an ugly-arsed giant ternary statement, but its much | |
// more readable like this. Uglify will convert it into said ternary anyway. | |
var result; | |
if (typeof part == 'string') { | |
if (maxlength != null) { | |
result = part.slice(position.offset, position.offset + maxlength); | |
} else { | |
result = part.slice(position.offset); | |
} | |
} else { | |
if (maxlength == null || tombsIndivisible) { | |
result = part - position.offset; | |
} else { | |
result = Math.min(maxlength, part - position.offset); | |
} | |
} | |
var resultLen = result.length || result; | |
if ((part.length || part) - position.offset > resultLen) { | |
position.offset += resultLen; | |
} else { | |
position.index++; | |
position.offset = 0; | |
} | |
return result; | |
}; | |
// Append a part to the end of a document | |
var appendDoc = type._appendDoc = function(doc, p) { | |
if (p === 0 || p === '') return; | |
if (typeof p === 'string') { | |
doc.charLength += p.length; | |
doc.totalLength += p.length; | |
} else { | |
doc.totalLength += p; | |
} | |
var data = doc.data; | |
if (data.length === 0) { | |
data.push(p); | |
} else if (typeof data[data.length - 1] === typeof p) { | |
data[data.length - 1] += p; | |
} else { | |
data.push(p); | |
} | |
}; | |
// Apply the op to the document. The document is not modified in the process. | |
type.apply = function(doc, op) { | |
if (doc.totalLength == null || doc.charLength == null || !isArray(doc.data)) { | |
throw new Error('Snapshot is invalid'); | |
} | |
checkOp(op); | |
var newDoc = type.create(); | |
var position = {index: 0, offset: 0}; | |
for (var i = 0; i < op.length; i++) { | |
var component = op[i]; | |
var remainder, part; | |
if (typeof component == 'number') { // Skip | |
remainder = component; | |
while (remainder > 0) { | |
part = takeDoc(doc, position, remainder); | |
appendDoc(newDoc, part); | |
remainder -= part.length || part; | |
} | |
} else if (component.i !== undefined) { // Insert | |
appendDoc(newDoc, component.i); | |
} else if (component.d !== undefined) { // Delete | |
remainder = component.d; | |
while (remainder > 0) { | |
part = takeDoc(doc, position, remainder); | |
remainder -= part.length || part; | |
} | |
appendDoc(newDoc, component.d); | |
} | |
} | |
return newDoc; | |
}; | |
// Append an op component to the end of the specified op. Exported for the | |
// randomOpGenerator. | |
var append = type._append = function(op, component) { | |
var last; | |
if (component === 0 || component.i === '' || component.i === 0 || component.d === 0) { | |
// Drop the new component. | |
} else if (op.length === 0) { | |
op.push(component); | |
} else { | |
last = op[op.length - 1]; | |
if (typeof component == 'number' && typeof last == 'number') { | |
op[op.length - 1] += component; | |
} else if (component.i != null && (last.i != null) && typeof last.i === typeof component.i) { | |
last.i += component.i; | |
} else if (component.d != null && (last.d != null)) { | |
last.d += component.d; | |
} else { | |
op.push(component); | |
} | |
} | |
}; | |
var take = function(op, cursor, maxlength, insertsIndivisible) { | |
if (cursor.index === op.length) return null; | |
var e = op[cursor.index]; | |
var current; | |
var result; | |
var offset = cursor.offset; | |
// if the current element is a skip, an insert of a number or a delete | |
if (typeof (current = e) == 'number' || typeof (current = e.i) == 'number' || (current = e.d) != null) { | |
var c; | |
if ((maxlength == null) || current - offset <= maxlength || (insertsIndivisible && e.i != null)) { | |
// Return the rest of the current element. | |
c = current - offset; | |
++cursor.index; | |
cursor.offset = 0; | |
} else { | |
cursor.offset += maxlength; | |
c = maxlength; | |
} | |
// Package the component back up. | |
if (e.i != null) { | |
return {i: c}; | |
} else if (e.d != null) { | |
return {d: c}; | |
} else { | |
return c; | |
} | |
} else { // Insert of a string. | |
if ((maxlength == null) || e.i.length - offset <= maxlength || insertsIndivisible) { | |
result = {i: e.i.slice(offset)}; | |
++cursor.index; | |
cursor.offset = 0; | |
} else { | |
result = {i: e.i.slice(offset, offset + maxlength)}; | |
cursor.offset += maxlength; | |
} | |
return result; | |
} | |
}; | |
// Find and return the length of an op component | |
var componentLength = function(component) { | |
if (typeof component === 'number') { | |
return component; | |
} else if (typeof component.i === 'string') { | |
return component.i.length; | |
} else { | |
return component.d || component.i; | |
} | |
}; | |
// Normalize an op, removing all empty skips and empty inserts / deletes. | |
// Concatenate adjacent inserts and deletes. | |
type.normalize = function(op) { | |
var newOp = []; | |
for (var i = 0; i < op.length; i++) { | |
append(newOp, op[i]); | |
} | |
return newOp; | |
}; | |
// This is a helper method to transform and prune. goForwards is true for transform, false for prune. | |
var transformer = function(op, otherOp, goForwards, side) { | |
checkOp(op); | |
checkOp(otherOp); | |
var newOp = []; | |
// Cursor moving over op. Used by take | |
var cursor = {index:0, offset:0}; | |
for (var i = 0; i < otherOp.length; i++) { | |
var component = otherOp[i]; | |
var len = componentLength(component); | |
var chunk; | |
if (component.i != null) { // Insert text or tombs | |
if (goForwards) { // Transform - insert skips over deleted parts. | |
if (side === 'left') { | |
// The left side insert should go first. | |
var next; | |
while ((next = op[cursor.index]) && next.i != null) { | |
append(newOp, take(op, cursor)); | |
} | |
} | |
// In any case, skip the inserted text. | |
append(newOp, len); | |
} else { // Prune. Remove skips for inserts. | |
while (len > 0) { | |
chunk = take(op, cursor, len, true); | |
// The chunk will be null if we run out of components in the other op. | |
if (chunk === null) throw new Error('The transformed op is invalid'); | |
if (chunk.d != null) | |
throw new Error('The transformed op deletes locally inserted characters - it cannot be purged of the insert.'); | |
if (typeof chunk == 'number') | |
len -= chunk; | |
else | |
append(newOp, chunk); | |
} | |
} | |
} else { // Skips or deletes. | |
while (len > 0) { | |
chunk = take(op, cursor, len, true); | |
if (chunk === null) throw new Error('The op traverses more elements than the document has'); | |
append(newOp, chunk); | |
if (!chunk.i) len -= componentLength(chunk); | |
} | |
} | |
} | |
// Append extras from op1. | |
var component; | |
while ((component = take(op, cursor))) { | |
if (component.i === undefined) { | |
throw new Error("Remaining fragments in the op: " + component); | |
} | |
append(newOp, component); | |
} | |
return newOp; | |
}; | |
// transform op1 by op2. Return transformed version of op1. op1 and op2 are | |
// unchanged by transform. Side should be 'left' or 'right', depending on if | |
// op1.id <> op2.id. | |
// | |
// 'left' == client op for ShareJS. | |
type.transform = function(op, otherOp, side) { | |
if (side != 'left' && side != 'right') | |
throw new Error("side (" + side + ") should be 'left' or 'right'"); | |
return transformer(op, otherOp, true, side); | |
}; | |
type.prune = function(op, otherOp) { | |
return transformer(op, otherOp, false); | |
}; | |
type.compose = function(op1, op2) { | |
//var chunk, chunkLength, component, length, result, take, _, _i, _len, _ref; | |
if (op1 == null) return op2; | |
checkOp(op1); | |
checkOp(op2); | |
var result = []; | |
// Cursor over op1. | |
var cursor = {index:0, offset:0}; | |
var component; | |
for (var i = 0; i < op2.length; i++) { | |
component = op2[i]; | |
var len, chunk; | |
if (typeof component === 'number') { // Skip | |
// Just copy from op1. | |
len = component; | |
while (len > 0) { | |
chunk = take(op1, cursor, len); | |
if (chunk === null) | |
throw new Error('The op traverses more elements than the document has'); | |
append(result, chunk); | |
len -= componentLength(chunk); | |
} | |
} else if (component.i !== undefined) { // Insert | |
append(result, {i: component.i}); | |
} else { // Delete | |
len = component.d; | |
while (len > 0) { | |
chunk = take(op1, cursor, len); | |
if (chunk === null) | |
throw new Error('The op traverses more elements than the document has'); | |
var chunkLength = componentLength(chunk); | |
if (chunk.i !== undefined) | |
append(result, {i: chunkLength}); | |
else | |
append(result, {d: chunkLength}); | |
len -= chunkLength; | |
} | |
} | |
} | |
// Append extras from op1. | |
while ((component = take(op1, cursor))) { | |
if (component.i === undefined) { | |
throw new Error("Remaining fragments in op1: " + component); | |
} | |
append(result, component); | |
} | |
return result; | |
}; | |
}); | |
var textTp2_1 = textTp2.name; | |
var textTp2_2 = textTp2.tp2; | |
var textTp2_3 = textTp2.uri; | |
var textTp2_4 = textTp2.create; | |
var textTp2_5 = textTp2.serialize; | |
var textTp2_6 = textTp2.deserialize; | |
// These methods let you build a transform function from a transformComponent | |
// function for OT types like JSON0 in which operations are lists of components | |
// and transforming them requires N^2 work. I find it kind of nasty that I need | |
// this, but I'm not really sure what a better solution is. Maybe I should do | |
// this automatically to types that don't have a compose function defined. | |
// Add transform and transformX functions for an OT type which has | |
// transformComponent defined. transformComponent(destination array, | |
// component, other component, side) | |
var _bootstrapTransform = function(type, transformComponent, checkValidOp, append) { | |
var transformComponentX = function(left, right, destLeft, destRight) { | |
transformComponent(destLeft, left, right, 'left'); | |
transformComponent(destRight, right, left, 'right'); | |
}; | |
var transformX = type.transformX = function(leftOp, rightOp) { | |
checkValidOp(leftOp); | |
checkValidOp(rightOp); | |
var newRightOp = []; | |
for (var i = 0; i < rightOp.length; i++) { | |
var rightComponent = rightOp[i]; | |
// Generate newLeftOp by composing leftOp by rightComponent | |
var newLeftOp = []; | |
var k = 0; | |
while (k < leftOp.length) { | |
var nextC = []; | |
transformComponentX(leftOp[k], rightComponent, newLeftOp, nextC); | |
k++; | |
if (nextC.length === 1) { | |
rightComponent = nextC[0]; | |
} else if (nextC.length === 0) { | |
for (var j = k; j < leftOp.length; j++) { | |
append(newLeftOp, leftOp[j]); | |
} | |
rightComponent = null; | |
break; | |
} else { | |
// Recurse. | |
var pair = transformX(leftOp.slice(k), nextC); | |
for (var l = 0; l < pair[0].length; l++) { | |
append(newLeftOp, pair[0][l]); | |
} | |
for (var r = 0; r < pair[1].length; r++) { | |
append(newRightOp, pair[1][r]); | |
} | |
rightComponent = null; | |
break; | |
} | |
} | |
if (rightComponent != null) { | |
append(newRightOp, rightComponent); | |
} | |
leftOp = newLeftOp; | |
} | |
return [leftOp, newRightOp]; | |
}; | |
// Transforms op with specified type ('left' or 'right') by otherOp. | |
type.transform = type['transform'] = function(op, otherOp, type) { | |
if (!(type === 'left' || type === 'right')) | |
throw new Error("type must be 'left' or 'right'"); | |
if (otherOp.length === 0) return op; | |
if (op.length === 1 && otherOp.length === 1) | |
return transformComponent([], op[0], otherOp[0], type); | |
if (type === 'left') | |
return transformX(op, otherOp)[0]; | |
else | |
return transformX(otherOp, op)[1]; | |
}; | |
}; | |
var helpers = { | |
_bootstrapTransform: _bootstrapTransform | |
}; | |
var text0$1 = createCommonjsModule(function (module, exports) { | |
// DEPRECATED! | |
// | |
// This type works, but is not exported, and will be removed in a future version of this library. | |
// A simple text implementation | |
// | |
// Operations are lists of components. | |
// Each component either inserts or deletes at a specified position in the document. | |
// | |
// Components are either: | |
// {i:'str', p:100}: Insert 'str' at position 100 in the document | |
// {d:'str', p:100}: Delete 'str' at position 100 in the document | |
// | |
// Components in an operation are executed sequentially, so the position of components | |
// assumes previous components have already executed. | |
// | |
// Eg: This op: | |
// [{i:'abc', p:0}] | |
// is equivalent to this op: | |
// [{i:'a', p:0}, {i:'b', p:1}, {i:'c', p:2}] | |
// NOTE: The global scope here is shared with other sharejs files when built with closure. | |
// Be careful what ends up in your namespace. | |
var text = module.exports = { | |
name: 'text0', | |
uri: 'http://sharejs.org/types/textv0', | |
create: function(initial) { | |
if ((initial != null) && typeof initial !== 'string') { | |
throw new Error('Initial data must be a string'); | |
} | |
return initial || ''; | |
} | |
}; | |
/** Insert s2 into s1 at pos. */ | |
var strInject = function(s1, pos, s2) { | |
return s1.slice(0, pos) + s2 + s1.slice(pos); | |
}; | |
/** Check that an operation component is valid. Throws if its invalid. */ | |
var checkValidComponent = function(c) { | |
if (typeof c.p !== 'number') | |
throw new Error('component missing position field'); | |
if ((typeof c.i === 'string') === (typeof c.d === 'string')) | |
throw new Error('component needs an i or d field'); | |
if (c.p < 0) | |
throw new Error('position cannot be negative'); | |
}; | |
/** Check that an operation is valid */ | |
var checkValidOp = function(op) { | |
for (var i = 0; i < op.length; i++) { | |
checkValidComponent(op[i]); | |
} | |
}; | |
/** Apply op to snapshot */ | |
text.apply = function(snapshot, op) { | |
var deleted; | |
checkValidOp(op); | |
for (var i = 0; i < op.length; i++) { | |
var component = op[i]; | |
if (component.i != null) { | |
snapshot = strInject(snapshot, component.p, component.i); | |
} else { | |
deleted = snapshot.slice(component.p, component.p + component.d.length); | |
if (component.d !== deleted) | |
throw new Error("Delete component '" + component.d + "' does not match deleted text '" + deleted + "'"); | |
snapshot = snapshot.slice(0, component.p) + snapshot.slice(component.p + component.d.length); | |
} | |
} | |
return snapshot; | |
}; | |
/** | |
* Append a component to the end of newOp. Exported for use by the random op | |
* generator and the JSON0 type. | |
*/ | |
var append = text._append = function(newOp, c) { | |
if (c.i === '' || c.d === '') return; | |
if (newOp.length === 0) { | |
newOp.push(c); | |
} else { | |
var last = newOp[newOp.length - 1]; | |
if (last.i != null && c.i != null && last.p <= c.p && c.p <= last.p + last.i.length) { | |
// Compose the insert into the previous insert | |
newOp[newOp.length - 1] = {i:strInject(last.i, c.p - last.p, c.i), p:last.p}; | |
} else if (last.d != null && c.d != null && c.p <= last.p && last.p <= c.p + c.d.length) { | |
// Compose the deletes together | |
newOp[newOp.length - 1] = {d:strInject(c.d, last.p - c.p, last.d), p:c.p}; | |
} else { | |
newOp.push(c); | |
} | |
} | |
}; | |
/** Compose op1 and op2 together */ | |
text.compose = function(op1, op2) { | |
checkValidOp(op1); | |
checkValidOp(op2); | |
var newOp = op1.slice(); | |
for (var i = 0; i < op2.length; i++) { | |
append(newOp, op2[i]); | |
} | |
return newOp; | |
}; | |
/** Clean up an op */ | |
text.normalize = function(op) { | |
var newOp = []; | |
// Normalize should allow ops which are a single (unwrapped) component: | |
// {i:'asdf', p:23}. | |
// There's no good way to test if something is an array: | |
// http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/ | |
// so this is probably the least bad solution. | |
if (op.i != null || op.p != null) op = [op]; | |
for (var i = 0; i < op.length; i++) { | |
var c = op[i]; | |
if (c.p == null) c.p = 0; | |
append(newOp, c); | |
} | |
return newOp; | |
}; | |
// This helper method transforms a position by an op component. | |
// | |
// If c is an insert, insertAfter specifies whether the transform | |
// is pushed after the insert (true) or before it (false). | |
// | |
// insertAfter is optional for deletes. | |
var transformPosition = function(pos, c, insertAfter) { | |
// This will get collapsed into a giant ternary by uglify. | |
if (c.i != null) { | |
if (c.p < pos || (c.p === pos && insertAfter)) { | |
return pos + c.i.length; | |
} else { | |
return pos; | |
} | |
} else { | |
// I think this could also be written as: Math.min(c.p, Math.min(c.p - | |
// otherC.p, otherC.d.length)) but I think its harder to read that way, and | |
// it compiles using ternary operators anyway so its no slower written like | |
// this. | |
if (pos <= c.p) { | |
return pos; | |
} else if (pos <= c.p + c.d.length) { | |
return c.p; | |
} else { | |
return pos - c.d.length; | |
} | |
} | |
}; | |
// Helper method to transform a cursor position as a result of an op. | |
// | |
// Like transformPosition above, if c is an insert, insertAfter specifies | |
// whether the cursor position is pushed after an insert (true) or before it | |
// (false). | |
text.transformCursor = function(position, op, side) { | |
var insertAfter = side === 'right'; | |
for (var i = 0; i < op.length; i++) { | |
position = transformPosition(position, op[i], insertAfter); | |
} | |
return position; | |
}; | |
// Transform an op component by another op component. Asymmetric. | |
// The result will be appended to destination. | |
// | |
// exported for use in JSON type | |
var transformComponent = text._tc = function(dest, c, otherC, side) { | |
//var cIntersect, intersectEnd, intersectStart, newC, otherIntersect, s; | |
checkValidComponent(c); | |
checkValidComponent(otherC); | |
if (c.i != null) { | |
// Insert. | |
append(dest, {i:c.i, p:transformPosition(c.p, otherC, side === 'right')}); | |
} else { | |
// Delete | |
if (otherC.i != null) { | |
// Delete vs insert | |
var s = c.d; | |
if (c.p < otherC.p) { | |
append(dest, {d:s.slice(0, otherC.p - c.p), p:c.p}); | |
s = s.slice(otherC.p - c.p); | |
} | |
if (s !== '') | |
append(dest, {d: s, p: c.p + otherC.i.length}); | |
} else { | |
// Delete vs delete | |
if (c.p >= otherC.p + otherC.d.length) | |
append(dest, {d: c.d, p: c.p - otherC.d.length}); | |
else if (c.p + c.d.length <= otherC.p) | |
append(dest, c); | |
else { | |
// They overlap somewhere. | |
var newC = {d: '', p: c.p}; | |
if (c.p < otherC.p) | |
newC.d = c.d.slice(0, otherC.p - c.p); | |
if (c.p + c.d.length > otherC.p + otherC.d.length) | |
newC.d += c.d.slice(otherC.p + otherC.d.length - c.p); | |
// This is entirely optional - I'm just checking the deleted text in | |
// the two ops matches | |
var intersectStart = Math.max(c.p, otherC.p); | |
var intersectEnd = Math.min(c.p + c.d.length, otherC.p + otherC.d.length); | |
var cIntersect = c.d.slice(intersectStart - c.p, intersectEnd - c.p); | |
var otherIntersect = otherC.d.slice(intersectStart - otherC.p, intersectEnd - otherC.p); | |
if (cIntersect !== otherIntersect) | |
throw new Error('Delete ops delete different text in the same region of the document'); | |
if (newC.d !== '') { | |
newC.p = transformPosition(newC.p, otherC); | |
append(dest, newC); | |
} | |
} | |
} | |
} | |
return dest; | |
}; | |
var invertComponent = function(c) { | |
return (c.i != null) ? {d:c.i, p:c.p} : {i:c.d, p:c.p}; | |
}; | |
// No need to use append for invert, because the components won't be able to | |
// cancel one another. | |
text.invert = function(op) { | |
// Shallow copy & reverse that sucka. | |
op = op.slice().reverse(); | |
for (var i = 0; i < op.length; i++) { | |
op[i] = invertComponent(op[i]); | |
} | |
return op; | |
}; | |
if (exports._bootstrapTransform) { | |
exports._bootstrapTransform(text, transformComponent, checkValidOp, append); | |
} else { | |
helpers._bootstrapTransform(text, transformComponent, checkValidOp, append); | |
} | |
}); | |
var text0_1$1 = text0$1.name; | |
var text0_2$1 = text0$1.uri; | |
var text0_3$1 = text0$1.create; | |
var json0$1 = createCommonjsModule(function (module, exports) { | |
/* | |
This is the implementation of the JSON OT type. | |
Spec is here: https://github.com/josephg/ShareJS/wiki/JSON-Operations | |
Note: This is being made obsolete. It will soon be replaced by the JSON2 type. | |
*/ | |
/** | |
* UTILITY FUNCTIONS | |
*/ | |
/** | |
* Checks if the passed object is an Array instance. Can't use Array.isArray | |
* yet because its not supported on IE8. | |
* | |
* @param obj | |
* @returns {boolean} | |
*/ | |
var isArray = function(obj) { | |
return Object.prototype.toString.call(obj) == '[object Array]'; | |
}; | |
// Checks if the passed object is a javascript object. | |
var isObject = function(obj) { | |
return (!!obj) && (obj.constructor === Object); | |
}; | |
/** | |
* Clones the passed object using JSON serialization (which is slow). | |
* | |
* hax, copied from test/types/json. Apparently this is still the fastest way | |
* to deep clone an object, assuming we have browser support for JSON. @see | |
* http://jsperf.com/cloning-an-object/12 | |
*/ | |
var clone = function(o) { | |
return JSON.parse(JSON.stringify(o)); | |
}; | |
/** | |
* JSON OT Type | |
* @type {*} | |
*/ | |
var json = { | |
name: 'json0', | |
uri: 'http://sharejs.org/types/JSONv0' | |
}; | |
// You can register another OT type as a subtype in a JSON document using | |
// the following function. This allows another type to handle certain | |
// operations instead of the builtin JSON type. | |
var subtypes = {}; | |
json.registerSubtype = function(subtype) { | |
subtypes[subtype.name] = subtype; | |
}; | |
json.create = function(data) { | |
// Null instead of undefined if you don't pass an argument. | |
return data === undefined ? null : clone(data); | |
}; | |
json.invertComponent = function(c) { | |
var c_ = {p: c.p}; | |
// handle subtype ops | |
if (c.t && subtypes[c.t]) { | |
c_.t = c.t; | |
c_.o = subtypes[c.t].invert(c.o); | |
} | |
if (c.si !== void 0) c_.sd = c.si; | |
if (c.sd !== void 0) c_.si = c.sd; | |
if (c.oi !== void 0) c_.od = c.oi; | |
if (c.od !== void 0) c_.oi = c.od; | |
if (c.li !== void 0) c_.ld = c.li; | |
if (c.ld !== void 0) c_.li = c.ld; | |
if (c.na !== void 0) c_.na = -c.na; | |
if (c.lm !== void 0) { | |
c_.lm = c.p[c.p.length-1]; | |
c_.p = c.p.slice(0,c.p.length-1).concat([c.lm]); | |
} | |
return c_; | |
}; | |
json.invert = function(op) { | |
var op_ = op.slice().reverse(); | |
var iop = []; | |
for (var i = 0; i < op_.length; i++) { | |
iop.push(json.invertComponent(op_[i])); | |
} | |
return iop; | |
}; | |
json.checkValidOp = function(op) { | |
for (var i = 0; i < op.length; i++) { | |
if (!isArray(op[i].p)) throw new Error('Missing path'); | |
} | |
}; | |
json.checkList = function(elem) { | |
if (!isArray(elem)) | |
throw new Error('Referenced element not a list'); | |
}; | |
json.checkObj = function(elem) { | |
if (!isObject(elem)) { | |
throw new Error("Referenced element not an object (it was " + JSON.stringify(elem) + ")"); | |
} | |
}; | |
// helper functions to convert old string ops to and from subtype ops | |
function convertFromText(c) { | |
c.t = 'text0'; | |
var o = {p: c.p.pop()}; | |
if (c.si != null) o.i = c.si; | |
if (c.sd != null) o.d = c.sd; | |
c.o = [o]; | |
} | |
function convertToText(c) { | |
c.p.push(c.o[0].p); | |
if (c.o[0].i != null) c.si = c.o[0].i; | |
if (c.o[0].d != null) c.sd = c.o[0].d; | |
delete c.t; | |
delete c.o; | |
} | |
json.apply = function(snapshot, op) { | |
json.checkValidOp(op); | |
op = clone(op); | |
var container = { | |
data: snapshot | |
}; | |
for (var i = 0; i < op.length; i++) { | |
var c = op[i]; | |
// convert old string ops to use subtype for backwards compatibility | |
if (c.si != null || c.sd != null) | |
convertFromText(c); | |
var parent = null; | |
var elem = container; | |
var key = 'data'; | |
for (var j = 0; j < c.p.length; j++) { | |
var p = c.p[j]; | |
parent = elem; | |
elem = elem[key]; | |
key = p; | |
if (parent == null) | |
throw new Error('Path invalid'); | |
} | |
// handle subtype ops | |
if (c.t && c.o !== void 0 && subtypes[c.t]) { | |
elem[key] = subtypes[c.t].apply(elem[key], c.o); | |
// Number add | |
} else if (c.na !== void 0) { | |
if (typeof elem[key] != 'number') | |
throw new Error('Referenced element not a number'); | |
elem[key] += c.na; | |
} | |
// List replace | |
else if (c.li !== void 0 && c.ld !== void 0) { | |
json.checkList(elem); | |
// Should check the list element matches c.ld | |
elem[key] = c.li; | |
} | |
// List insert | |
else if (c.li !== void 0) { | |
json.checkList(elem); | |
elem.splice(key,0, c.li); | |
} | |
// List delete | |
else if (c.ld !== void 0) { | |
json.checkList(elem); | |
// Should check the list element matches c.ld here too. | |
elem.splice(key,1); | |
} | |
// List move | |
else if (c.lm !== void 0) { | |
json.checkList(elem); | |
if (c.lm != key) { | |
var e = elem[key]; | |
// Remove it... | |
elem.splice(key,1); | |
// And insert it back. | |
elem.splice(c.lm,0,e); | |
} | |
} | |
// Object insert / replace | |
else if (c.oi !== void 0) { | |
json.checkObj(elem); | |
// Should check that elem[key] == c.od | |
elem[key] = c.oi; | |
} | |
// Object delete | |
else if (c.od !== void 0) { | |
json.checkObj(elem); | |
// Should check that elem[key] == c.od | |
delete elem[key]; | |
} | |
else { | |
throw new Error('invalid / missing instruction in op'); | |
} | |
} | |
return container.data; | |
}; | |
// Helper to break an operation up into a bunch of small ops. | |
json.shatter = function(op) { | |
var results = []; | |
for (var i = 0; i < op.length; i++) { | |
results.push([op[i]]); | |
} | |
return results; | |
}; | |
// Helper for incrementally applying an operation to a snapshot. Calls yield | |
// after each op component has been applied. | |
json.incrementalApply = function(snapshot, op, _yield) { | |
for (var i = 0; i < op.length; i++) { | |
var smallOp = [op[i]]; | |
snapshot = json.apply(snapshot, smallOp); | |
// I'd just call this yield, but thats a reserved keyword. Bah! | |
_yield(smallOp, snapshot); | |
} | |
return snapshot; | |
}; | |
// Checks if two paths, p1 and p2 match. | |
var pathMatches = json.pathMatches = function(p1, p2, ignoreLast) { | |
if (p1.length != p2.length) | |
return false; | |
for (var i = 0; i < p1.length; i++) { | |
if (p1[i] !== p2[i] && (!ignoreLast || i !== p1.length - 1)) | |
return false; | |
} | |
return true; | |
}; | |
json.append = function(dest,c) { | |
c = clone(c); | |
if (dest.length === 0) { | |
dest.push(c); | |
return; | |
} | |
var last = dest[dest.length - 1]; | |
// convert old string ops to use subtype for backwards compatibility | |
if ((c.si != null || c.sd != null) && (last.si != null || last.sd != null)) { | |
convertFromText(c); | |
convertFromText(last); | |
} | |
if (pathMatches(c.p, last.p)) { | |
// handle subtype ops | |
if (c.t && last.t && c.t === last.t && subtypes[c.t]) { | |
last.o = subtypes[c.t].compose(last.o, c.o); | |
// convert back to old string ops | |
if (c.si != null || c.sd != null) { | |
var p = c.p; | |
for (var i = 0; i < last.o.length - 1; i++) { | |
c.o = [last.o.pop()]; | |
c.p = p.slice(); | |
convertToText(c); | |
dest.push(c); | |
} | |
convertToText(last); | |
} | |
} else if (last.na != null && c.na != null) { | |
dest[dest.length - 1] = {p: last.p, na: last.na + c.na}; | |
} else if (last.li !== undefined && c.li === undefined && c.ld === last.li) { | |
// insert immediately followed by delete becomes a noop. | |
if (last.ld !== undefined) { | |
// leave the delete part of the replace | |
delete last.li; | |
} else { | |
dest.pop(); | |
} | |
} else if (last.od !== undefined && last.oi === undefined && c.oi !== undefined && c.od === undefined) { | |
last.oi = c.oi; | |
} else if (last.oi !== undefined && c.od !== undefined) { | |
// The last path component inserted something that the new component deletes (or replaces). | |
// Just merge them. | |
if (c.oi !== undefined) { | |
last.oi = c.oi; | |
} else if (last.od !== undefined) { | |
delete last.oi; | |
} else { | |
// An insert directly followed by a delete turns into a no-op and can be removed. | |
dest.pop(); | |
} | |
} else if (c.lm !== undefined && c.p[c.p.length - 1] === c.lm) { | |
// don't do anything | |
} else { | |
dest.push(c); | |
} | |
} else { | |
// convert string ops back | |
if ((c.si != null || c.sd != null) && (last.si != null || last.sd != null)) { | |
convertToText(c); | |
convertToText(last); | |
} | |
dest.push(c); | |
} | |
}; | |
json.compose = function(op1,op2) { | |
json.checkValidOp(op1); | |
json.checkValidOp(op2); | |
var newOp = clone(op1); | |
for (var i = 0; i < op2.length; i++) { | |
json.append(newOp,op2[i]); | |
} | |
return newOp; | |
}; | |
json.normalize = function(op) { | |
var newOp = []; | |
op = isArray(op) ? op : [op]; | |
for (var i = 0; i < op.length; i++) { | |
var c = op[i]; | |
if (c.p == null) c.p = []; | |
json.append(newOp,c); | |
} | |
return newOp; | |
}; | |
// Returns the common length of the paths of ops a and b | |
json.commonLengthForOps = function(a, b) { | |
var alen = a.p.length; | |
var blen = b.p.length; | |
if (a.na != null || a.t) | |
alen++; | |
if (b.na != null || b.t) | |
blen++; | |
if (alen === 0) return -1; | |
if (blen === 0) return null; | |
alen--; | |
blen--; | |
for (var i = 0; i < alen; i++) { | |
var p = a.p[i]; | |
if (i >= blen || p !== b.p[i]) | |
return null; | |
} | |
return alen; | |
}; | |
// Returns true if an op can affect the given path | |
json.canOpAffectPath = function(op, path) { | |
return json.commonLengthForOps({p:path}, op) != null; | |
}; | |
// transform c so it applies to a document with otherC applied. | |
json.transformComponent = function(dest, c, otherC, type) { | |
c = clone(c); | |
var common = json.commonLengthForOps(otherC, c); | |
var common2 = json.commonLengthForOps(c, otherC); | |
var cplength = c.p.length; | |
var otherCplength = otherC.p.length; | |
if (c.na != null || c.t) | |
cplength++; | |
if (otherC.na != null || otherC.t) | |
otherCplength++; | |
// if c is deleting something, and that thing is changed by otherC, we need to | |
// update c to reflect that change for invertibility. | |
if (common2 != null && otherCplength > cplength && c.p[common2] == otherC.p[common2]) { | |
if (c.ld !== void 0) { | |
var oc = clone(otherC); | |
oc.p = oc.p.slice(cplength); | |
c.ld = json.apply(clone(c.ld),[oc]); | |
} else if (c.od !== void 0) { | |
var oc = clone(otherC); | |
oc.p = oc.p.slice(cplength); | |
c.od = json.apply(clone(c.od),[oc]); | |
} | |
} | |
if (common != null) { | |
var commonOperand = cplength == otherCplength; | |
// backward compatibility for old string ops | |
var oc = otherC; | |
if ((c.si != null || c.sd != null) && (otherC.si != null || otherC.sd != null)) { | |
convertFromText(c); | |
oc = clone(otherC); | |
convertFromText(oc); | |
} | |
// handle subtype ops | |
if (oc.t && subtypes[oc.t]) { | |
if (c.t && c.t === oc.t) { | |
var res = subtypes[c.t].transform(c.o, oc.o, type); | |
if (res.length > 0) { | |
// convert back to old string ops | |
if (c.si != null || c.sd != null) { | |
var p = c.p; | |
for (var i = 0; i < res.length; i++) { | |
c.o = [res[i]]; | |
c.p = p.slice(); | |
convertToText(c); | |
json.append(dest, c); | |
} | |
} else { | |
c.o = res; | |
json.append(dest, c); | |
} | |
} | |
return dest; | |
} | |
} | |
// transform based on otherC | |
else if (otherC.na !== void 0) { | |
// this case is handled below | |
} else if (otherC.li !== void 0 && otherC.ld !== void 0) { | |
if (otherC.p[common] === c.p[common]) { | |
// noop | |
if (!commonOperand) { | |
return dest; | |
} else if (c.ld !== void 0) { | |
// we're trying to delete the same element, -> noop | |
if (c.li !== void 0 && type === 'left') { | |
// we're both replacing one element with another. only one can survive | |
c.ld = clone(otherC.li); | |
} else { | |
return dest; | |
} | |
} | |
} | |
} else if (otherC.li !== void 0) { | |
if (c.li !== void 0 && c.ld === undefined && commonOperand && c.p[common] === otherC.p[common]) { | |
// in li vs. li, left wins. | |
if (type === 'right') | |
c.p[common]++; | |
} else if (otherC.p[common] <= c.p[common]) { | |
c.p[common]++; | |
} | |
if (c.lm !== void 0) { | |
if (commonOperand) { | |
// otherC edits the same list we edit | |
if (otherC.p[common] <= c.lm) | |
c.lm++; | |
// changing c.from is handled above. | |
} | |
} | |
} else if (otherC.ld !== void 0) { | |
if (c.lm !== void 0) { | |
if (commonOperand) { | |
if (otherC.p[common] === c.p[common]) { | |
// they deleted the thing we're trying to move | |
return dest; | |
} | |
// otherC edits the same list we edit | |
var p = otherC.p[common]; | |
var from = c.p[common]; | |
var to = c.lm; | |
if (p < to || (p === to && from < to)) | |
c.lm--; | |
} | |
} | |
if (otherC.p[common] < c.p[common]) { | |
c.p[common]--; | |
} else if (otherC.p[common] === c.p[common]) { | |
if (otherCplength < cplength) { | |
// we're below the deleted element, so -> noop | |
return dest; | |
} else if (c.ld !== void 0) { | |
if (c.li !== void 0) { | |
// we're replacing, they're deleting. we become an insert. | |
delete c.ld; | |
} else { | |
// we're trying to delete the same element, -> noop | |
return dest; | |
} | |
} | |
} | |
} else if (otherC.lm !== void 0) { | |
if (c.lm !== void 0 && cplength === otherCplength) { | |
// lm vs lm, here we go! | |
var from = c.p[common]; | |
var to = c.lm; | |
var otherFrom = otherC.p[common]; | |
var otherTo = otherC.lm; | |
if (otherFrom !== otherTo) { | |
// if otherFrom == otherTo, we don't need to change our op. | |
// where did my thing go? | |
if (from === otherFrom) { | |
// they moved it! tie break. | |
if (type === 'left') { | |
c.p[common] = otherTo; | |
if (from === to) // ugh | |
c.lm = otherTo; | |
} else { | |
return dest; | |
} | |
} else { | |
// they moved around it | |
if (from > otherFrom) c.p[common]--; | |
if (from > otherTo) c.p[common]++; | |
else if (from === otherTo) { | |
if (otherFrom > otherTo) { | |
c.p[common]++; | |
if (from === to) // ugh, again | |
c.lm++; | |
} | |
} | |
// step 2: where am i going to put it? | |
if (to > otherFrom) { | |
c.lm--; | |
} else if (to === otherFrom) { | |
if (to > from) | |
c.lm--; | |
} | |
if (to > otherTo) { | |
c.lm++; | |
} else if (to === otherTo) { | |
// if we're both moving in the same direction, tie break | |
if ((otherTo > otherFrom && to > from) || | |
(otherTo < otherFrom && to < from)) { | |
if (type === 'right') c.lm++; | |
} else { | |
if (to > from) c.lm++; | |
else if (to === otherFrom) c.lm--; | |
} | |
} | |
} | |
} | |
} else if (c.li !== void 0 && c.ld === undefined && commonOperand) { | |
// li | |
var from = otherC.p[common]; | |
var to = otherC.lm; | |
p = c.p[common]; | |
if (p > from) c.p[common]--; | |
if (p > to) c.p[common]++; | |
} else { | |
// ld, ld+li, si, sd, na, oi, od, oi+od, any li on an element beneath | |
// the lm | |
// | |
// i.e. things care about where their item is after the move. | |
var from = otherC.p[common]; | |
var to = otherC.lm; | |
p = c.p[common]; | |
if (p === from) { | |
c.p[common] = to; | |
} else { | |
if (p > from) c.p[common]--; | |
if (p > to) c.p[common]++; | |
else if (p === to && from > to) c.p[common]++; | |
} | |
} | |
} | |
else if (otherC.oi !== void 0 && otherC.od !== void 0) { | |
if (c.p[common] === otherC.p[common]) { | |
if (c.oi !== void 0 && commonOperand) { | |
// we inserted where someone else replaced | |
if (type === 'right') { | |
// left wins | |
return dest; | |
} else { | |
// we win, make our op replace what they inserted | |
c.od = otherC.oi; | |
} | |
} else { | |
// -> noop if the other component is deleting the same object (or any parent) | |
return dest; | |
} | |
} | |
} else if (otherC.oi !== void 0) { | |
if (c.oi !== void 0 && c.p[common] === otherC.p[common]) { | |
// left wins if we try to insert at the same place | |
if (type === 'left') { | |
json.append(dest,{p: c.p, od:otherC.oi}); | |
} else { | |
return dest; | |
} | |
} | |
} else if (otherC.od !== void 0) { | |
if (c.p[common] == otherC.p[common]) { | |
if (!commonOperand) | |
return dest; | |
if (c.oi !== void 0) { | |
delete c.od; | |
} else { | |
return dest; | |
} | |
} | |
} | |
} | |
json.append(dest,c); | |
return dest; | |
}; | |
if (exports._bootstrapTransform) { | |
exports._bootstrapTransform(json, json.transformComponent, json.checkValidOp, json.append); | |
} else { | |
helpers._bootstrapTransform(json, json.transformComponent, json.checkValidOp, json.append); | |
} | |
/** | |
* Register a subtype for string operations, using the text0 type. | |
*/ | |
if (typeof text === 'undefined') | |
var text = typeof commonjsRequire !== "undefined" ? text0$1 : window.ottypes.text; | |
json.registerSubtype(text); | |
module.exports = json; | |
}); | |
var lib$1 = createCommonjsModule(function (module, exports) { | |
var register = function(type) { | |
exports[type.name] = type; | |
if (type.uri) { | |
return exports[type.uri] = type; | |
} | |
}; | |
// Import all the built-in types. Requiring directly rather than in register() | |
// so browserify works. | |
register(simple); | |
register(text); | |
register(textTp2); | |
// This is deprecated, but exported for json0. | |
register(text0$1); | |
register(json0$1); | |
}); | |
client.types.register(lib$1.text); | |
window.ShareDB = client; | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment