json-joy
Version:
Collection of libraries for building collaborative editing apps.
416 lines (415 loc) • 12.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PatchBuilder = void 0;
const operations_1 = require("./operations");
const clock_1 = require("./clock");
const isUint8Array_1 = require("@jsonjoy.com/util/lib/buffers/isUint8Array");
const Patch_1 = require("./Patch");
const constants_1 = require("./constants");
const Tuple_1 = require("./builder/Tuple");
const Konst_1 = require("./builder/Konst");
const DelayedValueBuilder_1 = require("./builder/DelayedValueBuilder");
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
*/
class PatchBuilder {
/**
* Creates a new PatchBuilder instance.
*
* @param clock Clock to use for generating timestamps.
*/
constructor(clock) {
this.clock = clock;
this.patch = new Patch_1.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_1.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_1.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_1.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_1.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_1.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_1.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.
*/
const(value) {
this.pad();
const id = this.clock.tick(1);
this.patch.ops.push(new operations_1.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_1.NewValOp(id));
return id;
}
/**
* Set value of document's root LWW-Register.
*
* @returns ID of the new operation.
*/
root(val) {
this.pad();
const id = this.clock.tick(1);
this.patch.ops.push(new operations_1.InsValOp(id, constants_1.ORIGIN, val));
return id;
}
/**
* Set fields of an "obj" object.
*
* @returns ID of the new operation.
*/
insObj(obj, data) {
this.pad();
if (!data.length)
throw new Error('EMPTY_TUPLES');
const id = this.clock.tick(1);
const op = new operations_1.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) {
this.pad();
if (!data.length)
throw new Error('EMPTY_TUPLES');
const id = this.clock.tick(1);
const op = new operations_1.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_1.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) {
this.pad();
if (!data.length)
throw new Error('EMPTY_STRING');
const id = this.clock.tick(1);
const op = new operations_1.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) {
this.pad();
if (!data.length)
throw new Error('EMPTY_BINARY');
const id = this.clock.tick(1);
const op = new operations_1.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_1.InsArrOp(id, arr, ref, data);
const span = op.span();
if (span > 1)
this.clock.tick(span - 1);
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_1.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_1.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 clock_1.Timestamp ? value : maybeConst(value) ? this.const(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.const(value);
this.setVal(valId, id);
return valId;
}
/**
* Run builder commands to create a tuple.
*/
jsonVec(vector) {
const id = this.vec();
const length = vector.length;
if (length) {
const writes = [];
for (let i = 0; i < length; i++)
writes.push([i, this.constOrJson(vector[i])]);
this.insVec(id, writes);
}
return id;
}
/**
* Run the necessary builder commands to create any arbitrary JSON value.
*/
json(json) {
if (json instanceof clock_1.Timestamp)
return json;
if (json === undefined)
return this.const(json);
if (json instanceof Array)
return this.jsonArr(json);
if ((0, isUint8Array_1.isUint8Array)(json))
return this.jsonBin(json);
if (json instanceof Tuple_1.VectorDelayedValue)
return this.jsonVec(json.slots);
if (json instanceof Konst_1.Konst)
return this.const(json.val);
if (json instanceof DelayedValueBuilder_1.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 clock_1.Timestamp)
return value;
return maybeConst(value) ? this.const(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 clock_1.Timestamp ? value : this.const(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 = (0, clock_1.ts)(this.clock.sid, nextTime);
const padding = new operations_1.NopOp(id, drift);
this.patch.ops.push(padding);
}
}
}
exports.PatchBuilder = PatchBuilder;