UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

298 lines (297 loc) 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ModelApi = void 0; const fanout_1 = require("thingies/lib/fanout"); const nodes_1 = require("../../nodes"); const nodes_2 = require("./nodes"); const PatchBuilder_1 = require("../../../json-crdt-patch/PatchBuilder"); const fanout_2 = require("./fanout"); const ExtNode_1 = require("../../extensions/ExtNode"); /** * Local changes API for a JSON CRDT model. This class is the main entry point * for executing local user actions on a JSON CRDT document. * * @category Local API */ class ModelApi { /** * @param model Model instance on which the API operates. */ constructor(model) { this.model = model; /** * Index of the next operation in builder's patch to be committed locally. * * @ignore */ this.next = 0; /** Emitted before the model is reset, using the `.reset()` method. */ this.onBeforeReset = new fanout_1.FanOut(); /** Emitted after the model is reset, using the `.reset()` method. */ this.onReset = new fanout_1.FanOut(); /** Emitted before a patch is applied using `model.applyPatch()`. */ this.onBeforePatch = new fanout_1.FanOut(); /** Emitted after a patch is applied using `model.applyPatch()`. */ this.onPatch = new fanout_1.FanOut(); /** Emitted before local changes through `model.api` are applied. */ this.onBeforeLocalChange = new fanout_1.FanOut(); /** Emitted after local changes through `model.api` are applied. */ this.onLocalChange = new fanout_1.FanOut(); /** * Emitted after local changes through `model.api` are applied. Same as * `.onLocalChange`, but this event buffered withing a microtask. */ this.onLocalChanges = new fanout_2.MicrotaskBufferFanOut(this.onLocalChange); /** Emitted before a transaction is started. */ this.onBeforeTransaction = new fanout_1.FanOut(); /** Emitted after transaction completes. */ this.onTransaction = new fanout_1.FanOut(); /** Emitted when the model changes. Combines `onReset`, `onPatch` and `onLocalChange`. */ this.onChange = new fanout_2.MergeFanOut([ this.onReset, this.onPatch, this.onLocalChange, ]); /** Emitted when the model changes. Same as `.onChange`, but this event is emitted once per microtask. */ this.onChanges = new fanout_2.MicrotaskBufferFanOut(this.onChange); /** Emitted when the `model.api` builder change buffer is flushed. */ this.onFlush = new fanout_1.FanOut(); this.inTx = false; this.stopAutoFlush = undefined; // ---------------------------------------------------------------- SyncStore this.subscribe = (callback) => this.onChanges.listen(() => callback()); this.getSnapshot = () => this.view(); this.builder = new PatchBuilder_1.PatchBuilder(model.clock); model.onbeforereset = () => this.onBeforeReset.emit(); model.onreset = () => this.onReset.emit(); model.onbeforepatch = (patch) => this.onBeforePatch.emit(patch); model.onpatch = (patch) => this.onPatch.emit(patch); } wrap(node) { if (node instanceof nodes_1.ValNode) return node.api || (node.api = new nodes_2.ValApi(node, this)); else if (node instanceof nodes_1.StrNode) return node.api || (node.api = new nodes_2.StrApi(node, this)); else if (node instanceof nodes_1.BinNode) return node.api || (node.api = new nodes_2.BinApi(node, this)); else if (node instanceof nodes_1.ArrNode) return node.api || (node.api = new nodes_2.ArrApi(node, this)); else if (node instanceof nodes_1.ObjNode) return node.api || (node.api = new nodes_2.ObjApi(node, this)); else if (node instanceof nodes_1.ConNode) return node.api || (node.api = new nodes_2.ConApi(node, this)); else if (node instanceof nodes_1.VecNode) return node.api || (node.api = new nodes_2.VecApi(node, this)); else if (node instanceof ExtNode_1.ExtNode) { if (node.api) return node.api; const extension = this.model.ext.get(node.extId); return (node.api = new extension.Api(node, this)); } else throw new Error('UNKNOWN_NODE'); } /** * Local changes API for the root node. */ get r() { return new nodes_2.ValApi(this.model.root, this); } /** @ignore */ get node() { return this.r.get(); } /** * Traverses the model starting from the root node and returns a local * changes API for a node at the given path. * * @param path Path at which to locate a node. * @returns A local changes API for a node at the given path. */ in(path) { return this.r.in(path); } /** * Locates a JSON CRDT node, throws an error if the node doesn't exist. * * @param path Path at which to locate a node. * @returns A JSON CRDT node. */ find(path) { return this.node.find(path); } /** * Locates a `con` node and returns a local changes API for it. If the node * doesn't exist or the node at the path is not a `con` node, throws an error. * * @todo Rename to `con`. * * @param path Path at which to locate a node. * @returns A local changes API for a `con` node. */ con(path) { return this.node.con(path); } /** * Locates a `val` node and returns a local changes API for it. If the node * doesn't exist or the node at the path is not a `val` node, throws an error. * * @param path Path at which to locate a node. * @returns A local changes API for a `val` node. */ val(path) { return this.node.val(path); } /** * Locates a `vec` node and returns a local changes API for it. If the node * doesn't exist or the node at the path is not a `vec` node, throws an error. * * @param path Path at which to locate a node. * @returns A local changes API for a `vec` node. */ vec(path) { return this.node.vec(path); } /** * Locates an `obj` node and returns a local changes API for it. If the node * doesn't exist or the node at the path is not an `obj` node, throws an error. * * @param path Path at which to locate a node. * @returns A local changes API for an `obj` node. */ obj(path) { return this.node.obj(path); } /** * Locates a `str` node and returns a local changes API for it. If the node * doesn't exist or the node at the path is not a `str` node, throws an error. * * @param path Path at which to locate a node. * @returns A local changes API for a `str` node. */ str(path) { return this.node.str(path); } /** * Locates a `bin` node and returns a local changes API for it. If the node * doesn't exist or the node at the path is not a `bin` node, throws an error. * * @param path Path at which to locate a node. * @returns A local changes API for a `bin` node. */ bin(path) { return this.node.bin(path); } /** * Locates an `arr` node and returns a local changes API for it. If the node * doesn't exist or the node at the path is not an `arr` node, throws an error. * * @param path Path at which to locate a node. * @returns A local changes API for an `arr` node. */ arr(path) { return this.node.arr(path); } /** * Given a JSON/CBOR value, constructs CRDT nodes recursively out of it and * sets the root node of the model to the constructed nodes. * * @param json JSON/CBOR value to set as the view of the model. * @returns Reference to itself. */ root(json) { const builder = this.builder; builder.root(builder.json(json)); this.apply(); return this; } /** * Apply locally any operations from the `.builder`, which haven't been * applied yet. */ apply() { const ops = this.builder.patch.ops; const length = ops.length; const model = this.model; const from = this.next; this.onBeforeLocalChange.emit(from); for (let i = this.next; i < length; i++) model.applyOperation(ops[i]); this.next = length; model.tick++; this.onLocalChange.emit(from); } /** * Advance patch pointer to the end without applying the operations. With the * idea that they have already been applied locally. * * You need to manually call `this.onBeforeLocalChange.emit(this.next)` before * calling this method. * * @ignore */ advance() { const from = this.next; this.next = this.builder.patch.ops.length; this.model.tick++; this.onLocalChange.emit(from); } /** * Returns the view of the model. * * @returns JSON/CBOR of the model. */ view() { return this.model.view(); } transaction(callback) { if (this.inTx) callback(); else { this.inTx = true; try { this.onBeforeTransaction.emit(); callback(); this.onTransaction.emit(); } finally { this.inTx = false; } } } /** * Flushes the builder and returns a patch. * * @returns A JSON CRDT patch. * @todo Make this return undefined if there are no operations in the builder. */ flush() { const patch = this.builder.flush(); this.next = 0; if (patch.ops.length) this.onFlush.emit(patch); return patch; } /** * Begins to automatically flush buffered operations into patches, grouping * operations by microtasks or by transactions. To capture the patch, listen * to the `.onFlush` event. * * @returns Callback to stop auto flushing. */ autoFlush(drainNow = false) { const drain = () => this.builder.patch.ops.length && this.flush(); const onLocalChangesUnsubscribe = this.onLocalChanges.listen(drain); const onBeforeTransactionUnsubscribe = this.onBeforeTransaction.listen(drain); const onTransactionUnsubscribe = this.onTransaction.listen(drain); if (drainNow) drain(); return (this.stopAutoFlush = () => { this.stopAutoFlush = undefined; onLocalChangesUnsubscribe(); onBeforeTransactionUnsubscribe(); onTransactionUnsubscribe(); }); } } exports.ModelApi = ModelApi;