/* globals crypto */ import { BinaryDecoder, BinaryEncoder } from './Encoding.js' /* EventHandler is an helper class for constructing custom types. Why: When constructing custom types, you sometimes want your types to work synchronous: E.g. ``` Synchronous mytype.setSomething("yay") mytype.getSomething() === "yay" ``` versus ``` Asynchronous mytype.setSomething("yay") mytype.getSomething() === undefined mytype.waitForSomething().then(function(){ mytype.getSomething() === "yay" }) ``` The structures usually work asynchronously (you have to wait for the database request to finish). EventHandler helps you to make your type synchronous. */ export default function Utils (Y) { Y.utils = { BinaryDecoder: BinaryDecoder, BinaryEncoder: BinaryEncoder } Y.utils.bubbleEvent = function (type, event) { type.eventHandler.callEventListeners(event) event.path = [] while (type != null && type._deepEventHandler != null) { type._deepEventHandler.callEventListeners(event) var parent = null if (type._parent != null) { parent = type.os.getType(type._parent) } if (parent != null && parent._getPathToChild != null) { event.path = [parent._getPathToChild(type._model)].concat(event.path) type = parent } else { type = null } } } class NamedEventHandler { constructor () { this._eventListener = {} } on (name, f) { if (this._eventListener[name] == null) { this._eventListener[name] = [] } this._eventListener[name].push(f) } off (name, f) { if (name == null || f == null) { throw new Error('You must specify event name and function!') } let listener = this._eventListener[name] || [] this._eventListener[name] = listener.filter(e => e !== f) } emit (name, value) { (this._eventListener[name] || []).forEach(l => l(value)) } destroy () { this._eventListener = null } } Y.utils.NamedEventHandler = NamedEventHandler class EventListenerHandler { constructor () { this.eventListeners = [] } destroy () { this.eventListeners = null } /* Basic event listener boilerplate... */ addEventListener (f) { this.eventListeners.push(f) } removeEventListener (f) { this.eventListeners = this.eventListeners.filter(function (g) { return f !== g }) } removeAllEventListeners () { this.eventListeners = [] } callEventListeners (event) { for (var i = 0; i < this.eventListeners.length; i++) { try { var _event = {} for (var name in event) { _event[name] = event[name] } this.eventListeners[i](_event) } catch (e) { /* Your observer threw an error. This error was caught so that Yjs can ensure data consistency! In order to debug this error you have to check "Pause On Caught Exceptions" in developer tools. */ console.error(e) } } } } Y.utils.EventListenerHandler = EventListenerHandler class EventHandler extends EventListenerHandler { /* :: waiting: Array; awaiting: number; onevent: Function; eventListeners: Array; */ /* onevent: is called when the structure changes. Note: "awaiting opertations" is used to denote operations that were prematurely called. Events for received operations can not be executed until all prematurely called operations were executed ("waiting operations") */ constructor (onevent /* : Function */) { super() this.waiting = [] this.awaiting = 0 this.onevent = onevent } destroy () { super.destroy() this.waiting = null this.onevent = null } /* Call this when a new operation arrives. It will be executed right away if there are no waiting operations, that you prematurely executed */ receivedOp (op) { if (this.awaiting <= 0) { this.onevent(op) } else if (op.struct === 'Delete') { var self = this var checkDelete = function checkDelete (d) { if (d.length == null) { throw new Error('This shouldn\'t happen! d.length must be defined!') } // we check if o deletes something in self.waiting // if so, we remove the deleted operation for (var w = 0; w < self.waiting.length; w++) { var i = self.waiting[w] if (i.struct === 'Insert' && i.id[0] === d.target[0]) { var iLength = i.hasOwnProperty('content') ? i.content.length : 1 var dStart = d.target[1] var dEnd = d.target[1] + (d.length || 1) var iStart = i.id[1] var iEnd = i.id[1] + iLength // Check if they don't overlap if (iEnd <= dStart || dEnd <= iStart) { // no overlapping continue } // we check all overlapping cases. All cases: /* 1) iiiii ddddd --> modify i and d 2) iiiiiii ddddd --> modify i, remove d 3) iiiiiii ddd --> remove d, modify i, and create another i (for the right hand side) 4) iiiii ddddddd --> remove i, modify d 5) iiiiiii ddddddd --> remove both i and d (**) 6) iiiiiii ddddd --> modify i, remove d 7) iii ddddddd --> remove i, create and apply two d with checkDelete(d) (**) 8) iiiii ddddddd --> remove i, modify d (**) 9) iiiii ddddd --> modify i and d (**) (also check if i contains content or type) */ // TODO: I left some debugger statements, because I want to debug all cases once in production. REMEMBER END TODO if (iStart < dStart) { if (dStart < iEnd) { if (iEnd < dEnd) { // Case 1 // remove the right part of i's content i.content.splice(dStart - iStart) // remove the start of d's deletion d.length = dEnd - iEnd d.target = [d.target[0], iEnd] continue } else if (iEnd === dEnd) { // Case 2 i.content.splice(dStart - iStart) // remove d, we do that by simply ending this function return } else { // (dEnd < iEnd) // Case 3 var newI = { id: [i.id[0], dEnd], content: i.content.slice(dEnd - iStart), struct: 'Insert' } self.waiting.push(newI) i.content.splice(dStart - iStart) return } } } else if (dStart === iStart) { if (iEnd < dEnd) { // Case 4 d.length = dEnd - iEnd d.target = [d.target[0], iEnd] i.content = [] continue } else if (iEnd === dEnd) { // Case 5 self.waiting.splice(w, 1) return } else { // (dEnd < iEnd) // Case 6 i.content = i.content.slice(dEnd - iStart) i.id = [i.id[0], dEnd] return } } else { // (dStart < iStart) if (iStart < dEnd) { // they overlap /* 7) iii ddddddd --> remove i, create and apply two d with checkDelete(d) (**) 8) iiiii ddddddd --> remove i, modify d (**) 9) iiiii ddddd --> modify i and d */ if (iEnd < dEnd) { // Case 7 // debugger // TODO: You did not test this case yet!!!! (add the debugger here) self.waiting.splice(w, 1) checkDelete({ target: [d.target[0], dStart], length: iStart - dStart, struct: 'Delete' }) checkDelete({ target: [d.target[0], iEnd], length: iEnd - dEnd, struct: 'Delete' }) return } else if (iEnd === dEnd) { // Case 8 self.waiting.splice(w, 1) w-- d.length -= iLength continue } else { // dEnd < iEnd // Case 9 d.length = iStart - dStart i.content.splice(0, dEnd - iStart) i.id = [i.id[0], dEnd] continue } } } } } // finished with remaining operations self.waiting.push(d) } if (op.key == null) { // deletes in list checkDelete(op) } else { // deletes in map this.waiting.push(op) } } else { this.waiting.push(op) } } /* You created some operations, and you want the `onevent` function to be called right away. Received operations will not be executed untill all prematurely called operations are executed */ awaitAndPrematurelyCall (ops) { this.awaiting++ ops.map(Y.utils.copyOperation).forEach(this.onevent) } * awaitOps (transaction, f, args) { function notSoSmartSort (array) { // this function sorts insertions in a executable order var result = [] while (array.length > 0) { for (var i = 0; i < array.length; i++) { var independent = true for (var j = 0; j < array.length; j++) { if (Y.utils.matchesId(array[j], array[i].left)) { // array[i] depends on array[j] independent = false break } } if (independent) { result.push(array.splice(i, 1)[0]) i-- } } } return result } var before = this.waiting.length // somehow create new operations yield * f.apply(transaction, args) // remove all appended ops / awaited ops this.waiting.splice(before) if (this.awaiting > 0) this.awaiting-- // if there are no awaited ops anymore, we can update all waiting ops, and send execute them (if there are still no awaited ops) if (this.awaiting === 0 && this.waiting.length > 0) { // update all waiting ops for (let i = 0; i < this.waiting.length; i++) { var o = this.waiting[i] if (o.struct === 'Insert') { var _o = yield * transaction.getInsertion(o.id) if (_o.parentSub != null && _o.left != null) { // if o is an insertion of a map struc (parentSub is defined), then it shouldn't be necessary to compute left this.waiting.splice(i, 1) i-- // update index } else if (!Y.utils.compareIds(_o.id, o.id)) { // o got extended o.left = [o.id[0], o.id[1] - 1] } else if (_o.left == null) { o.left = null } else { // find next undeleted op var left = yield * transaction.getInsertion(_o.left) while (left.deleted != null) { if (left.left != null) { left = yield * transaction.getInsertion(left.left) } else { left = null break } } o.left = left != null ? Y.utils.getLastId(left) : null } } } // the previous stuff was async, so we have to check again! // We also pull changes from the bindings, if there exists such a method, this could increase awaiting too if (this._pullChanges != null) { this._pullChanges() } if (this.awaiting === 0) { // sort by type, execute inserts first var ins = [] var dels = [] this.waiting.forEach(function (o) { if (o.struct === 'Delete') { dels.push(o) } else { ins.push(o) } }) this.waiting = [] // put in executable order ins = notSoSmartSort(ins) // this.onevent can trigger the creation of another operation // -> check if this.awaiting increased & stop computation if it does for (var i = 0; i < ins.length; i++) { if (this.awaiting === 0) { this.onevent(ins[i]) } else { this.waiting = this.waiting.concat(ins.slice(i)) break } } for (i = 0; i < dels.length; i++) { if (this.awaiting === 0) { this.onevent(dels[i]) } else { this.waiting = this.waiting.concat(dels.slice(i)) break } } } } } // TODO: Remove awaitedInserts and awaitedDeletes in favor of awaitedOps, as they are deprecated and do not always work // Do this in one of the coming releases that are breaking anyway /* Call this when you successfully awaited the execution of n Insert operations */ awaitedInserts (n) { var ops = this.waiting.splice(this.waiting.length - n) for (var oid = 0; oid < ops.length; oid++) { var op = ops[oid] if (op.struct === 'Insert') { for (var i = this.waiting.length - 1; i >= 0; i--) { let w = this.waiting[i] // TODO: do I handle split operations correctly here? Super unlikely, but yeah.. // Also: can this case happen? Can op be inserted in the middle of a larger op that is in $waiting? if (w.struct === 'Insert') { if (Y.utils.matchesId(w, op.left)) { // include the effect of op in w w.right = op.id // exclude the effect of w in op op.left = w.left } else if (Y.utils.compareIds(w.id, op.right)) { // similar.. w.left = Y.utils.getLastId(op) op.right = w.right } } } } else { throw new Error('Expected Insert Operation!') } } this._tryCallEvents(n) } /* Call this when you successfully awaited the execution of n Delete operations */ awaitedDeletes (n, newLeft) { var ops = this.waiting.splice(this.waiting.length - n) for (var j = 0; j < ops.length; j++) { var del = ops[j] if (del.struct === 'Delete') { if (newLeft != null) { for (var i = 0; i < this.waiting.length; i++) { let w = this.waiting[i] // We will just care about w.left if (w.struct === 'Insert' && Y.utils.compareIds(del.target, w.left)) { w.left = newLeft } } } } else { throw new Error('Expected Delete Operation!') } } this._tryCallEvents(n) } /* (private) Try to execute the events for the waiting operations */ _tryCallEvents () { function notSoSmartSort (array) { var result = [] while (array.length > 0) { for (var i = 0; i < array.length; i++) { var independent = true for (var j = 0; j < array.length; j++) { if (Y.utils.matchesId(array[j], array[i].left)) { // array[i] depends on array[j] independent = false break } } if (independent) { result.push(array.splice(i, 1)[0]) i-- } } } return result } if (this.awaiting > 0) this.awaiting-- if (this.awaiting === 0 && this.waiting.length > 0) { var ins = [] var dels = [] this.waiting.forEach(function (o) { if (o.struct === 'Delete') { dels.push(o) } else { ins.push(o) } }) ins = notSoSmartSort(ins) ins.forEach(this.onevent) dels.forEach(this.onevent) this.waiting = [] } } } Y.utils.EventHandler = EventHandler /* Default class of custom types! */ class CustomType { getPath () { var parent = null if (this._parent != null) { parent = this.os.getType(this._parent) } if (parent != null && parent._getPathToChild != null) { var firstKey = parent._getPathToChild(this._model) var parentKeys = parent.getPath() parentKeys.push(firstKey) return parentKeys } else { return [] } } } Y.utils.CustomType = CustomType /* A wrapper for the definition of a custom type. Every custom type must have three properties: * struct - Structname of this type * initType - Given a model, creates a custom type * class - the constructor of the custom type (e.g. in order to inherit from a type) */ class CustomTypeDefinition { // eslint-disable-line /* :: struct: any; initType: any; class: Function; name: String; */ constructor (def) { if (def.struct == null || def.initType == null || def.class == null || def.name == null || def.createType == null ) { throw new Error('Custom type was not initialized correctly!') } this.struct = def.struct this.initType = def.initType this.createType = def.createType this.class = def.class this.name = def.name if (def.appendAdditionalInfo != null) { this.appendAdditionalInfo = def.appendAdditionalInfo } this.parseArguments = (def.parseArguments || function () { return [this] }).bind(this) this.parseArguments.typeDefinition = this } } Y.utils.CustomTypeDefinition = CustomTypeDefinition Y.utils.isTypeDefinition = function isTypeDefinition (v) { if (v != null) { if (v instanceof Y.utils.CustomTypeDefinition) return [v] else if (v.constructor === Array && v[0] instanceof Y.utils.CustomTypeDefinition) return v else if (v instanceof Function && v.typeDefinition instanceof Y.utils.CustomTypeDefinition) return [v.typeDefinition] } return false } /* Make a flat copy of an object (just copy properties) */ function copyObject (o) { var c = {} for (var key in o) { c[key] = o[key] } return c } Y.utils.copyObject = copyObject /* Copy an operation, so that it can be manipulated. Note: You must not change subproperties (except o.content)! */ function copyOperation (o) { o = copyObject(o) if (o.content != null) { o.content = o.content.map(function (c) { return c }) } return o } Y.utils.copyOperation = copyOperation /* Defines a smaller relation on Id's */ function smaller (a, b) { return a[0] < b[0] || (a[0] === b[0] && (a[1] < b[1] || typeof a[1] < typeof b[1])) } Y.utils.smaller = smaller function inDeletionRange (del, ins) { return del.target[0] === ins[0] && del.target[1] <= ins[1] && ins[1] < del.target[1] + (del.length || 1) } Y.utils.inDeletionRange = inDeletionRange function compareIds (id1, id2) { if (id1 == null || id2 == null) { return id1 === id2 } else { return id1[0] === id2[0] && id1[1] === id2[1] } } Y.utils.compareIds = compareIds function matchesId (op, id) { if (id == null || op == null) { return id === op } else { if (id[0] === op.id[0]) { if (op.content == null) { return id[1] === op.id[1] } else { return id[1] >= op.id[1] && id[1] < op.id[1] + op.content.length } } } return false } Y.utils.matchesId = matchesId function getLastId (op) { if (op.content == null || op.content.length === 1) { return op.id } else { return [op.id[0], op.id[1] + op.content.length - 1] } } Y.utils.getLastId = getLastId function createEmptyOpsArray (n) { var a = new Array(n) for (var i = 0; i < a.length; i++) { a[i] = { id: [null, null] } } return a } function createSmallLookupBuffer (Store) { /* This buffer implements a very small buffer that temporarily stores operations after they are read / before they are written. The buffer basically implements FIFO. Often requested lookups will be re-queued every time they are looked up / written. It can speed up lookups on Operation Stores and State Stores. But it does not require notable use of memory or processing power. Good for os and ss, bot not for ds (because it often uses methods that require a flush) I tried to optimize this for performance, therefore no highlevel operations. */ class SmallLookupBuffer extends Store { constructor (arg1, arg2) { // super(...arguments) -- do this when this is supported by stable nodejs super(arg1, arg2) this.writeBuffer = createEmptyOpsArray(5) this.readBuffer = createEmptyOpsArray(10) } * find (id, noSuperCall) { var i, r for (i = this.readBuffer.length - 1; i >= 0; i--) { r = this.readBuffer[i] // we don't have to use compareids, because id is always defined! if (r.id[1] === id[1] && r.id[0] === id[0]) { // found r // move r to the end of readBuffer for (; i < this.readBuffer.length - 1; i++) { this.readBuffer[i] = this.readBuffer[i + 1] } this.readBuffer[this.readBuffer.length - 1] = r return r } } var o for (i = this.writeBuffer.length - 1; i >= 0; i--) { r = this.writeBuffer[i] if (r.id[1] === id[1] && r.id[0] === id[0]) { o = r break } } if (i < 0 && noSuperCall === undefined) { // did not reach break in last loop // read id and put it to the end of readBuffer o = yield * super.find(id) } if (o != null) { for (i = 0; i < this.readBuffer.length - 1; i++) { this.readBuffer[i] = this.readBuffer[i + 1] } this.readBuffer[this.readBuffer.length - 1] = o } return o } * put (o) { var id = o.id var i, r // helper variables for (i = this.writeBuffer.length - 1; i >= 0; i--) { r = this.writeBuffer[i] if (r.id[1] === id[1] && r.id[0] === id[0]) { // is already in buffer // forget r, and move o to the end of writeBuffer for (; i < this.writeBuffer.length - 1; i++) { this.writeBuffer[i] = this.writeBuffer[i + 1] } this.writeBuffer[this.writeBuffer.length - 1] = o break } } if (i < 0) { // did not reach break in last loop // write writeBuffer[0] var write = this.writeBuffer[0] if (write.id[0] !== null) { yield * super.put(write) } // put o to the end of writeBuffer for (i = 0; i < this.writeBuffer.length - 1; i++) { this.writeBuffer[i] = this.writeBuffer[i + 1] } this.writeBuffer[this.writeBuffer.length - 1] = o } // check readBuffer for every occurence of o.id, overwrite if found // whether found or not, we'll append o to the readbuffer for (i = 0; i < this.readBuffer.length - 1; i++) { r = this.readBuffer[i + 1] if (r.id[1] === id[1] && r.id[0] === id[0]) { this.readBuffer[i] = o } else { this.readBuffer[i] = r } } this.readBuffer[this.readBuffer.length - 1] = o } * delete (id) { var i, r for (i = 0; i < this.readBuffer.length; i++) { r = this.readBuffer[i] if (r.id[1] === id[1] && r.id[0] === id[0]) { this.readBuffer[i] = { id: [null, null] } } } yield * this.flush() yield * super.delete(id) } * findWithLowerBound (id) { var o = yield * this.find(id, true) if (o != null) { return o } else { yield * this.flush() return yield * super.findWithLowerBound.apply(this, arguments) } } * findWithUpperBound (id) { var o = yield * this.find(id, true) if (o != null) { return o } else { yield * this.flush() return yield * super.findWithUpperBound.apply(this, arguments) } } * findNext () { yield * this.flush() return yield * super.findNext.apply(this, arguments) } * findPrev () { yield * this.flush() return yield * super.findPrev.apply(this, arguments) } * iterate () { yield * this.flush() yield * super.iterate.apply(this, arguments) } * flush () { for (var i = 0; i < this.writeBuffer.length; i++) { var write = this.writeBuffer[i] if (write.id[0] !== null) { yield * super.put(write) this.writeBuffer[i] = { id: [null, null] } } } } } return SmallLookupBuffer } Y.utils.createSmallLookupBuffer = createSmallLookupBuffer function generateUserId () { if (typeof crypto !== 'undefined' && crypto.getRandomValue != null) { // browser let arr = new Uint32Array(1) crypto.getRandomValues(arr) return arr[0] } else if (typeof crypto !== 'undefined' && crypto.randomBytes != null) { // node let buf = crypto.randomBytes(4) return new Uint32Array(buf.buffer)[0] } else { return Math.ceil(Math.random() * 0xFFFFFFFF) } } Y.utils.generateUserId = generateUserId Y.utils.parseTypeDefinition = function parseTypeDefinition (type, typeArgs) { var args = [] try { args = JSON.parse('[' + typeArgs + ']') } catch (e) { throw new Error('Was not able to parse type definition!') } if (type.typeDefinition.parseArguments != null) { args = type.typeDefinition.parseArguments(args[0])[1] } return args } }