json-joy
Version:
Collection of libraries for building collaborative editing apps.
298 lines (297 loc) • 10.9 kB
JavaScript
"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;