UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

404 lines 11.7 kB
import * as operations from './operations'; import { ts, Timestamp } from './clock'; import { isUint8Array } from '@jsonjoy.com/buffers/lib/isUint8Array'; import { Patch } from './Patch'; import { ORIGIN } from './constants'; import { NodeBuilder } from './schema'; const maybeConst = (x) => { switch (typeof x) { case 'number': case 'boolean': return true; default: return x === null; } }; /** * Utility class that helps in Patch construction. * * @category Patch */ export class PatchBuilder { clock; /** The patch being constructed. */ patch; /** * Creates a new PatchBuilder instance. * * @param clock Clock to use for generating timestamps. */ constructor(clock) { this.clock = clock; this.patch = new Patch(); } /** * Retrieve the sequence number of the next timestamp. * * @returns The next timestamp sequence number that will be used by the builder. */ nextTime() { return this.patch.nextTime() || this.clock.time; } /** * Returns the current {@link Patch} instance and resets the builder. * * @returns A new {@link Patch} instance containing all operations created * using this builder. */ flush() { const patch = this.patch; this.patch = new Patch(); return patch; } // --------------------------------------------------------- Basic operations /** * Create a new "obj" LWW-Map object. * * @returns ID of the new operation. */ obj() { this.pad(); const id = this.clock.tick(1); this.patch.ops.push(new operations.NewObjOp(id)); return id; } /** * Create a new "arr" RGA-Array object. * * @returns ID of the new operation. */ arr() { this.pad(); const id = this.clock.tick(1); this.patch.ops.push(new operations.NewArrOp(id)); return id; } /** * Create a new "vec" LWW-Array vector. * * @returns ID of the new operation. */ vec() { this.pad(); const id = this.clock.tick(1); this.patch.ops.push(new operations.NewVecOp(id)); return id; } /** * Create a new "str" RGA-String object. * * @returns ID of the new operation. */ str() { this.pad(); const id = this.clock.tick(1); this.patch.ops.push(new operations.NewStrOp(id)); return id; } /** * Create a new "bin" RGA-Binary object. * * @returns ID of the new operation. */ bin() { this.pad(); const id = this.clock.tick(1); this.patch.ops.push(new operations.NewBinOp(id)); return id; } /** * Create a new immutable constant JSON value. Can be anything, including * nested arrays and objects. * * @param value JSON value * @returns ID of the new operation. */ con(value) { this.pad(); const id = this.clock.tick(1); this.patch.ops.push(new operations.NewConOp(id, value)); return id; } /** * Create a new "val" LWW-Register object. Can be anything, including * nested arrays and objects. * * @param val Reference to another object. * @returns ID of the new operation. * @todo Rename to `newVal`. */ val() { this.pad(); const id = this.clock.tick(1); this.patch.ops.push(new operations.NewValOp(id)); return id; } /** * Set value of document's root LWW-Register. * * @returns ID of the new operation. */ root(val) { return this.setVal(ORIGIN, val); } /** * Set fields of an "obj" object. * * @returns ID of the new operation. */ insObj(obj, data) { if (!data.length) throw new Error('EMPTY_TUPLES'); this.pad(); const id = this.clock.tick(1); const op = new operations.InsObjOp(id, obj, data); const span = op.span(); if (span > 1) this.clock.tick(span - 1); this.patch.ops.push(op); return id; } /** * Set elements of a "vec" object. * * @returns ID of the new operation. */ insVec(obj, data) { if (!data.length) throw new Error('EMPTY_TUPLES'); this.pad(); const id = this.clock.tick(1); const op = new operations.InsVecOp(id, obj, data); const span = op.span(); if (span > 1) this.clock.tick(span - 1); this.patch.ops.push(op); return id; } /** * Set value of a "val" object. * * @returns ID of the new operation. * @todo Rename to "insVal". */ setVal(obj, val) { this.pad(); const id = this.clock.tick(1); const op = new operations.InsValOp(id, obj, val); this.patch.ops.push(op); return id; } /** * Insert a substring into a "str" object. * * @returns ID of the new operation. */ insStr(obj, ref, data) { if (!data.length) throw new Error('EMPTY_STRING'); this.pad(); const id = this.clock.tick(1); const op = new operations.InsStrOp(id, obj, ref, data); const span = op.span(); if (span > 1) this.clock.tick(span - 1); this.patch.ops.push(op); return id; } /** * Insert binary data into a "bin" object. * * @returns ID of the new operation. */ insBin(obj, ref, data) { if (!data.length) throw new Error('EMPTY_BINARY'); this.pad(); const id = this.clock.tick(1); const op = new operations.InsBinOp(id, obj, ref, data); const span = op.span(); if (span > 1) this.clock.tick(span - 1); this.patch.ops.push(op); return id; } /** * Insert elements into an "arr" object. * * @returns ID of the new operation. */ insArr(arr, ref, data) { this.pad(); const id = this.clock.tick(1); const op = new operations.InsArrOp(id, arr, ref, data); const span = op.span(); if (span > 1) this.clock.tick(span - 1); this.patch.ops.push(op); return id; } /** * Update an element in an "arr" object. * * @returns ID of the new operation. */ updArr(arr, ref, val) { this.pad(); const id = this.clock.tick(1); const op = new operations.UpdArrOp(id, arr, ref, val); this.patch.ops.push(op); return id; } /** * Delete a span of operations. * * @param obj Object in which to delete something. * @param what List of time spans to delete. * @returns ID of the new operation. */ del(obj, what) { this.pad(); const id = this.clock.tick(1); this.patch.ops.push(new operations.DelOp(id, obj, what)); return id; } /** * Operation that does nothing just skips IDs in the patch. * * @param span Length of the operation. * @returns ID of the new operation. * */ nop(span) { this.pad(); const id = this.clock.tick(span); this.patch.ops.push(new operations.NopOp(id, span)); return id; } // --------------------------------------- JSON value construction operations /** * Run the necessary builder commands to create an arbitrary JSON object. */ jsonObj(obj) { const id = this.obj(); const keys = Object.keys(obj); if (keys.length) { const tuples = []; for (const k of keys) { const value = obj[k]; const valueId = value instanceof Timestamp ? value : maybeConst(value) ? this.con(value) : this.json(value); tuples.push([k, valueId]); } this.insObj(id, tuples); } return id; } /** * Run the necessary builder commands to create an arbitrary JSON array. */ jsonArr(arr) { const id = this.arr(); if (arr.length) { const values = []; for (const el of arr) values.push(this.json(el)); this.insArr(id, id, values); } return id; } /** * Run builder commands to create a JSON string. */ jsonStr(str) { const id = this.str(); if (str) this.insStr(id, id, str); return id; } /** * Run builder commands to create a binary data type. */ jsonBin(bin) { const id = this.bin(); if (bin.length) this.insBin(id, id, bin); return id; } /** * Run builder commands to create a JSON value. */ jsonVal(value) { const valId = this.val(); const id = this.con(value); this.setVal(valId, id); return valId; } /** * Run the necessary builder commands to create any arbitrary JSON value. */ json(json) { if (json instanceof Timestamp) return json; if (json === undefined) return this.con(json); if (json instanceof Array) return this.jsonArr(json); if (isUint8Array(json)) return this.jsonBin(json); if (json instanceof NodeBuilder) return json.build(this); switch (typeof json) { case 'object': return json === null ? this.jsonVal(json) : this.jsonObj(json); case 'string': return this.jsonStr(json); case 'number': case 'boolean': return this.jsonVal(json); } throw new Error('INVALID_JSON'); } /** * Given a JSON `value` creates the necessary builder commands to create * JSON CRDT Patch operations to construct the value. If the `value` is a * timestamp, it is returned as-is. If the `value` is a JSON primitive is * a number, boolean, or `null`, it is converted to a "con" data type. Otherwise, * the `value` is converted using the {@link PatchBuilder.json} method. * * @param value A JSON value for which to create JSON CRDT Patch construction operations. * @returns ID of the root constructed CRDT object. */ constOrJson(value) { if (value instanceof Timestamp) return value; return maybeConst(value) ? this.con(value) : this.json(value); } /** * Creates a "con" data type unless the value is already a timestamp, in which * case it is returned as-is. * * @param value Value to convert to a "con" data type. * @returns ID of the new "con" object. */ maybeConst(value) { return value instanceof Timestamp ? value : this.con(value); } // ------------------------------------------------------------------ Private /** * Add padding "noop" operation if clock's time has jumped. This method checks * if clock has advanced past the ID of the last operation of the patch and, * if so, adds a "noop" operation to the patch to pad the gap. */ pad() { const nextTime = this.patch.nextTime(); if (!nextTime) return; const drift = this.clock.time - nextTime; if (drift > 0) { const id = ts(this.clock.sid, nextTime); const padding = new operations.NopOp(id, drift); this.patch.ops.push(padding); } } } //# sourceMappingURL=PatchBuilder.js.map