UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

192 lines (191 loc) 6.9 kB
import * as operations from './operations'; import { ts, printTs, Timestamp } from './clock'; import { printTree } from 'tree-dump/lib/printTree'; import { encode, decode } from './codec/binary'; /** * Represents a JSON CRDT patch. * * Normally, you would create a new patch using the {@link PatchBuilder} class. * * ```ts * import {Patch, PatchBuilder, LogicalClock} from 'json-joy/lib/json-crdt-patch'; * * const clock = new LogicalClock(3, 100); * const builder = new PatchBuilder(clock); * const patch = builder.flush(); * ``` * * Save patch to a binary representation: * * ```ts * const binary = patch.toBinary(); * ``` * * Load patch from a binary representation: * * ```ts * const patch = Patch.fromBinary(binary); * ``` * * @category Patch */ export class Patch { /** * Un-marshals a JSON CRDT patch from a binary representation. */ static fromBinary(data) { return decode(data); } /** * A list of operations in the patch. */ ops = []; /** * Arbitrary metadata associated with the patch, which is not used by the * library. */ meta = undefined; /** * Returns the patch ID, which is equal to the ID of the first operation * in the patch. * * @returns The ID of the first operation in the patch. */ getId() { const op = this.ops[0]; if (!op) return undefined; return op.id; } /** * Returns the total time span of the patch, which is the sum of all * operation spans. * * @returns The length of the patch. */ span() { let span = 0; for (const op of this.ops) span += op.span(); return span; } /** * Returns the expected time of the next inserted operation. */ nextTime() { const ops = this.ops; const length = ops.length; if (!length) return 0; const lastOp = ops[length - 1]; return lastOp.id.time + lastOp.span(); } /** * Creates a new patch where all timestamps are transformed using the * provided function. * * @param ts Timestamp transformation function. * @returns A new patch with transformed timestamps. */ rewriteTime(ts) { const patch = new Patch(); const ops = this.ops; const length = ops.length; const patchOps = patch.ops; for (let i = 0; i < length; i++) { const op = ops[i]; if (op instanceof operations.DelOp) patchOps.push(new operations.DelOp(ts(op.id), ts(op.obj), op.what)); else if (op instanceof operations.NewConOp) patchOps.push(new operations.NewConOp(ts(op.id), op.val instanceof Timestamp ? ts(op.val) : op.val)); else if (op instanceof operations.NewVecOp) patchOps.push(new operations.NewVecOp(ts(op.id))); else if (op instanceof operations.NewValOp) patchOps.push(new operations.NewValOp(ts(op.id))); else if (op instanceof operations.NewObjOp) patchOps.push(new operations.NewObjOp(ts(op.id))); else if (op instanceof operations.NewStrOp) patchOps.push(new operations.NewStrOp(ts(op.id))); else if (op instanceof operations.NewBinOp) patchOps.push(new operations.NewBinOp(ts(op.id))); else if (op instanceof operations.NewArrOp) patchOps.push(new operations.NewArrOp(ts(op.id))); else if (op instanceof operations.InsArrOp) patchOps.push(new operations.InsArrOp(ts(op.id), ts(op.obj), ts(op.ref), op.data.map(ts))); else if (op instanceof operations.InsStrOp) patchOps.push(new operations.InsStrOp(ts(op.id), ts(op.obj), ts(op.ref), op.data)); else if (op instanceof operations.InsBinOp) patchOps.push(new operations.InsBinOp(ts(op.id), ts(op.obj), ts(op.ref), op.data)); else if (op instanceof operations.InsValOp) patchOps.push(new operations.InsValOp(ts(op.id), ts(op.obj), ts(op.val))); else if (op instanceof operations.InsObjOp) patchOps.push(new operations.InsObjOp(ts(op.id), ts(op.obj), op.data.map(([key, value]) => [key, ts(value)]))); else if (op instanceof operations.InsVecOp) patchOps.push(new operations.InsVecOp(ts(op.id), ts(op.obj), op.data.map(([key, value]) => [key, ts(value)]))); else if (op instanceof operations.NopOp) patchOps.push(new operations.NopOp(ts(op.id), op.len)); } return patch; } /** * The `.rebase()` operation is meant to be applied to patches which have not * yet been advertised to the server (other peers), or when * the server clock is used and concurrent change on the server happened. * * The .rebase() operation returns a new `Patch` with the IDs recalculated * such that the first operation has the `time` equal to `newTime`. * * @param newTime Time where the patch ID should begin (ID of the first operation). * @param transformAfter Time after (and including) which the IDs should be * transformed. If not specified, equals to the time of the first operation. */ rebase(newTime, transformAfter) { const id = this.getId(); if (!id) throw new Error('EMPTY_PATCH'); const sid = id.sid; const patchStartTime = id.time; transformAfter ??= patchStartTime; if (patchStartTime === newTime) return this; const delta = newTime - patchStartTime; return this.rewriteTime((id) => { if (id.sid !== sid) return id; const time = id.time; if (time < transformAfter) return id; return ts(sid, time + delta); }); } /** * Creates a deep clone of the patch. * * @returns A deep clone of the patch. */ clone() { return this.rewriteTime((id) => id); } /** * Marshals the patch into a binary representation. * * @returns A binary representation of the patch. */ toBinary() { return encode(this); } // ---------------------------------------------------------------- Printable /** * Returns a textual human-readable representation of the patch. This can be * used for debugging purposes. * * @param tab Start string for each line. * @returns Text representation of the patch. */ toString(tab = '') { const id = this.getId(); const header = `Patch ${id ? printTs(id) : '(nil)'}!${this.span()}`; return (header + printTree(tab, this.ops.map((op) => (tab) => op.toString(tab)))); } }