diff --git a/src/json-crdt-patch/types.ts b/src/json-crdt-patch/types.ts index 0da75584f3..3a09671b1d 100644 --- a/src/json-crdt-patch/types.ts +++ b/src/json-crdt-patch/types.ts @@ -1,3 +1,4 @@ +import type {Printable} from '../util/print/types'; import type {ITimestampStruct} from './clock'; import type {JsonCrdtPatchMnemonic} from './codec/verbose'; @@ -5,7 +6,7 @@ import type {JsonCrdtPatchMnemonic} from './codec/verbose'; * Something in the document that can be identified by ID. All operations have * IDs and operations result into JSON nodes and chunks, which also have IDs. */ -export interface Identifiable { +export interface Identifiable extends Printable { /** * Unique ID within a document. */ @@ -17,11 +18,6 @@ export interface Identifiable { * the number of entries. */ span?(): number; - - /** - * Used for debugging. - */ - toString(tab?: string): string; } /** @@ -46,13 +42,6 @@ export interface IJsonCrdtPatchOperation extends Identifiable { * User friendly name of the operation. */ name(): JsonCrdtPatchMnemonic; - - /** - * Returns a textual human-readable representation of the operation. - * - * @param tab String to use for indentation. - */ - toString(tab?: string): string; } /** diff --git a/src/json-crdt/codec/indexed/binary/Decoder.ts b/src/json-crdt/codec/indexed/binary/Decoder.ts index ac4690076a..b537d6b9c7 100644 --- a/src/json-crdt/codec/indexed/binary/Decoder.ts +++ b/src/json-crdt/codec/indexed/binary/Decoder.ts @@ -6,7 +6,7 @@ import {CrdtReader} from '../../../../json-crdt-patch/util/binary/CrdtDecoder'; import {IndexedFields, FieldName, IndexedNodeFields} from './types'; import {ITimestampStruct, IVectorClock, Timestamp, VectorClock} from '../../../../json-crdt-patch/clock'; import {JsonNode} from '../../../types'; -import {Model, UNDEFINED} from '../../../model'; +import {Model, UNDEFINED} from '../../../model/Model'; import {MsgPackDecoderFast} from '../../../../json-pack/msgpack'; import {ObjectLww} from '../../../types/lww-object/ObjectLww'; import {StringChunk, StringRga} from '../../../types/rga-string/StringRga'; diff --git a/src/json-crdt/codec/structural/binary/Decoder.ts b/src/json-crdt/codec/structural/binary/Decoder.ts index 29521ca5b6..e6376118cf 100644 --- a/src/json-crdt/codec/structural/binary/Decoder.ts +++ b/src/json-crdt/codec/structural/binary/Decoder.ts @@ -4,7 +4,7 @@ import {ClockDecoder} from '../../../../json-crdt-patch/codec/clock/ClockDecoder import {Const} from '../../../types/const/Const'; import {CrdtReader} from '../../../../json-crdt-patch/util/binary/CrdtDecoder'; import {ITimestampStruct, Timestamp} from '../../../../json-crdt-patch/clock'; -import {Model, UNDEFINED} from '../../../model'; +import {Model, UNDEFINED} from '../../../model/Model'; import {MsgPackDecoderFast} from '../../../../json-pack/msgpack'; import {ObjectLww} from '../../../types/lww-object/ObjectLww'; import {RootLww} from '../../../types/lww-root/RootLww'; diff --git a/src/json-crdt/index.ts b/src/json-crdt/index.ts index 5bf3e3cfb5..47ab0d3ab9 100644 --- a/src/json-crdt/index.ts +++ b/src/json-crdt/index.ts @@ -1,3 +1,3 @@ -export * from './types/types'; +export * from './types'; export * from './extensions/types'; export * from './model'; diff --git a/src/json-crdt/model/Model.ts b/src/json-crdt/model/Model.ts index 2ff992122c..0a824a5bc1 100644 --- a/src/json-crdt/model/Model.ts +++ b/src/json-crdt/model/Model.ts @@ -1,6 +1,4 @@ import * as operations from '../../json-crdt-patch/operations'; -import {ArrayRga} from '../types/rga-array/ArrayRga'; -import {BinaryRga} from '../types/rga-binary/BinaryRga'; import {Const} from '../types/const/Const'; import {encoder, decoder} from '../codec/structural/binary/shared'; import { @@ -13,13 +11,9 @@ import { } from '../../json-crdt-patch/clock'; import {JsonCrdtPatchOperation, Patch} from '../../json-crdt-patch/Patch'; import {ModelApi} from './api/ModelApi'; -import {ObjectLww} from '../types/lww-object/ObjectLww'; import {ORIGIN, SESSION, SYSTEM_SESSION_TIME} from '../../json-crdt-patch/constants'; import {randomSessionId} from './util'; -import {RootLww} from '../types/lww-root/RootLww'; -import {StringRga} from '../types/rga-string/StringRga'; -import {ValueLww} from '../types/lww-value/ValueLww'; -import {ArrayLww} from '../types/lww-array/ArrayLww'; +import {RootLww, ValueLww, ArrayLww, ObjectLww, StringRga, BinaryRga, ArrayRga} from '../types'; import {printTree} from '../../util/print/printTree'; import {Extensions} from '../extensions/Extensions'; import {AvlMap} from '../../util/trees/avl/AvlMap'; @@ -30,8 +24,7 @@ export const UNDEFINED = new Const(ORIGIN, undefined); /** * In instance of Model class represents the underlying data structure, - * i.e. model, of the JSON CRDT document. The `.toJson()` can be called to - * compute the "view" of the model. + * i.e. model, of the JSON CRDT document. */ export class Model implements Printable { /** @@ -92,12 +85,16 @@ export class Model implements Printabl /** * Index of all known node objects (objects, array, strings, values) * in this document. + * + * @ignore */ public index = new AvlMap(compare); /** * Extensions to the JSON CRDT protocol. Extensions are used to implement * custom data types on top of the JSON CRDT protocol. + * + * @ignore */ public ext: Extensions = new Extensions(); @@ -106,10 +103,11 @@ export class Model implements Printabl if (!clock.time) clock.time = 1; } + /** @ignore */ private _api?: ModelApi; /** - * API for applying changes to the current document. + * API for applying local changes to the current document. */ public get api(): ModelApi { if (!this._api) this._api = new ModelApi(this); @@ -117,18 +115,33 @@ export class Model implements Printabl } /** - * @private * Experimental node retrieval API using proxy objects. */ public get find() { return this.api.r.proxy(); } - /** Tracks number of times the `applyPatch` was called. */ + /** + * Tracks number of times the `applyPatch` was called. + * + * @ignore + */ public tick: number = 0; + /** + * Callback called after every `applyPatch` call. + * + * When using the `.api` API, this property is set automatically by + * the {@link ModelApi} class. In that case use the `mode.api.evens.on('change')` + * to subscribe to changes. + */ public onchange: undefined | (() => void) = undefined; + /** + * Applies a batch of patches to the document. + * + * @param patches A batch, i.e. an array of patches. + */ public applyBatch(patches: Patch[]) { const length = patches.length; for (let i = 0; i < length; i++) this.applyPatch(patches[i]); @@ -154,6 +167,7 @@ export class Model implements Printabl * the `tick` property and call `onchange` after calling this method. * * @param op Any JSON CRDT Patch operation + * @ignore */ public applyOperation(op: JsonCrdtPatchOperation): void { this.clock.observe(op.id, op.span()); @@ -261,6 +275,8 @@ export class Model implements Printabl /** * Recursively deletes a tree of nodes. Used when root node is overwritten or * when object contents of container node (object or array) is removed. + * + * @ignore */ protected deleteNodeTree(value: ITimestampStruct) { const isSystemNode = value.sid === SESSION.SYSTEM; @@ -272,7 +288,11 @@ export class Model implements Printabl } /** - * Creates a copy of this model with a new session ID. + * Creates a copy of this model with a new session ID. If the session ID is + * not provided, a random session ID is generated. + * + * @param sessionId Session ID to use for the new model. + * @returns A copy of this model with a new session ID. */ public fork(sessionId: number = randomSessionId()): Model { const copy = Model.fromBinary(this.toBinary()); @@ -283,21 +303,33 @@ export class Model implements Printabl /** * Creates a copy of this model with the same session ID. + * + * @returns A copy of this model with the same session ID. */ public clone(): Model { return this.fork(this.clock.sid); } /** - * @returns Returns the view of the model. + * Returns the view of the model. + * + * @returns JSON/CBOR of the model. */ public view(): Readonly> { return this.root.view(); } /** - * @returns Returns human-readable text for debugging. + * Serialize this model using "binary" structural encoding. + * + * @returns This model encoded in octets. */ + public toBinary(): Uint8Array { + return encoder.encode(this); + } + + // ---------------------------------------------------------------- Printable + public toString(tab: string = ''): string { const nl = () => ''; const hasExtensions = this.ext.size() > 0; @@ -314,12 +346,4 @@ export class Model implements Printabl ]) ); } - - /** - * Serialize this model using "binary" structural encoding. - * @returns This model encoded in octets. - */ - public toBinary(): Uint8Array { - return encoder.encode(this); - } } diff --git a/src/json-crdt/model/api/ModelApi.ts b/src/json-crdt/model/api/ModelApi.ts index e0c470f0ea..2e09f11002 100644 --- a/src/json-crdt/model/api/ModelApi.ts +++ b/src/json-crdt/model/api/ModelApi.ts @@ -1,29 +1,40 @@ -import {ApiPath, ArrayApi, BinaryApi, ConstApi, NodeApi, ObjectApi, StringApi, TupleApi, ValueApi} from './nodes'; +import {ArrayLww, Const, ObjectLww, ArrayRga, BinaryRga, StringRga, ValueLww} from '../../types'; +import {ApiPath, ArrayApi, BinaryApi, ConstApi, NodeApi, ObjectApi, StringApi, VectorApi, ValueApi} from './nodes'; import {Patch} from '../../../json-crdt-patch/Patch'; import {PatchBuilder} from '../../../json-crdt-patch/PatchBuilder'; -import {JsonNode} from '../../types'; -import {ArrayLww} from '../../types/lww-array/ArrayLww'; -import {Const} from '../../types/const/Const'; -import {ObjectLww} from '../../types/lww-object/ObjectLww'; -import {ArrayRga} from '../../types/rga-array/ArrayRga'; -import {BinaryRga} from '../../types/rga-binary/BinaryRga'; -import {StringRga} from '../../types/rga-string/StringRga'; -import {ValueLww} from '../../types/lww-value/ValueLww'; +import type {JsonNode} from '../../types'; import type {Model} from '../Model'; -import type {JsonNodeApi} from './types'; +/** + * 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 + */ export class ModelApi { + /** + * Patch builder for the local changes. + */ public builder: PatchBuilder; - /** Index of the next operation in builder's patch to be committed locally. */ + /** + * Index of the next operation in builder's patch to be committed locally. + * + * @ignore + */ public next: number = 0; + /** + * @param model Model instance on which the API operates. + */ constructor(public readonly model: Model) { this.builder = new PatchBuilder(this.model.clock); } + /** @ignore */ private changeQueued: boolean = false; + /** @ignore */ private readonly queueChange = (): void => { if (this.changeQueued) return; this.changeQueued = true; @@ -34,7 +45,12 @@ export class ModelApi { }); }; + /** @ignore */ private et: undefined | EventTarget = undefined; + + /** + * Event target for listening to {@link Model} changes. + */ public get events(): EventTarget { let et = this.et; if (!et) { @@ -44,13 +60,17 @@ export class ModelApi { return et; } + /** + * Returns a local change API for the given node. If an instance already + * exists, returns the existing instance. + */ public wrap(node: ValueLww): ValueApi; public wrap(node: StringRga): StringApi; public wrap(node: BinaryRga): BinaryApi; public wrap(node: ArrayRga): ArrayApi; public wrap(node: ObjectLww): ObjectApi; public wrap(node: Const): ConstApi; - public wrap(node: ArrayLww): TupleApi; + public wrap(node: ArrayLww): VectorApi; public wrap(node: JsonNode): NodeApi; public wrap(node: JsonNode) { if (node instanceof ValueLww) return node.api || (node.api = new ValueApi(node, this)); @@ -59,54 +79,127 @@ export class ModelApi { else if (node instanceof ArrayRga) return node.api || (node.api = new ArrayApi(node, this)); else if (node instanceof ObjectLww) return node.api || (node.api = new ObjectApi(node, this)); else if (node instanceof Const) return node.api || (node.api = new ConstApi(node, this)); - else if (node instanceof ArrayLww) return node.api || (node.api = new TupleApi(node, this)); + else if (node instanceof ArrayLww) return node.api || (node.api = new VectorApi(node, this)); else throw new Error('UNKNOWN_NODE'); } + /** @ignore */ public get node() { return new NodeApi(this.model.root.node(), this); } + /** + * Local changes API for the root node. + */ public get r() { return new ValueApi(this.model.root, this); } + /** + * 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. + */ public in(path?: ApiPath) { 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. + */ public find(path?: ApiPath) { return this.node.find(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. + */ public val(path?: ApiPath) { return this.node.val(path); } - public tup(path?: ApiPath) { + /** + * 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. + */ + public vec(path?: ApiPath) { return this.node.tup(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. + */ public str(path?: ApiPath) { 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. + */ public bin(path?: ApiPath) { 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. + */ public arr(path?: ApiPath) { return this.node.arr(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. + */ public obj(path?: ApiPath) { return this.node.obj(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. + * + * @param path Path at which to locate a node. + * @returns A local changes API for a `con` node. + */ public const(path?: ApiPath) { return this.node.const(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. + */ public root(json: unknown): this { const builder = this.builder; builder.root(builder.json(json)); @@ -131,6 +224,8 @@ export class ModelApi { /** * Advance patch pointer to the end without applying the operations. With the * idea that they have already been applied locally. + * + * @ignore */ public advance() { this.next = this.builder.patch.ops.length; @@ -139,10 +234,20 @@ export class ModelApi { model.onchange?.(); } - public view(): unknown { + /** + * Returns the view of the model. + * + * @returns JSON/CBOR of the model. + */ + public view() { return this.model.view(); } + /** + * Flushes the builder and returns a patch. + * + * @returns A JSON CRDT patch. + */ public flush(): Patch { const patch = this.builder.flush(); this.next = 0; diff --git a/src/json-crdt/model/api/__tests__/ModelApi.proxy.spec.ts b/src/json-crdt/model/api/__tests__/ModelApi.proxy.spec.ts index c4f1f9e56b..1d12527621 100644 --- a/src/json-crdt/model/api/__tests__/ModelApi.proxy.spec.ts +++ b/src/json-crdt/model/api/__tests__/ModelApi.proxy.spec.ts @@ -1,5 +1,5 @@ import {Model} from '../../Model'; -import {ConstApi, ObjectApi, StringApi, TupleApi, ValueApi} from '../nodes'; +import {ConstApi, ObjectApi, StringApi, VectorApi, ValueApi} from '../nodes'; import {RootLww} from '../../../types/lww-root/RootLww'; import {ObjectLww} from '../../../types/lww-object/ObjectLww'; import {StringRga} from '../../../types/rga-string/StringRga'; @@ -109,8 +109,8 @@ describe('supports all node types', () => { test('vector', () => { const proxy = model.api.r.proxy(); const vec = proxy.val.vec; - const vecApi: TupleApi = vec.toApi(); - expect(vecApi).toBeInstanceOf(TupleApi); + const vecApi: VectorApi = vec.toApi(); + expect(vecApi).toBeInstanceOf(VectorApi); expect(vecApi.node).toBeInstanceOf(ArrayLww); expect(vecApi.view()).toStrictEqual(['asdf', 1234, true, null]); }); diff --git a/src/json-crdt/model/api/__tests__/TupleApi.spec.ts b/src/json-crdt/model/api/__tests__/TupleApi.spec.ts index 4cb5784913..6b6ec9e167 100644 --- a/src/json-crdt/model/api/__tests__/TupleApi.spec.ts +++ b/src/json-crdt/model/api/__tests__/TupleApi.spec.ts @@ -6,8 +6,8 @@ test('can edit a tuple', () => { const doc = Model.withLogicalClock(); const api = doc.api; api.root(api.builder.vec()); - api.tup([]).set([[1, 'a']]); - expect(api.tup([]).view()).toEqual([undefined, 'a']); + api.vec([]).set([[1, 'a']]); + expect(api.vec([]).view()).toEqual([undefined, 'a']); }); onlyOnNode20('events', () => { @@ -17,7 +17,7 @@ onlyOnNode20('events', () => { api.root(vec(1, 2)); let cnt = 0; const onView = () => cnt++; - const tuple = api.tup([]); + const tuple = api.vec([]); tuple.events.on('view', onView); expect(cnt).toBe(0); tuple.set([[0, 1.5]]); @@ -37,7 +37,7 @@ onlyOnNode20('events', () => { api.root(vec(1, 2)); let cnt = 0; const onView = () => cnt++; - const tuple = api.tup([]); + const tuple = api.vec([]); tuple.events.on('view', onView); expect(cnt).toBe(0); tuple.set([[0, 1.5]]); @@ -60,7 +60,7 @@ onlyOnNode20('events', () => { api.root(vec(1, 2)); let cnt = 0; const onView = () => cnt++; - const tuple = api.tup([]); + const tuple = api.vec([]); tuple.events.on('view', onView); expect(cnt).toBe(0); tuple.set([[0, 1.5]]); @@ -81,7 +81,7 @@ onlyOnNode20('events', () => { api.root(vec(1, 2)); let cnt = 0; const onView = () => cnt++; - const tuple = api.tup([]); + const tuple = api.vec([]); tuple.events.on('view', onView); expect(cnt).toBe(0); tuple.set([[0, 1.5]]); diff --git a/src/json-crdt/model/api/index.ts b/src/json-crdt/model/api/index.ts new file mode 100644 index 0000000000..a015c30a4c --- /dev/null +++ b/src/json-crdt/model/api/index.ts @@ -0,0 +1,2 @@ +export {ModelApi} from './ModelApi'; +export * from './nodes'; diff --git a/src/json-crdt/model/api/nodes.ts b/src/json-crdt/model/api/nodes.ts index 3523e2f7a7..15084d7141 100644 --- a/src/json-crdt/model/api/nodes.ts +++ b/src/json-crdt/model/api/nodes.ts @@ -18,15 +18,38 @@ import type {Printable} from '../../../util/print/types'; export type ApiPath = string | number | Path | void; +/** + * A generic local changes API for a JSON CRDT node. + * + * @category Local API + */ export class NodeApi implements Printable { constructor(public readonly node: N, public readonly api: ModelApi) {} + /** @ignore */ private ev: undefined | NodeEvents = undefined; + + /** + * Event target for listening to node changes. You can subscribe to `"view"` + * events, which are triggered every time the node's view changes. + * + * ```typescript + * node.events.on('view', () => { + * // do something... + * }); + * ``` + */ public get events(): NodeEvents { const et = this.ev; return et || (this.ev = new NodeEvents(this)); } + /** + * Find a child node at the given path starting from this node. + * + * @param path Path to the child node to find. + * @returns JSON CRDT node at the given path. + */ public find(path?: ApiPath): JsonNode { const node = this.node; if (path === undefined) { @@ -42,6 +65,13 @@ export class NodeApi implements Printable { return find(this.node, path); } + /** + * Find a child node at the given path starting from this node and wrap it in + * a local changes API. + * + * @param path Path to the child node to find. + * @returns Local changes API for the child node at the given path. + */ public in(path?: ApiPath) { const node = this.find(path); return this.api.wrap(node as any); @@ -67,7 +97,7 @@ export class NodeApi implements Printable { throw new Error('NOT_ARR'); } - public asTup(): TupleApi { + public asTup(): VectorApi { if (this.node instanceof ArrayLww) return this.api.wrap(this.node as ArrayLww); throw new Error('NOT_ARR'); } @@ -109,7 +139,7 @@ export class NodeApi implements Printable { return this.in(path).asArr(); } - public tup(path?: ApiPath): TupleApi { + public tup(path?: ApiPath): VectorApi { return this.in(path).asTup(); } @@ -130,7 +160,15 @@ export class NodeApi implements Printable { } } +/** + * Represents the local changes API for the `con` JSON CRDT node {@link Const}. + * + * @category Local API + */ export class ConstApi = Const> extends NodeApi { + /** + * Returns a proxy object for this node. + */ public proxy(): types.ProxyNodeConst { return { toApi: () => this, @@ -138,7 +176,18 @@ export class ConstApi = Const> extends NodeApi { } } +/** + * Local changes API for the `val` JSON CRDT node {@link ValueLww}. + * + * @category Local API + */ export class ValueApi = ValueLww> extends NodeApi { + /** + * Sets the value of the node. + * + * @param json JSON/CBOR value or ID (logical timestamp) of the value to set. + * @returns Reference to itself. + */ public set(json: JsonNodeView): this { const {api, node} = this; const builder = api.builder; @@ -148,6 +197,10 @@ export class ValueApi = ValueLww> extends NodeApi { const self = this; const proxy = { @@ -161,7 +214,19 @@ export class ValueApi = ValueLww> extends NodeApi = ArrayLww> extends NodeApi { +/** + * Local changes API for the `vec` JSON CRDT node {@link ArrayLww}. + * + * @category Local API + * @todo Rename to VectorApi. + */ +export class VectorApi = ArrayLww> extends NodeApi { + /** + * Sets a list of elements to the given values. + * + * @param entries List of index-value pairs to set. + * @returns Reference to itself. + */ public set(entries: [index: number, value: unknown][]): this { const {api, node} = this; const {builder} = api; @@ -173,6 +238,10 @@ export class TupleApi = ArrayLww> extends NodeApi { const proxy = new Proxy( {}, @@ -191,7 +260,18 @@ export class TupleApi = ArrayLww> extends NodeApi = ObjectLww> extends NodeApi { + /** + * Sets a list of keys to the given values. + * + * @param entries List of key-value pairs to set. + * @returns Reference to itself. + */ public set(entries: Partial>): this { const {api, node} = this; const {builder} = api; @@ -203,6 +283,12 @@ export class ObjectApi = ObjectLww> extends NodeAp return this; } + /** + * Deletes a list of keys from the object. + * + * @param keys List of keys to delete. + * @returns Reference to itself. + */ public del(keys: string[]): this { const {api, node} = this; const {builder} = api; @@ -214,6 +300,10 @@ export class ObjectApi = ObjectLww> extends NodeAp return this; } + /** + * Returns a proxy object for this node. Allows to access object properties + * by key. + */ public proxy(): types.ProxyNodeObj { const self = this; const proxy = new Proxy( @@ -232,7 +322,21 @@ export class ObjectApi = ObjectLww> extends NodeAp } } +/** + * Local changes API for the `str` JSON CRDT node {@link StringRga}. This API + * allows to insert and delete bytes in the UTF-16 string by referencing its + * local character positions. + * + * @category Local API + */ export class StringApi extends NodeApi { + /** + * Inserts text at a given position. + * + * @param index Position at which to insert text. + * @param text Text to insert. + * @returns Reference to itself. + */ public ins(index: number, text: string): this { const {api, node} = this; const builder = api.builder; @@ -246,6 +350,13 @@ export class StringApi extends NodeApi { return this; } + /** + * Deletes a range of text at a given position. + * + * @param index Position at which to delete text. + * @param length Number of UTF-16 code units to delete. + * @returns Reference to itself. + */ public del(index: number, length: number): this { const {api, node} = this; const builder = api.builder; @@ -258,6 +369,9 @@ export class StringApi extends NodeApi { return this; } + /** + * Returns a proxy object for this node. + */ public proxy(): types.ProxyNodeStr { return { toApi: () => this, @@ -265,7 +379,21 @@ export class StringApi extends NodeApi { } } +/** + * Local changes API for the `bin` JSON CRDT node {@link BinaryRga}. This API + * allows to insert and delete bytes in the binary string by referencing their + * local index. + * + * @category Local API + */ export class BinaryApi extends NodeApi { + /** + * Inserts octets at a given position. + * + * @param index Position at which to insert octets. + * @param data Octets to insert. + * @returns Reference to itself. + */ public ins(index: number, data: Uint8Array): this { const {api, node} = this; const after = !index ? node.id : node.find(index - 1); @@ -275,6 +403,13 @@ export class BinaryApi extends NodeApi { return this; } + /** + * Deletes a range of octets at a given position. + * + * @param index Position at which to delete octets. + * @param length Number of octets to delete. + * @returns Reference to itself. + */ public del(index: number, length: number): this { const {api, node} = this; const spans = node.findInterval(index, length); @@ -284,6 +419,9 @@ export class BinaryApi extends NodeApi { return this; } + /** + * Returns a proxy object for this node. + */ public proxy(): types.ProxyNodeBin { return { toApi: () => this, @@ -291,7 +429,21 @@ export class BinaryApi extends NodeApi { } } +/** + * Local changes API for the `arr` JSON CRDT node {@link ArrayRga}. This API + * allows to insert and delete elements in the array by referencing their local + * index. + * + * @category Local API + */ export class ArrayApi = ArrayRga> extends NodeApi { + /** + * Inserts elements at a given position. + * + * @param index Position at which to insert elements. + * @param values JSON/CBOR values or IDs of the values to insert. + * @returns Reference to itself. + */ public ins(index: number, values: Array[number]>): this { const {api, node} = this; const {builder} = api; @@ -304,6 +456,13 @@ export class ArrayApi = ArrayRga> extends NodeApi = ArrayRga> extends NodeApi { const proxy = new Proxy( {}, diff --git a/src/json-crdt/model/api/types.ts b/src/json-crdt/model/api/types.ts index 5d6cb38b8a..da62287bc7 100644 --- a/src/json-crdt/model/api/types.ts +++ b/src/json-crdt/model/api/types.ts @@ -6,7 +6,7 @@ import type {ValueLww} from '../../types/lww-value/ValueLww'; import type {ArrayRga} from '../../types/rga-array/ArrayRga'; import type {BinaryRga} from '../../types/rga-binary/BinaryRga'; import type {StringRga} from '../../types/rga-string/StringRga'; -import type {ArrayApi, BinaryApi, ConstApi, ObjectApi, StringApi, TupleApi, ValueApi} from './nodes'; +import type {ArrayApi, BinaryApi, ConstApi, ObjectApi, StringApi, VectorApi, ValueApi} from './nodes'; // prettier-ignore export type JsonNodeApi = N extends Const @@ -24,5 +24,5 @@ export type JsonNodeApi = N extends Const : N extends ObjectLww ? ObjectApi : N extends ArrayLww - ? TupleApi + ? VectorApi : never; diff --git a/src/json-crdt/model/index.ts b/src/json-crdt/model/index.ts index 3556bbc943..ef97590c71 100644 --- a/src/json-crdt/model/index.ts +++ b/src/json-crdt/model/index.ts @@ -1 +1,2 @@ -export * from './Model'; +export {Model} from './Model'; +export * from './api'; diff --git a/src/json-crdt/types/const/Const.ts b/src/json-crdt/types/const/Const.ts index c95009e171..229558f88d 100644 --- a/src/json-crdt/types/const/Const.ts +++ b/src/json-crdt/types/const/Const.ts @@ -3,21 +3,39 @@ import type {JsonNode} from '../types'; import type {Printable} from '../../../util/print/types'; /** - * Constant type represents an immutable JSON value. It can be any JSON value - * including deeply nested objects and arrays, Uint8Array binary data. The - * constant value cannot be edited. + * Represents the `con` type of the JSON CRDT specification. + * + * Constant type represents an immutable JSON value. It can be any JSON/CBOR + * value including deeply nested objects and arrays, Uint8Array binary data, or + * it can store a logical timestamp. The constant value cannot be edited. + * + * @category CRDT Node */ export class Const implements JsonNode, Printable { + /** + * @param id ID of the CRDT node. + * @param val Raw value of the constant. It can be any JSON/CBOR value, or + * a logical timestamp {@link Timestamp}. + */ constructor(public readonly id: ITimestampStruct, public readonly val: View) {} // ----------------------------------------------------------------- JsonNode + /** + * @ignore + */ public children() {} + /** + * @ignore + */ public child() { return undefined; } + /** + * @ignore + */ public container(): JsonNode | undefined { return undefined; } @@ -26,6 +44,9 @@ export class Const implements JsonNode, return this.val; } + /** + * @ignore + */ public api: undefined | unknown = undefined; // ---------------------------------------------------------------- Printable diff --git a/src/json-crdt/types/index.ts b/src/json-crdt/types/index.ts index fcb073fefc..34e9239a4d 100644 --- a/src/json-crdt/types/index.ts +++ b/src/json-crdt/types/index.ts @@ -1 +1,9 @@ export * from './types'; +export {Const} from './const/Const'; +export {ValueLww} from './lww-value/ValueLww'; +export {RootLww} from './lww-root/RootLww'; +export {ArrayLww} from './lww-array/ArrayLww'; +export {ObjectLww} from './lww-object/ObjectLww'; +export {ArrayRga, ArrayChunk} from './rga-array/ArrayRga'; +export {BinaryRga, BinaryChunk} from './rga-binary/BinaryRga'; +export {StringRga, StringChunk} from './rga-string/StringRga'; diff --git a/src/json-crdt/types/lww-array/ArrayLww.ts b/src/json-crdt/types/lww-array/ArrayLww.ts index c90ce278cf..250de5f5de 100644 --- a/src/json-crdt/types/lww-array/ArrayLww.ts +++ b/src/json-crdt/types/lww-array/ArrayLww.ts @@ -6,23 +6,56 @@ import type {Model} from '../../model'; import type {JsonNode, JsonNodeView} from '../../types'; import type {Printable} from '../../../util/print/types'; +/** + * Represents a `vec` JSON CRDT node, which is a LWW array. + * + * Vector is, usually a fixed length, last-write-wins array. Each element + * in the array is a reference to another JSON CRDT node. The vector + * can be extended by adding new elements to the end of the array. + * + * @category CRDT Node + */ export class ArrayLww implements JsonNode>>, Printable { + /** + * @ignore + */ public readonly elements: (ITimestampStruct | undefined)[] = []; - constructor(public readonly doc: Model, public readonly id: ITimestampStruct) {} - + constructor( + /** + * @ignore + */ + public readonly doc: Model, + public readonly id: ITimestampStruct, + ) {} + + /** + * Retrieves the ID of an element at the given index. + * + * @param index Index of the element to get. + * @returns ID of the element at the given index, if any. + */ public val(index: number): undefined | ITimestampStruct { return this.elements[index] as ITimestampStruct | undefined; } + /** + * Retrieves the JSON CRDT node at the given index. + * + * @param index Index of the element to get. + * @returns JSON CRDT node at the given index, if any. + */ public get(index: Index): undefined | Value[Index] { const id = this.val(index); if (!id) return undefined; return this.doc.index.get(id); } + /** + * @ignore + */ public put(index: number, id: ITimestampStruct): undefined | ITimestampStruct { if (index > CRDT_CONSTANTS.MAX_TUPLE_LENGTH) throw new Error('OUT_OF_BOUNDS'); const currentId = this.val(index); @@ -33,8 +66,14 @@ export class ArrayLww return currentId; } + /** + * @ignore + */ private __extNode: JsonNode | undefined; + /** + * @ignore + */ public ext(): JsonNode | undefined { if (this.__extNode) return this.__extNode; const extensionId = this.getExtId(); @@ -46,10 +85,16 @@ export class ArrayLww return this.__extNode; } + /** + * @ignore + */ public isExt(): boolean { return !!this.ext(); } + /** + * @ignore + */ public getExtId(): number { if (this.elements.length !== 2) return -1; const type = this.get(0); @@ -63,14 +108,23 @@ export class ArrayLww // ----------------------------------------------------------------- JsonNode + /** + * @ignore + */ public child(): JsonNode | undefined { return this.ext(); } + /** + * @ignore + */ public container(): JsonNode | undefined { return this; } + /** + * @ignore + */ public children(callback: (node: JsonNode) => void) { if (this.isExt()) return; const elements = this.elements; @@ -84,7 +138,14 @@ export class ArrayLww } } + /** + * @ignore + */ private _view = [] as JsonNodeView; + + /** + * @ignore + */ public view(): Readonly> { const extNode = this.ext(); if (extNode) return extNode.view() as any; @@ -104,6 +165,9 @@ export class ArrayLww return useCache ? _view : (this._view = arr); } + /** + * @ignore + */ public api: undefined | unknown = undefined; // ---------------------------------------------------------------- Printable diff --git a/src/json-crdt/types/lww-object/ObjectLww.ts b/src/json-crdt/types/lww-object/ObjectLww.ts index 00698293e1..a8427640dd 100644 --- a/src/json-crdt/types/lww-object/ObjectLww.ts +++ b/src/json-crdt/types/lww-object/ObjectLww.ts @@ -4,13 +4,35 @@ import type {Model} from '../../model'; import type {Printable} from '../../../util/print/types'; import type {JsonNode, JsonNodeView} from '../../types'; +/** + * Represents a `obj` JSON CRDT node, which is a Last-write-wins (LWW) object. + * It is a map of string keys to LWW registers. The value of each register is + * a reference to another JSON CRDT node. + * + * @category CRDT Node + */ export class ObjectLww = Record> implements JsonNode>>, Printable { + /** + * @ignore + */ public readonly keys: Map = new Map(); - constructor(protected readonly doc: Model, public readonly id: ITimestampStruct) {} + constructor( + /** + * @ignore + */ + protected readonly doc: Model, + public readonly id: ITimestampStruct, + ) {} + /** + * Retrieves a JSON CRDT node at the given key. + * + * @param key A key of the object. + * @returns JSON CRDT node at the given key, if any. + */ public get(key: K): undefined | Value[K] { const id = this.keys.get(key as string); if (!id) return undefined; @@ -19,9 +41,11 @@ export class ObjectLww = Record = Record void) { const index = this.doc.index; this.keys.forEach((id, key) => callback(index.get(id)!, key)); @@ -37,21 +66,41 @@ export class ObjectLww = Record void) { const index = this.doc.index; this.keys.forEach((id, key) => callback(index.get(id)!)); } + /** + * @ignore + */ public child() { return undefined; } + /** + * @ignore + */ public container(): JsonNode | undefined { return this; } + /** + * @ignore + */ private _tick: number = 0; + + /** + * @ignore + */ private _view = {} as Readonly>; + + /** + * @ignore + */ public view(): Readonly> { const doc = this.doc; const tick = doc.clock.time + doc.tick; @@ -70,6 +119,9 @@ export class ObjectLww = Record extends ValueLww { /** * @param val Latest value of the document root. diff --git a/src/json-crdt/types/lww-value/ValueLww.ts b/src/json-crdt/types/lww-value/ValueLww.ts index 1b8b8756ff..fb6fb44241 100644 --- a/src/json-crdt/types/lww-value/ValueLww.ts +++ b/src/json-crdt/types/lww-value/ValueLww.ts @@ -5,11 +5,30 @@ import type {JsonNode, JsonNodeView} from '../../types'; import type {Model} from '../../model'; import type {Printable} from '../../../util/print/types'; +/** + * Represents a `val` JSON CRDT node, which is a Last-write-wins (LWW) register. + * The `val` node holds a single value, which is a reference to another JSON + * CRDT node. + * + * @category CRDT Node + */ export class ValueLww implements JsonNode>>, Printable { - public api: undefined | unknown = undefined; - - constructor(public readonly doc: Model, public readonly id: ITimestampStruct, public val: ITimestampStruct) {} + constructor( + /** + * @ignore + */ + public readonly doc: Model, + public readonly id: ITimestampStruct, + /** + * The current value of the node, which is a reference to another JSON CRDT + * node. + */ + public val: ITimestampStruct, + ) {} + /** + * @ignore + */ public set(val: ITimestampStruct): ITimestampStruct | undefined { if (compare(val, this.val) <= 0 && this.val.sid !== SESSION.SYSTEM) return; const oldVal = this.val; @@ -17,29 +36,49 @@ export class ValueLww implements JsonNode> { return this.node().view() as Readonly>; } + /** + * @ignore + */ public children(callback: (node: Value) => void) { callback(this.node()); } + /** + * @ignore + */ public child(): Value { return this.doc.index.get(this.val)! as Value; } + /** + * @ignore + */ public container(): JsonNode | undefined { const child = this.node(); return child ? child.container() : undefined; } + /** + * @ignore + */ + public api: undefined | unknown = undefined; + // ---------------------------------------------------------------- Printable public toString(tab: string = ''): string { diff --git a/src/json-crdt/types/rga-array/ArrayRga.ts b/src/json-crdt/types/rga-array/ArrayRga.ts index b442206d85..1e6f0efca7 100644 --- a/src/json-crdt/types/rga-array/ArrayRga.ts +++ b/src/json-crdt/types/rga-array/ArrayRga.ts @@ -10,6 +10,10 @@ type E = ITimestampStruct; const Empty = [] as any[]; +/** + * @ignore + * @category CRDT Node + */ export class ArrayChunk implements Chunk { public readonly id: ITimestampStruct; public span: number; @@ -63,6 +67,12 @@ export class ArrayChunk implements Chunk { } } +/** + * Represents the `arr` JSON CRDT type, which is a Replicated Growable Array + * (RGA). Each element ot the array is a reference to another JSON CRDT node. + * + * @category CRDT Node + */ export class ArrayRga extends AbstractRga implements JsonNode[]>>, Printable @@ -71,12 +81,24 @@ export class ArrayRga super(id); } + /** + * Returns a reference to an element at a given position in the array. + * + * @param position The position of the element to get. + * @returns An element of the array, if any. + */ public get(position: number): E | undefined { const pair = this.findChunk(position); if (!pair) return undefined; return pair[0].data![pair[1]]; } + /** + * Returns a JSON node at a given position in the array. + * + * @param position The position of the element to get. + * @returns A JSON node, if any. + */ public getNode(position: number): JsonNode | undefined { const id = this.get(position); if (!id) return undefined; @@ -92,25 +114,31 @@ export class ArrayRga // -------------------------------------------------------------- AbstractRga + /** @ignore */ public createChunk(id: ITimestampStruct, data: E[] | undefined): ArrayChunk { return new ArrayChunk(id, data ? data.length : 0, data); } + /** @ignore */ protected onChange(): void { this._view = Empty as any; } // ----------------------------------------------------------------- JsonNode + /** @ignore */ public child() { return undefined; } + /** @ignore */ public container(): JsonNode | undefined { return this; } + /** @ignore */ private _tick: number = 0; + /** @ignore */ private _view = Empty; public view(): Readonly[]> { const doc = this.doc; @@ -132,16 +160,19 @@ export class ArrayRga return useCache ? _view : ((this._tick = tick), (this._view = view)); } + /** @ignore */ public children(callback: (node: JsonNode) => void) { const index = this.doc.index; for (let chunk = this.first(); chunk; chunk = this.next(chunk)) if (!chunk.del) for (const node of chunk.data!) callback(index.get(node)!); } + /** @ignore */ public api: undefined | unknown = undefined; // ---------------------------------------------------------------- Printable + /** @ignore */ protected printChunk(tab: string, chunk: ArrayChunk): string { const pos = this.pos(chunk); let valueTree = ''; diff --git a/src/json-crdt/types/rga-binary/BinaryRga.ts b/src/json-crdt/types/rga-binary/BinaryRga.ts index 2c9cad42e8..8b9b91bcc2 100644 --- a/src/json-crdt/types/rga-binary/BinaryRga.ts +++ b/src/json-crdt/types/rga-binary/BinaryRga.ts @@ -2,6 +2,10 @@ import type {JsonNode} from '../../types'; import {ITimestampStruct, tick} from '../../../json-crdt-patch/clock'; import {AbstractRga, Chunk} from '../rga/AbstractRga'; +/** + * @ignore + * @category CRDT Node + */ export class BinaryChunk implements Chunk { public readonly id: ITimestampStruct; public span: number; @@ -62,9 +66,16 @@ export class BinaryChunk implements Chunk { } } +/** + * Represents the `bin` type in JSON CRDT. The `bin` is a blob of binary data, + * powered by a Replicated Growable Array (RGA) algorithm. + * + * @category CRDT Node + */ export class BinaryRga extends AbstractRga implements JsonNode { // ----------------------------------------------------------------- JsonNode + /** @ignore */ private _view: null | Uint8Array = null; public view(): Readonly { if (this._view) return this._view; @@ -82,24 +93,30 @@ export class BinaryRga extends AbstractRga implements JsonNode { public readonly id: ITimestampStruct; public span: number; @@ -60,19 +64,29 @@ export class StringChunk implements Chunk { } } +/** + * Represents the `str` type in JSON CRDT. The `str` type is a RGA (Replicated + * Growable Array) of UTF-16 code units. + * + * @category CRDT Node + */ export class StringRga extends AbstractRga implements JsonNode { // ----------------------------------------------------------------- JsonNode + /** @ignore */ public children() {} + /** @ignore */ public child() { return undefined; } + /** @ignore */ public container(): JsonNode | undefined { return undefined; } + /** @ignore */ private _view: string = ''; public view(): string { if (this._view) return this._view; @@ -83,14 +97,17 @@ export class StringRga extends AbstractRga implements JsonNode { return (this._view = str); } + /** @ignore */ public api: undefined | unknown = undefined; // -------------------------------------------------------------- AbstractRga + /** @ignore */ public createChunk(id: ITimestampStruct, str: string | undefined): StringChunk { return new StringChunk(id, str ? str.length : 0, str || ''); } + /** @ignore */ protected onChange(): void { this._view = ''; } diff --git a/src/json-crdt/types/rga/AbstractRga.ts b/src/json-crdt/types/rga/AbstractRga.ts index bdd92ff4e8..8f8202d1c7 100644 --- a/src/json-crdt/types/rga/AbstractRga.ts +++ b/src/json-crdt/types/rga/AbstractRga.ts @@ -16,6 +16,9 @@ import {printBinary} from '../../../util/print/printBinary'; import {printTree} from '../../../util/print/printTree'; import {ORIGIN} from '../../../json-crdt-patch/constants'; +/** + * @category CRDT Node + */ export interface Chunk { /** Unique sortable ID of this chunk and its span. */ id: ITimestampStruct; @@ -110,6 +113,9 @@ const prev = (curr: Chunk): Chunk | undefined => { return p; }; +/** + * @category CRDT Node + */ export abstract class AbstractRga { public root: Chunk | undefined = undefined; public ids: Chunk | undefined = undefined; diff --git a/src/json-crdt/types/types.ts b/src/json-crdt/types/types.ts index f2d8b71984..b2fbe84edc 100644 --- a/src/json-crdt/types/types.ts +++ b/src/json-crdt/types/types.ts @@ -34,7 +34,7 @@ export interface JsonNode extends Identifiable { container(): JsonNode | undefined; /** - * Instance which provides public API for this node. + * A singleton cache, instance which provides public API for this node. */ api: undefined | unknown; // JsonNodeApi; } diff --git a/src/json-expression/__bench__/main.ts b/src/json-expression/__bench__/main.ts index 290778330c..3114234808 100644 --- a/src/json-expression/__bench__/main.ts +++ b/src/json-expression/__bench__/main.ts @@ -1,9 +1,13 @@ /* tslint:disable no-console */ +// npx ts-node src/json-expression/__bench__/main.ts + import * as Benchmark from 'benchmark'; import {JsonExpressionCodegen} from '../codegen'; import {Expr} from '../types'; import {evaluate} from '../evaluate'; +import {operatorsMap} from '../operators'; +import {Vars} from '../Vars'; const jsonLogic = require('json-logic-js'); const json = { @@ -40,21 +44,21 @@ const jsonLogicExpression = { ], }; -const codegen = new JsonExpressionCodegen({expression}); +const codegen = new JsonExpressionCodegen({expression, operators: operatorsMap}); const fn = codegen.run().compile(); const suite = new Benchmark.Suite(); suite .add(`json-joy/json-expression JsonExpressionCodegen`, () => { - fn({data: json}); + fn({vars: new Vars(json)}); }) .add(`json-joy/json-expression JsonExpressionCodegen with codegen`, () => { - const codegen = new JsonExpressionCodegen({expression}); + const codegen = new JsonExpressionCodegen({expression, operators: operatorsMap}); const fn = codegen.run().compile(); - fn({data: json}); + fn({vars: new Vars(json)}); }) .add(`json-joy/json-expression evaluate`, () => { - evaluate(expression, {data: json}); + evaluate(expression, {vars: new Vars(json)}); }) .add(`json-logic-js`, () => { jsonLogic.apply(jsonLogicExpression, json); diff --git a/src/util/print/types.ts b/src/util/print/types.ts index 326dfa4b66..801a45d101 100644 --- a/src/util/print/types.ts +++ b/src/util/print/types.ts @@ -1,3 +1,8 @@ export interface Printable { - toString(tab: string): string; + /** + * Returns a human-readable tabbed string representation of the object as a tree. + * + * @param tab String to use for indentation. + */ + toString(tab?: string): string; }