/* @flow */ 'use strict' /* 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 will help you to make your type synchronous. */ module.exports = function (Y /* : any*/) { Y.utils = {} class EventHandler { /* :: 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 */) { this.waiting = [] this.awaiting = 0 this.onevent = onevent this.eventListeners = [] } /* 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 { this.waiting.push(Y.utils.copyObject(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++ this.onevent(ops) } /* Basic event listener boilerplate... TODO: maybe put this in a different type.. */ 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 { this.eventListeners[i](event) } catch (e) { console.log('User events must not throw Errors!') // eslint-disable-line } } } /* 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] if (w.struct === 'Insert') { if (Y.utils.compareIds(op.left, w.id)) { // 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(op.right, w.id)) { // similar.. w.left = op.id op.right = w.right } } } } else { throw new Error('Expected Insert Operation!') } } this._tryCallEvents() } /* 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() } /* (private) Try to execute the events for the waiting operations */ _tryCallEvents () { this.awaiting-- if (this.awaiting <= 0 && this.waiting.length > 0) { var events = this.waiting this.waiting = [] this.onevent(events) } } } Y.utils.EventHandler = EventHandler /* 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 CustomType { // 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 ) { throw new Error('Custom type was not initialized correctly!') } this.struct = def.struct this.initType = def.initType this.class = def.class this.name = def.name } } Y.utils.CustomType = CustomType /* 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 /* 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 compareIds (id1, id2) { if (id1 == null || id2 == null) { if (id1 == null && id2 == null) { return true } return false } if (id1[0] === id2[0] && id1[1] === id2[1]) { return true } else { return false } } Y.utils.compareIds = compareIds 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 () { super(...arguments) this.writeBuffer = createEmptyOpsArray(5) this.readBuffer = createEmptyOpsArray(10) } * find (id) { 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) { // 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 () { yield* this.flush() return yield* super.findWithLowerBound.apply(this, arguments) } * findWithUpperBound () { 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 }