From 07f4c8a71e40a9c205267276fa8765a615d5ab2c Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 2 May 2024 14:56:51 +0200 Subject: [PATCH 01/37] =?UTF-8?q?ci:=20=F0=9F=8E=A1=20run=20tests=20on=20N?= =?UTF-8?q?ode=20v22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/gh-pages.yml | 2 +- .github/workflows/pr.yml | 6 +++--- .github/workflows/release.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 73c2d77f04..65e6f69cbf 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20.x] + node-version: [22.x] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 34201655b6..b8e9c07b68 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20.x] + node-version: [20.x, 22.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20.x] + node-version: [20.x, 22.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} @@ -48,7 +48,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20.x] + node-version: [22.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e2bc9e0450..c3f487d32a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20.x] + node-version: [22.x] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} From 032bab1ec3fc65d20f5670cc7566b34bb457c93b Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 2 May 2024 19:24:02 +0200 Subject: [PATCH 02/37] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20define=20Peritext=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/constants.ts | 7 +++ src/json-crdt-extensions/index.ts | 1 + .../peritext/PeritextApi.ts | 6 +++ .../peritext/PeritextNode.ts | 43 +++++++++++++++++++ .../peritext/constants.ts | 26 +++++++++++ .../peritext/editor/Editor.ts | 2 +- src/json-crdt-extensions/peritext/index.ts | 15 +++++++ .../peritext/slice/Slices.ts | 1 + src/json-crdt-extensions/peritext/types.ts | 8 ++++ src/json-crdt/extensions/index.ts | 5 ++- 10 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 src/json-crdt-extensions/peritext/PeritextApi.ts create mode 100644 src/json-crdt-extensions/peritext/PeritextNode.ts create mode 100644 src/json-crdt-extensions/peritext/index.ts diff --git a/src/json-crdt-extensions/constants.ts b/src/json-crdt-extensions/constants.ts index 2364d76b94..cc81ad74f0 100644 --- a/src/json-crdt-extensions/constants.ts +++ b/src/json-crdt-extensions/constants.ts @@ -4,3 +4,10 @@ export const enum ExtensionId { peritext = 2, quill = 3, } + +export enum ExtensionName { + mval = ExtensionId.mval, + cnt = ExtensionId.cnt, + peritext = ExtensionId.peritext, + quill = ExtensionId.quill, +} diff --git a/src/json-crdt-extensions/index.ts b/src/json-crdt-extensions/index.ts index 745c3b1ab5..955c7056c2 100644 --- a/src/json-crdt-extensions/index.ts +++ b/src/json-crdt-extensions/index.ts @@ -1,2 +1,3 @@ export * from './mval'; export * from './cnt'; +export * from './peritext'; diff --git a/src/json-crdt-extensions/peritext/PeritextApi.ts b/src/json-crdt-extensions/peritext/PeritextApi.ts new file mode 100644 index 0000000000..57deb7345e --- /dev/null +++ b/src/json-crdt-extensions/peritext/PeritextApi.ts @@ -0,0 +1,6 @@ +import {NodeApi} from '../../json-crdt/model/api/nodes'; +import type {PeritextNode} from './PeritextNode'; +import type {ExtensionApi} from '../../json-crdt'; + +export class PeritextApi extends NodeApi implements ExtensionApi { +} diff --git a/src/json-crdt-extensions/peritext/PeritextNode.ts b/src/json-crdt-extensions/peritext/PeritextNode.ts new file mode 100644 index 0000000000..170b72232a --- /dev/null +++ b/src/json-crdt-extensions/peritext/PeritextNode.ts @@ -0,0 +1,43 @@ +import {printTree} from 'tree-dump/lib/printTree'; +import {MNEMONIC} from './constants'; +import type {ITimestampStruct} from '../../json-crdt-patch/clock'; +import type {ExtensionJsonNode, JsonNode} from '../../json-crdt'; +import type {Printable} from 'tree-dump/lib/types'; +import type {PeritextDataNode} from './types'; + +export class PeritextNode implements ExtensionJsonNode, Printable { + public readonly id: ITimestampStruct; + + constructor(public readonly data: PeritextDataNode) { + this.id = data.id; + } + + // -------------------------------------------------------- ExtensionJsonNode + + public name(): string { + return MNEMONIC; + } + + public view(): string { + const str = this.data.get(0)!; + return str.view(); + } + + public children(callback: (node: JsonNode) => void): void {} + + public child?(): PeritextDataNode { + return this.data; + } + + public container(): JsonNode | undefined { + return this.data.container(); + } + + public api: undefined | unknown = undefined; + + // ---------------------------------------------------------------- Printable + + public toString(tab?: string): string { + return this.name() + printTree(tab, [(tab) => this.data.toString(tab)]); + } +} diff --git a/src/json-crdt-extensions/peritext/constants.ts b/src/json-crdt-extensions/peritext/constants.ts index 05532030bb..a6191aa6c3 100644 --- a/src/json-crdt-extensions/peritext/constants.ts +++ b/src/json-crdt-extensions/peritext/constants.ts @@ -1,3 +1,29 @@ +import {nodes, s} from "../../json-crdt-patch"; +import {ExtensionId, ExtensionName} from "../constants"; +import {SliceSchema} from "./slice/types"; + export const enum Chars { BlockSplitSentinel = '\n', } + +export const MNEMONIC = ExtensionName[ExtensionId.peritext]; + +export const BUILD_SCHEMA = (text: string) => ( + s.vec<[ + /** + * The text of the node. All rich-text textual data is stored in this node. + */ + str: nodes.str, + + /** + * The slices of the node. All rich-text annotations are stored in this + * node. + */ + slices: nodes.arr, + ]>( + s.str(text), + s.arr([]), + ) +); + +export const SCHEMA = BUILD_SCHEMA(''); diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index d66f1e2adc..d20ceee1da 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -13,7 +13,7 @@ import type {MarkerSlice} from '../slice/MarkerSlice'; import type {Slices} from '../slice/Slices'; /** - * Rename to `PeritextApi`. + * @todo Rename to `PeritextApi`. */ export class Editor implements Printable { /** diff --git a/src/json-crdt-extensions/peritext/index.ts b/src/json-crdt-extensions/peritext/index.ts new file mode 100644 index 0000000000..05d1166503 --- /dev/null +++ b/src/json-crdt-extensions/peritext/index.ts @@ -0,0 +1,15 @@ +import {ext} from '../../json-crdt/extensions'; +import {ExtensionId} from '../constants'; +import {PeritextNode} from './PeritextNode'; +import {PeritextApi} from './PeritextApi'; +import {BUILD_SCHEMA, MNEMONIC} from './constants'; +import type {PeritextDataNode} from './types'; +import type {ExtensionDefinition} from '../../json-crdt'; + +export const PeritextExt: ExtensionDefinition = { + id: ExtensionId.peritext, + name: MNEMONIC, + new: (text: string) => ext(ExtensionId.peritext, BUILD_SCHEMA(text)), + Node: PeritextNode, + Api: PeritextApi, +}; diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index bf83f539be..4997c73cb2 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -62,6 +62,7 @@ export class Slices implements Stateful, Printable { if (data !== undefined) tupleKeysUpdate.push([SliceTupleIndex.Data, builder.json(data)]); builder.insVec(tupleId, tupleKeysUpdate); const chunkId = builder.insArr(set.id, set.id, [tupleId]); + // TODO: Consider using `s` schema here. api.apply(); const tuple = model.index.get(tupleId) as VecNode; const chunk = set.findById(chunkId)!; diff --git a/src/json-crdt-extensions/peritext/types.ts b/src/json-crdt-extensions/peritext/types.ts index 44f873dd52..b400bd91b0 100644 --- a/src/json-crdt-extensions/peritext/types.ts +++ b/src/json-crdt-extensions/peritext/types.ts @@ -1,5 +1,10 @@ +import type {SchemaToJsonNode} from "../../json-crdt/schema/types"; +import type {SCHEMA} from "./constants"; + /** * Represents an object which state can change over time. + * + * @todo Move to /src/utils. */ export interface Stateful { /** @@ -13,3 +18,6 @@ export interface Stateful { */ refresh(): number; } + +export type PeritextDataNodeSchema = typeof SCHEMA; +export type PeritextDataNode = SchemaToJsonNode; diff --git a/src/json-crdt/extensions/index.ts b/src/json-crdt/extensions/index.ts index 2b5446622d..ea3cbe3ff6 100644 --- a/src/json-crdt/extensions/index.ts +++ b/src/json-crdt/extensions/index.ts @@ -1,8 +1,11 @@ import {PatchBuilder} from '../../json-crdt-patch/PatchBuilder'; import {NodeBuilder} from '../../json-crdt-patch/builder/DelayedValueBuilder'; import {konst} from '../../json-crdt-patch/builder/Konst'; -import {ITimestampStruct} from '../../json-crdt-patch/clock'; +import type {ITimestampStruct} from '../../json-crdt-patch/clock'; +/** + * @todo Replace this by `s` builder. + */ export const ext = (extensionId: number, nodeBuilder: NodeBuilder) => new NodeBuilder((builder: PatchBuilder): ITimestampStruct => { // Extension tuple starts with a 3-byte header: From 5240f08fc93c2e70a730bd757adcc545b0b48973 Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 2 May 2024 19:28:36 +0200 Subject: [PATCH 03/37] =?UTF-8?q?style(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=84=20run=20Prettier=20and=20remove=20todo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/PeritextApi.ts | 3 +- .../peritext/constants.ts | 38 +++++++++---------- src/json-crdt-extensions/peritext/types.ts | 6 +-- src/json-crdt/extensions/index.ts | 3 -- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/json-crdt-extensions/peritext/PeritextApi.ts b/src/json-crdt-extensions/peritext/PeritextApi.ts index 57deb7345e..266dda972b 100644 --- a/src/json-crdt-extensions/peritext/PeritextApi.ts +++ b/src/json-crdt-extensions/peritext/PeritextApi.ts @@ -2,5 +2,4 @@ import {NodeApi} from '../../json-crdt/model/api/nodes'; import type {PeritextNode} from './PeritextNode'; import type {ExtensionApi} from '../../json-crdt'; -export class PeritextApi extends NodeApi implements ExtensionApi { -} +export class PeritextApi extends NodeApi implements ExtensionApi {} diff --git a/src/json-crdt-extensions/peritext/constants.ts b/src/json-crdt-extensions/peritext/constants.ts index a6191aa6c3..7d5a772248 100644 --- a/src/json-crdt-extensions/peritext/constants.ts +++ b/src/json-crdt-extensions/peritext/constants.ts @@ -1,6 +1,6 @@ -import {nodes, s} from "../../json-crdt-patch"; -import {ExtensionId, ExtensionName} from "../constants"; -import {SliceSchema} from "./slice/types"; +import {nodes, s} from '../../json-crdt-patch'; +import {ExtensionId, ExtensionName} from '../constants'; +import {SliceSchema} from './slice/types'; export const enum Chars { BlockSplitSentinel = '\n', @@ -8,22 +8,20 @@ export const enum Chars { export const MNEMONIC = ExtensionName[ExtensionId.peritext]; -export const BUILD_SCHEMA = (text: string) => ( - s.vec<[ - /** - * The text of the node. All rich-text textual data is stored in this node. - */ - str: nodes.str, - - /** - * The slices of the node. All rich-text annotations are stored in this - * node. - */ - slices: nodes.arr, - ]>( - s.str(text), - s.arr([]), - ) -); +export const BUILD_SCHEMA = (text: string) => + s.vec< + [ + /** + * The text of the node. All rich-text textual data is stored in this node. + */ + str: nodes.str, + + /** + * The slices of the node. All rich-text annotations are stored in this + * node. + */ + slices: nodes.arr, + ] + >(s.str(text), s.arr([])); export const SCHEMA = BUILD_SCHEMA(''); diff --git a/src/json-crdt-extensions/peritext/types.ts b/src/json-crdt-extensions/peritext/types.ts index b400bd91b0..c4f516049a 100644 --- a/src/json-crdt-extensions/peritext/types.ts +++ b/src/json-crdt-extensions/peritext/types.ts @@ -1,9 +1,9 @@ -import type {SchemaToJsonNode} from "../../json-crdt/schema/types"; -import type {SCHEMA} from "./constants"; +import type {SchemaToJsonNode} from '../../json-crdt/schema/types'; +import type {SCHEMA} from './constants'; /** * Represents an object which state can change over time. - * + * * @todo Move to /src/utils. */ export interface Stateful { diff --git a/src/json-crdt/extensions/index.ts b/src/json-crdt/extensions/index.ts index ea3cbe3ff6..c55d43d037 100644 --- a/src/json-crdt/extensions/index.ts +++ b/src/json-crdt/extensions/index.ts @@ -3,9 +3,6 @@ import {NodeBuilder} from '../../json-crdt-patch/builder/DelayedValueBuilder'; import {konst} from '../../json-crdt-patch/builder/Konst'; import type {ITimestampStruct} from '../../json-crdt-patch/clock'; -/** - * @todo Replace this by `s` builder. - */ export const ext = (extensionId: number, nodeBuilder: NodeBuilder) => new NodeBuilder((builder: PatchBuilder): ITimestampStruct => { // Extension tuple starts with a 3-byte header: From 5a79cec92871e9899d0206e5ff964445f30b0287 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Thu, 2 May 2024 21:07:47 +0200 Subject: [PATCH 04/37] =?UTF-8?q?feat(json-crdt):=20=F0=9F=8E=B8=20add=20a?= =?UTF-8?q?bstract=20ExtNode=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/PeritextNode.ts | 33 ++--------------- src/json-crdt-extensions/peritext/index.ts | 1 + src/json-crdt/extensions/ExtNode.ts | 35 +++++++++++++++++++ 3 files changed, 39 insertions(+), 30 deletions(-) create mode 100644 src/json-crdt/extensions/ExtNode.ts diff --git a/src/json-crdt-extensions/peritext/PeritextNode.ts b/src/json-crdt-extensions/peritext/PeritextNode.ts index 170b72232a..d627e1c08d 100644 --- a/src/json-crdt-extensions/peritext/PeritextNode.ts +++ b/src/json-crdt-extensions/peritext/PeritextNode.ts @@ -1,18 +1,9 @@ -import {printTree} from 'tree-dump/lib/printTree'; import {MNEMONIC} from './constants'; -import type {ITimestampStruct} from '../../json-crdt-patch/clock'; -import type {ExtensionJsonNode, JsonNode} from '../../json-crdt'; -import type {Printable} from 'tree-dump/lib/types'; +import {ExtNode} from '../../json-crdt/extensions/ExtNode';; import type {PeritextDataNode} from './types'; -export class PeritextNode implements ExtensionJsonNode, Printable { - public readonly id: ITimestampStruct; - - constructor(public readonly data: PeritextDataNode) { - this.id = data.id; - } - - // -------------------------------------------------------- ExtensionJsonNode +export class PeritextNode extends ExtNode { + // ------------------------------------------------------------------ ExtNode public name(): string { return MNEMONIC; @@ -22,22 +13,4 @@ export class PeritextNode implements ExtensionJsonNode, Printable { const str = this.data.get(0)!; return str.view(); } - - public children(callback: (node: JsonNode) => void): void {} - - public child?(): PeritextDataNode { - return this.data; - } - - public container(): JsonNode | undefined { - return this.data.container(); - } - - public api: undefined | unknown = undefined; - - // ---------------------------------------------------------------- Printable - - public toString(tab?: string): string { - return this.name() + printTree(tab, [(tab) => this.data.toString(tab)]); - } } diff --git a/src/json-crdt-extensions/peritext/index.ts b/src/json-crdt-extensions/peritext/index.ts index 05d1166503..45fbdb4813 100644 --- a/src/json-crdt-extensions/peritext/index.ts +++ b/src/json-crdt-extensions/peritext/index.ts @@ -9,6 +9,7 @@ import type {ExtensionDefinition} from '../../json-crdt'; export const PeritextExt: ExtensionDefinition = { id: ExtensionId.peritext, name: MNEMONIC, + // TODO: Simplify to `BUILD_SCHEMA(text)`. new: (text: string) => ext(ExtensionId.peritext, BUILD_SCHEMA(text)), Node: PeritextNode, Api: PeritextApi, diff --git a/src/json-crdt/extensions/ExtNode.ts b/src/json-crdt/extensions/ExtNode.ts new file mode 100644 index 0000000000..12bad7851c --- /dev/null +++ b/src/json-crdt/extensions/ExtNode.ts @@ -0,0 +1,35 @@ +import {printTree} from 'tree-dump/lib/printTree'; +import type {ITimestampStruct} from '../../json-crdt-patch/clock'; +import type {ExtensionJsonNode, JsonNode} from '..'; +import type {Printable} from 'tree-dump/lib/types'; + +export abstract class ExtNode implements ExtensionJsonNode, Printable { + public readonly id: ITimestampStruct; + + constructor(public readonly data: N) { + this.id = data.id; + } + + // -------------------------------------------------------- ExtensionJsonNode + + public abstract name(): string; + public abstract view(): unknown; + + public children(callback: (node: JsonNode) => void): void {} + + public child?(): N { + return this.data; + } + + public container(): JsonNode | undefined { + return this.data.container(); + } + + public api: undefined | unknown = undefined; + + // ---------------------------------------------------------------- Printable + + public toString(tab?: string): string { + return this.name() + printTree(tab, [(tab) => this.data.toString(tab)]); + } +} From 69e120048d5132bfcc7141243cdfbaaf7d6e9971 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Thu, 2 May 2024 21:09:20 +0200 Subject: [PATCH 05/37] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20setup=20ModelWithExt=20static=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/ModelWithExt.ts | 35 +++++++++++++++++++ .../peritext/__tests__/extension.spec.ts | 11 ++++++ src/json-crdt/model/Model.ts | 1 + 3 files changed, 47 insertions(+) create mode 100644 src/json-crdt-extensions/ModelWithExt.ts create mode 100644 src/json-crdt-extensions/peritext/__tests__/extension.spec.ts diff --git a/src/json-crdt-extensions/ModelWithExt.ts b/src/json-crdt-extensions/ModelWithExt.ts new file mode 100644 index 0000000000..5652275818 --- /dev/null +++ b/src/json-crdt-extensions/ModelWithExt.ts @@ -0,0 +1,35 @@ +import * as clock from '../json-crdt-patch/clock'; +import {NodeBuilder} from "../json-crdt-patch"; +import {Extensions} from "../json-crdt/extensions/Extensions"; +import {Model} from "../json-crdt/model"; +import {SchemaToJsonNode} from "../json-crdt/schema/types"; +import {CntExt} from './cnt'; +import {MvalExt} from "./mval"; +import {PeritextExt} from "./peritext"; + +const extensions = new Extensions(); + +extensions.register(CntExt); +extensions.register(MvalExt); +extensions.register(PeritextExt); + +export class ModelWithExt { + public static readonly create = ( + schema?: S, + sidOrClock: clock.ClockVector | number = Model.sid(), + ): Model> => { + const model = Model.create(schema, sidOrClock); + model.ext = extensions; + return model; + }; + + public static readonly load = ( + data: Uint8Array, + sid?: number, + schema?: S, + ): Model> => { + const model = Model.load(data, sid, schema); + model.ext = extensions; + return model; + }; +} diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts new file mode 100644 index 0000000000..453a753f6d --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -0,0 +1,11 @@ +import {PeritextExt} from ".."; +import {ModelWithExt} from "../../ModelWithExt"; + +test('..', () => { + const model = ModelWithExt.create(); + model.api.root({ + text: PeritextExt.new('Hello, world\n'), + }); + + console.log('' + model); +}); diff --git a/src/json-crdt/model/Model.ts b/src/json-crdt/model/Model.ts index 8a5924ad35..486354d38c 100644 --- a/src/json-crdt/model/Model.ts +++ b/src/json-crdt/model/Model.ts @@ -224,6 +224,7 @@ export class Model> implements Printable { * custom data types on top of the JSON CRDT protocol. * * @ignore + * @todo Allow this to be `undefined`. */ public ext: Extensions = new Extensions(); From ed6a55827100209aba432cdabcdf5c07a0603283 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Thu, 2 May 2024 23:27:12 +0200 Subject: [PATCH 06/37] =?UTF-8?q?feat(json-crdt-patch):=20=F0=9F=8E=B8=20a?= =?UTF-8?q?dd=20node.json=20schema=20builder=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-patch/builder/schema.ts | 52 ++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/json-crdt-patch/builder/schema.ts b/src/json-crdt-patch/builder/schema.ts index 5e63ec4d49..92d9d8e8fe 100644 --- a/src/json-crdt-patch/builder/schema.ts +++ b/src/json-crdt-patch/builder/schema.ts @@ -1,5 +1,5 @@ -import type {ITimestampStruct} from '../clock'; import {NodeBuilder} from './DelayedValueBuilder'; +import type {ITimestampStruct} from '../clock'; /* tslint:disable no-namespace class-name */ @@ -15,7 +15,7 @@ export namespace nodes { * * Example: * - * ```typescript + * ```ts * s.con(0); * s.con(''); * s.con(123); @@ -36,7 +36,7 @@ export namespace nodes { * * Example: * - * ```typescript + * ```ts * s.str(''); * s.str('hello'); * s.str('world'); @@ -68,7 +68,7 @@ export namespace nodes { * * Example: * - * ```typescript + * ```ts * s.val(s.con(0)); * s.val(s.str('')); * s.val(s.str('hello')); @@ -93,7 +93,7 @@ export namespace nodes { * * Example: * - * ```typescript + * ```ts * s.vec(s.con(0), s.con(1)); * s.vec(s.str(''), s.str('hello')); * ``` @@ -126,7 +126,7 @@ export namespace nodes { * * Example: * - * ```typescript + * ```ts * s.obj({ * name: s.str(''), * age: s.con(0), @@ -172,7 +172,7 @@ export namespace nodes { * * Example: * - * ```typescript + * ```ts * s.map> * ``` */ @@ -184,7 +184,7 @@ export namespace nodes { * * Example: * - * ```typescript + * ```ts * s.arr([s.con(0), s.con(1)]); * s.arr([s.str(''), s.str('hello')]); * ``` @@ -205,6 +205,25 @@ export namespace nodes { }); } } + + /** + * Convenience class for recursively creating a node tree from any POJO. It + * uses the {@link Builder.json} method to create a JSON node. It can be used + * similar to TypeScript's *any* type, where the value can be anything. + * + * Example: + * + * ```typescript + * s.json({name: 'Alice', age: 30}); + * ``` + */ + export class json extends NodeBuilder { + public readonly type = 'json'; + + constructor(public readonly value: unknown) { + super((builder) => builder.json(value)); + } + } } /* tslint:enable no-namespace class-name */ @@ -223,36 +242,42 @@ export namespace nodes { export const schema = { /** * Creates a "con" node schema and the default value. + * * @param raw Raw default value. */ con: (raw: T) => new nodes.con(raw), /** * Creates a "str" node schema and the default value. + * * @param str Default value. */ str: (str: T) => new nodes.str(str || ('' as T)), /** * Creates a "bin" node schema and the default value. + * * @param bin Default value. */ bin: (bin: Uint8Array) => new nodes.bin(bin), /** * Creates a "val" node schema and the default value. + * * @param val Default value. */ val: (val: T) => new nodes.val(val), /** * Creates a "vec" node schema and the default value. + * * @param vec Default value. */ vec: (...vec: T) => new nodes.vec(vec), /** * Creates a "obj" node schema and the default value. + * * @param obj Default value, required object keys. * @param opt Default value of optional object keys. */ @@ -263,6 +288,7 @@ export const schema = { * This is an alias for {@link schema.obj}. It creates a "map" node schema, * which is an object where a key can be any string and the value is of the * same type. + * * @param obj Default value. */ map: (obj: Record): nodes.map => @@ -270,9 +296,19 @@ export const schema = { /** * Creates an "arr" node schema and the default value. + * * @param arr Default value. */ arr: (arr: T[]) => new nodes.arr(arr), + + /** + * Recursively creates a node tree from any POJO. It uses the + * {@link Builder.json} method to create a JSON node. It can be used similar + * to TypeScript's *any* type, where the value can be anything. + * + * @param value Default value. + */ + json: (value: unknown) => new nodes.json(value), }; /** From 9ec8db15c2b489b9e57d73654229e7ceeab19b17 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Thu, 2 May 2024 23:28:14 +0200 Subject: [PATCH 07/37] =?UTF-8?q?refactor(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=A1=20cleanupt=20the=20mval=20extension=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mval/{ValueMvApi.ts => MvalApi.ts} | 4 +- src/json-crdt-extensions/mval/MvalNode.ts | 15 +++++++ src/json-crdt-extensions/mval/ValueMv.ts | 41 ------------------- src/json-crdt-extensions/mval/constants.ts | 3 ++ src/json-crdt-extensions/mval/index.ts | 21 ++++------ 5 files changed, 29 insertions(+), 55 deletions(-) rename src/json-crdt-extensions/mval/{ValueMvApi.ts => MvalApi.ts} (77%) create mode 100644 src/json-crdt-extensions/mval/MvalNode.ts delete mode 100644 src/json-crdt-extensions/mval/ValueMv.ts create mode 100644 src/json-crdt-extensions/mval/constants.ts diff --git a/src/json-crdt-extensions/mval/ValueMvApi.ts b/src/json-crdt-extensions/mval/MvalApi.ts similarity index 77% rename from src/json-crdt-extensions/mval/ValueMvApi.ts rename to src/json-crdt-extensions/mval/MvalApi.ts index bf145a97fe..dc05a46771 100644 --- a/src/json-crdt-extensions/mval/ValueMvApi.ts +++ b/src/json-crdt-extensions/mval/MvalApi.ts @@ -1,8 +1,8 @@ import {ArrApi, NodeApi} from '../../json-crdt/model/api/nodes'; -import type {ValueMv} from './ValueMv'; +import type {MvalNode} from './MvalNode'; import type {ExtensionApi} from '../../json-crdt'; -export class ValueMvApi extends NodeApi implements ExtensionApi { +export class MvalApi extends NodeApi implements ExtensionApi { public set(json: unknown): this { const {api, node} = this; const builder = api.builder; diff --git a/src/json-crdt-extensions/mval/MvalNode.ts b/src/json-crdt-extensions/mval/MvalNode.ts new file mode 100644 index 0000000000..10faee1abc --- /dev/null +++ b/src/json-crdt-extensions/mval/MvalNode.ts @@ -0,0 +1,15 @@ +import {MNEMONIC} from './constants'; +import {ExtNode} from '../../json-crdt/extensions/ExtNode'; +import type {ArrNode} from '../../json-crdt/nodes/arr/ArrNode'; + +export class MvalNode extends ExtNode { + // ------------------------------------------------------------------ ExtNode + + public name(): string { + return MNEMONIC; + } + + public view(): unknown[] { + return this.data.view(); + } +} diff --git a/src/json-crdt-extensions/mval/ValueMv.ts b/src/json-crdt-extensions/mval/ValueMv.ts deleted file mode 100644 index ebaffc2818..0000000000 --- a/src/json-crdt-extensions/mval/ValueMv.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {ArrNode} from '../../json-crdt/nodes/arr/ArrNode'; -import {printTree} from 'tree-dump/lib/printTree'; -import type {ITimestampStruct} from '../../json-crdt-patch/clock'; -import type {ExtensionJsonNode, JsonNode} from '../../json-crdt'; -import type {Printable} from 'tree-dump/lib/types'; - -export class ValueMv implements ExtensionJsonNode, Printable { - public readonly id: ITimestampStruct; - - constructor(public readonly data: ArrNode) { - this.id = data.id; - } - - // -------------------------------------------------------- ExtensionJsonNode - - public name(): string { - return 'mval'; - } - - public view(): unknown[] { - return this.data.view(); - } - - public children(callback: (node: JsonNode) => void): void {} - - public child?(): JsonNode | undefined { - return this.data; - } - - public container(): JsonNode | undefined { - return this.data.container(); - } - - public api: undefined | unknown = undefined; - - // ---------------------------------------------------------------- Printable - - public toString(tab?: string): string { - return this.name() + printTree(tab, [(tab) => this.data.toString(tab)]); - } -} diff --git a/src/json-crdt-extensions/mval/constants.ts b/src/json-crdt-extensions/mval/constants.ts new file mode 100644 index 0000000000..5a5ea09669 --- /dev/null +++ b/src/json-crdt-extensions/mval/constants.ts @@ -0,0 +1,3 @@ +import {ExtensionId, ExtensionName} from '../constants'; + +export const MNEMONIC = ExtensionName[ExtensionId.mval]; diff --git a/src/json-crdt-extensions/mval/index.ts b/src/json-crdt-extensions/mval/index.ts index b1e64acc9f..3ad1fe38ba 100644 --- a/src/json-crdt-extensions/mval/index.ts +++ b/src/json-crdt-extensions/mval/index.ts @@ -1,20 +1,17 @@ -import {delayed} from '../../json-crdt-patch/builder/DelayedValueBuilder'; import {ext} from '../../json-crdt/extensions'; import {ExtensionId} from '../constants'; -import {ValueMv} from './ValueMv'; -import {ValueMvApi} from './ValueMvApi'; +import {MvalNode} from './MvalNode'; +import {MvalApi} from './MvalApi'; +import {MNEMONIC} from './constants'; import type {ITimestampStruct} from '../../json-crdt-patch/clock'; import type {ArrNode} from '../../json-crdt/nodes/arr/ArrNode'; -import type {ExtensionDefinition} from '../../json-crdt'; +import {s, type ExtensionDefinition} from '../../json-crdt'; -export const MvalExt: ExtensionDefinition = { +export const MvalExt: ExtensionDefinition = { id: ExtensionId.mval, - name: 'mval', + name: MNEMONIC, new: (value: unknown | ITimestampStruct) => - ext( - ExtensionId.mval, - delayed((builder) => builder.jsonArr([value])), - ), - Node: ValueMv, - Api: ValueMvApi, + ext(ExtensionId.mval, s.arr([s.json(value)])), + Node: MvalNode, + Api: MvalApi, }; From 0f7910b938301f4025ed2067824085cde7aefe67 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 3 May 2024 00:26:41 +0200 Subject: [PATCH 08/37] =?UTF-8?q?feat(json-crdt-patch):=20=F0=9F=8E=B8=20a?= =?UTF-8?q?dd=20s.ext()=20schema=20builder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/cnt/index.ts | 10 +-- src/json-crdt-extensions/mval/index.ts | 4 +- src/json-crdt-extensions/peritext/index.ts | 6 +- src/json-crdt-patch/builder/schema.ts | 88 +++++++++++++++++++--- src/json-crdt/extensions/index.ts | 23 ------ 5 files changed, 82 insertions(+), 49 deletions(-) delete mode 100644 src/json-crdt/extensions/index.ts diff --git a/src/json-crdt-extensions/cnt/index.ts b/src/json-crdt-extensions/cnt/index.ts index b7363e07ab..24f0b8bb9c 100644 --- a/src/json-crdt-extensions/cnt/index.ts +++ b/src/json-crdt-extensions/cnt/index.ts @@ -1,9 +1,7 @@ -import {delayed} from '../../json-crdt-patch/builder/DelayedValueBuilder'; -import {ext} from '../../json-crdt/extensions'; import {ExtensionId} from '../constants'; import {printTree} from 'tree-dump/lib/printTree'; import {NodeApi} from '../../json-crdt/model/api/nodes'; -import type {ExtensionDefinition, ObjNode} from '../../json-crdt'; +import {s, type ExtensionDefinition, type ObjNode} from '../../json-crdt'; import type {ITimestampStruct} from '../../json-crdt-patch/clock'; import type {ExtensionJsonNode, JsonNode} from '../../json-crdt'; import type {Printable} from 'tree-dump/lib/types'; @@ -68,11 +66,7 @@ class CntApi extends NodeApi implements ExtensionApi { export const CntExt: ExtensionDefinition = { id: ExtensionId.cnt, name, - new: (value?: number, sid: number = 0) => - ext( - ExtensionId.cnt, - delayed((builder) => builder.constOrJson(value ? {[sid]: value} : {})), - ), + new: (value?: number, sid: number = 0) => s.ext(ExtensionId.cnt, s.obj({[sid]: s.jsonCon(value)})), Node: CntNode, Api: CntApi, }; diff --git a/src/json-crdt-extensions/mval/index.ts b/src/json-crdt-extensions/mval/index.ts index 3ad1fe38ba..c13c5bd3bf 100644 --- a/src/json-crdt-extensions/mval/index.ts +++ b/src/json-crdt-extensions/mval/index.ts @@ -1,4 +1,3 @@ -import {ext} from '../../json-crdt/extensions'; import {ExtensionId} from '../constants'; import {MvalNode} from './MvalNode'; import {MvalApi} from './MvalApi'; @@ -10,8 +9,7 @@ import {s, type ExtensionDefinition} from '../../json-crdt'; export const MvalExt: ExtensionDefinition = { id: ExtensionId.mval, name: MNEMONIC, - new: (value: unknown | ITimestampStruct) => - ext(ExtensionId.mval, s.arr([s.json(value)])), + new: (value: unknown | ITimestampStruct) => s.ext(ExtensionId.mval, s.arr([s.json(value)])), Node: MvalNode, Api: MvalApi, }; diff --git a/src/json-crdt-extensions/peritext/index.ts b/src/json-crdt-extensions/peritext/index.ts index 45fbdb4813..5ace068fd1 100644 --- a/src/json-crdt-extensions/peritext/index.ts +++ b/src/json-crdt-extensions/peritext/index.ts @@ -1,16 +1,14 @@ -import {ext} from '../../json-crdt/extensions'; import {ExtensionId} from '../constants'; import {PeritextNode} from './PeritextNode'; import {PeritextApi} from './PeritextApi'; import {BUILD_SCHEMA, MNEMONIC} from './constants'; import type {PeritextDataNode} from './types'; -import type {ExtensionDefinition} from '../../json-crdt'; +import {s, type ExtensionDefinition} from '../../json-crdt'; export const PeritextExt: ExtensionDefinition = { id: ExtensionId.peritext, name: MNEMONIC, - // TODO: Simplify to `BUILD_SCHEMA(text)`. - new: (text: string) => ext(ExtensionId.peritext, BUILD_SCHEMA(text)), + new: (text: string) => s.ext(ExtensionId.peritext, BUILD_SCHEMA(text)), Node: PeritextNode, Api: PeritextApi, }; diff --git a/src/json-crdt-patch/builder/schema.ts b/src/json-crdt-patch/builder/schema.ts index 92d9d8e8fe..6c62fa5512 100644 --- a/src/json-crdt-patch/builder/schema.ts +++ b/src/json-crdt-patch/builder/schema.ts @@ -25,7 +25,7 @@ export namespace nodes { export class con extends NodeBuilder { public readonly type = 'con'; - constructor(public readonly raw: T) { + constructor(raw: T) { super((builder) => builder.const(raw)); } } @@ -46,7 +46,7 @@ export namespace nodes { export class str extends NodeBuilder { public readonly type = 'str'; - constructor(public readonly raw: T) { + constructor(raw: T) { super((builder) => builder.json(raw)); } } @@ -57,7 +57,7 @@ export namespace nodes { export class bin extends NodeBuilder { public readonly type = 'bin'; - constructor(public readonly raw: Uint8Array) { + constructor(raw: Uint8Array) { super((builder) => builder.json(raw)); } } @@ -77,7 +77,7 @@ export namespace nodes { export class val extends NodeBuilder { public readonly type = 'val'; - constructor(public readonly value: T) { + constructor(value: T) { super((builder) => { const valId = builder.val(); const valueId = value.build(builder); @@ -101,7 +101,7 @@ export namespace nodes { export class vec extends NodeBuilder { public readonly type = 'vec'; - constructor(public readonly value: T) { + constructor(value: T) { super((builder) => { const vecId = builder.vec(); const length = value.length; @@ -139,10 +139,7 @@ export namespace nodes { > extends NodeBuilder { public readonly type = 'obj'; - constructor( - public readonly obj: T, - public readonly opt?: O, - ) { + constructor(obj: T, opt?: O) { super((builder) => { const objId = builder.obj(); const keyValuePairs: [key: string, value: ITimestampStruct][] = []; @@ -192,7 +189,7 @@ export namespace nodes { export class arr extends NodeBuilder { public readonly type = 'arr'; - constructor(public readonly arr: T[]) { + constructor(arr: T[]) { super((builder) => { const arrId = builder.arr(); const length = arr.length; @@ -220,10 +217,62 @@ export namespace nodes { export class json extends NodeBuilder { public readonly type = 'json'; - constructor(public readonly value: unknown) { + constructor(value: unknown) { super((builder) => builder.json(value)); } } + + /** + * Convenience class for recursively creating a node tree from any POJO. It + * uses the {@link Builder.constOrJson} method to create a JSON node. It can + * be used similar to TypeScript's *any* type, where the value can be anything. + * + * Example: + * + * ```typescript + * s.jsonCon({name: 'Alice', age: 30}); + * ``` + */ + export class jsonCon extends NodeBuilder { + public readonly type = 'jsonCon'; + + constructor(value: unknown) { + super((builder) => builder.constOrJson(value)); + } + } + + /** + * Creates an extension node schema. The extension node is a tuple with a + * header and a data node. The header is a 3-byte {@link Uint8Array} with the + * extension ID, the SID of the tuple ID, and the time of the tuple ID. + * + * - 1 byte for the extension id + * - 1 byte for the sid of the tuple id, modulo 256 + * - 1 byte for the time of the tuple id, modulo 256 + */ + export class ext extends NodeBuilder { + public readonly type = 'ext'; + + /** + * @param id A unique extension ID. + * @param data Schema of the data node of the extension. + */ + constructor(id: ID, data: T) { + super((builder) => { + const buf = new Uint8Array([id, 0, 0]); + const tupleId = builder.vec(); + buf[1] = tupleId.sid % 256; + buf[2] = tupleId.time % 256; + const bufId = builder.constOrJson(s.con(buf)); + const valueId = data.build(builder); + builder.insVec(tupleId, [ + [0, bufId], + [1, valueId], + ]); + return tupleId; + }); + } + } } /* tslint:enable no-namespace class-name */ @@ -309,6 +358,23 @@ export const schema = { * @param value Default value. */ json: (value: unknown) => new nodes.json(value), + + /** + * Recursively creates a node tree from any POJO. It uses the + * {@link Builder.constOrJson} method to create a JSON node. It can be used + * similar to TypeScript's *any* type, where the value can be anything. + * + * @param value Default value. + */ + jsonCon: (value: unknown) => new nodes.jsonCon(value), + + /** + * Creates an extension node schema. + * + * @param id A unique extension ID. + * @param data Schema of the data node of the extension. + */ + ext: (id: ID, data: T) => new nodes.ext(id, data), }; /** diff --git a/src/json-crdt/extensions/index.ts b/src/json-crdt/extensions/index.ts deleted file mode 100644 index c55d43d037..0000000000 --- a/src/json-crdt/extensions/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {PatchBuilder} from '../../json-crdt-patch/PatchBuilder'; -import {NodeBuilder} from '../../json-crdt-patch/builder/DelayedValueBuilder'; -import {konst} from '../../json-crdt-patch/builder/Konst'; -import type {ITimestampStruct} from '../../json-crdt-patch/clock'; - -export const ext = (extensionId: number, nodeBuilder: NodeBuilder) => - new NodeBuilder((builder: PatchBuilder): ITimestampStruct => { - // Extension tuple starts with a 3-byte header: - // - 1 byte for the extension id - // - 1 byte for the sid of the tuple id, modulo 256 - // - 1 byte for the time of the tuple id, modulo 256 - const buf = new Uint8Array([extensionId, 0, 0]); - const tupleId = builder.vec(); - buf[1] = tupleId.sid % 256; - buf[2] = tupleId.time % 256; - const bufId = builder.constOrJson(konst(buf)); - const valueId = nodeBuilder.build(builder); - builder.insVec(tupleId, [ - [0, bufId], - [1, valueId], - ]); - return tupleId; - }); From a571a687f41a54e9ab102911d36f7b51921f40a3 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 3 May 2024 00:39:16 +0200 Subject: [PATCH 09/37] =?UTF-8?q?refactor(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=A1=20cleanup=20cnt=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/cnt/index.ts | 41 +++++---------------------- 1 file changed, 7 insertions(+), 34 deletions(-) diff --git a/src/json-crdt-extensions/cnt/index.ts b/src/json-crdt-extensions/cnt/index.ts index 24f0b8bb9c..1a0c156558 100644 --- a/src/json-crdt-extensions/cnt/index.ts +++ b/src/json-crdt-extensions/cnt/index.ts @@ -1,25 +1,16 @@ -import {ExtensionId} from '../constants'; -import {printTree} from 'tree-dump/lib/printTree'; +import {ExtensionId, ExtensionName} from '../constants'; import {NodeApi} from '../../json-crdt/model/api/nodes'; +import {ExtNode} from '../../json-crdt/extensions/ExtNode'; import {s, type ExtensionDefinition, type ObjNode} from '../../json-crdt'; -import type {ITimestampStruct} from '../../json-crdt-patch/clock'; -import type {ExtensionJsonNode, JsonNode} from '../../json-crdt'; -import type {Printable} from 'tree-dump/lib/types'; import type {ExtensionApi} from '../../json-crdt'; -const name = 'cnt'; +const MNEMONIC = ExtensionName[ExtensionId.cnt]; -class CntNode implements ExtensionJsonNode, Printable { - public readonly id: ITimestampStruct; - - constructor(public readonly data: ObjNode) { - this.id = data.id; - } - - // -------------------------------------------------------- ExtensionJsonNode +class CntNode extends ExtNode { + // ------------------------------------------------------------------ ExtNode public name(): string { - return name; + return MNEMONIC; } public view(): number { @@ -28,24 +19,6 @@ class CntNode implements ExtensionJsonNode, Printable { for (const key in obj) sum += Number(obj[key]); return sum; } - - public children(callback: (node: JsonNode) => void): void {} - - public child?(): JsonNode | undefined { - return this.data; - } - - public container(): JsonNode | undefined { - return this.data.container(); - } - - public api: undefined | unknown = undefined; - - // ---------------------------------------------------------------- Printable - - public toString(tab?: string): string { - return `${this.name()} (${this.view()})` + printTree(tab, [(tab) => this.data.toString(tab)]); - } } class CntApi extends NodeApi implements ExtensionApi { @@ -65,7 +38,7 @@ class CntApi extends NodeApi implements ExtensionApi { export const CntExt: ExtensionDefinition = { id: ExtensionId.cnt, - name, + name: MNEMONIC, new: (value?: number, sid: number = 0) => s.ext(ExtensionId.cnt, s.obj({[sid]: s.jsonCon(value)})), Node: CntNode, Api: CntApi, From d3b05e33a69535b8aba04adc88459fe434e5a3c3 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 3 May 2024 00:39:42 +0200 Subject: [PATCH 10/37] =?UTF-8?q?style:=20=F0=9F=92=84=20run=20Prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/ModelWithExt.ts | 12 ++++++------ src/json-crdt-extensions/peritext/PeritextNode.ts | 2 +- .../peritext/__tests__/extension.spec.ts | 4 ++-- src/json-crdt-patch/builder/schema.ts | 12 ++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/json-crdt-extensions/ModelWithExt.ts b/src/json-crdt-extensions/ModelWithExt.ts index 5652275818..9105f57cd8 100644 --- a/src/json-crdt-extensions/ModelWithExt.ts +++ b/src/json-crdt-extensions/ModelWithExt.ts @@ -1,11 +1,11 @@ import * as clock from '../json-crdt-patch/clock'; -import {NodeBuilder} from "../json-crdt-patch"; -import {Extensions} from "../json-crdt/extensions/Extensions"; -import {Model} from "../json-crdt/model"; -import {SchemaToJsonNode} from "../json-crdt/schema/types"; +import {NodeBuilder} from '../json-crdt-patch'; +import {Extensions} from '../json-crdt/extensions/Extensions'; +import {Model} from '../json-crdt/model'; +import {SchemaToJsonNode} from '../json-crdt/schema/types'; import {CntExt} from './cnt'; -import {MvalExt} from "./mval"; -import {PeritextExt} from "./peritext"; +import {MvalExt} from './mval'; +import {PeritextExt} from './peritext'; const extensions = new Extensions(); diff --git a/src/json-crdt-extensions/peritext/PeritextNode.ts b/src/json-crdt-extensions/peritext/PeritextNode.ts index d627e1c08d..58a06f7b7c 100644 --- a/src/json-crdt-extensions/peritext/PeritextNode.ts +++ b/src/json-crdt-extensions/peritext/PeritextNode.ts @@ -1,5 +1,5 @@ import {MNEMONIC} from './constants'; -import {ExtNode} from '../../json-crdt/extensions/ExtNode';; +import {ExtNode} from '../../json-crdt/extensions/ExtNode'; import type {PeritextDataNode} from './types'; export class PeritextNode extends ExtNode { diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts index 453a753f6d..0d20f6a85b 100644 --- a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -1,5 +1,5 @@ -import {PeritextExt} from ".."; -import {ModelWithExt} from "../../ModelWithExt"; +import {PeritextExt} from '..'; +import {ModelWithExt} from '../../ModelWithExt'; test('..', () => { const model = ModelWithExt.create(); diff --git a/src/json-crdt-patch/builder/schema.ts b/src/json-crdt-patch/builder/schema.ts index 6c62fa5512..10a2dce3df 100644 --- a/src/json-crdt-patch/builder/schema.ts +++ b/src/json-crdt-patch/builder/schema.ts @@ -207,9 +207,9 @@ export namespace nodes { * Convenience class for recursively creating a node tree from any POJO. It * uses the {@link Builder.json} method to create a JSON node. It can be used * similar to TypeScript's *any* type, where the value can be anything. - * + * * Example: - * + * * ```typescript * s.json({name: 'Alice', age: 30}); * ``` @@ -226,9 +226,9 @@ export namespace nodes { * Convenience class for recursively creating a node tree from any POJO. It * uses the {@link Builder.constOrJson} method to create a JSON node. It can * be used similar to TypeScript's *any* type, where the value can be anything. - * + * * Example: - * + * * ```typescript * s.jsonCon({name: 'Alice', age: 30}); * ``` @@ -245,7 +245,7 @@ export namespace nodes { * Creates an extension node schema. The extension node is a tuple with a * header and a data node. The header is a 3-byte {@link Uint8Array} with the * extension ID, the SID of the tuple ID, and the time of the tuple ID. - * + * * - 1 byte for the extension id * - 1 byte for the sid of the tuple id, modulo 256 * - 1 byte for the time of the tuple id, modulo 256 @@ -269,7 +269,7 @@ export namespace nodes { [0, bufId], [1, valueId], ]); - return tupleId; + return tupleId; }); } } From 7da926277dff314b51ac6adfeb358c3d0a3a3597 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 3 May 2024 00:55:22 +0200 Subject: [PATCH 11/37] =?UTF-8?q?feat(json-crdt):=20=F0=9F=8E=B8=20start?= =?UTF-8?q?=20Extension=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt/extensions/Extension.ts | 24 ++++++++++++++++++++++++ src/json-crdt/extensions/types.ts | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/json-crdt/extensions/Extension.ts diff --git a/src/json-crdt/extensions/Extension.ts b/src/json-crdt/extensions/Extension.ts new file mode 100644 index 0000000000..1f1eb62754 --- /dev/null +++ b/src/json-crdt/extensions/Extension.ts @@ -0,0 +1,24 @@ +import {s, type NodeBuilder, type nodes} from "../../json-crdt-patch"; +import type {ModelApi} from "../model"; +import type {JsonNode} from "../nodes"; +import type {ExtensionApi, ExtensionJsonNode} from "./types"; + +export class Extension< + Id extends number, + Node extends JsonNode, + ENode extends ExtensionJsonNode, + EApi extends ExtensionApi, + ESchema extends NodeBuilder, +> { + constructor( + public readonly id: Id, + public readonly name: string, + public readonly schema: (...args: any[]) => ESchema, + public readonly Node: new (data: Node) => ENode, + public readonly Api: new (node: ENode, api: ModelApi) => EApi, + ) {} + + public new(...args: any[]): nodes.ext { + return s.ext(this.id, this.schema(...args)); + } +} diff --git a/src/json-crdt/extensions/types.ts b/src/json-crdt/extensions/types.ts index 182d4222a8..9f0e71be64 100644 --- a/src/json-crdt/extensions/types.ts +++ b/src/json-crdt/extensions/types.ts @@ -13,10 +13,11 @@ export interface ExtensionDefinition< Node extends JsonNode = JsonNode, ENode extends ExtensionJsonNode = ExtensionJsonNode, EApi extends ExtensionApi = ExtensionApi, + ESchema extends NodeBuilder = NodeBuilder, > { id: number; name: string; - new: (...args: any[]) => NodeBuilder; + new: (...args: any[]) => ESchema; Node: new (data: Node) => ENode; Api: new (node: ENode, api: ModelApi) => EApi; } From 7ebc04962e3904a608638a7c0e10daafa9460254 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 3 May 2024 09:50:04 +0200 Subject: [PATCH 12/37] =?UTF-8?q?feat(json-crdt-patch):=20=F0=9F=8E=B8=20i?= =?UTF-8?q?mprove=20schema=20types=20and=20value=20preservatin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-patch/builder/schema.ts | 32 ++++++++++--------- .../vec/__tests__/VecNode-extension.spec.ts | 7 +--- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/json-crdt-patch/builder/schema.ts b/src/json-crdt-patch/builder/schema.ts index 10a2dce3df..8bf64acfa1 100644 --- a/src/json-crdt-patch/builder/schema.ts +++ b/src/json-crdt-patch/builder/schema.ts @@ -25,7 +25,7 @@ export namespace nodes { export class con extends NodeBuilder { public readonly type = 'con'; - constructor(raw: T) { + constructor(public readonly raw: T) { super((builder) => builder.const(raw)); } } @@ -46,7 +46,7 @@ export namespace nodes { export class str extends NodeBuilder { public readonly type = 'str'; - constructor(raw: T) { + constructor(public readonly raw: T) { super((builder) => builder.json(raw)); } } @@ -57,7 +57,7 @@ export namespace nodes { export class bin extends NodeBuilder { public readonly type = 'bin'; - constructor(raw: Uint8Array) { + constructor(public readonly raw: Uint8Array) { super((builder) => builder.json(raw)); } } @@ -77,7 +77,7 @@ export namespace nodes { export class val extends NodeBuilder { public readonly type = 'val'; - constructor(value: T) { + constructor(public readonly value: T) { super((builder) => { const valId = builder.val(); const valueId = value.build(builder); @@ -101,7 +101,7 @@ export namespace nodes { export class vec extends NodeBuilder { public readonly type = 'vec'; - constructor(value: T) { + constructor(public readonly value: T) { super((builder) => { const vecId = builder.vec(); const length = value.length; @@ -139,7 +139,7 @@ export namespace nodes { > extends NodeBuilder { public readonly type = 'obj'; - constructor(obj: T, opt?: O) { + constructor(public readonly obj: T, public readonly opt?: O) { super((builder) => { const objId = builder.obj(); const keyValuePairs: [key: string, value: ITimestampStruct][] = []; @@ -189,7 +189,7 @@ export namespace nodes { export class arr extends NodeBuilder { public readonly type = 'arr'; - constructor(arr: T[]) { + constructor(public readonly arr: T[]) { super((builder) => { const arrId = builder.arr(); const length = arr.length; @@ -214,10 +214,10 @@ export namespace nodes { * s.json({name: 'Alice', age: 30}); * ``` */ - export class json extends NodeBuilder { + export class json extends NodeBuilder { public readonly type = 'json'; - constructor(value: unknown) { + constructor(public readonly value: T) { super((builder) => builder.json(value)); } } @@ -233,10 +233,10 @@ export namespace nodes { * s.jsonCon({name: 'Alice', age: 30}); * ``` */ - export class jsonCon extends NodeBuilder { + export class jsonCon extends NodeBuilder { public readonly type = 'jsonCon'; - constructor(value: unknown) { + constructor(public readonly value: T) { super((builder) => builder.constOrJson(value)); } } @@ -257,7 +257,7 @@ export namespace nodes { * @param id A unique extension ID. * @param data Schema of the data node of the extension. */ - constructor(id: ID, data: T) { + constructor(public readonly id: ID, public readonly data: T) { super((builder) => { const buf = new Uint8Array([id, 0, 0]); const tupleId = builder.vec(); @@ -278,7 +278,9 @@ export namespace nodes { /** * Schema builder. Use this to create a JSON CRDT model schema and the default - * value. Example: + * value. + * + * Example: * * ```typescript * const schema = s.obj({ @@ -357,7 +359,7 @@ export const schema = { * * @param value Default value. */ - json: (value: unknown) => new nodes.json(value), + json: (value: T) => new nodes.json(value), /** * Recursively creates a node tree from any POJO. It uses the @@ -366,7 +368,7 @@ export const schema = { * * @param value Default value. */ - jsonCon: (value: unknown) => new nodes.jsonCon(value), + jsonCon: (value: T) => new nodes.jsonCon(value), /** * Creates an extension node schema. diff --git a/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts b/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts index e1a5011d48..7fbdbe26b0 100644 --- a/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts +++ b/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts @@ -1,6 +1,5 @@ import {ITimestampStruct, delayed, s} from '../../../../json-crdt-patch'; import {printTree, Printable} from 'tree-dump'; -import {ext} from '../../../extensions'; import {ExtensionApi, ExtensionDefinition, ExtensionJsonNode} from '../../../extensions/types'; import {Model, NodeApi} from '../../../model'; import {StrNode} from '../../nodes'; @@ -35,11 +34,7 @@ describe('sample extension', () => { const DoubleConcatExt: ExtensionDefinition = { id: 123, name: 'double-concat', - new: (value: string = '') => - ext( - 123, - delayed((builder) => builder.json(value)), - ), + new: (value: string = '') => s.ext(123, s.json(value)), Node: class CntNode implements ExtensionJsonNode, Printable { public readonly id: ITimestampStruct; From 675319bbdf3d6194df00fc8d8add81490fd84d62 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 3 May 2024 09:50:29 +0200 Subject: [PATCH 13/37] =?UTF-8?q?style:=20=F0=9F=92=84=20run=20Prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-patch/builder/schema.ts | 12 +++++++++--- src/json-crdt/extensions/Extension.ts | 8 ++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/json-crdt-patch/builder/schema.ts b/src/json-crdt-patch/builder/schema.ts index 8bf64acfa1..fbee3623bd 100644 --- a/src/json-crdt-patch/builder/schema.ts +++ b/src/json-crdt-patch/builder/schema.ts @@ -139,7 +139,10 @@ export namespace nodes { > extends NodeBuilder { public readonly type = 'obj'; - constructor(public readonly obj: T, public readonly opt?: O) { + constructor( + public readonly obj: T, + public readonly opt?: O, + ) { super((builder) => { const objId = builder.obj(); const keyValuePairs: [key: string, value: ITimestampStruct][] = []; @@ -257,7 +260,10 @@ export namespace nodes { * @param id A unique extension ID. * @param data Schema of the data node of the extension. */ - constructor(public readonly id: ID, public readonly data: T) { + constructor( + public readonly id: ID, + public readonly data: T, + ) { super((builder) => { const buf = new Uint8Array([id, 0, 0]); const tupleId = builder.vec(); @@ -279,7 +285,7 @@ export namespace nodes { /** * Schema builder. Use this to create a JSON CRDT model schema and the default * value. - * + * * Example: * * ```typescript diff --git a/src/json-crdt/extensions/Extension.ts b/src/json-crdt/extensions/Extension.ts index 1f1eb62754..c785fe4c51 100644 --- a/src/json-crdt/extensions/Extension.ts +++ b/src/json-crdt/extensions/Extension.ts @@ -1,7 +1,7 @@ -import {s, type NodeBuilder, type nodes} from "../../json-crdt-patch"; -import type {ModelApi} from "../model"; -import type {JsonNode} from "../nodes"; -import type {ExtensionApi, ExtensionJsonNode} from "./types"; +import {s, type NodeBuilder, type nodes} from '../../json-crdt-patch'; +import type {ModelApi} from '../model'; +import type {JsonNode} from '../nodes'; +import type {ExtensionApi, ExtensionJsonNode} from './types'; export class Extension< Id extends number, From 273d013821af614c746ef532f1c0363d1c9d7f4c Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 3 May 2024 10:50:13 +0200 Subject: [PATCH 14/37] =?UTF-8?q?feat(json-crdt):=20=F0=9F=8E=B8=20use=20E?= =?UTF-8?q?xtension=20class=20to=20construct=20extensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/ModelWithExt.ts | 10 +++---- .../{CntExt.spec.ts => extension.spec.ts} | 16 ++++++------ src/json-crdt-extensions/cnt/index.ts | 26 ++++++++++--------- src/json-crdt-extensions/ext.ts | 9 +++++++ src/json-crdt-extensions/index.ts | 3 +++ src/json-crdt-extensions/mval/MvalNode.ts | 6 ++--- src/json-crdt-extensions/mval/index.ts | 17 ++++++------ .../peritext/PeritextNode.ts | 6 ++--- .../peritext/__tests__/extension.spec.ts | 2 +- .../peritext/constants.ts | 4 +-- src/json-crdt-extensions/peritext/index.ts | 18 ++++++------- src/json-crdt-extensions/peritext/types.ts | 2 +- src/json-crdt/extensions/Extension.ts | 24 ++++++++++------- .../{ExtNode.ts => ExtensionNode.ts} | 2 +- src/json-crdt/extensions/Extensions.ts | 8 +++--- src/json-crdt/extensions/types.ts | 20 +++----------- src/json-crdt/model/api/nodes.ts | 9 ++++--- 17 files changed, 90 insertions(+), 92 deletions(-) rename src/json-crdt-extensions/cnt/__tests__/{CntExt.spec.ts => extension.spec.ts} (70%) create mode 100644 src/json-crdt-extensions/ext.ts rename src/json-crdt/extensions/{ExtNode.ts => ExtensionNode.ts} (90%) diff --git a/src/json-crdt-extensions/ModelWithExt.ts b/src/json-crdt-extensions/ModelWithExt.ts index 9105f57cd8..4bf92b77ed 100644 --- a/src/json-crdt-extensions/ModelWithExt.ts +++ b/src/json-crdt-extensions/ModelWithExt.ts @@ -1,17 +1,15 @@ import * as clock from '../json-crdt-patch/clock'; +import * as ext from './ext'; import {NodeBuilder} from '../json-crdt-patch'; import {Extensions} from '../json-crdt/extensions/Extensions'; import {Model} from '../json-crdt/model'; import {SchemaToJsonNode} from '../json-crdt/schema/types'; -import {CntExt} from './cnt'; -import {MvalExt} from './mval'; -import {PeritextExt} from './peritext'; const extensions = new Extensions(); -extensions.register(CntExt); -extensions.register(MvalExt); -extensions.register(PeritextExt); +extensions.register(ext.cnt); +extensions.register(ext.mval); +extensions.register(ext.peritext); export class ModelWithExt { public static readonly create = ( diff --git a/src/json-crdt-extensions/cnt/__tests__/CntExt.spec.ts b/src/json-crdt-extensions/cnt/__tests__/extension.spec.ts similarity index 70% rename from src/json-crdt-extensions/cnt/__tests__/CntExt.spec.ts rename to src/json-crdt-extensions/cnt/__tests__/extension.spec.ts index 95c5f02c1d..3a6ccf55a8 100644 --- a/src/json-crdt-extensions/cnt/__tests__/CntExt.spec.ts +++ b/src/json-crdt-extensions/cnt/__tests__/extension.spec.ts @@ -1,14 +1,14 @@ -import {CntExt} from '..'; +import {cnt} from '..'; import {Model} from '../../../json-crdt/model'; test('can set new values in single fork', () => { const model = Model.withLogicalClock(); - model.ext.register(CntExt); + model.ext.register(cnt); model.api.root({ - counter: CntExt.new(24), + counter: cnt.new(24), }); expect(model.view()).toEqual({counter: 24}); - const counter = model.api.in(['counter']).asExt(CntExt); + const counter = model.api.in(['counter']).asExt(cnt); expect(counter.view()).toBe(24); counter.inc(2); expect(model.view()).toEqual({counter: 26}); @@ -18,15 +18,15 @@ test('can set new values in single fork', () => { test('two concurrent users can increment the counter', () => { const model = Model.withLogicalClock(); - model.ext.register(CntExt); + model.ext.register(cnt); model.api.root({ - counter: CntExt.new(), + counter: cnt.new(), }); expect(model.view()).toEqual({counter: 0}); - const counter = model.api.in(['counter']).asExt(CntExt); + const counter = model.api.in(['counter']).asExt(cnt); expect(counter.view()).toBe(0); const model2 = model.fork(); - const counter2 = model2.api.in(['counter']).asExt(CntExt); + const counter2 = model2.api.in(['counter']).asExt(cnt); counter.inc(2); counter2.inc(3); model.applyPatch(model2.api.flush()); diff --git a/src/json-crdt-extensions/cnt/index.ts b/src/json-crdt-extensions/cnt/index.ts index 1a0c156558..c29384c4d1 100644 --- a/src/json-crdt-extensions/cnt/index.ts +++ b/src/json-crdt-extensions/cnt/index.ts @@ -1,14 +1,13 @@ import {ExtensionId, ExtensionName} from '../constants'; import {NodeApi} from '../../json-crdt/model/api/nodes'; -import {ExtNode} from '../../json-crdt/extensions/ExtNode'; -import {s, type ExtensionDefinition, type ObjNode} from '../../json-crdt'; +import {ExtensionNode} from '../../json-crdt/extensions/ExtensionNode'; +import {Extension} from '../../json-crdt/extensions/Extension'; +import {nodes, s, type ObjNode} from '../../json-crdt'; import type {ExtensionApi} from '../../json-crdt'; const MNEMONIC = ExtensionName[ExtensionId.cnt]; -class CntNode extends ExtNode { - // ------------------------------------------------------------------ ExtNode - +class CntNode extends ExtensionNode { public name(): string { return MNEMONIC; } @@ -36,10 +35,13 @@ class CntApi extends NodeApi implements ExtensionApi { } } -export const CntExt: ExtensionDefinition = { - id: ExtensionId.cnt, - name: MNEMONIC, - new: (value?: number, sid: number = 0) => s.ext(ExtensionId.cnt, s.obj({[sid]: s.jsonCon(value)})), - Node: CntNode, - Api: CntApi, -}; +export const cnt = new Extension< + ExtensionId.cnt, + ObjNode, + CntNode, + CntApi, + [value?: number, sid?: number], + nodes.map> +>(ExtensionId.cnt, MNEMONIC, CntNode, CntApi, (value?: any, sid: any = 0) => + value === undefined ? s.map>({}) : s.map>({[sid]: s.con(value ?? 0)}), +); diff --git a/src/json-crdt-extensions/ext.ts b/src/json-crdt-extensions/ext.ts new file mode 100644 index 0000000000..023c3ee4ae --- /dev/null +++ b/src/json-crdt-extensions/ext.ts @@ -0,0 +1,9 @@ +import {cnt} from './cnt'; +import {mval} from './mval'; +import {peritext} from './peritext'; + +export { + cnt, + mval, + peritext, +}; diff --git a/src/json-crdt-extensions/index.ts b/src/json-crdt-extensions/index.ts index 955c7056c2..3df9dfcfec 100644 --- a/src/json-crdt-extensions/index.ts +++ b/src/json-crdt-extensions/index.ts @@ -1,3 +1,6 @@ export * from './mval'; export * from './cnt'; export * from './peritext'; +export * from './ext'; +export * from './ModelWithExt'; +export * from './constants'; diff --git a/src/json-crdt-extensions/mval/MvalNode.ts b/src/json-crdt-extensions/mval/MvalNode.ts index 10faee1abc..678856b7c4 100644 --- a/src/json-crdt-extensions/mval/MvalNode.ts +++ b/src/json-crdt-extensions/mval/MvalNode.ts @@ -1,10 +1,8 @@ import {MNEMONIC} from './constants'; -import {ExtNode} from '../../json-crdt/extensions/ExtNode'; +import {ExtensionNode} from '../../json-crdt/extensions/ExtensionNode'; import type {ArrNode} from '../../json-crdt/nodes/arr/ArrNode'; -export class MvalNode extends ExtNode { - // ------------------------------------------------------------------ ExtNode - +export class MvalNode extends ExtensionNode { public name(): string { return MNEMONIC; } diff --git a/src/json-crdt-extensions/mval/index.ts b/src/json-crdt-extensions/mval/index.ts index c13c5bd3bf..b955f4bedc 100644 --- a/src/json-crdt-extensions/mval/index.ts +++ b/src/json-crdt-extensions/mval/index.ts @@ -2,14 +2,15 @@ import {ExtensionId} from '../constants'; import {MvalNode} from './MvalNode'; import {MvalApi} from './MvalApi'; import {MNEMONIC} from './constants'; +import {s} from '../../json-crdt'; +import {Extension} from '../../json-crdt/extensions/Extension'; import type {ITimestampStruct} from '../../json-crdt-patch/clock'; import type {ArrNode} from '../../json-crdt/nodes/arr/ArrNode'; -import {s, type ExtensionDefinition} from '../../json-crdt'; -export const MvalExt: ExtensionDefinition = { - id: ExtensionId.mval, - name: MNEMONIC, - new: (value: unknown | ITimestampStruct) => s.ext(ExtensionId.mval, s.arr([s.json(value)])), - Node: MvalNode, - Api: MvalApi, -}; +export const mval = new Extension( + ExtensionId.mval, + MNEMONIC, + MvalNode, + MvalApi, + (value: unknown | ITimestampStruct) => s.arr([s.json(value)]), +); diff --git a/src/json-crdt-extensions/peritext/PeritextNode.ts b/src/json-crdt-extensions/peritext/PeritextNode.ts index 58a06f7b7c..5b65b19e94 100644 --- a/src/json-crdt-extensions/peritext/PeritextNode.ts +++ b/src/json-crdt-extensions/peritext/PeritextNode.ts @@ -1,10 +1,8 @@ import {MNEMONIC} from './constants'; -import {ExtNode} from '../../json-crdt/extensions/ExtNode'; +import {ExtensionNode} from '../../json-crdt/extensions/ExtensionNode'; import type {PeritextDataNode} from './types'; -export class PeritextNode extends ExtNode { - // ------------------------------------------------------------------ ExtNode - +export class PeritextNode extends ExtensionNode { public name(): string { return MNEMONIC; } diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts index 0d20f6a85b..619d32a24f 100644 --- a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -7,5 +7,5 @@ test('..', () => { text: PeritextExt.new('Hello, world\n'), }); - console.log('' + model); + // console.log('' + model); }); diff --git a/src/json-crdt-extensions/peritext/constants.ts b/src/json-crdt-extensions/peritext/constants.ts index 7d5a772248..edf41fd29d 100644 --- a/src/json-crdt-extensions/peritext/constants.ts +++ b/src/json-crdt-extensions/peritext/constants.ts @@ -8,7 +8,7 @@ export const enum Chars { export const MNEMONIC = ExtensionName[ExtensionId.peritext]; -export const BUILD_SCHEMA = (text: string) => +export const SCHEMA = (text: string) => s.vec< [ /** @@ -23,5 +23,3 @@ export const BUILD_SCHEMA = (text: string) => slices: nodes.arr, ] >(s.str(text), s.arr([])); - -export const SCHEMA = BUILD_SCHEMA(''); diff --git a/src/json-crdt-extensions/peritext/index.ts b/src/json-crdt-extensions/peritext/index.ts index 5ace068fd1..2fa9487f97 100644 --- a/src/json-crdt-extensions/peritext/index.ts +++ b/src/json-crdt-extensions/peritext/index.ts @@ -1,14 +1,14 @@ import {ExtensionId} from '../constants'; import {PeritextNode} from './PeritextNode'; import {PeritextApi} from './PeritextApi'; -import {BUILD_SCHEMA, MNEMONIC} from './constants'; +import {SCHEMA, MNEMONIC} from './constants'; +import {Extension} from '../../json-crdt/extensions/Extension'; import type {PeritextDataNode} from './types'; -import {s, type ExtensionDefinition} from '../../json-crdt'; -export const PeritextExt: ExtensionDefinition = { - id: ExtensionId.peritext, - name: MNEMONIC, - new: (text: string) => s.ext(ExtensionId.peritext, BUILD_SCHEMA(text)), - Node: PeritextNode, - Api: PeritextApi, -}; +export const peritext = new Extension< + ExtensionId.peritext, + PeritextDataNode, + PeritextNode, + PeritextApi, + [text: string] +>(ExtensionId.peritext, MNEMONIC, PeritextNode, PeritextApi, (text: string) => SCHEMA(text)); diff --git a/src/json-crdt-extensions/peritext/types.ts b/src/json-crdt-extensions/peritext/types.ts index c4f516049a..dcdf462c20 100644 --- a/src/json-crdt-extensions/peritext/types.ts +++ b/src/json-crdt-extensions/peritext/types.ts @@ -19,5 +19,5 @@ export interface Stateful { refresh(): number; } -export type PeritextDataNodeSchema = typeof SCHEMA; +export type PeritextDataNodeSchema = ReturnType; export type PeritextDataNode = SchemaToJsonNode; diff --git a/src/json-crdt/extensions/Extension.ts b/src/json-crdt/extensions/Extension.ts index c785fe4c51..164f296d1d 100644 --- a/src/json-crdt/extensions/Extension.ts +++ b/src/json-crdt/extensions/Extension.ts @@ -1,24 +1,28 @@ -import {s, type NodeBuilder, type nodes} from '../../json-crdt-patch'; +import {NodeBuilder, s, type nodes} from '../../json-crdt-patch'; import type {ModelApi} from '../model'; import type {JsonNode} from '../nodes'; +import type {JsonNodeToSchema} from '../schema/types'; import type {ExtensionApi, ExtensionJsonNode} from './types'; +export type AnyExtension = Extension; + export class Extension< Id extends number, - Node extends JsonNode, - ENode extends ExtensionJsonNode, - EApi extends ExtensionApi, - ESchema extends NodeBuilder, + DataNode extends JsonNode, + ExtNode extends ExtensionJsonNode, + ExtApi extends ExtensionApi, + DataArgs extends any[] = any[], + DataSchema extends NodeBuilder = JsonNodeToSchema, > { constructor( public readonly id: Id, public readonly name: string, - public readonly schema: (...args: any[]) => ESchema, - public readonly Node: new (data: Node) => ENode, - public readonly Api: new (node: ENode, api: ModelApi) => EApi, + public readonly Node: new (data: DataNode) => ExtNode, + public readonly Api: new (node: ExtNode, api: ModelApi) => ExtApi, + public readonly schema: (...args: DataArgs) => DataSchema, ) {} - public new(...args: any[]): nodes.ext { - return s.ext(this.id, this.schema(...args)); + public new(...args: DataArgs): nodes.ext { + return s.ext(this.id, this.schema(...args)); } } diff --git a/src/json-crdt/extensions/ExtNode.ts b/src/json-crdt/extensions/ExtensionNode.ts similarity index 90% rename from src/json-crdt/extensions/ExtNode.ts rename to src/json-crdt/extensions/ExtensionNode.ts index 12bad7851c..c4df69ee80 100644 --- a/src/json-crdt/extensions/ExtNode.ts +++ b/src/json-crdt/extensions/ExtensionNode.ts @@ -3,7 +3,7 @@ import type {ITimestampStruct} from '../../json-crdt-patch/clock'; import type {ExtensionJsonNode, JsonNode} from '..'; import type {Printable} from 'tree-dump/lib/types'; -export abstract class ExtNode implements ExtensionJsonNode, Printable { +export abstract class ExtensionNode implements ExtensionJsonNode, Printable { public readonly id: ITimestampStruct; constructor(public readonly data: N) { diff --git a/src/json-crdt/extensions/Extensions.ts b/src/json-crdt/extensions/Extensions.ts index 21183a1641..88cf0f659e 100644 --- a/src/json-crdt/extensions/Extensions.ts +++ b/src/json-crdt/extensions/Extensions.ts @@ -1,15 +1,15 @@ import {printTree} from 'tree-dump/lib/printTree'; -import type {ExtensionDefinition} from './types'; +import type {AnyExtension} from './Extension'; import type {Printable} from 'tree-dump/lib/types'; export class Extensions implements Printable { - protected readonly ext: Record = {}; + protected readonly ext: Record = {}; - public register(extension: ExtensionDefinition) { + public register(extension: AnyExtension) { this.ext[extension.id] = extension; } - public get(id: number): ExtensionDefinition | undefined { + public get(id: number): AnyExtension | undefined { return this.ext[id]; } diff --git a/src/json-crdt/extensions/types.ts b/src/json-crdt/extensions/types.ts index 9f0e71be64..4aad5c594a 100644 --- a/src/json-crdt/extensions/types.ts +++ b/src/json-crdt/extensions/types.ts @@ -1,23 +1,9 @@ -import type {NodeBuilder} from '../../json-crdt-patch/builder/DelayedValueBuilder'; -import type {ModelApi} from '../model/api/ModelApi'; import type {NodeApi} from '../model/api/nodes'; import type {JsonNode} from '../nodes'; +import type {ExtensionNode} from './ExtensionNode'; export type ExtensionValue = [type: Uint8Array, data: unknown]; -export interface ExtensionJsonNode extends JsonNode {} +export interface ExtensionJsonNode extends ExtensionNode {} -export interface ExtensionApi extends NodeApi {} - -export interface ExtensionDefinition< - Node extends JsonNode = JsonNode, - ENode extends ExtensionJsonNode = ExtensionJsonNode, - EApi extends ExtensionApi = ExtensionApi, - ESchema extends NodeBuilder = NodeBuilder, -> { - id: number; - name: string; - new: (...args: any[]) => ESchema; - Node: new (data: Node) => ENode; - Api: new (node: ENode, api: ModelApi) => EApi; -} +export interface ExtensionApi> extends NodeApi {} diff --git a/src/json-crdt/model/api/nodes.ts b/src/json-crdt/model/api/nodes.ts index 1de93f8932..b8858281e0 100644 --- a/src/json-crdt/model/api/nodes.ts +++ b/src/json-crdt/model/api/nodes.ts @@ -1,10 +1,11 @@ +import {printTree} from 'tree-dump/lib/printTree'; import {find} from './find'; import {ITimestampStruct, Timestamp} from '../../../json-crdt-patch/clock'; import {Path} from '../../../json-pointer'; import {ObjNode, ArrNode, BinNode, ConNode, VecNode, ValNode, StrNode} from '../../nodes'; -import {ExtensionApi, ExtensionDefinition, ExtensionJsonNode} from '../../extensions/types'; +import {ExtensionApi, ExtensionJsonNode} from '../../extensions/types'; import {NodeEvents} from './NodeEvents'; -import {printTree} from 'tree-dump/lib/printTree'; +import type {Extension} from '../../extensions/Extension'; import type {JsonNode, JsonNodeView} from '../../nodes'; import type * as types from './proxy'; import type {ModelApi} from './ModelApi'; @@ -110,8 +111,8 @@ export class NodeApi implements Printable { throw new Error('NOT_CONST'); } - public asExt>( - ext: ExtensionDefinition, + public asExt, EApi extends ExtensionApi>( + ext: Extension, ): EApi { let node: JsonNode | undefined = this.node; while (node) { From 6eb4dacbdafec18cb1b7b19f0d8f48bcb21b83f0 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 3 May 2024 10:57:30 +0200 Subject: [PATCH 15/37] =?UTF-8?q?test:=20=F0=9F=92=8D=20make=20all=20tests?= =?UTF-8?q?=20pass=20after=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mval/__tests__/MvalExt.spec.ts | 30 +++++++------- src/json-crdt-extensions/mval/index.ts | 4 +- .../peritext/__tests__/extension.spec.ts | 4 +- .../vec/__tests__/VecNode-extension.spec.ts | 41 ++++++------------- .../nodes/vec/__tests__/extension.spec.ts | 36 ++++++++-------- 5 files changed, 49 insertions(+), 66 deletions(-) diff --git a/src/json-crdt-extensions/mval/__tests__/MvalExt.spec.ts b/src/json-crdt-extensions/mval/__tests__/MvalExt.spec.ts index d94e8bc45e..c59056d097 100644 --- a/src/json-crdt-extensions/mval/__tests__/MvalExt.spec.ts +++ b/src/json-crdt-extensions/mval/__tests__/MvalExt.spec.ts @@ -1,14 +1,14 @@ -import {MvalExt} from '..'; +import {mval} from '..'; import {Model} from '../../../json-crdt/model'; test('can set new values in single fork', () => { const model = Model.withLogicalClock(); - model.ext.register(MvalExt); + model.ext.register(mval); model.api.root({ - mv: MvalExt.new(1), + mv: mval.new(1), }); expect(model.view()).toEqual({mv: [1]}); - const register = model.api.in(['mv']).asExt(MvalExt); + const register = model.api.in(['mv']).asExt(mval); register.set(2); expect(model.view()).toEqual({mv: [2]}); register.set(3); @@ -17,11 +17,11 @@ test('can set new values in single fork', () => { test('removes tombstones on insert', () => { const model = Model.withLogicalClock(); - model.ext.register(MvalExt); + model.ext.register(mval); model.api.root({ - mv: MvalExt.new(1), + mv: mval.new(1), }); - const register = model.api.in(['mv']).asExt(MvalExt); + const register = model.api.in(['mv']).asExt(mval); expect(register.node.data.size()).toBe(1); register.set(2); expect(register.node.data.size()).toBe(1); @@ -33,13 +33,13 @@ test('removes tombstones on insert', () => { test('contains two values when two forks set value concurrently', () => { const model1 = Model.withLogicalClock(); - model1.ext.register(MvalExt); + model1.ext.register(mval); model1.api.root({ - mv: MvalExt.new(1), + mv: mval.new(1), }); const model2 = model1.fork(); - const register1 = model1.api.in(['mv']).asExt(MvalExt); - const register2 = model2.api.in(['mv']).asExt(MvalExt); + const register1 = model1.api.in(['mv']).asExt(mval); + const register2 = model2.api.in(['mv']).asExt(mval); register1.set(2); register2.set(3); expect(model1.view()).toEqual({mv: [2]}); @@ -56,13 +56,13 @@ test('contains two values when two forks set value concurrently', () => { test('contains one value when a fork overwrites a register', () => { const model1 = Model.withLogicalClock(); - model1.ext.register(MvalExt); + model1.ext.register(mval); model1.api.root({ - mv: MvalExt.new(1), + mv: mval.new(1), }); const model2 = model1.fork(); - const register1 = model1.api.in(['mv']).asExt(MvalExt); - const register2 = model2.api.in(['mv']).asExt(MvalExt); + const register1 = model1.api.in(['mv']).asExt(mval); + const register2 = model2.api.in(['mv']).asExt(mval); register1.set(2); register2.set(3); model1.applyPatch(model2.api.flush()); diff --git a/src/json-crdt-extensions/mval/index.ts b/src/json-crdt-extensions/mval/index.ts index b955f4bedc..06a9c22478 100644 --- a/src/json-crdt-extensions/mval/index.ts +++ b/src/json-crdt-extensions/mval/index.ts @@ -7,10 +7,10 @@ import {Extension} from '../../json-crdt/extensions/Extension'; import type {ITimestampStruct} from '../../json-crdt-patch/clock'; import type {ArrNode} from '../../json-crdt/nodes/arr/ArrNode'; -export const mval = new Extension( +export const mval = new Extension( ExtensionId.mval, MNEMONIC, MvalNode, MvalApi, - (value: unknown | ITimestampStruct) => s.arr([s.json(value)]), + (value: unknown | ITimestampStruct) => s.arr(value === undefined ? [] : [s.json(value)]), ); diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts index 619d32a24f..cfec75bd80 100644 --- a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -1,10 +1,10 @@ -import {PeritextExt} from '..'; +import {peritext} from '..'; import {ModelWithExt} from '../../ModelWithExt'; test('..', () => { const model = ModelWithExt.create(); model.api.root({ - text: PeritextExt.new('Hello, world\n'), + text: peritext.new('Hello, world\n'), }); // console.log('' + model); diff --git a/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts b/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts index 7fbdbe26b0..f97246be50 100644 --- a/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts +++ b/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts @@ -1,9 +1,10 @@ -import {ITimestampStruct, delayed, s} from '../../../../json-crdt-patch'; -import {printTree, Printable} from 'tree-dump'; -import {ExtensionApi, ExtensionDefinition, ExtensionJsonNode} from '../../../extensions/types'; +import {s} from '../../../../json-crdt-patch'; +import {printTree} from 'tree-dump'; +import {ExtensionApi} from '../../../extensions/types'; import {Model, NodeApi} from '../../../model'; import {StrNode} from '../../nodes'; -import {JsonNode} from '../../types'; +import {Extension} from '../../../extensions/Extension'; +import {ExtensionNode} from '../../../extensions/ExtensionNode'; test('treats a simple "vec" node as a vector', () => { const model = Model.withLogicalClock(); @@ -31,17 +32,10 @@ test('does not treat "vec" node as extension, if extension is not registered in }); describe('sample extension', () => { - const DoubleConcatExt: ExtensionDefinition = { - id: 123, - name: 'double-concat', - new: (value: string = '') => s.ext(123, s.json(value)), - Node: class CntNode implements ExtensionJsonNode, Printable { - public readonly id: ITimestampStruct; - - constructor(public readonly data: StrNode) { - this.id = data.id; - } - + const DoubleConcatExt = new Extension<123, StrNode, any, any, any, any>( + 123, + 'double-concat', + class extends ExtensionNode> { public name(): string { return 'double-concat'; } @@ -51,23 +45,11 @@ describe('sample extension', () => { return str + str; } - public children(callback: (node: JsonNode) => void): void {} - - public child?(): JsonNode | undefined { - return this.data; - } - - public container(): JsonNode | undefined { - return this.data.container(); - } - - public api: undefined | unknown = undefined; - public toString(tab?: string): string { return `${this.name()} (${this.view()})` + printTree(tab, [(tab) => this.data.toString(tab)]); } }, - Api: class CntApi extends NodeApi implements ExtensionApi { + class extends NodeApi implements ExtensionApi { public ins(index: number, text: string): this { const {api, node} = this; const dataApi = api.wrap(node.data as StrNode); @@ -75,7 +57,8 @@ describe('sample extension', () => { return this; } }, - }; + (value: string = '') => s.json(value), + ); test('can run an extension', () => { const model = Model.withLogicalClock(); diff --git a/src/json-crdt/nodes/vec/__tests__/extension.spec.ts b/src/json-crdt/nodes/vec/__tests__/extension.spec.ts index d5f4a78ac7..cd9801f706 100644 --- a/src/json-crdt/nodes/vec/__tests__/extension.spec.ts +++ b/src/json-crdt/nodes/vec/__tests__/extension.spec.ts @@ -1,16 +1,16 @@ -import {MvalExt} from '../../../../json-crdt-extensions/mval'; +import {mval} from '../../../../json-crdt-extensions/mval'; import {konst} from '../../../../json-crdt-patch/builder/Konst'; import {Model} from '../../../../json-crdt/model'; test('can specify extension name', () => { - expect(MvalExt.name).toBe('mval'); + expect(mval.name).toBe('mval'); }); test('can create a new multi-value register', () => { const model = Model.withLogicalClock(); - model.ext.register(MvalExt); + model.ext.register(mval); model.api.root({ - mv: MvalExt.new(), + mv: mval.new(), }); expect(model.view()).toEqual({ mv: [], @@ -19,9 +19,9 @@ test('can create a new multi-value register', () => { test('can provide initial value', () => { const model = Model.withLogicalClock(); - model.ext.register(MvalExt); + model.ext.register(mval); model.api.root({ - mv: MvalExt.new({foo: 'bar'}), + mv: mval.new({foo: 'bar'}), }); expect(model.view()).toEqual({ mv: [{foo: 'bar'}], @@ -30,22 +30,22 @@ test('can provide initial value', () => { test('can read view from node or API node', () => { const model = Model.withLogicalClock(); - model.ext.register(MvalExt); + model.ext.register(mval); model.api.root({ - mv: MvalExt.new('foo'), + mv: mval.new('foo'), }); - const api = model.api.in('mv').asExt(MvalExt); + const api = model.api.in('mv').asExt(mval); expect(api.view()).toEqual(['foo']); expect(api.node.view()).toEqual(['foo']); }); test('exposes API to edit extension data', () => { const model = Model.withLogicalClock(); - model.ext.register(MvalExt); + model.ext.register(mval); model.api.root({ - mv: MvalExt.new(), + mv: mval.new(), }); - const nodeApi = model.api.in('mv').asExt(MvalExt); + const nodeApi = model.api.in('mv').asExt(mval); nodeApi.set(konst('lol')); expect(model.view()).toEqual({ mv: ['lol'], @@ -55,9 +55,9 @@ test('exposes API to edit extension data', () => { describe('extension validity checks', () => { test('does not treat ArrNode as extension if header is too long', () => { const model = Model.withLogicalClock(); - model.ext.register(MvalExt); + model.ext.register(mval); model.api.root({ - mv: MvalExt.new(), + mv: mval.new(), }); const buf = new Uint8Array(4); buf.set(model.api.const(['mv', 0]).node.view() as Uint8Array, 0); @@ -70,9 +70,9 @@ describe('extension validity checks', () => { test('does not treat ArrNode as extension if header sid is wrong', () => { const model = Model.withLogicalClock(); - model.ext.register(MvalExt); + model.ext.register(mval); model.api.root({ - mv: MvalExt.new(), + mv: mval.new(), }); const buf = model.api.const(['mv', 0]).node.view() as Uint8Array; buf[1] += 1; @@ -84,9 +84,9 @@ describe('extension validity checks', () => { test('does not treat ArrNode as extension if header time is wrong', () => { const model = Model.withLogicalClock(); - model.ext.register(MvalExt); + model.ext.register(mval); model.api.root({ - mv: MvalExt.new(), + mv: mval.new(), }); const buf = model.api.const(['mv', 0]).node.view() as Uint8Array; buf[2] += 1; From 3ef93cd4cb2743cbf744d02faf93e3f9e204d58f Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 3 May 2024 11:02:21 +0200 Subject: [PATCH 16/37] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20simplify=20cnt=20extension=20definition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/cnt/index.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/json-crdt-extensions/cnt/index.ts b/src/json-crdt-extensions/cnt/index.ts index c29384c4d1..d1ddb57175 100644 --- a/src/json-crdt-extensions/cnt/index.ts +++ b/src/json-crdt-extensions/cnt/index.ts @@ -35,13 +35,7 @@ class CntApi extends NodeApi implements ExtensionApi { } } -export const cnt = new Extension< - ExtensionId.cnt, - ObjNode, - CntNode, - CntApi, - [value?: number, sid?: number], - nodes.map> ->(ExtensionId.cnt, MNEMONIC, CntNode, CntApi, (value?: any, sid: any = 0) => - value === undefined ? s.map>({}) : s.map>({[sid]: s.con(value ?? 0)}), -); +const create = (value?: any, sid: any = 0) => + value === undefined ? s.map>({}) : s.map>({[sid]: s.con(value ?? 0)}); + +export const cnt = new Extension(ExtensionId.cnt, MNEMONIC, CntNode, CntApi, create); From d13cc658e56e23be27acb2d21b09ff9e730e4b4d Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 3 May 2024 11:15:41 +0200 Subject: [PATCH 17/37] =?UTF-8?q?feat(json-crdt):=20=F0=9F=8E=B8=20improve?= =?UTF-8?q?=20extension=20presentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt/extensions/ExtensionNode.ts | 7 +++---- src/json-crdt/nodes/vec/VecNode.ts | 9 +++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/json-crdt/extensions/ExtensionNode.ts b/src/json-crdt/extensions/ExtensionNode.ts index c4df69ee80..2668c3f013 100644 --- a/src/json-crdt/extensions/ExtensionNode.ts +++ b/src/json-crdt/extensions/ExtensionNode.ts @@ -1,5 +1,4 @@ -import {printTree} from 'tree-dump/lib/printTree'; -import type {ITimestampStruct} from '../../json-crdt-patch/clock'; +import {toDisplayString, type ITimestampStruct} from '../../json-crdt-patch/clock'; import type {ExtensionJsonNode, JsonNode} from '..'; import type {Printable} from 'tree-dump/lib/types'; @@ -29,7 +28,7 @@ export abstract class ExtensionNode implements ExtensionJson // ---------------------------------------------------------------- Printable - public toString(tab?: string): string { - return this.name() + printTree(tab, [(tab) => this.data.toString(tab)]); + public toString(tab?: string, parentId?: ITimestampStruct): string { + return this.name() + (parentId ? ' ' + toDisplayString(parentId) : '') + ', ' + this.data.toString(tab); } } diff --git a/src/json-crdt/nodes/vec/VecNode.ts b/src/json-crdt/nodes/vec/VecNode.ts index 34751fe1b7..f39d98c743 100644 --- a/src/json-crdt/nodes/vec/VecNode.ts +++ b/src/json-crdt/nodes/vec/VecNode.ts @@ -5,6 +5,7 @@ import {compare, ITimestampStruct, toDisplayString} from '../../../json-crdt-pat import type {Model} from '../../model'; import type {JsonNode, JsonNodeView} from '..'; import type {Printable} from 'tree-dump/lib/types'; +import type {ExtensionNode} from '../../extensions/ExtensionNode'; /** * Represents a `vec` JSON CRDT node, which is a LWW array. @@ -67,14 +68,14 @@ export class VecNode implements JsonNode< /** * @ignore */ - private __extNode: JsonNode | undefined; + private __extNode: ExtensionNode | undefined; /** * @ignore * @returns Returns the extension data node if this is an extension node, * otherwise `undefined`. The node is cached after the first access. */ - public ext(): JsonNode | undefined { + public ext(): ExtensionNode | undefined { if (this.__extNode) return this.__extNode; const extensionId = this.getExtId(); const isExtension = extensionId >= 0; @@ -112,7 +113,7 @@ export class VecNode implements JsonNode< /** * @ignore */ - public child(): JsonNode | undefined { + public child(): ExtensionNode | undefined { return this.ext(); } @@ -182,7 +183,7 @@ export class VecNode implements JsonNode< const header = this.name() + ' ' + toDisplayString(this.id) + (extNode ? ` { extension = ${this.getExtId()} }` : ''); if (extNode) { - return this.child()!.toString(tab); + return this.child()!.toString(tab, this.id); } const index = this.doc.index; return ( From 763cb39b40e5e3a1a513f19861e5f6255888f098 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 3 May 2024 11:17:28 +0200 Subject: [PATCH 18/37] =?UTF-8?q?refactor(json-crdt-patch):=20=F0=9F=92=A1?= =?UTF-8?q?=20rename=20timestamp=20printing=20utility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/rga/Point.ts | 4 +- .../peritext/util/ChunkSlice.ts | 4 +- src/json-crdt-patch/Batch.ts | 4 +- src/json-crdt-patch/Patch.ts | 4 +- src/json-crdt-patch/clock/clock.ts | 2 +- src/json-crdt-patch/operations.ts | 48 +++++++++---------- src/json-crdt/extensions/ExtensionNode.ts | 4 +- src/json-crdt/model/Model.ts | 2 +- src/json-crdt/nodes/const/ConNode.ts | 6 +-- src/json-crdt/nodes/obj/ObjNode.ts | 4 +- src/json-crdt/nodes/rga/AbstractRga.ts | 6 +-- .../nodes/str/__tests__/StrNodeFuzzer.ts | 6 +-- src/json-crdt/nodes/val/ValNode.ts | 6 +-- src/json-crdt/nodes/vec/VecNode.ts | 4 +- 14 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/json-crdt-extensions/peritext/rga/Point.ts b/src/json-crdt-extensions/peritext/rga/Point.ts index ab87b70bab..3b68ffdf36 100644 --- a/src/json-crdt-extensions/peritext/rga/Point.ts +++ b/src/json-crdt-extensions/peritext/rga/Point.ts @@ -1,4 +1,4 @@ -import {compare, type ITimestampStruct, toDisplayString, equal, tick, containsId} from '../../../json-crdt-patch/clock'; +import {compare, type ITimestampStruct, printTs, equal, tick, containsId} from '../../../json-crdt-patch/clock'; import {Anchor} from './constants'; import {ChunkSlice} from '../util/ChunkSlice'; import {updateId} from '../../../json-crdt/hash'; @@ -449,7 +449,7 @@ export class Point implements Pick, Printable { public toString(tab: string = '', lite?: boolean): string { const name = lite ? '' : this.constructor.name + ' '; const pos = this.pos(); - const id = toDisplayString(this.id); + const id = printTs(this.id); const anchor = this.anchor === Anchor.Before ? '.▢' : '▢.'; return `${name}{ ${pos}, ${id}, ${anchor} }`; } diff --git a/src/json-crdt-extensions/peritext/util/ChunkSlice.ts b/src/json-crdt-extensions/peritext/util/ChunkSlice.ts index 8f775fb4e4..273032355e 100644 --- a/src/json-crdt-extensions/peritext/util/ChunkSlice.ts +++ b/src/json-crdt-extensions/peritext/util/ChunkSlice.ts @@ -1,6 +1,6 @@ import {CONST, updateNum} from '../../../json-hash'; import {updateId} from '../../../json-crdt/hash'; -import {ITimestampStruct, Timestamp, toDisplayString} from '../../../json-crdt-patch/clock'; +import {ITimestampStruct, Timestamp, printTs} from '../../../json-crdt-patch/clock'; import type {IChunkSlice} from './types'; import type {Stateful} from '../types'; import type {Printable} from 'tree-dump/lib/types'; @@ -62,7 +62,7 @@ export class ChunkSlice implements IChunkSlice, Stateful, Printab const str = this.view() + ''; const truncate = str.length > 32; const view = JSON.stringify(truncate ? str.slice(0, 32) : str) + (truncate ? ' …' : ''); - const id = toDisplayString(this.chunk.id); + const id = printTs(this.chunk.id); return `${name} ${id} [${off}..${off + len}) ${view}`; } } diff --git a/src/json-crdt-patch/Batch.ts b/src/json-crdt-patch/Batch.ts index 744bba1182..c731f108fd 100644 --- a/src/json-crdt-patch/Batch.ts +++ b/src/json-crdt-patch/Batch.ts @@ -1,4 +1,4 @@ -import {ITimestampStruct, toDisplayString} from './clock'; +import {ITimestampStruct, printTs} from './clock'; import {Patch} from './Patch'; export class Batch { @@ -30,7 +30,7 @@ export class Batch { public toString(tab: string = ''): string { const id = this.getId(); - let out = `${this.constructor.name} ${id ? toDisplayString(id) : '(nil)'}\n`; + let out = `${this.constructor.name} ${id ? printTs(id) : '(nil)'}\n`; for (let i = 0; i < this.patches.length; i++) { const patch = this.patches[i]; const isLast = i === this.patches.length - 1; diff --git a/src/json-crdt-patch/Patch.ts b/src/json-crdt-patch/Patch.ts index d7db35e185..d87884c8b3 100644 --- a/src/json-crdt-patch/Patch.ts +++ b/src/json-crdt-patch/Patch.ts @@ -1,5 +1,5 @@ import * as operations from './operations'; -import {ITimestampStruct, ts, toDisplayString} from './clock'; +import {ITimestampStruct, ts, printTs} from './clock'; import {SESSION} from './constants'; import {encode, decode} from './codec/binary'; import type {Printable} from 'tree-dump/lib/types'; @@ -205,7 +205,7 @@ export class Patch implements Printable { */ public toString(tab: string = ''): string { const id = this.getId(); - let out = `${this.constructor.name} ${id ? toDisplayString(id) : '(nil)'}!${this.span()}`; + let out = `${this.constructor.name} ${id ? printTs(id) : '(nil)'}!${this.span()}`; for (let i = 0; i < this.ops.length; i++) { const isLast = i === this.ops.length - 1; out += `\n${tab}${isLast ? '└─' : '├─'} ${this.ops[i].toString(tab + (isLast ? ' ' : '│ '))}`; diff --git a/src/json-crdt-patch/clock/clock.ts b/src/json-crdt-patch/clock/clock.ts index 1545368666..998f96acca 100644 --- a/src/json-crdt-patch/clock/clock.ts +++ b/src/json-crdt-patch/clock/clock.ts @@ -110,7 +110,7 @@ export const containsId = (ts1: ITimestampStruct, span1: number, ts2: ITimestamp * @param id A timestamp. * @returns Human-readable string representation of the timestamp. */ -export const toDisplayString = (id: ITimestampStruct): string => { +export const printTs = (id: ITimestampStruct): string => { if (id.sid === SESSION.SERVER) return '.' + id.time; let session = '' + id.sid; if (session.length > 4) session = '..' + session.slice(session.length - 4); diff --git a/src/json-crdt-patch/operations.ts b/src/json-crdt-patch/operations.ts index b21470b9e6..ad523b05a9 100644 --- a/src/json-crdt-patch/operations.ts +++ b/src/json-crdt-patch/operations.ts @@ -1,5 +1,5 @@ import type {IJsonCrdtPatchEditOperation, IJsonCrdtPatchOperation} from './types'; -import {type ITimestampStruct, type ITimespanStruct, Timestamp, toDisplayString} from './clock'; +import {type ITimestampStruct, type ITimespanStruct, Timestamp, printTs} from './clock'; /** * Operation which creates a constant "con" data type. @@ -24,13 +24,13 @@ export class NewConOp implements IJsonCrdtPatchOperation { const val = this.val; const valFormatted = val instanceof Timestamp - ? `{ ${toDisplayString(val)} }` + ? `{ ${printTs(val)} }` : val instanceof Uint8Array ? val.length < 13 ? `Uint8Array { ${('' + val).replaceAll(',', ', ')} }` : `Uint8Array(${val.length})` : `{ ${JSON.stringify(val)} }`; - return `${this.name()} ${toDisplayString(this.id)} ${valFormatted}`; + return `${this.name()} ${printTs(this.id)} ${valFormatted}`; } } @@ -51,7 +51,7 @@ export class NewValOp implements IJsonCrdtPatchOperation { } public toString(): string { - return `${this.name()} ${toDisplayString(this.id)}`; + return `${this.name()} ${printTs(this.id)}`; } } @@ -72,7 +72,7 @@ export class NewObjOp implements IJsonCrdtPatchOperation { } public toString(): string { - return `${this.name()} ${toDisplayString(this.id)}`; + return `${this.name()} ${printTs(this.id)}`; } } @@ -93,7 +93,7 @@ export class NewVecOp implements IJsonCrdtPatchOperation { } public toString(): string { - return `${this.name()} ${toDisplayString(this.id)}`; + return `${this.name()} ${printTs(this.id)}`; } } @@ -114,7 +114,7 @@ export class NewStrOp implements IJsonCrdtPatchOperation { } public toString(): string { - return `${this.name()} ${toDisplayString(this.id)}`; + return `${this.name()} ${printTs(this.id)}`; } } @@ -135,7 +135,7 @@ export class NewBinOp implements IJsonCrdtPatchOperation { } public toString(tab: string = ''): string { - return `${this.name()} ${toDisplayString(this.id)}`; + return `${this.name()} ${printTs(this.id)}`; } } @@ -156,7 +156,7 @@ export class NewArrOp implements IJsonCrdtPatchOperation { } public toString(): string { - return `${this.name()} ${toDisplayString(this.id)}`; + return `${this.name()} ${printTs(this.id)}`; } } @@ -182,9 +182,9 @@ export class InsValOp implements IJsonCrdtPatchEditOperation { } public toString(tab: string = ''): string { - return `${this.name()} ${toDisplayString(this.id)}!${this.span()}, obj = ${toDisplayString( + return `${this.name()} ${printTs(this.id)}!${this.span()}, obj = ${printTs( this.obj, - )}, val = ${toDisplayString(this.val)}`; + )}, val = ${printTs(this.val)}`; } } @@ -209,10 +209,10 @@ export class InsObjOp implements IJsonCrdtPatchEditOperation { } public toString(tab: string = ''): string { - let out = `${this.name()} ${toDisplayString(this.id)}!${this.span()}, obj = ${toDisplayString(this.obj)}`; + let out = `${this.name()} ${printTs(this.id)}!${this.span()}, obj = ${printTs(this.obj)}`; for (let i = 0; i < this.data.length; i++) { const isLast = i === this.data.length - 1; - out += `\n${tab} ${isLast ? '└─' : '├─'} ${JSON.stringify(this.data[i][0])}: ${toDisplayString( + out += `\n${tab} ${isLast ? '└─' : '├─'} ${JSON.stringify(this.data[i][0])}: ${printTs( this.data[i][1], )}`; } @@ -241,10 +241,10 @@ export class InsVecOp implements IJsonCrdtPatchEditOperation { } public toString(tab: string = ''): string { - let out = `${this.name()} ${toDisplayString(this.id)}!${this.span()}, obj = ${toDisplayString(this.obj)}`; + let out = `${this.name()} ${printTs(this.id)}!${this.span()}, obj = ${printTs(this.obj)}`; for (let i = 0; i < this.data.length; i++) { const isLast = i === this.data.length - 1; - out += `\n${tab} ${isLast ? '└─' : '├─'} ${JSON.stringify(this.data[i][0])}: ${toDisplayString( + out += `\n${tab} ${isLast ? '└─' : '├─'} ${JSON.stringify(this.data[i][0])}: ${printTs( this.data[i][1], )}`; } @@ -274,9 +274,9 @@ export class InsStrOp implements IJsonCrdtPatchEditOperation { } public toString(): string { - return `${this.name()} ${toDisplayString(this.id)}!${this.span()}, obj = ${toDisplayString( + return `${this.name()} ${printTs(this.id)}!${this.span()}, obj = ${printTs( this.obj, - )} { ${toDisplayString(this.ref)} ← ${JSON.stringify(this.data)} }`; + )} { ${printTs(this.ref)} ← ${JSON.stringify(this.data)} }`; } } @@ -302,8 +302,8 @@ export class InsBinOp implements IJsonCrdtPatchEditOperation { } public toString(tab: string = ''): string { - const ref = toDisplayString(this.ref); - return `${this.name()} ${toDisplayString(this.id)}!${this.span()}, obj = ${toDisplayString(this.obj)} { ${ref} ← ${ + const ref = printTs(this.ref); + return `${this.name()} ${printTs(this.id)}!${this.span()}, obj = ${printTs(this.obj)} { ${ref} ← ${ this.data } }`; } @@ -343,9 +343,9 @@ export class InsArrOp implements IJsonCrdtPatchEditOperation { } public toString(): string { - return `${this.name()} ${toDisplayString(this.id)}!${this.span()}, obj = ${toDisplayString( + return `${this.name()} ${printTs(this.id)}!${this.span()}, obj = ${printTs( this.obj, - )} { ${toDisplayString(this.ref)} ← ${this.data.map(toDisplayString).join(', ')} }`; + )} { ${printTs(this.ref)} ← ${this.data.map(printTs).join(', ')} }`; } } @@ -376,8 +376,8 @@ export class DelOp implements IJsonCrdtPatchEditOperation { } public toString(): string { - const spans = this.what.map((span) => toDisplayString(span) + '!' + span.span).join(', '); - return `${this.name()} ${toDisplayString(this.id)}, obj = ${toDisplayString(this.obj)} { ${spans} }`; + const spans = this.what.map((span) => printTs(span) + '!' + span.span).join(', '); + return `${this.name()} ${printTs(this.id)}, obj = ${printTs(this.obj)} { ${spans} }`; } } @@ -402,6 +402,6 @@ export class NopOp implements IJsonCrdtPatchOperation { } public toString(): string { - return `${this.name()} ${toDisplayString(this.id)}!${this.len}`; + return `${this.name()} ${printTs(this.id)}!${this.len}`; } } diff --git a/src/json-crdt/extensions/ExtensionNode.ts b/src/json-crdt/extensions/ExtensionNode.ts index 2668c3f013..fef6ae8037 100644 --- a/src/json-crdt/extensions/ExtensionNode.ts +++ b/src/json-crdt/extensions/ExtensionNode.ts @@ -1,4 +1,4 @@ -import {toDisplayString, type ITimestampStruct} from '../../json-crdt-patch/clock'; +import {printTs, type ITimestampStruct} from '../../json-crdt-patch/clock'; import type {ExtensionJsonNode, JsonNode} from '..'; import type {Printable} from 'tree-dump/lib/types'; @@ -29,6 +29,6 @@ export abstract class ExtensionNode implements ExtensionJson // ---------------------------------------------------------------- Printable public toString(tab?: string, parentId?: ITimestampStruct): string { - return this.name() + (parentId ? ' ' + toDisplayString(parentId) : '') + ', ' + this.data.toString(tab); + return this.name() + (parentId ? ' ' + printTs(parentId) : '') + ', ' + this.data.toString(tab); } } diff --git a/src/json-crdt/model/Model.ts b/src/json-crdt/model/Model.ts index 486354d38c..21e503c7fd 100644 --- a/src/json-crdt/model/Model.ts +++ b/src/json-crdt/model/Model.ts @@ -567,7 +567,7 @@ export class Model> implements Printable { (nodes.length ? printTree( tab, - nodes.map((node) => (tab) => `${node.name()} ${clock.toDisplayString(node.id)}`), + nodes.map((node) => (tab) => `${node.name()} ${clock.printTs(node.id)}`), ) : '') ); diff --git a/src/json-crdt/nodes/const/ConNode.ts b/src/json-crdt/nodes/const/ConNode.ts index 51dd0f9fe9..b0a3377a45 100644 --- a/src/json-crdt/nodes/const/ConNode.ts +++ b/src/json-crdt/nodes/const/ConNode.ts @@ -1,4 +1,4 @@ -import {type ITimestampStruct, toDisplayString, Timestamp} from '../../../json-crdt-patch/clock'; +import {type ITimestampStruct, printTs, Timestamp} from '../../../json-crdt-patch/clock'; import type {JsonNode} from '../types'; import type {Printable} from 'tree-dump/lib/types'; @@ -63,7 +63,7 @@ export class ConNode implements JsonNode = Record { if (isUint8Array(view)) value += ` { ${printOctets(view) || '∅'} }`; else if (typeof view === 'string') value += `{ ${view.length > 32 ? JSON.stringify(view.substring(0, 32)) + ' …' : JSON.stringify(view)} }`; - const header = `${this.toStringName()} ${toDisplayString(this.id)} ${value}`; + const header = `${this.toStringName()} ${printTs(this.id)} ${value}`; return header + printTree(tab, [(tab) => (this.root ? this.printChunk(tab, this.root) : '∅')]); } @@ -903,7 +903,7 @@ export abstract class AbstractRga { } protected formatChunk(chunk: Chunk): string { - const id = toDisplayString(chunk.id); + const id = printTs(chunk.id); let str = `${chunk.constructor.name} ${id}!${chunk.span} len:${chunk.len}`; if (chunk.del) str += ` [${chunk.span}]`; else { diff --git a/src/json-crdt/nodes/str/__tests__/StrNodeFuzzer.ts b/src/json-crdt/nodes/str/__tests__/StrNodeFuzzer.ts index 51cd702209..439294dc21 100644 --- a/src/json-crdt/nodes/str/__tests__/StrNodeFuzzer.ts +++ b/src/json-crdt/nodes/str/__tests__/StrNodeFuzzer.ts @@ -1,5 +1,5 @@ import {equal} from 'assert'; -import {ITimespanStruct, ITimestampStruct, ClockVector, toDisplayString, ts} from '../../../../json-crdt-patch/clock'; +import {ITimespanStruct, ITimestampStruct, ClockVector, printTs, ts} from '../../../../json-crdt-patch/clock'; import {Fuzzer} from '@jsonjoy.com/util/lib/Fuzzer'; import {randomSessionId} from '../../../model/util'; import {StrNode} from '../StrNode'; @@ -7,9 +7,9 @@ import {printTree, Printable} from 'tree-dump'; const printOp = (op: Op) => { if ('content' in op) { - return `ins ${toDisplayString(op.after)} ${JSON.stringify(op.content)}`; + return `ins ${printTs(op.after)} ${JSON.stringify(op.content)}`; } else { - return `del ${op.range.map((r) => toDisplayString(r) + '!' + r.span).join(', ')}`; + return `del ${op.range.map((r) => printTs(r) + '!' + r.span).join(', ')}`; } }; diff --git a/src/json-crdt/nodes/val/ValNode.ts b/src/json-crdt/nodes/val/ValNode.ts index f1e51283bf..4df353c506 100644 --- a/src/json-crdt/nodes/val/ValNode.ts +++ b/src/json-crdt/nodes/val/ValNode.ts @@ -1,4 +1,4 @@ -import {compare, ITimestampStruct, toDisplayString} from '../../../json-crdt-patch/clock'; +import {compare, ITimestampStruct, printTs} from '../../../json-crdt-patch/clock'; import {SESSION} from '../../../json-crdt-patch/constants'; import {printTree} from 'tree-dump/lib/printTree'; import {UNDEFINED} from '../../model/Model'; @@ -89,7 +89,7 @@ export class ValNode implements JsonNode (node ? node.toString(tab) : toDisplayString(this.val))]); + const header = this.name() + ' ' + printTs(this.id); + return header + printTree(tab, [(tab) => (node ? node.toString(tab) : printTs(this.val))]); } } diff --git a/src/json-crdt/nodes/vec/VecNode.ts b/src/json-crdt/nodes/vec/VecNode.ts index f39d98c743..74281fd688 100644 --- a/src/json-crdt/nodes/vec/VecNode.ts +++ b/src/json-crdt/nodes/vec/VecNode.ts @@ -1,7 +1,7 @@ import {ConNode} from '../const/ConNode'; import {CRDT_CONSTANTS} from '../../constants'; import {printTree} from 'tree-dump/lib/printTree'; -import {compare, ITimestampStruct, toDisplayString} from '../../../json-crdt-patch/clock'; +import {compare, ITimestampStruct, printTs} from '../../../json-crdt-patch/clock'; import type {Model} from '../../model'; import type {JsonNode, JsonNodeView} from '..'; import type {Printable} from 'tree-dump/lib/types'; @@ -181,7 +181,7 @@ export class VecNode implements JsonNode< public toString(tab: string = ''): string { const extNode = this.ext(); const header = - this.name() + ' ' + toDisplayString(this.id) + (extNode ? ` { extension = ${this.getExtId()} }` : ''); + this.name() + ' ' + printTs(this.id) + (extNode ? ` { extension = ${this.getExtId()} }` : ''); if (extNode) { return this.child()!.toString(tab, this.id); } From f0435a15a013eb541f471e3226937efadafc7816 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 3 May 2024 11:59:37 +0200 Subject: [PATCH 19/37] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20infer=20sessin=20ID=20from=20builder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/cnt/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/json-crdt-extensions/cnt/index.ts b/src/json-crdt-extensions/cnt/index.ts index d1ddb57175..bfcad15185 100644 --- a/src/json-crdt-extensions/cnt/index.ts +++ b/src/json-crdt-extensions/cnt/index.ts @@ -2,7 +2,7 @@ import {ExtensionId, ExtensionName} from '../constants'; import {NodeApi} from '../../json-crdt/model/api/nodes'; import {ExtensionNode} from '../../json-crdt/extensions/ExtensionNode'; import {Extension} from '../../json-crdt/extensions/Extension'; -import {nodes, s, type ObjNode} from '../../json-crdt'; +import {NodeBuilder, nodes, s, type ObjNode} from '../../json-crdt'; import type {ExtensionApi} from '../../json-crdt'; const MNEMONIC = ExtensionName[ExtensionId.cnt]; @@ -35,7 +35,10 @@ class CntApi extends NodeApi implements ExtensionApi { } } -const create = (value?: any, sid: any = 0) => - value === undefined ? s.map>({}) : s.map>({[sid]: s.con(value ?? 0)}); +const create = (value?: any, sid: any = 0) => new NodeBuilder(builder => { + if (!sid) sid = builder.clock.sid; + const schema = value === undefined ? s.map>({}) : s.map>({[sid]: s.con(value ?? 0)}); + return schema.build(builder); +}); export const cnt = new Extension(ExtensionId.cnt, MNEMONIC, CntNode, CntApi, create); From 7bbee8f12eda12168e512476c7b22a764424c35a Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 3 May 2024 12:21:44 +0200 Subject: [PATCH 20/37] =?UTF-8?q?fix(json-crdt):=20=F0=9F=90=9B=20make=20e?= =?UTF-8?q?xtension=20schema=20copyable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt/extensions/ExtensionNode.ts | 2 +- src/json-crdt/nodes/vec/VecNode.ts | 1 - .../schema/__tests__/toSchema.spec.ts | 26 +++++++++++++------ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/json-crdt/extensions/ExtensionNode.ts b/src/json-crdt/extensions/ExtensionNode.ts index fef6ae8037..5a903fdcd6 100644 --- a/src/json-crdt/extensions/ExtensionNode.ts +++ b/src/json-crdt/extensions/ExtensionNode.ts @@ -29,6 +29,6 @@ export abstract class ExtensionNode implements ExtensionJson // ---------------------------------------------------------------- Printable public toString(tab?: string, parentId?: ITimestampStruct): string { - return this.name() + (parentId ? ' ' + printTs(parentId) : '') + ', ' + this.data.toString(tab); + return this.name() + (parentId ? ' ' + printTs(parentId) : '') + ' ' + this.data.toString(tab); } } diff --git a/src/json-crdt/nodes/vec/VecNode.ts b/src/json-crdt/nodes/vec/VecNode.ts index 74281fd688..638e2b1005 100644 --- a/src/json-crdt/nodes/vec/VecNode.ts +++ b/src/json-crdt/nodes/vec/VecNode.ts @@ -128,7 +128,6 @@ export class VecNode implements JsonNode< * @ignore */ public children(callback: (node: JsonNode) => void) { - if (this.isExt()) return; const elements = this.elements; const length = elements.length; const index = this.doc.index; diff --git a/src/json-crdt/schema/__tests__/toSchema.spec.ts b/src/json-crdt/schema/__tests__/toSchema.spec.ts index 9a08bc874b..0964697dd8 100644 --- a/src/json-crdt/schema/__tests__/toSchema.spec.ts +++ b/src/json-crdt/schema/__tests__/toSchema.spec.ts @@ -3,6 +3,7 @@ import {deepEqual} from '../../../json-equal/deepEqual'; import {cmpUint8Array} from '@jsonjoy.com/util/lib/buffers/cmpUint8Array'; import {Model} from '../../model'; import {toSchema} from '../toSchema'; +import {cnt} from '../../../json-crdt-extensions'; const cmp = (a: NodeBuilder, b: NodeBuilder): boolean => { if (a instanceof nodes.con && b instanceof nodes.con) return deepEqual(a.raw, b.raw); @@ -62,7 +63,7 @@ test('can infer schema of a document nodes', () => { vec: s.vec(s.con(1), s.con({foo: 'bar'})), arr: s.arr([s.con(1), s.con({foo: 'bar'})]), }); - const model = Model.withLogicalClock().setSchema(schema); + const model = Model.create(schema); const node = model.root.node(); const schema2 = toSchema(node); expect(cmp(schema, schema2)).toBe(true); @@ -77,13 +78,22 @@ test('can infer schema of a document nodes', () => { expect(cmp(con, objSchema)).toBe(false); }); -test('can infer schema of a typed model', () => { +test('can copy a model with extension', () => { const schema = s.obj({ - id: s.con('id'), - val: s.val(s.str('world')), + count: cnt.new(1), + }); + const model = Model.create(); + model.api.root(schema); + model.ext.register(cnt); + const copy = toSchema(model.root.node()); + const model2 = Model.create(copy, model.clock.sid); + expect(model2.view()).toMatchObject({ + count: [ + expect.any(Uint8Array), + { + [model2.clock.sid.toString(36)]: 1, + }, + ], }); - const model = Model.withLogicalClock().setSchema(schema); - const schema2 = toSchema(model.root.node()); - expect(schema2.obj.id).toBeInstanceOf(nodes.con); - expect(schema2.obj.val).toBeInstanceOf(nodes.val); }); + From 7401ac5b7fdeb45682e23fe07b2b1f31ef6040aa Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 3 May 2024 13:54:38 +0200 Subject: [PATCH 21/37] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20add=20type=20safety=20when=20accessing=20Peritex?= =?UTF-8?q?t=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/ModelWithExt.ts | 6 ++++ .../peritext/__tests__/extension.spec.ts | 32 ++++++++++++++----- src/json-crdt-extensions/peritext/index.ts | 5 +++ src/json-crdt/model/api/proxy.ts | 5 ++- src/json-crdt/model/api/types.ts | 5 ++- src/json-crdt/schema/__tests__/types.spec.ts | 8 +++++ src/json-crdt/schema/types.ts | 16 ++++++++-- 7 files changed, 65 insertions(+), 12 deletions(-) diff --git a/src/json-crdt-extensions/ModelWithExt.ts b/src/json-crdt-extensions/ModelWithExt.ts index 4bf92b77ed..ab6004db34 100644 --- a/src/json-crdt-extensions/ModelWithExt.ts +++ b/src/json-crdt-extensions/ModelWithExt.ts @@ -11,7 +11,13 @@ extensions.register(ext.cnt); extensions.register(ext.mval); extensions.register(ext.peritext); +export { + ext, +}; + export class ModelWithExt { + public static readonly ext = ext; + public static readonly create = ( schema?: S, sidOrClock: clock.ClockVector | number = Model.sid(), diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts index cfec75bd80..fb71e0c6f2 100644 --- a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -1,11 +1,27 @@ -import {peritext} from '..'; -import {ModelWithExt} from '../../ModelWithExt'; +import {s} from '../../../json-crdt-patch'; +import {ModelWithExt, ext} from '../../ModelWithExt'; +import {PeritextNode} from '../PeritextNode'; -test('..', () => { - const model = ModelWithExt.create(); - model.api.root({ - text: peritext.new('Hello, world\n'), - }); +const schema = s.obj({ + nested: s.obj({ + obj: s.obj({ + text: ext.peritext.new('Hello, world\n'), + }), + }), +}); + +test('can access PeritextNode in type safe way', () => { + const model = ModelWithExt.create(schema); + const node = model.root.node().get('nested')!.get('obj')!.get('text')!.child!(); + expect(node).toBeInstanceOf(PeritextNode); + expect(node.view()).toBe('Hello, world\n'); +}); - // console.log('' + model); +test('can access PeritextApi using proxy selector', () => { + const model = ModelWithExt.create(schema); + model.api.str(['nested', 'obj', 'text', 1, 0]).ins(12, '!'); + const api = model.s.nested.obj.text.toApi(); + expect(api.view()).toBe('Hello, world!\n'); }); + +test.todo('can access nested nodes using proxy selector'); diff --git a/src/json-crdt-extensions/peritext/index.ts b/src/json-crdt-extensions/peritext/index.ts index 2fa9487f97..b759c0853e 100644 --- a/src/json-crdt-extensions/peritext/index.ts +++ b/src/json-crdt-extensions/peritext/index.ts @@ -5,6 +5,11 @@ import {SCHEMA, MNEMONIC} from './constants'; import {Extension} from '../../json-crdt/extensions/Extension'; import type {PeritextDataNode} from './types'; +export { + PeritextNode, + PeritextApi, +}; + export const peritext = new Extension< ExtensionId.peritext, PeritextDataNode, diff --git a/src/json-crdt/model/api/proxy.ts b/src/json-crdt/model/api/proxy.ts index d95f1f8bbd..1ca068df49 100644 --- a/src/json-crdt/model/api/proxy.ts +++ b/src/json-crdt/model/api/proxy.ts @@ -1,5 +1,6 @@ import type {JsonNodeApi} from './types'; import type * as nodes from '../../nodes'; +import type {PeritextNode} from '../../../json-crdt-extensions'; export interface ProxyNode { toApi(): JsonNodeApi; @@ -38,4 +39,6 @@ export type JsonNodeToProxyNode = N extends nodes.ConNode ? ProxyNodeObj : N extends nodes.VecNode ? ProxyNodeVec - : never; + : N extends PeritextNode + ? ProxyNode + : never; diff --git a/src/json-crdt/model/api/types.ts b/src/json-crdt/model/api/types.ts index 37887dd333..fe68c38bde 100644 --- a/src/json-crdt/model/api/types.ts +++ b/src/json-crdt/model/api/types.ts @@ -1,3 +1,4 @@ +import type {PeritextNode, PeritextApi} from '../../../json-crdt-extensions/peritext'; import type * as types from '../../nodes'; import type * as nodes from './nodes'; @@ -18,4 +19,6 @@ export type JsonNodeApi = N extends types.ConNode ? nodes.ObjApi : N extends types.VecNode ? nodes.VecApi - : never; + : N extends PeritextNode + ? PeritextApi + : never; diff --git a/src/json-crdt/schema/__tests__/types.spec.ts b/src/json-crdt/schema/__tests__/types.spec.ts index a4eefe5eed..502381ff06 100644 --- a/src/json-crdt/schema/__tests__/types.spec.ts +++ b/src/json-crdt/schema/__tests__/types.spec.ts @@ -1,4 +1,5 @@ import {s} from '../../../json-crdt-patch'; +import * as ext from '../../../json-crdt-extensions/ext'; import {Model} from '../../model'; import {JsonNodeToSchema, SchemaToJsonNode} from '../types'; @@ -40,6 +41,13 @@ describe('can infer schema of JSON CRDT nodes', () => { const schema2: JsonNodeToSchema> = schema1; }); + test('ext: peritext', () => { + const schema1 = s.obj({ + richText: ext.peritext.new('hello'), + }); + const schema2: JsonNodeToSchema> = schema1; + }); + test('from typed model', () => { const model = Model.withLogicalClock().setSchema( s.obj({ diff --git a/src/json-crdt/schema/types.ts b/src/json-crdt/schema/types.ts index 01928ae3c9..4ac32d6fc7 100644 --- a/src/json-crdt/schema/types.ts +++ b/src/json-crdt/schema/types.ts @@ -1,3 +1,6 @@ +import type {ExtensionId} from '../../json-crdt-extensions'; +import type {MvalNode} from '../../json-crdt-extensions/mval/MvalNode'; +import type {PeritextNode} from '../../json-crdt-extensions/peritext/PeritextNode'; import type {nodes as builder} from '../../json-crdt-patch'; import type * as nodes from '../nodes'; @@ -16,7 +19,12 @@ export type SchemaToJsonNode = S extends builder.str ? nodes.ObjNode<{[K in keyof T]: SchemaToJsonNode}> : S extends builder.arr ? nodes.ArrNode> - : nodes.JsonNode; + : S extends builder.ext + ? PeritextNode + : S extends builder.ext + ? MvalNode + : nodes.JsonNode; + // prettier-ignore export type JsonNodeToSchema = N extends nodes.StrNode @@ -33,4 +41,8 @@ export type JsonNodeToSchema = N extends nodes.StrNode ? builder.obj<{[K in keyof T]: JsonNodeToSchema}> : N extends nodes.ArrNode ? builder.arr> - : builder.con; + : N extends PeritextNode + ? builder.ext + : N extends MvalNode + ? builder.ext + : builder.con; From 264d45c4ad46938d18282db72b687da7c5bff4cd Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 3 May 2024 13:55:02 +0200 Subject: [PATCH 22/37] =?UTF-8?q?fix(json-crdt-extensions):=20=F0=9F=90=9B?= =?UTF-8?q?=20correctly=20encode=20cnt=20extension=20sid=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/cnt/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/json-crdt-extensions/cnt/index.ts b/src/json-crdt-extensions/cnt/index.ts index bfcad15185..a68e9c6d59 100644 --- a/src/json-crdt-extensions/cnt/index.ts +++ b/src/json-crdt-extensions/cnt/index.ts @@ -37,7 +37,9 @@ class CntApi extends NodeApi implements ExtensionApi { const create = (value?: any, sid: any = 0) => new NodeBuilder(builder => { if (!sid) sid = builder.clock.sid; - const schema = value === undefined ? s.map>({}) : s.map>({[sid]: s.con(value ?? 0)}); + const schema = value === undefined + ? s.map>({}) + : s.map>({[sid.toString(36)]: s.con(value ?? 0)}); return schema.build(builder); }); From 14e5b56a5d25bd891369757fc5f52717b77ba93a Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 3 May 2024 22:05:45 +0200 Subject: [PATCH 23/37] =?UTF-8?q?chore(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=A4=96=20progress=20on=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/__tests__/extension.spec.ts | 8 +++++++- src/json-crdt/model/api/nodes.ts | 11 ++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts index fb71e0c6f2..0835261abd 100644 --- a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -1,5 +1,6 @@ import {s} from '../../../json-crdt-patch'; import {ModelWithExt, ext} from '../../ModelWithExt'; +import {PeritextApi} from '../PeritextApi'; import {PeritextNode} from '../PeritextNode'; const schema = s.obj({ @@ -21,7 +22,12 @@ test('can access PeritextApi using proxy selector', () => { const model = ModelWithExt.create(schema); model.api.str(['nested', 'obj', 'text', 1, 0]).ins(12, '!'); const api = model.s.nested.obj.text.toApi(); + expect(api).toBeInstanceOf(PeritextApi); expect(api.view()).toBe('Hello, world!\n'); }); -test.todo('can access nested nodes using proxy selector'); +// test('can access nested nodes using proxy selector', () => { +// const model = ModelWithExt.create(schema); +// const api = model.s.nested.obj.text.toApi(); +// console.log(api + ''); +// }); diff --git a/src/json-crdt/model/api/nodes.ts b/src/json-crdt/model/api/nodes.ts index b8858281e0..ce9ababe59 100644 --- a/src/json-crdt/model/api/nodes.ts +++ b/src/json-crdt/model/api/nodes.ts @@ -18,6 +18,8 @@ export type ApiPath = string | number | Path | void; * A generic local changes API for a JSON CRDT node. * * @category Local API + * + * @todo Separate this into leaf and container nodes. */ export class NodeApi implements Printable { constructor( @@ -32,7 +34,7 @@ export class NodeApi implements Printable { * Event target for listening to node changes. You can subscribe to `"view"` * events, which are triggered every time the node's view changes. * - * ```typescript + * ```ts * node.events.on('view', () => { * // do something... * }); @@ -154,6 +156,13 @@ export class NodeApi implements Printable { return this.node.view() as unknown as JsonNodeView; } + public proxy(): types.ProxyNode { + return { + toApi: () => this, + toView: () => this.node.view() as any, + }; + } + public toString(tab: string = ''): string { return this.constructor.name + printTree(tab, [(tab) => this.node.toString(tab)]); } From ef5c5819517786072a7bc30fa7c3b5df8c2b1e5f Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 4 May 2024 08:44:26 +0200 Subject: [PATCH 24/37] =?UTF-8?q?feat(json-crdt):=20=F0=9F=8E=B8=20constru?= =?UTF-8?q?ct=20extension=20API=20nodes=20when=20accessing=20by=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/cnt/index.ts | 4 +++- src/json-crdt-extensions/mval/MvalNode.ts | 3 +++ src/json-crdt-extensions/peritext/PeritextNode.ts | 3 +++ src/json-crdt/extensions/Extension.ts | 7 ++++--- src/json-crdt/extensions/ExtensionNode.ts | 7 ++++--- src/json-crdt/extensions/types.ts | 5 +---- src/json-crdt/model/api/ModelApi.ts | 10 +++++++++- src/json-crdt/model/api/nodes.ts | 5 +++-- 8 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/json-crdt-extensions/cnt/index.ts b/src/json-crdt-extensions/cnt/index.ts index a68e9c6d59..a93fbf7952 100644 --- a/src/json-crdt-extensions/cnt/index.ts +++ b/src/json-crdt-extensions/cnt/index.ts @@ -7,7 +7,9 @@ import type {ExtensionApi} from '../../json-crdt'; const MNEMONIC = ExtensionName[ExtensionId.cnt]; -class CntNode extends ExtensionNode { +class CntNode extends ExtensionNode { + public readonly extId = ExtensionId.cnt; + public name(): string { return MNEMONIC; } diff --git a/src/json-crdt-extensions/mval/MvalNode.ts b/src/json-crdt-extensions/mval/MvalNode.ts index 678856b7c4..8f35741efc 100644 --- a/src/json-crdt-extensions/mval/MvalNode.ts +++ b/src/json-crdt-extensions/mval/MvalNode.ts @@ -1,8 +1,11 @@ import {MNEMONIC} from './constants'; import {ExtensionNode} from '../../json-crdt/extensions/ExtensionNode'; +import {ExtensionId} from '../constants'; import type {ArrNode} from '../../json-crdt/nodes/arr/ArrNode'; export class MvalNode extends ExtensionNode { + public readonly extId = ExtensionId.mval; + public name(): string { return MNEMONIC; } diff --git a/src/json-crdt-extensions/peritext/PeritextNode.ts b/src/json-crdt-extensions/peritext/PeritextNode.ts index 5b65b19e94..714068844c 100644 --- a/src/json-crdt-extensions/peritext/PeritextNode.ts +++ b/src/json-crdt-extensions/peritext/PeritextNode.ts @@ -1,8 +1,11 @@ import {MNEMONIC} from './constants'; import {ExtensionNode} from '../../json-crdt/extensions/ExtensionNode'; +import {ExtensionId} from '../constants'; import type {PeritextDataNode} from './types'; export class PeritextNode extends ExtensionNode { + public readonly extId = ExtensionId.peritext; + public name(): string { return MNEMONIC; } diff --git a/src/json-crdt/extensions/Extension.ts b/src/json-crdt/extensions/Extension.ts index 164f296d1d..49bb1b4bd8 100644 --- a/src/json-crdt/extensions/Extension.ts +++ b/src/json-crdt/extensions/Extension.ts @@ -1,15 +1,16 @@ import {NodeBuilder, s, type nodes} from '../../json-crdt-patch'; +import {ExtensionNode} from './ExtensionNode'; import type {ModelApi} from '../model'; import type {JsonNode} from '../nodes'; import type {JsonNodeToSchema} from '../schema/types'; -import type {ExtensionApi, ExtensionJsonNode} from './types'; +import type {ExtensionApi} from './types'; export type AnyExtension = Extension; export class Extension< Id extends number, DataNode extends JsonNode, - ExtNode extends ExtensionJsonNode, + ExtNode extends ExtensionNode, ExtApi extends ExtensionApi, DataArgs extends any[] = any[], DataSchema extends NodeBuilder = JsonNodeToSchema, @@ -18,7 +19,7 @@ export class Extension< public readonly id: Id, public readonly name: string, public readonly Node: new (data: DataNode) => ExtNode, - public readonly Api: new (node: ExtNode, api: ModelApi) => ExtApi, + public readonly Api: new (node: ExtNode, api: ModelApi) => ExtApi, public readonly schema: (...args: DataArgs) => DataSchema, ) {} diff --git a/src/json-crdt/extensions/ExtensionNode.ts b/src/json-crdt/extensions/ExtensionNode.ts index 5a903fdcd6..2582103865 100644 --- a/src/json-crdt/extensions/ExtensionNode.ts +++ b/src/json-crdt/extensions/ExtensionNode.ts @@ -1,8 +1,9 @@ import {printTs, type ITimestampStruct} from '../../json-crdt-patch/clock'; -import type {ExtensionJsonNode, JsonNode} from '..'; +import type {JsonNode} from '..'; import type {Printable} from 'tree-dump/lib/types'; -export abstract class ExtensionNode implements ExtensionJsonNode, Printable { +export abstract class ExtensionNode implements JsonNode, Printable { + public abstract readonly extId: number; public readonly id: ITimestampStruct; constructor(public readonly data: N) { @@ -12,7 +13,7 @@ export abstract class ExtensionNode implements ExtensionJson // -------------------------------------------------------- ExtensionJsonNode public abstract name(): string; - public abstract view(): unknown; + public abstract view(): View; public children(callback: (node: JsonNode) => void): void {} diff --git a/src/json-crdt/extensions/types.ts b/src/json-crdt/extensions/types.ts index 4aad5c594a..3966717a1d 100644 --- a/src/json-crdt/extensions/types.ts +++ b/src/json-crdt/extensions/types.ts @@ -1,9 +1,6 @@ import type {NodeApi} from '../model/api/nodes'; -import type {JsonNode} from '../nodes'; import type {ExtensionNode} from './ExtensionNode'; export type ExtensionValue = [type: Uint8Array, data: unknown]; -export interface ExtensionJsonNode extends ExtensionNode {} - -export interface ExtensionApi> extends NodeApi {} +export interface ExtensionApi> extends NodeApi {} diff --git a/src/json-crdt/model/api/ModelApi.ts b/src/json-crdt/model/api/ModelApi.ts index d9b68f9605..cbd7b9d564 100644 --- a/src/json-crdt/model/api/ModelApi.ts +++ b/src/json-crdt/model/api/ModelApi.ts @@ -85,7 +85,15 @@ export class ModelApi implements SyncStore implements Printable { throw new Error('NOT_CONST'); } - public asExt, EApi extends ExtensionApi>( + public asExt, EApi extends ExtensionApi>( ext: Extension, ): EApi { let node: JsonNode | undefined = this.node; From c09dc8fc2034da4a086b40292ee329b9f2cc3f32 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 4 May 2024 18:52:11 +0200 Subject: [PATCH 25/37] =?UTF-8?q?fix(json-crdt):=20=F0=9F=90=9B=20allow=20?= =?UTF-8?q?extension=20API=20selection=20through=20path=20selector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/__tests__/extension.spec.ts | 13 ++++++++++--- .../overlay/__tests__/Overlay.refresh.spec.ts | 6 +++--- src/json-crdt/extensions/ExtensionNode.ts | 4 ++++ src/json-crdt/extensions/types.ts | 1 + src/json-crdt/model/api/ModelApi.ts | 13 ++++++------- src/json-crdt/model/api/nodes.ts | 10 ++++++++++ .../nodes/vec/__tests__/VecNode-extension.spec.ts | 2 ++ src/json-crdt/nodes/vec/__tests__/extension.spec.ts | 9 +++++---- 8 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts index 0835261abd..ec57b4b5ea 100644 --- a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -1,4 +1,5 @@ import {s} from '../../../json-crdt-patch'; +import {VecApi} from '../../../json-crdt/model'; import {ModelWithExt, ext} from '../../ModelWithExt'; import {PeritextApi} from '../PeritextApi'; import {PeritextNode} from '../PeritextNode'; @@ -18,14 +19,20 @@ test('can access PeritextNode in type safe way', () => { expect(node.view()).toBe('Hello, world\n'); }); -test('can access PeritextApi using proxy selector', () => { +test.only('can access PeritextApi using path selector', () => { const model = ModelWithExt.create(schema); + const api = model.api.vec(['nested', 'obj', 'text']); + expect(api).toBeInstanceOf(VecApi); + const api2 = api.ext(); + expect(api2).toBeInstanceOf(PeritextApi); model.api.str(['nested', 'obj', 'text', 1, 0]).ins(12, '!'); - const api = model.s.nested.obj.text.toApi(); - expect(api).toBeInstanceOf(PeritextApi); expect(api.view()).toBe('Hello, world!\n'); }); +// test.only('can access PeritextApi using proxy selector', () => { + // const api = model.s.nested.obj.text.toApi(); +// }); + // test('can access nested nodes using proxy selector', () => { // const model = ModelWithExt.create(schema); // const api = model.s.nested.obj.text.toApi(); diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.refresh.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.refresh.spec.ts index 8c1cbc752d..8ef70930d5 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.refresh.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.refresh.spec.ts @@ -102,7 +102,7 @@ describe('Overlay.refresh()', () => { kit.peritext.editor.cursor.setAt(0, 1); const slice = kit.peritext.editor.insStackSlice(123, {foo: 'bar'}); refresh(); - const api = slice.dataNode()! as ObjApi; + const api = slice.dataNode()! as ObjApi; api.set({foo: 'baz'}); }); @@ -200,7 +200,7 @@ describe('Overlay.refresh()', () => { const range = kit.peritext.rangeAt(1, 1); const slice = kit.peritext.extraSlices.insStack(range, 123, {foo: 'bar'}); refresh(); - const api = slice.dataNode()! as ObjApi; + const api = slice.dataNode()! as ObjApi; api.set({foo: 'baz'}); }); @@ -270,7 +270,7 @@ describe('Overlay.refresh()', () => { const slice = kit.peritext.editor.cursor; slice.update({data: {a: 'b'}}); refresh(); - const api = slice.dataNode()! as ObjApi; + const api = slice.dataNode()! as ObjApi; api.set({a: 'c'}); }); }); diff --git a/src/json-crdt/extensions/ExtensionNode.ts b/src/json-crdt/extensions/ExtensionNode.ts index 2582103865..952d27c8cc 100644 --- a/src/json-crdt/extensions/ExtensionNode.ts +++ b/src/json-crdt/extensions/ExtensionNode.ts @@ -2,6 +2,7 @@ import {printTs, type ITimestampStruct} from '../../json-crdt-patch/clock'; import type {JsonNode} from '..'; import type {Printable} from 'tree-dump/lib/types'; +/** @todo Rename to `ExtNode` to be inline with other classes. */ export abstract class ExtensionNode implements JsonNode, Printable { public abstract readonly extId: number; public readonly id: ITimestampStruct; @@ -12,6 +13,9 @@ export abstract class ExtensionNode implemen // -------------------------------------------------------- ExtensionJsonNode + /** + * @todo Maybe hardcode this as `ext` to be inline with other classes. And retrieve extension names differently. + */ public abstract name(): string; public abstract view(): View; diff --git a/src/json-crdt/extensions/types.ts b/src/json-crdt/extensions/types.ts index 3966717a1d..f23e76ae0b 100644 --- a/src/json-crdt/extensions/types.ts +++ b/src/json-crdt/extensions/types.ts @@ -3,4 +3,5 @@ import type {ExtensionNode} from './ExtensionNode'; export type ExtensionValue = [type: Uint8Array, data: unknown]; +/** @todo Rename to `ExtApi`. */ export interface ExtensionApi> extends NodeApi {} diff --git a/src/json-crdt/model/api/ModelApi.ts b/src/json-crdt/model/api/ModelApi.ts index cbd7b9d564..788aaece52 100644 --- a/src/json-crdt/model/api/ModelApi.ts +++ b/src/json-crdt/model/api/ModelApi.ts @@ -7,6 +7,7 @@ import {SyncStore} from '../../../util/events/sync-store'; import {MergeFanOut, MicrotaskBufferFanOut} from './fanout'; import type {Model} from '../Model'; import type {JsonNode, JsonNodeView} from '../../nodes'; +import {ExtensionNode} from '../../extensions/ExtensionNode'; /** * Local changes API for a JSON CRDT model. This class is the main entry point @@ -78,6 +79,7 @@ export class ModelApi implements SyncStore): NodeApi; public wrap(node: JsonNode) { if (node instanceof ValNode) return node.api || (node.api = new ValApi(node, this)); else if (node instanceof StrNode) return node.api || (node.api = new StrApi(node, this)); @@ -85,14 +87,11 @@ export class ModelApi implements SyncStore implements Printable { throw new Error('NOT_ARR'); } + /** + * @todo rename as `asVec`. + */ public asTup(): VecApi { if (this.node instanceof VecNode) return this.api.wrap(this.node as VecNode); throw new Error('NOT_ARR'); @@ -282,6 +285,13 @@ export class VecApi = VecNode> extends NodeApi { return this.node.elements.length; } + public ext(): ExtensionApi | undefined { + const node = this.node.ext(); + if (!node) return node; + const api = this.api.wrap(node); + return api; + } + /** * Returns a proxy object for this node. Allows to access vector elements by * index. diff --git a/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts b/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts index f97246be50..2717d63792 100644 --- a/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts +++ b/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts @@ -36,6 +36,8 @@ describe('sample extension', () => { 123, 'double-concat', class extends ExtensionNode> { + public extId: number = 123; + public name(): string { return 'double-concat'; } diff --git a/src/json-crdt/nodes/vec/__tests__/extension.spec.ts b/src/json-crdt/nodes/vec/__tests__/extension.spec.ts index cd9801f706..f73b6f3d3c 100644 --- a/src/json-crdt/nodes/vec/__tests__/extension.spec.ts +++ b/src/json-crdt/nodes/vec/__tests__/extension.spec.ts @@ -53,7 +53,7 @@ test('exposes API to edit extension data', () => { }); describe('extension validity checks', () => { - test('does not treat ArrNode as extension if header is too long', () => { + test('does not treat VecNode as extension if header is too long', () => { const model = Model.withLogicalClock(); model.ext.register(mval); model.api.root({ @@ -61,14 +61,15 @@ describe('extension validity checks', () => { }); const buf = new Uint8Array(4); buf.set(model.api.const(['mv', 0]).node.view() as Uint8Array, 0); - model.api.vec(['mv']).set([[0, buf]]); + const api = model.api.vec(['mv']); + api.set([[0, buf]]); expect(model.view()).toEqual({ mv: [buf, []], }); expect(model.api.vec(['mv']).node.isExt()).toBe(false); }); - test('does not treat ArrNode as extension if header sid is wrong', () => { + test('does not treat VecNode as extension if header sid is wrong', () => { const model = Model.withLogicalClock(); model.ext.register(mval); model.api.root({ @@ -82,7 +83,7 @@ describe('extension validity checks', () => { expect(model.api.vec(['mv']).node.isExt()).toBe(false); }); - test('does not treat ArrNode as extension if header time is wrong', () => { + test('does not treat VecNode as extension if header time is wrong', () => { const model = Model.withLogicalClock(); model.ext.register(mval); model.api.root({ From ebf1eea0ff2c5761f3b184d07a2ec32e74e963ab Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 4 May 2024 19:29:37 +0200 Subject: [PATCH 26/37] =?UTF-8?q?feat(json-crdt):=20=F0=9F=8E=B8=20improve?= =?UTF-8?q?=20extension=20node=20selection=20by=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/__tests__/extension.spec.ts | 34 ++++++++++++++----- src/json-crdt/model/api/nodes.ts | 5 +-- src/json-crdt/nodes/vec/VecNode.ts | 9 ++--- src/json-crdt/schema/__tests__/types.spec.ts | 3 +- src/json-crdt/schema/types.ts | 30 +++++++++++----- 5 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts index ec57b4b5ea..6c3dcee2f2 100644 --- a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -13,13 +13,13 @@ const schema = s.obj({ }); test('can access PeritextNode in type safe way', () => { - const model = ModelWithExt.create(schema); - const node = model.root.node().get('nested')!.get('obj')!.get('text')!.child!(); - expect(node).toBeInstanceOf(PeritextNode); - expect(node.view()).toBe('Hello, world\n'); + // const model = ModelWithExt.create(schema); + // const node = model.root.node().get('nested')!.get('obj')!.get('text')!.child!(); + // expect(node).toBeInstanceOf(PeritextNode); + // expect(node.view()).toBe('Hello, world\n'); }); -test.only('can access PeritextApi using path selector', () => { +test('can access PeritextApi using path selector', () => { const model = ModelWithExt.create(schema); const api = model.api.vec(['nested', 'obj', 'text']); expect(api).toBeInstanceOf(VecApi); @@ -29,9 +29,27 @@ test.only('can access PeritextApi using path selector', () => { expect(api.view()).toBe('Hello, world!\n'); }); -// test.only('can access PeritextApi using proxy selector', () => { - // const api = model.s.nested.obj.text.toApi(); -// }); +test('can access PeritextApi using step-wise selector', () => { + // const model = ModelWithExt.create(schema); + // const api = model.api.vec(['nested', 'obj', 'text']); + // expect(api).toBeInstanceOf(VecApi); + // const api2 = api.ext(); + // expect(api2).toBeInstanceOf(PeritextApi); + // model.api.str(['nested', 'obj', 'text', 1, 0]).ins(12, '!'); + // expect(api.view()).toBe('Hello, world!\n'); +}); + +test('can access PeritextApi using parent proxy selector', () => { + const model = ModelWithExt.create(schema); + const api = model.s.nested.obj.text.toApi(); + expect(api).toBeInstanceOf(VecApi); + let node = api.node.ext(); + expect(node).toBeInstanceOf(PeritextNode); + node = new PeritextNode(node.data); + let api2 = api.ext()!; + expect(api2).toBeInstanceOf(PeritextApi); + api2 = new PeritextApi(node, api.api); +}); // test('can access nested nodes using proxy selector', () => { // const model = ModelWithExt.create(schema); diff --git a/src/json-crdt/model/api/nodes.ts b/src/json-crdt/model/api/nodes.ts index 6cf2c5555d..2303b1fcc5 100644 --- a/src/json-crdt/model/api/nodes.ts +++ b/src/json-crdt/model/api/nodes.ts @@ -12,6 +12,7 @@ import type {ModelApi} from './ModelApi'; import type {Printable} from 'tree-dump/lib/types'; import type {JsonNodeApi} from './types'; import type {ExtensionNode} from '../../extensions/ExtensionNode'; +import type {VecNodeExtensionData} from '../../schema/types'; export type ApiPath = string | number | Path | void; @@ -285,11 +286,11 @@ export class VecApi = VecNode> extends NodeApi { return this.node.elements.length; } - public ext(): ExtensionApi | undefined { + public ext(): JsonNodeApi> | undefined { const node = this.node.ext(); if (!node) return node; const api = this.api.wrap(node); - return api; + return api; } /** diff --git a/src/json-crdt/nodes/vec/VecNode.ts b/src/json-crdt/nodes/vec/VecNode.ts index 638e2b1005..b47937226b 100644 --- a/src/json-crdt/nodes/vec/VecNode.ts +++ b/src/json-crdt/nodes/vec/VecNode.ts @@ -6,6 +6,7 @@ import type {Model} from '../../model'; import type {JsonNode, JsonNodeView} from '..'; import type {Printable} from 'tree-dump/lib/types'; import type {ExtensionNode} from '../../extensions/ExtensionNode'; +import type {VecNodeExtensionData} from '../../schema/types'; /** * Represents a `vec` JSON CRDT node, which is a LWW array. @@ -68,20 +69,20 @@ export class VecNode implements JsonNode< /** * @ignore */ - private __extNode: ExtensionNode | undefined; + private __extNode: VecNodeExtensionData = undefined; /** * @ignore * @returns Returns the extension data node if this is an extension node, * otherwise `undefined`. The node is cached after the first access. */ - public ext(): ExtensionNode | undefined { + public ext(): VecNodeExtensionData { if (this.__extNode) return this.__extNode; const extensionId = this.getExtId(); const isExtension = extensionId >= 0; - if (!isExtension) return undefined; + if (!isExtension) return undefined; const extension = this.doc.ext.get(extensionId); - if (!extension) return undefined; + if (!extension) return undefined; this.__extNode = new extension.Node(this.get(1)!); return this.__extNode; } diff --git a/src/json-crdt/schema/__tests__/types.spec.ts b/src/json-crdt/schema/__tests__/types.spec.ts index 502381ff06..edb2519db3 100644 --- a/src/json-crdt/schema/__tests__/types.spec.ts +++ b/src/json-crdt/schema/__tests__/types.spec.ts @@ -45,7 +45,8 @@ describe('can infer schema of JSON CRDT nodes', () => { const schema1 = s.obj({ richText: ext.peritext.new('hello'), }); - const schema2: JsonNodeToSchema> = schema1; + type Nodes = SchemaToJsonNode; + const schema2: JsonNodeToSchema = schema1; }); test('from typed model', () => { diff --git a/src/json-crdt/schema/types.ts b/src/json-crdt/schema/types.ts index 4ac32d6fc7..92b9eae4a0 100644 --- a/src/json-crdt/schema/types.ts +++ b/src/json-crdt/schema/types.ts @@ -2,6 +2,7 @@ import type {ExtensionId} from '../../json-crdt-extensions'; import type {MvalNode} from '../../json-crdt-extensions/mval/MvalNode'; import type {PeritextNode} from '../../json-crdt-extensions/peritext/PeritextNode'; import type {nodes as builder} from '../../json-crdt-patch'; +import {ExtensionNode} from '../extensions/ExtensionNode'; import type * as nodes from '../nodes'; // prettier-ignore @@ -20,11 +21,22 @@ export type SchemaToJsonNode = S extends builder.str : S extends builder.arr ? nodes.ArrNode> : S extends builder.ext - ? PeritextNode + ? nodes.VecNode> : S extends builder.ext - ? MvalNode + ? nodes.VecNode> : nodes.JsonNode; +export type ExtensionVecData> = {__BRAND__: 'ExtVecData'} & [ + header: nodes.ConNode, + data: EDataNode, +]; + +// prettier-ignore +export type VecNodeExtensionData = N extends nodes.VecNode + ? VecNodeExtensionData + : N extends ExtensionVecData + ? EDataNode + : undefined; // prettier-ignore export type JsonNodeToSchema = N extends nodes.StrNode @@ -36,13 +48,15 @@ export type JsonNodeToSchema = N extends nodes.StrNode : N extends nodes.ValNode ? builder.val> : N extends nodes.VecNode - ? builder.vec<{[K in keyof T]: JsonNodeToSchema}> + ? (T extends ExtensionVecData + ? EDataNode extends PeritextNode + ? builder.ext + : EDataNode extends MvalNode + ? builder.ext + : builder.ext + : builder.vec<{[K in keyof T]: JsonNodeToSchema}>) : N extends nodes.ObjNode ? builder.obj<{[K in keyof T]: JsonNodeToSchema}> : N extends nodes.ArrNode ? builder.arr> - : N extends PeritextNode - ? builder.ext - : N extends MvalNode - ? builder.ext - : builder.con; + : builder.con; From 8af589ce8effb1126af4823d2120332aeed75f4c Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 4 May 2024 19:52:49 +0200 Subject: [PATCH 27/37] =?UTF-8?q?feat(json-crdt):=20=F0=9F=8E=B8=20ability?= =?UTF-8?q?=20to=20select=20extension=20api=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/__tests__/extension.spec.ts | 7 +++++++ src/json-crdt/model/api/nodes.ts | 1 + src/json-crdt/model/api/proxy.ts | 3 +++ 3 files changed, 11 insertions(+) diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts index 6c3dcee2f2..371ed4b1bd 100644 --- a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -51,6 +51,13 @@ test('can access PeritextApi using parent proxy selector', () => { api2 = new PeritextApi(node, api.api); }); +test('can access PeritextApi using inline proxy selector', () => { + const model = ModelWithExt.create(schema); + let api = model.s.nested.obj.text.ext(); + expect(api).toBeInstanceOf(PeritextApi); + api = new PeritextApi(api.node, api.api); +}); + // test('can access nested nodes using proxy selector', () => { // const model = ModelWithExt.create(schema); // const api = model.s.nested.obj.text.toApi(); diff --git a/src/json-crdt/model/api/nodes.ts b/src/json-crdt/model/api/nodes.ts index 2303b1fcc5..7c8e4387b8 100644 --- a/src/json-crdt/model/api/nodes.ts +++ b/src/json-crdt/model/api/nodes.ts @@ -304,6 +304,7 @@ export class VecApi = VecNode> extends NodeApi { get: (target, prop, receiver) => { if (prop === 'toApi') return () => this; if (prop === 'toView') return () => this.view(); + if (prop === 'ext') return () => this.ext(); const index = Number(prop); if (Number.isNaN(index)) throw new Error('INVALID_INDEX'); const child = this.node.get(index); diff --git a/src/json-crdt/model/api/proxy.ts b/src/json-crdt/model/api/proxy.ts index 1ca068df49..3fc18212a5 100644 --- a/src/json-crdt/model/api/proxy.ts +++ b/src/json-crdt/model/api/proxy.ts @@ -1,6 +1,7 @@ import type {JsonNodeApi} from './types'; import type * as nodes from '../../nodes'; import type {PeritextNode} from '../../../json-crdt-extensions'; +import type {VecNodeExtensionData} from '../../schema/types'; export interface ProxyNode { toApi(): JsonNodeApi; @@ -13,6 +14,8 @@ export type ProxyNodeVal> = ProxyNode & { }; export type ProxyNodeVec> = ProxyNode & { [K in keyof nodes.JsonNodeView]: JsonNodeToProxyNode[K]>; +} & { + ext: () => JsonNodeApi>; }; export type ProxyNodeObj> = ProxyNode & { [K in keyof nodes.JsonNodeView]: JsonNodeToProxyNode<(N extends nodes.ObjNode ? M : never)[K]>; From 9f223b4a1f0fd0d032cfdd6c7ccd59ce61b5ad15 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 4 May 2024 20:04:57 +0200 Subject: [PATCH 28/37] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20step-wise=20selector=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/__tests__/extension.spec.ts | 26 ++++++++++++++----- src/json-crdt/model/api/nodes.ts | 3 +++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts index 371ed4b1bd..bccaeac7f2 100644 --- a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -29,14 +29,26 @@ test('can access PeritextApi using path selector', () => { expect(api.view()).toBe('Hello, world!\n'); }); +test('can access PeritextApi using JSON Pointer path selector', () => { + const model = ModelWithExt.create(schema); + const api = model.api.vec('/nested/obj/text'); + expect(api).toBeInstanceOf(VecApi); + const api2 = api.ext(); + expect(api2).toBeInstanceOf(PeritextApi); + model.api.str('/nested/obj/text/1/0').ins(12, '!'); + expect(api.view()).toBe('Hello, world!\n'); +}); + test('can access PeritextApi using step-wise selector', () => { - // const model = ModelWithExt.create(schema); - // const api = model.api.vec(['nested', 'obj', 'text']); - // expect(api).toBeInstanceOf(VecApi); - // const api2 = api.ext(); - // expect(api2).toBeInstanceOf(PeritextApi); - // model.api.str(['nested', 'obj', 'text', 1, 0]).ins(12, '!'); - // expect(api.view()).toBe('Hello, world!\n'); + const model = ModelWithExt.create(schema); + const api = model.api.in('nested').in('obj').in('text').asTup(); + expect(api).toBeInstanceOf(VecApi); + const api2 = api.ext(); + expect(api2).toBeInstanceOf(PeritextApi); + model.api.in('nested').in('obj').in('text').in(1).in(0).asStr().ins(12, '!'); + model.api.in('/nested/obj/text/1/0').asStr().ins(12, '!'); + model.api.in(['nested', 'obj', 'text', 1, 0]).asStr().ins(12, '!'); + expect(api.view()).toBe('Hello, world!!!\n'); }); test('can access PeritextApi using parent proxy selector', () => { diff --git a/src/json-crdt/model/api/nodes.ts b/src/json-crdt/model/api/nodes.ts index 7c8e4387b8..7681dc1bc9 100644 --- a/src/json-crdt/model/api/nodes.ts +++ b/src/json-crdt/model/api/nodes.ts @@ -118,6 +118,9 @@ export class NodeApi implements Printable { throw new Error('NOT_CONST'); } + /** + * @todo The `ext` param shall not be necessary. + */ public asExt, EApi extends ExtensionApi>( ext: Extension, ): EApi { From 41ec5645c78d27cb4aecd28b45f0ba328c03f680 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 4 May 2024 20:16:59 +0200 Subject: [PATCH 29/37] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20add=20ability=20to=20access=20Peritext=20"str"?= =?UTF-8?q?=20node=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/PeritextApi.ts | 8 +++-- .../peritext/PeritextNode.ts | 8 +++-- .../peritext/__tests__/extension.spec.ts | 33 ++++++++----------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/json-crdt-extensions/peritext/PeritextApi.ts b/src/json-crdt-extensions/peritext/PeritextApi.ts index 266dda972b..1562f5d231 100644 --- a/src/json-crdt-extensions/peritext/PeritextApi.ts +++ b/src/json-crdt-extensions/peritext/PeritextApi.ts @@ -1,5 +1,9 @@ import {NodeApi} from '../../json-crdt/model/api/nodes'; import type {PeritextNode} from './PeritextNode'; -import type {ExtensionApi} from '../../json-crdt'; +import type {ExtensionApi, StrApi} from '../../json-crdt'; -export class PeritextApi extends NodeApi implements ExtensionApi {} +export class PeritextApi extends NodeApi implements ExtensionApi { + public text(): StrApi { + return this.api.wrap(this.node.text()); + } +} diff --git a/src/json-crdt-extensions/peritext/PeritextNode.ts b/src/json-crdt-extensions/peritext/PeritextNode.ts index 714068844c..fa3190f7ee 100644 --- a/src/json-crdt-extensions/peritext/PeritextNode.ts +++ b/src/json-crdt-extensions/peritext/PeritextNode.ts @@ -1,6 +1,7 @@ import {MNEMONIC} from './constants'; import {ExtensionNode} from '../../json-crdt/extensions/ExtensionNode'; import {ExtensionId} from '../constants'; +import type {StrNode} from '../../json-crdt/nodes'; import type {PeritextDataNode} from './types'; export class PeritextNode extends ExtensionNode { @@ -10,8 +11,11 @@ export class PeritextNode extends ExtensionNode { return MNEMONIC; } + public text(): StrNode { + return this.data.get(0)!; + } + public view(): string { - const str = this.data.get(0)!; - return str.view(); + return this.text().view(); } } diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts index bccaeac7f2..649c2bad14 100644 --- a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -1,5 +1,5 @@ import {s} from '../../../json-crdt-patch'; -import {VecApi} from '../../../json-crdt/model'; +import {StrApi, VecApi} from '../../../json-crdt/model'; import {ModelWithExt, ext} from '../../ModelWithExt'; import {PeritextApi} from '../PeritextApi'; import {PeritextNode} from '../PeritextNode'; @@ -12,11 +12,19 @@ const schema = s.obj({ }), }); -test('can access PeritextNode in type safe way', () => { - // const model = ModelWithExt.create(schema); - // const node = model.root.node().get('nested')!.get('obj')!.get('text')!.child!(); - // expect(node).toBeInstanceOf(PeritextNode); - // expect(node.view()).toBe('Hello, world\n'); +test('can access PeritextNode in type safe way (using the proxy selector)', () => { + const model = ModelWithExt.create(schema); + let api = model.s.nested.obj.text.ext(); + expect(api).toBeInstanceOf(PeritextApi); + api = new PeritextApi(api.node, api.api); +}); + +test('can access raw text "str" node in type safe way', () => { + const model = ModelWithExt.create(schema); + const str = model.s.nested.obj.text.ext().text(); + expect(str).toBeInstanceOf(StrApi); + str.ins(str.length() - 1, '!'); + expect(model.view().nested.obj.text).toBe('Hello, world!\n'); }); test('can access PeritextApi using path selector', () => { @@ -62,16 +70,3 @@ test('can access PeritextApi using parent proxy selector', () => { expect(api2).toBeInstanceOf(PeritextApi); api2 = new PeritextApi(node, api.api); }); - -test('can access PeritextApi using inline proxy selector', () => { - const model = ModelWithExt.create(schema); - let api = model.s.nested.obj.text.ext(); - expect(api).toBeInstanceOf(PeritextApi); - api = new PeritextApi(api.node, api.api); -}); - -// test('can access nested nodes using proxy selector', () => { -// const model = ModelWithExt.create(schema); -// const api = model.s.nested.obj.text.toApi(); -// console.log(api + ''); -// }); From 019ab8959afc8278e22eac3122bb568193f81698 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 4 May 2024 20:17:26 +0200 Subject: [PATCH 30/37] =?UTF-8?q?style:=20=F0=9F=92=84=20run=20Prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/ModelWithExt.ts | 4 +--- src/json-crdt-extensions/cnt/index.ts | 16 +++++++++------- src/json-crdt-extensions/ext.ts | 6 +----- src/json-crdt-extensions/peritext/index.ts | 5 +---- src/json-crdt-patch/operations.ts | 16 ++++------------ src/json-crdt/model/api/ModelApi.ts | 5 ++--- src/json-crdt/model/api/nodes.ts | 2 +- src/json-crdt/nodes/vec/VecNode.ts | 3 +-- .../vec/__tests__/VecNode-extension.spec.ts | 2 +- src/json-crdt/schema/__tests__/toSchema.spec.ts | 1 - 10 files changed, 21 insertions(+), 39 deletions(-) diff --git a/src/json-crdt-extensions/ModelWithExt.ts b/src/json-crdt-extensions/ModelWithExt.ts index ab6004db34..665d784f52 100644 --- a/src/json-crdt-extensions/ModelWithExt.ts +++ b/src/json-crdt-extensions/ModelWithExt.ts @@ -11,9 +11,7 @@ extensions.register(ext.cnt); extensions.register(ext.mval); extensions.register(ext.peritext); -export { - ext, -}; +export {ext}; export class ModelWithExt { public static readonly ext = ext; diff --git a/src/json-crdt-extensions/cnt/index.ts b/src/json-crdt-extensions/cnt/index.ts index a93fbf7952..8df20ac8e1 100644 --- a/src/json-crdt-extensions/cnt/index.ts +++ b/src/json-crdt-extensions/cnt/index.ts @@ -37,12 +37,14 @@ class CntApi extends NodeApi implements ExtensionApi { } } -const create = (value?: any, sid: any = 0) => new NodeBuilder(builder => { - if (!sid) sid = builder.clock.sid; - const schema = value === undefined - ? s.map>({}) - : s.map>({[sid.toString(36)]: s.con(value ?? 0)}); - return schema.build(builder); -}); +const create = (value?: any, sid: any = 0) => + new NodeBuilder((builder) => { + if (!sid) sid = builder.clock.sid; + const schema = + value === undefined + ? s.map>({}) + : s.map>({[sid.toString(36)]: s.con(value ?? 0)}); + return schema.build(builder); + }); export const cnt = new Extension(ExtensionId.cnt, MNEMONIC, CntNode, CntApi, create); diff --git a/src/json-crdt-extensions/ext.ts b/src/json-crdt-extensions/ext.ts index 023c3ee4ae..e825f27c3c 100644 --- a/src/json-crdt-extensions/ext.ts +++ b/src/json-crdt-extensions/ext.ts @@ -2,8 +2,4 @@ import {cnt} from './cnt'; import {mval} from './mval'; import {peritext} from './peritext'; -export { - cnt, - mval, - peritext, -}; +export {cnt, mval, peritext}; diff --git a/src/json-crdt-extensions/peritext/index.ts b/src/json-crdt-extensions/peritext/index.ts index b759c0853e..52e996fbed 100644 --- a/src/json-crdt-extensions/peritext/index.ts +++ b/src/json-crdt-extensions/peritext/index.ts @@ -5,10 +5,7 @@ import {SCHEMA, MNEMONIC} from './constants'; import {Extension} from '../../json-crdt/extensions/Extension'; import type {PeritextDataNode} from './types'; -export { - PeritextNode, - PeritextApi, -}; +export {PeritextNode, PeritextApi}; export const peritext = new Extension< ExtensionId.peritext, diff --git a/src/json-crdt-patch/operations.ts b/src/json-crdt-patch/operations.ts index ad523b05a9..5f7f52eeac 100644 --- a/src/json-crdt-patch/operations.ts +++ b/src/json-crdt-patch/operations.ts @@ -182,9 +182,7 @@ export class InsValOp implements IJsonCrdtPatchEditOperation { } public toString(tab: string = ''): string { - return `${this.name()} ${printTs(this.id)}!${this.span()}, obj = ${printTs( - this.obj, - )}, val = ${printTs(this.val)}`; + return `${this.name()} ${printTs(this.id)}!${this.span()}, obj = ${printTs(this.obj)}, val = ${printTs(this.val)}`; } } @@ -212,9 +210,7 @@ export class InsObjOp implements IJsonCrdtPatchEditOperation { let out = `${this.name()} ${printTs(this.id)}!${this.span()}, obj = ${printTs(this.obj)}`; for (let i = 0; i < this.data.length; i++) { const isLast = i === this.data.length - 1; - out += `\n${tab} ${isLast ? '└─' : '├─'} ${JSON.stringify(this.data[i][0])}: ${printTs( - this.data[i][1], - )}`; + out += `\n${tab} ${isLast ? '└─' : '├─'} ${JSON.stringify(this.data[i][0])}: ${printTs(this.data[i][1])}`; } return out; } @@ -244,9 +240,7 @@ export class InsVecOp implements IJsonCrdtPatchEditOperation { let out = `${this.name()} ${printTs(this.id)}!${this.span()}, obj = ${printTs(this.obj)}`; for (let i = 0; i < this.data.length; i++) { const isLast = i === this.data.length - 1; - out += `\n${tab} ${isLast ? '└─' : '├─'} ${JSON.stringify(this.data[i][0])}: ${printTs( - this.data[i][1], - )}`; + out += `\n${tab} ${isLast ? '└─' : '├─'} ${JSON.stringify(this.data[i][0])}: ${printTs(this.data[i][1])}`; } return out; } @@ -303,9 +297,7 @@ export class InsBinOp implements IJsonCrdtPatchEditOperation { public toString(tab: string = ''): string { const ref = printTs(this.ref); - return `${this.name()} ${printTs(this.id)}!${this.span()}, obj = ${printTs(this.obj)} { ${ref} ← ${ - this.data - } }`; + return `${this.name()} ${printTs(this.id)}!${this.span()}, obj = ${printTs(this.obj)} { ${ref} ← ${this.data} }`; } } diff --git a/src/json-crdt/model/api/ModelApi.ts b/src/json-crdt/model/api/ModelApi.ts index 788aaece52..e63bb42b9a 100644 --- a/src/json-crdt/model/api/ModelApi.ts +++ b/src/json-crdt/model/api/ModelApi.ts @@ -91,9 +91,8 @@ export class ModelApi implements SyncStore implements Printable { diff --git a/src/json-crdt/nodes/vec/VecNode.ts b/src/json-crdt/nodes/vec/VecNode.ts index b47937226b..855f2ec39c 100644 --- a/src/json-crdt/nodes/vec/VecNode.ts +++ b/src/json-crdt/nodes/vec/VecNode.ts @@ -180,8 +180,7 @@ export class VecNode implements JsonNode< public toString(tab: string = ''): string { const extNode = this.ext(); - const header = - this.name() + ' ' + printTs(this.id) + (extNode ? ` { extension = ${this.getExtId()} }` : ''); + const header = this.name() + ' ' + printTs(this.id) + (extNode ? ` { extension = ${this.getExtId()} }` : ''); if (extNode) { return this.child()!.toString(tab, this.id); } diff --git a/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts b/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts index 2717d63792..cd8196f600 100644 --- a/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts +++ b/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts @@ -37,7 +37,7 @@ describe('sample extension', () => { 'double-concat', class extends ExtensionNode> { public extId: number = 123; - + public name(): string { return 'double-concat'; } diff --git a/src/json-crdt/schema/__tests__/toSchema.spec.ts b/src/json-crdt/schema/__tests__/toSchema.spec.ts index 0964697dd8..e01cdc83bc 100644 --- a/src/json-crdt/schema/__tests__/toSchema.spec.ts +++ b/src/json-crdt/schema/__tests__/toSchema.spec.ts @@ -96,4 +96,3 @@ test('can copy a model with extension', () => { ], }); }); - From 2ea4f3441e0c7a281b2ed432fb8f7d30e3f1cc57 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 4 May 2024 20:24:54 +0200 Subject: [PATCH 31/37] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20add=20ability=20to=20access=20Peritext=20slices?= =?UTF-8?q?=20"arr"=20node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/PeritextApi.ts | 7 ++++++- .../peritext/PeritextNode.ts | 16 +++++++++++----- .../peritext/__tests__/extension.spec.ts | 9 ++++++++- src/json-crdt/model/Model.ts | 2 ++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/json-crdt-extensions/peritext/PeritextApi.ts b/src/json-crdt-extensions/peritext/PeritextApi.ts index 1562f5d231..172b923981 100644 --- a/src/json-crdt-extensions/peritext/PeritextApi.ts +++ b/src/json-crdt-extensions/peritext/PeritextApi.ts @@ -1,9 +1,14 @@ import {NodeApi} from '../../json-crdt/model/api/nodes'; import type {PeritextNode} from './PeritextNode'; -import type {ExtensionApi, StrApi} from '../../json-crdt'; +import type {ExtensionApi, StrApi, ArrApi, ArrNode} from '../../json-crdt'; +import type {SliceNode} from './slice/types'; export class PeritextApi extends NodeApi implements ExtensionApi { public text(): StrApi { return this.api.wrap(this.node.text()); } + + public slices(): ArrApi> { + return this.api.wrap(this.node.slices()); + } } diff --git a/src/json-crdt-extensions/peritext/PeritextNode.ts b/src/json-crdt-extensions/peritext/PeritextNode.ts index fa3190f7ee..ca93fe19a4 100644 --- a/src/json-crdt-extensions/peritext/PeritextNode.ts +++ b/src/json-crdt-extensions/peritext/PeritextNode.ts @@ -1,20 +1,26 @@ import {MNEMONIC} from './constants'; import {ExtensionNode} from '../../json-crdt/extensions/ExtensionNode'; import {ExtensionId} from '../constants'; -import type {StrNode} from '../../json-crdt/nodes'; +import type {ArrNode, StrNode} from '../../json-crdt/nodes'; import type {PeritextDataNode} from './types'; +import type {SliceNode} from './slice/types'; export class PeritextNode extends ExtensionNode { + public text(): StrNode { + return this.data.get(0)!; + } + + public slices(): ArrNode { + return this.data.get(1)!; + } + + // ------------------------------------------------------------ ExtensionNode public readonly extId = ExtensionId.peritext; public name(): string { return MNEMONIC; } - public text(): StrNode { - return this.data.get(0)!; - } - public view(): string { return this.text().view(); } diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts index 649c2bad14..802d022945 100644 --- a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -1,5 +1,5 @@ import {s} from '../../../json-crdt-patch'; -import {StrApi, VecApi} from '../../../json-crdt/model'; +import {ArrApi, StrApi, VecApi} from '../../../json-crdt/model'; import {ModelWithExt, ext} from '../../ModelWithExt'; import {PeritextApi} from '../PeritextApi'; import {PeritextNode} from '../PeritextNode'; @@ -27,6 +27,13 @@ test('can access raw text "str" node in type safe way', () => { expect(model.view().nested.obj.text).toBe('Hello, world!\n'); }); +test('can access slices "arr" node in type safe way', () => { + const model = ModelWithExt.create(schema); + const arr = model.s.nested.obj.text.ext().slices(); + expect(arr).toBeInstanceOf(ArrApi); + expect(arr.view()).toEqual([]); +}); + test('can access PeritextApi using path selector', () => { const model = ModelWithExt.create(schema); const api = model.api.vec(['nested', 'obj', 'text']); diff --git a/src/json-crdt/model/Model.ts b/src/json-crdt/model/Model.ts index 21e503c7fd..e2ba8f4ad6 100644 --- a/src/json-crdt/model/Model.ts +++ b/src/json-crdt/model/Model.ts @@ -254,6 +254,8 @@ export class Model> implements Printable { /** * Experimental node retrieval API using proxy objects. Returns a strictly * typed proxy wrapper around the value of the root node. + * + * @todo consider renaming this to `_`. */ public get s() { return this.api.r.proxy().val; From 5c0b1b4d635babf77e5468b13a83b77133b35e4b Mon Sep 17 00:00:00 2001 From: streamich Date: Sun, 5 May 2024 09:43:43 +0200 Subject: [PATCH 32/37] =?UTF-8?q?refactor(json-crdt):=20=F0=9F=92=A1=20ren?= =?UTF-8?q?ame=20.asTup()=20to=20.asVec()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/__tests__/extension.spec.ts | 2 +- src/json-crdt/model/api/nodes.ts | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts index 802d022945..e8b4f45dfb 100644 --- a/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/extension.spec.ts @@ -56,7 +56,7 @@ test('can access PeritextApi using JSON Pointer path selector', () => { test('can access PeritextApi using step-wise selector', () => { const model = ModelWithExt.create(schema); - const api = model.api.in('nested').in('obj').in('text').asTup(); + const api = model.api.in('nested').in('obj').in('text').asVec(); expect(api).toBeInstanceOf(VecApi); const api2 = api.ext(); expect(api2).toBeInstanceOf(PeritextApi); diff --git a/src/json-crdt/model/api/nodes.ts b/src/json-crdt/model/api/nodes.ts index 3a22781fcd..f019ffc0f6 100644 --- a/src/json-crdt/model/api/nodes.ts +++ b/src/json-crdt/model/api/nodes.ts @@ -100,10 +100,7 @@ export class NodeApi implements Printable { throw new Error('NOT_ARR'); } - /** - * @todo rename as `asVec`. - */ - public asTup(): VecApi { + public asVec(): VecApi { if (this.node instanceof VecNode) return this.api.wrap(this.node as VecNode); throw new Error('NOT_ARR'); } @@ -149,7 +146,7 @@ export class NodeApi implements Printable { } public tup(path?: ApiPath): VecApi { - return this.in(path).asTup(); + return this.in(path).asVec(); } public obj(path?: ApiPath): ObjApi { From 1fe1a0af9076747736c638f8a005b2d9eb4517c0 Mon Sep 17 00:00:00 2001 From: streamich Date: Sun, 5 May 2024 10:33:03 +0200 Subject: [PATCH 33/37] =?UTF-8?q?refactor(json-crdt):=20=F0=9F=92=A1=20ren?= =?UTF-8?q?ame=20"con"=20and=20"vec"=20node=20finders=20to=20match=20the?= =?UTF-8?q?=20spec=20naming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt/__demos__/docs-con.ts | 2 +- .../__tests__/Model.node-deletion.spec.ts | 8 +-- src/json-crdt/model/api/ModelApi.ts | 52 +++++++++---------- src/json-crdt/model/api/nodes.ts | 4 +- .../nodes/vec/__tests__/extension.spec.ts | 6 +-- .../schema/__tests__/toSchema.spec.ts | 2 +- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/json-crdt/__demos__/docs-con.ts b/src/json-crdt/__demos__/docs-con.ts index d65c0224ae..a9cf2c79ce 100644 --- a/src/json-crdt/__demos__/docs-con.ts +++ b/src/json-crdt/__demos__/docs-con.ts @@ -51,6 +51,6 @@ model.api.root({ console.log(model + ''); console.log(model.view()); -const conApi = model.api.const(['foo', 'bar']); +const conApi = model.api.con(['foo', 'bar']); console.log(conApi.view()); console.log(conApi.node.toString()); diff --git a/src/json-crdt/model/__tests__/Model.node-deletion.spec.ts b/src/json-crdt/model/__tests__/Model.node-deletion.spec.ts index 292c750d1d..70cb8e9af9 100644 --- a/src/json-crdt/model/__tests__/Model.node-deletion.spec.ts +++ b/src/json-crdt/model/__tests__/Model.node-deletion.spec.ts @@ -67,7 +67,7 @@ test('removes from index recursively after object key overwrite', () => { }, }); const val1 = doc.api.obj(['foo']).node.id; - const val2 = doc.api.const(['foo', 'bar']).node.id; + const val2 = doc.api.con(['foo', 'bar']).node.id; const val3 = doc.api.arr(['foo', 'baz']).node.id; const val4 = doc.api.val(['foo', 'baz', 0]).node.id; expect(!!doc.index.get(val1)).toBe(true); @@ -94,7 +94,7 @@ test('removes from index recursively after array element delete', () => { }, ]); const val1 = doc.api.obj([2, 'foo']).node.id; - const val2 = doc.api.const([2, 'foo', 'bar']).node.id; + const val2 = doc.api.con([2, 'foo', 'bar']).node.id; const val3 = doc.api.arr([2, 'foo', 'baz']).node.id; const val4 = doc.api.val([2, 'foo', 'baz', 0]).node.id; expect(!!doc.index.get(val1)).toBe(true); @@ -119,7 +119,7 @@ test('removes from index recursively after LWW register write', () => { }); const val0 = doc.api.val([2]).node.id; const val1 = doc.api.obj([2, 'foo']).node.id; - const val2 = doc.api.const([2, 'foo', 'bar']).node.id; + const val2 = doc.api.con([2, 'foo', 'bar']).node.id; const val3 = doc.api.arr([2, 'foo', 'baz']).node.id; const val4 = doc.api.val([2, 'foo', 'baz', 0]).node.id; expect(!!doc.index.get(val1)).toBe(true); @@ -146,7 +146,7 @@ test('removes from index recursively after LWW register write', () => { }); const val0 = doc.api.val([2]).node.id; const val1 = doc.api.obj([2, 'foo']).node.id; - const val2 = doc.api.const([2, 'foo', 'bar']).node.id; + const val2 = doc.api.con([2, 'foo', 'bar']).node.id; const val3 = doc.api.arr([2, 'foo', 'baz']).node.id; const val4 = doc.api.val([2, 'foo', 'baz', 0]).node.id; expect(!!doc.index.get(val1)).toBe(true); diff --git a/src/json-crdt/model/api/ModelApi.ts b/src/json-crdt/model/api/ModelApi.ts index e63bb42b9a..b37d1b404a 100644 --- a/src/json-crdt/model/api/ModelApi.ts +++ b/src/json-crdt/model/api/ModelApi.ts @@ -5,9 +5,9 @@ import {Patch} from '../../../json-crdt-patch/Patch'; import {PatchBuilder} from '../../../json-crdt-patch/PatchBuilder'; import {SyncStore} from '../../../util/events/sync-store'; import {MergeFanOut, MicrotaskBufferFanOut} from './fanout'; +import {ExtensionNode} from '../../extensions/ExtensionNode'; import type {Model} from '../Model'; import type {JsonNode, JsonNodeView} from '../../nodes'; -import {ExtensionNode} from '../../extensions/ExtensionNode'; /** * Local changes API for a JSON CRDT model. This class is the main entry point @@ -128,6 +128,19 @@ export class ModelApi implements SyncStore implements SyncStore implements SyncStore implements Printable { return this.in(path).asArr(); } - public tup(path?: ApiPath): VecApi { + public vec(path?: ApiPath): VecApi { return this.in(path).asVec(); } @@ -153,7 +153,7 @@ export class NodeApi implements Printable { return this.in(path).asObj(); } - public const(path?: ApiPath): ConApi { + public con(path?: ApiPath): ConApi { return this.in(path).asCon(); } diff --git a/src/json-crdt/nodes/vec/__tests__/extension.spec.ts b/src/json-crdt/nodes/vec/__tests__/extension.spec.ts index f73b6f3d3c..a1c4390df4 100644 --- a/src/json-crdt/nodes/vec/__tests__/extension.spec.ts +++ b/src/json-crdt/nodes/vec/__tests__/extension.spec.ts @@ -60,7 +60,7 @@ describe('extension validity checks', () => { mv: mval.new(), }); const buf = new Uint8Array(4); - buf.set(model.api.const(['mv', 0]).node.view() as Uint8Array, 0); + buf.set(model.api.con(['mv', 0]).node.view() as Uint8Array, 0); const api = model.api.vec(['mv']); api.set([[0, buf]]); expect(model.view()).toEqual({ @@ -75,7 +75,7 @@ describe('extension validity checks', () => { model.api.root({ mv: mval.new(), }); - const buf = model.api.const(['mv', 0]).node.view() as Uint8Array; + const buf = model.api.con(['mv', 0]).node.view() as Uint8Array; buf[1] += 1; expect(model.view()).toEqual({ mv: [buf, []], @@ -89,7 +89,7 @@ describe('extension validity checks', () => { model.api.root({ mv: mval.new(), }); - const buf = model.api.const(['mv', 0]).node.view() as Uint8Array; + const buf = model.api.con(['mv', 0]).node.view() as Uint8Array; buf[2] += 1; expect(model.view()).toEqual({ mv: [buf, []], diff --git a/src/json-crdt/schema/__tests__/toSchema.spec.ts b/src/json-crdt/schema/__tests__/toSchema.spec.ts index e01cdc83bc..0683c8c867 100644 --- a/src/json-crdt/schema/__tests__/toSchema.spec.ts +++ b/src/json-crdt/schema/__tests__/toSchema.spec.ts @@ -67,7 +67,7 @@ test('can infer schema of a document nodes', () => { const node = model.root.node(); const schema2 = toSchema(node); expect(cmp(schema, schema2)).toBe(true); - const conSchema = toSchema(model.api.const('con').node); + const conSchema = toSchema(model.api.con('con').node); expect(cmp(con, conSchema)).toBe(true); expect(cmp(str, conSchema)).toBe(false); const strSchema = toSchema(model.api.str('str').node); From 3d9c8606d62d2b11203816e2d8fbf87213f6f19b Mon Sep 17 00:00:00 2001 From: streamich Date: Sun, 5 May 2024 10:37:29 +0200 Subject: [PATCH 34/37] =?UTF-8?q?refactor(json-crdt):=20=F0=9F=92=A1=20ren?= =?UTF-8?q?ame=20ExtensionNode=20to=20ExtNode,=20ExtensionApi=20to=20ExtAp?= =?UTF-8?q?i?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/cnt/index.ts | 8 ++++---- src/json-crdt-extensions/mval/MvalApi.ts | 4 ++-- src/json-crdt-extensions/mval/MvalNode.ts | 4 ++-- src/json-crdt-extensions/peritext/PeritextApi.ts | 4 ++-- src/json-crdt-extensions/peritext/PeritextNode.ts | 4 ++-- .../extensions/{ExtensionNode.ts => ExtNode.ts} | 3 +-- src/json-crdt/extensions/Extension.ts | 14 +++++++------- src/json-crdt/extensions/types.ts | 5 ++--- src/json-crdt/model/api/ModelApi.ts | 6 +++--- src/json-crdt/model/api/nodes.ts | 6 +++--- src/json-crdt/nodes/vec/VecNode.ts | 4 ++-- .../nodes/vec/__tests__/VecNode-extension.spec.ts | 8 ++++---- src/json-crdt/schema/types.ts | 4 ++-- 13 files changed, 36 insertions(+), 38 deletions(-) rename src/json-crdt/extensions/{ExtensionNode.ts => ExtNode.ts} (86%) diff --git a/src/json-crdt-extensions/cnt/index.ts b/src/json-crdt-extensions/cnt/index.ts index 8df20ac8e1..7bf8845ed1 100644 --- a/src/json-crdt-extensions/cnt/index.ts +++ b/src/json-crdt-extensions/cnt/index.ts @@ -1,13 +1,13 @@ import {ExtensionId, ExtensionName} from '../constants'; import {NodeApi} from '../../json-crdt/model/api/nodes'; -import {ExtensionNode} from '../../json-crdt/extensions/ExtensionNode'; +import {ExtNode} from '../../json-crdt/extensions/ExtNode'; import {Extension} from '../../json-crdt/extensions/Extension'; import {NodeBuilder, nodes, s, type ObjNode} from '../../json-crdt'; -import type {ExtensionApi} from '../../json-crdt'; +import type {ExtApi} from '../../json-crdt'; const MNEMONIC = ExtensionName[ExtensionId.cnt]; -class CntNode extends ExtensionNode { +class CntNode extends ExtNode { public readonly extId = ExtensionId.cnt; public name(): string { @@ -22,7 +22,7 @@ class CntNode extends ExtensionNode { } } -class CntApi extends NodeApi implements ExtensionApi { +class CntApi extends NodeApi implements ExtApi { public inc(increment: number): this { const {api, node} = this; const sid = api.model.clock.sid; diff --git a/src/json-crdt-extensions/mval/MvalApi.ts b/src/json-crdt-extensions/mval/MvalApi.ts index dc05a46771..1b2e520bbf 100644 --- a/src/json-crdt-extensions/mval/MvalApi.ts +++ b/src/json-crdt-extensions/mval/MvalApi.ts @@ -1,8 +1,8 @@ import {ArrApi, NodeApi} from '../../json-crdt/model/api/nodes'; import type {MvalNode} from './MvalNode'; -import type {ExtensionApi} from '../../json-crdt'; +import type {ExtApi} from '../../json-crdt'; -export class MvalApi extends NodeApi implements ExtensionApi { +export class MvalApi extends NodeApi implements ExtApi { public set(json: unknown): this { const {api, node} = this; const builder = api.builder; diff --git a/src/json-crdt-extensions/mval/MvalNode.ts b/src/json-crdt-extensions/mval/MvalNode.ts index 8f35741efc..889f2271f5 100644 --- a/src/json-crdt-extensions/mval/MvalNode.ts +++ b/src/json-crdt-extensions/mval/MvalNode.ts @@ -1,9 +1,9 @@ import {MNEMONIC} from './constants'; -import {ExtensionNode} from '../../json-crdt/extensions/ExtensionNode'; +import {ExtNode} from '../../json-crdt/extensions/ExtNode'; import {ExtensionId} from '../constants'; import type {ArrNode} from '../../json-crdt/nodes/arr/ArrNode'; -export class MvalNode extends ExtensionNode { +export class MvalNode extends ExtNode { public readonly extId = ExtensionId.mval; public name(): string { diff --git a/src/json-crdt-extensions/peritext/PeritextApi.ts b/src/json-crdt-extensions/peritext/PeritextApi.ts index 172b923981..a413a41272 100644 --- a/src/json-crdt-extensions/peritext/PeritextApi.ts +++ b/src/json-crdt-extensions/peritext/PeritextApi.ts @@ -1,9 +1,9 @@ import {NodeApi} from '../../json-crdt/model/api/nodes'; import type {PeritextNode} from './PeritextNode'; -import type {ExtensionApi, StrApi, ArrApi, ArrNode} from '../../json-crdt'; +import type {ExtApi, StrApi, ArrApi, ArrNode} from '../../json-crdt'; import type {SliceNode} from './slice/types'; -export class PeritextApi extends NodeApi implements ExtensionApi { +export class PeritextApi extends NodeApi implements ExtApi { public text(): StrApi { return this.api.wrap(this.node.text()); } diff --git a/src/json-crdt-extensions/peritext/PeritextNode.ts b/src/json-crdt-extensions/peritext/PeritextNode.ts index ca93fe19a4..a01ccc45a2 100644 --- a/src/json-crdt-extensions/peritext/PeritextNode.ts +++ b/src/json-crdt-extensions/peritext/PeritextNode.ts @@ -1,11 +1,11 @@ import {MNEMONIC} from './constants'; -import {ExtensionNode} from '../../json-crdt/extensions/ExtensionNode'; +import {ExtNode} from '../../json-crdt/extensions/ExtNode'; import {ExtensionId} from '../constants'; import type {ArrNode, StrNode} from '../../json-crdt/nodes'; import type {PeritextDataNode} from './types'; import type {SliceNode} from './slice/types'; -export class PeritextNode extends ExtensionNode { +export class PeritextNode extends ExtNode { public text(): StrNode { return this.data.get(0)!; } diff --git a/src/json-crdt/extensions/ExtensionNode.ts b/src/json-crdt/extensions/ExtNode.ts similarity index 86% rename from src/json-crdt/extensions/ExtensionNode.ts rename to src/json-crdt/extensions/ExtNode.ts index 952d27c8cc..7a4fe2386c 100644 --- a/src/json-crdt/extensions/ExtensionNode.ts +++ b/src/json-crdt/extensions/ExtNode.ts @@ -2,8 +2,7 @@ import {printTs, type ITimestampStruct} from '../../json-crdt-patch/clock'; import type {JsonNode} from '..'; import type {Printable} from 'tree-dump/lib/types'; -/** @todo Rename to `ExtNode` to be inline with other classes. */ -export abstract class ExtensionNode implements JsonNode, Printable { +export abstract class ExtNode implements JsonNode, Printable { public abstract readonly extId: number; public readonly id: ITimestampStruct; diff --git a/src/json-crdt/extensions/Extension.ts b/src/json-crdt/extensions/Extension.ts index 49bb1b4bd8..df300ff6bc 100644 --- a/src/json-crdt/extensions/Extension.ts +++ b/src/json-crdt/extensions/Extension.ts @@ -1,25 +1,25 @@ -import {NodeBuilder, s, type nodes} from '../../json-crdt-patch'; -import {ExtensionNode} from './ExtensionNode'; +import {type NodeBuilder, s, type nodes} from '../../json-crdt-patch'; +import type {ExtNode} from './ExtNode'; import type {ModelApi} from '../model'; import type {JsonNode} from '../nodes'; import type {JsonNodeToSchema} from '../schema/types'; -import type {ExtensionApi} from './types'; +import type {ExtApi} from './types'; export type AnyExtension = Extension; export class Extension< Id extends number, DataNode extends JsonNode, - ExtNode extends ExtensionNode, - ExtApi extends ExtensionApi, + ENode extends ExtNode, + EApi extends ExtApi, DataArgs extends any[] = any[], DataSchema extends NodeBuilder = JsonNodeToSchema, > { constructor( public readonly id: Id, public readonly name: string, - public readonly Node: new (data: DataNode) => ExtNode, - public readonly Api: new (node: ExtNode, api: ModelApi) => ExtApi, + public readonly Node: new (data: DataNode) => ENode, + public readonly Api: new (node: ENode, api: ModelApi) => EApi, public readonly schema: (...args: DataArgs) => DataSchema, ) {} diff --git a/src/json-crdt/extensions/types.ts b/src/json-crdt/extensions/types.ts index f23e76ae0b..cacf0519a6 100644 --- a/src/json-crdt/extensions/types.ts +++ b/src/json-crdt/extensions/types.ts @@ -1,7 +1,6 @@ import type {NodeApi} from '../model/api/nodes'; -import type {ExtensionNode} from './ExtensionNode'; +import type {ExtNode} from './ExtNode'; export type ExtensionValue = [type: Uint8Array, data: unknown]; -/** @todo Rename to `ExtApi`. */ -export interface ExtensionApi> extends NodeApi {} +export interface ExtApi> extends NodeApi {} diff --git a/src/json-crdt/model/api/ModelApi.ts b/src/json-crdt/model/api/ModelApi.ts index b37d1b404a..f672403b11 100644 --- a/src/json-crdt/model/api/ModelApi.ts +++ b/src/json-crdt/model/api/ModelApi.ts @@ -5,7 +5,7 @@ import {Patch} from '../../../json-crdt-patch/Patch'; import {PatchBuilder} from '../../../json-crdt-patch/PatchBuilder'; import {SyncStore} from '../../../util/events/sync-store'; import {MergeFanOut, MicrotaskBufferFanOut} from './fanout'; -import {ExtensionNode} from '../../extensions/ExtensionNode'; +import {ExtNode} from '../../extensions/ExtNode'; import type {Model} from '../Model'; import type {JsonNode, JsonNodeView} from '../../nodes'; @@ -79,7 +79,7 @@ export class ModelApi implements SyncStore): NodeApi; + public wrap(node: ExtNode): NodeApi; public wrap(node: JsonNode) { if (node instanceof ValNode) return node.api || (node.api = new ValApi(node, this)); else if (node instanceof StrNode) return node.api || (node.api = new StrApi(node, this)); @@ -88,7 +88,7 @@ export class ModelApi implements SyncStore implements Printable { /** * @todo The `ext` param shall not be necessary. */ - public asExt, EApi extends ExtensionApi>( + public asExt, EApi extends ExtApi>( ext: Extension, ): EApi { let node: JsonNode | undefined = this.node; diff --git a/src/json-crdt/nodes/vec/VecNode.ts b/src/json-crdt/nodes/vec/VecNode.ts index 855f2ec39c..f2c37ad228 100644 --- a/src/json-crdt/nodes/vec/VecNode.ts +++ b/src/json-crdt/nodes/vec/VecNode.ts @@ -5,7 +5,7 @@ import {compare, ITimestampStruct, printTs} from '../../../json-crdt-patch/clock import type {Model} from '../../model'; import type {JsonNode, JsonNodeView} from '..'; import type {Printable} from 'tree-dump/lib/types'; -import type {ExtensionNode} from '../../extensions/ExtensionNode'; +import type {ExtNode} from '../../extensions/ExtNode'; import type {VecNodeExtensionData} from '../../schema/types'; /** @@ -114,7 +114,7 @@ export class VecNode implements JsonNode< /** * @ignore */ - public child(): ExtensionNode | undefined { + public child(): ExtNode | undefined { return this.ext(); } diff --git a/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts b/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts index cd8196f600..e8b3fb2824 100644 --- a/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts +++ b/src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts @@ -1,10 +1,10 @@ import {s} from '../../../../json-crdt-patch'; import {printTree} from 'tree-dump'; -import {ExtensionApi} from '../../../extensions/types'; +import {ExtApi} from '../../../extensions/types'; import {Model, NodeApi} from '../../../model'; import {StrNode} from '../../nodes'; import {Extension} from '../../../extensions/Extension'; -import {ExtensionNode} from '../../../extensions/ExtensionNode'; +import {ExtNode} from '../../../extensions/ExtNode'; test('treats a simple "vec" node as a vector', () => { const model = Model.withLogicalClock(); @@ -35,7 +35,7 @@ describe('sample extension', () => { const DoubleConcatExt = new Extension<123, StrNode, any, any, any, any>( 123, 'double-concat', - class extends ExtensionNode> { + class extends ExtNode> { public extId: number = 123; public name(): string { @@ -51,7 +51,7 @@ describe('sample extension', () => { return `${this.name()} (${this.view()})` + printTree(tab, [(tab) => this.data.toString(tab)]); } }, - class extends NodeApi implements ExtensionApi { + class extends NodeApi implements ExtApi { public ins(index: number, text: string): this { const {api, node} = this; const dataApi = api.wrap(node.data as StrNode); diff --git a/src/json-crdt/schema/types.ts b/src/json-crdt/schema/types.ts index 92b9eae4a0..037207deec 100644 --- a/src/json-crdt/schema/types.ts +++ b/src/json-crdt/schema/types.ts @@ -2,7 +2,7 @@ import type {ExtensionId} from '../../json-crdt-extensions'; import type {MvalNode} from '../../json-crdt-extensions/mval/MvalNode'; import type {PeritextNode} from '../../json-crdt-extensions/peritext/PeritextNode'; import type {nodes as builder} from '../../json-crdt-patch'; -import {ExtensionNode} from '../extensions/ExtensionNode'; +import {ExtNode} from '../extensions/ExtNode'; import type * as nodes from '../nodes'; // prettier-ignore @@ -26,7 +26,7 @@ export type SchemaToJsonNode = S extends builder.str ? nodes.VecNode> : nodes.JsonNode; -export type ExtensionVecData> = {__BRAND__: 'ExtVecData'} & [ +export type ExtensionVecData> = {__BRAND__: 'ExtVecData'} & [ header: nodes.ConNode, data: EDataNode, ]; From fe7e6a9ad997f0d32fe6bc778d12ab2373d7b89f Mon Sep 17 00:00:00 2001 From: streamich Date: Sun, 5 May 2024 10:56:13 +0200 Subject: [PATCH 35/37] =?UTF-8?q?feat(json-crdt):=20=F0=9F=8E=B8=20make=20?= =?UTF-8?q?extenion=20object=20optional=20when=20calling=20.asExt()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-patch/builder/schema.ts | 14 ++++++----- src/json-crdt/model/api/nodes.ts | 25 +++++++++++++------ .../nodes/vec/__tests__/extension.spec.ts | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/json-crdt-patch/builder/schema.ts b/src/json-crdt-patch/builder/schema.ts index fbee3623bd..f7505a7ab9 100644 --- a/src/json-crdt-patch/builder/schema.ts +++ b/src/json-crdt-patch/builder/schema.ts @@ -246,8 +246,12 @@ export namespace nodes { /** * Creates an extension node schema. The extension node is a tuple with a - * header and a data node. The header is a 3-byte {@link Uint8Array} with the - * extension ID, the SID of the tuple ID, and the time of the tuple ID. + * sentinel header and a data node. The sentinel header is a 3-byte + * {@link Uint8Array}, which makes this "vec" node to be treated as an + * extension "ext" node. + * + * The 3-byte header consists of the extension ID, the SID of the tuple ID, + * and the time of the tuple ID: * * - 1 byte for the extension id * - 1 byte for the sid of the tuple id, modulo 256 @@ -269,11 +273,9 @@ export namespace nodes { const tupleId = builder.vec(); buf[1] = tupleId.sid % 256; buf[2] = tupleId.time % 256; - const bufId = builder.constOrJson(s.con(buf)); - const valueId = data.build(builder); builder.insVec(tupleId, [ - [0, bufId], - [1, valueId], + [0, builder.constOrJson(s.con(buf))], + [1, data.build(builder)], ]); return tupleId; }); diff --git a/src/json-crdt/model/api/nodes.ts b/src/json-crdt/model/api/nodes.ts index b8cdd1f703..2a9047731b 100644 --- a/src/json-crdt/model/api/nodes.ts +++ b/src/json-crdt/model/api/nodes.ts @@ -3,15 +3,15 @@ import {find} from './find'; import {ITimestampStruct, Timestamp} from '../../../json-crdt-patch/clock'; import {Path} from '../../../json-pointer'; import {ObjNode, ArrNode, BinNode, ConNode, VecNode, ValNode, StrNode} from '../../nodes'; -import {ExtApi} from '../../extensions/types'; import {NodeEvents} from './NodeEvents'; +import {ExtNode} from '../../extensions/ExtNode'; import type {Extension} from '../../extensions/Extension'; +import type {ExtApi} from '../../extensions/types'; import type {JsonNode, JsonNodeView} from '../../nodes'; import type * as types from './proxy'; import type {ModelApi} from './ModelApi'; import type {Printable} from 'tree-dump/lib/types'; import type {JsonNodeApi} from './types'; -import type {ExtNode} from '../../extensions/ExtNode'; import type {VecNodeExtensionData} from '../../schema/types'; export type ApiPath = string | number | Path | void; @@ -116,16 +116,25 @@ export class NodeApi implements Printable { } /** - * @todo The `ext` param shall not be necessary. + * Returns the API object of the extension if the node is an extension node. + * When the `ext` parameter is provided, it checks if the node is an instance + * of the given extension and returns the object's TypeScript type. Otherwise, + * it returns the API object of the extension, but without any type checking. + * + * @param ext Extension of the node + * @returns API of the extension */ public asExt, EApi extends ExtApi>( - ext: Extension, + ext?: Extension, ): EApi { + let extNode: ExtNode | undefined = undefined; let node: JsonNode | undefined = this.node; - while (node) { - if (node instanceof ext.Node) return new ext.Api(node, this.api); - node = node.child ? node.child() : undefined; - } + if (node instanceof ExtNode) extNode = node; + if (node instanceof VecNode) extNode = node.ext(); + if (!extNode) throw new Error('NOT_EXT'); + const api = this.api.wrap(extNode); + if (!ext) return api as any; + if (api instanceof ext.Api) return api; throw new Error('NOT_EXT'); } diff --git a/src/json-crdt/nodes/vec/__tests__/extension.spec.ts b/src/json-crdt/nodes/vec/__tests__/extension.spec.ts index a1c4390df4..72de56a819 100644 --- a/src/json-crdt/nodes/vec/__tests__/extension.spec.ts +++ b/src/json-crdt/nodes/vec/__tests__/extension.spec.ts @@ -34,7 +34,7 @@ test('can read view from node or API node', () => { model.api.root({ mv: mval.new('foo'), }); - const api = model.api.in('mv').asExt(mval); + const api = model.api.in('mv').asExt(); expect(api.view()).toEqual(['foo']); expect(api.node.view()).toEqual(['foo']); }); From 79e64acee8b9fa2d6fc60a073a5e4fafc03f68e6 Mon Sep 17 00:00:00 2001 From: streamich Date: Sun, 5 May 2024 10:56:55 +0200 Subject: [PATCH 36/37] =?UTF-8?q?style:=20=F0=9F=92=84=20address=20linter?= =?UTF-8?q?=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-patch/builder/schema.ts | 2 +- src/json-crdt/model/Model.ts | 2 +- src/json-crdt/model/api/nodes.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/json-crdt-patch/builder/schema.ts b/src/json-crdt-patch/builder/schema.ts index f7505a7ab9..b916588af8 100644 --- a/src/json-crdt-patch/builder/schema.ts +++ b/src/json-crdt-patch/builder/schema.ts @@ -249,7 +249,7 @@ export namespace nodes { * sentinel header and a data node. The sentinel header is a 3-byte * {@link Uint8Array}, which makes this "vec" node to be treated as an * extension "ext" node. - * + * * The 3-byte header consists of the extension ID, the SID of the tuple ID, * and the time of the tuple ID: * diff --git a/src/json-crdt/model/Model.ts b/src/json-crdt/model/Model.ts index e2ba8f4ad6..0f09278dc2 100644 --- a/src/json-crdt/model/Model.ts +++ b/src/json-crdt/model/Model.ts @@ -254,7 +254,7 @@ export class Model> implements Printable { /** * Experimental node retrieval API using proxy objects. Returns a strictly * typed proxy wrapper around the value of the root node. - * + * * @todo consider renaming this to `_`. */ public get s() { diff --git a/src/json-crdt/model/api/nodes.ts b/src/json-crdt/model/api/nodes.ts index 2a9047731b..45a2a52051 100644 --- a/src/json-crdt/model/api/nodes.ts +++ b/src/json-crdt/model/api/nodes.ts @@ -128,7 +128,7 @@ export class NodeApi implements Printable { ext?: Extension, ): EApi { let extNode: ExtNode | undefined = undefined; - let node: JsonNode | undefined = this.node; + const node: JsonNode | undefined = this.node; if (node instanceof ExtNode) extNode = node; if (node instanceof VecNode) extNode = node.ext(); if (!extNode) throw new Error('NOT_EXT'); From 1fb6f11f47400f06cd1d0b1d07584eb5c9af24a1 Mon Sep 17 00:00:00 2001 From: streamich Date: Sun, 5 May 2024 11:56:33 +0200 Subject: [PATCH 37/37] =?UTF-8?q?feat(json-crdt):=20=F0=9F=8E=B8=20remove?= =?UTF-8?q?=20chaining=20from=20node=20APIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: 🧨 A number of JSON CRDT NodeApi methods have been changed. --- src/json-crdt/model/api/nodes.ts | 32 ++++++------------- .../partial-edit/__tests__/scenario.spec.ts | 4 ++- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/json-crdt/model/api/nodes.ts b/src/json-crdt/model/api/nodes.ts index 45a2a52051..9a2374483f 100644 --- a/src/json-crdt/model/api/nodes.ts +++ b/src/json-crdt/model/api/nodes.ts @@ -20,8 +20,6 @@ export type ApiPath = string | number | Path | void; * A generic local changes API for a JSON CRDT node. * * @category Local API - * - * @todo Separate this into leaf and container nodes. */ export class NodeApi implements Printable { constructor( @@ -219,13 +217,12 @@ export class ValApi = ValNode> extends NodeApi { * @param json JSON/CBOR value or ID (logical timestamp) of the value to set. * @returns Reference to itself. */ - public set(json: JsonNodeView): this { + public set(json: JsonNodeView): void { const {api, node} = this; const builder = api.builder; const val = builder.constOrJson(json); api.builder.setVal(node.id, val); api.apply(); - return this; } /** @@ -270,7 +267,7 @@ export class VecApi = VecNode> extends NodeApi { * @param entries List of index-value pairs to set. * @returns Reference to itself. */ - public set(entries: [index: number, value: unknown][]): this { + public set(entries: [index: number, value: unknown][]): void { const {api, node} = this; const {builder} = api; builder.insVec( @@ -278,7 +275,6 @@ export class VecApi = VecNode> extends NodeApi { entries.map(([index, json]) => [index, builder.constOrJson(json)]), ); api.apply(); - return this; // TODO: remove this ...? } public push(...values: unknown[]): void { @@ -350,7 +346,7 @@ export class ObjApi = ObjNode> extends NodeApi { * @param entries List of key-value pairs to set. * @returns Reference to itself. */ - public set(entries: Partial>): this { + public set(entries: Partial>): void { const {api, node} = this; const {builder} = api; builder.insObj( @@ -358,7 +354,6 @@ export class ObjApi = ObjNode> extends NodeApi { Object.entries(entries).map(([key, json]) => [key, builder.constOrJson(json)]), ); api.apply(); - return this; } /** @@ -367,7 +362,7 @@ export class ObjApi = ObjNode> extends NodeApi { * @param keys List of keys to delete. * @returns Reference to itself. */ - public del(keys: string[]): this { + public del(keys: string[]): void { const {api, node} = this; const {builder} = api; api.builder.insObj( @@ -375,7 +370,6 @@ export class ObjApi = ObjNode> extends NodeApi { keys.map((key) => [key, builder.const(undefined)]), ); api.apply(); - return this; } /** @@ -415,7 +409,7 @@ export class StrApi extends NodeApi { * @param text Text to insert. * @returns Reference to itself. */ - public ins(index: number, text: string): this { + public ins(index: number, text: string): void { const {api, node} = this; api.onBeforeLocalChange.emit(api.next); const builder = api.builder; @@ -426,7 +420,6 @@ export class StrApi extends NodeApi { if (!after) throw new Error('OUT_OF_BOUNDS'); builder.insStr(node.id, after, text); api.advance(); - return this; } /** @@ -436,7 +429,7 @@ export class StrApi extends NodeApi { * @param length Number of UTF-16 code units to delete. * @returns Reference to itself. */ - public del(index: number, length: number): this { + public del(index: number, length: number): void { const {api, node} = this; api.onBeforeLocalChange.emit(api.next); const builder = api.builder; @@ -446,7 +439,6 @@ export class StrApi extends NodeApi { node.delete(spans); builder.del(node.id, spans); api.advance(); - return this; } /** @@ -519,13 +511,12 @@ export class BinApi extends NodeApi { * @param data Octets to insert. * @returns Reference to itself. */ - public ins(index: number, data: Uint8Array): this { + public ins(index: number, data: Uint8Array): void { const {api, node} = this; const after = !index ? node.id : node.find(index - 1); if (!after) throw new Error('OUT_OF_BOUNDS'); api.builder.insBin(node.id, after, data); api.apply(); - return this; } /** @@ -535,13 +526,12 @@ export class BinApi extends NodeApi { * @param length Number of octets to delete. * @returns Reference to itself. */ - public del(index: number, length: number): this { + public del(index: number, length: number): void { const {api, node} = this; const spans = node.findInterval(index, length); if (!spans) throw new Error('OUT_OF_BOUNDS'); api.builder.del(node.id, spans); api.apply(); - return this; } /** @@ -591,7 +581,7 @@ export class ArrApi = ArrNode> extends NodeApi { * @param values JSON/CBOR values or IDs of the values to insert. * @returns Reference to itself. */ - public ins(index: number, values: Array[number]>): this { + public ins(index: number, values: Array[number]>): void { const {api, node} = this; const {builder} = api; const after = !index ? node.id : node.find(index - 1); @@ -600,7 +590,6 @@ export class ArrApi = ArrNode> extends NodeApi { for (let i = 0; i < values.length; i++) valueIds.push(builder.json(values[i])); builder.insArr(node.id, after, valueIds); api.apply(); - return this; } /** @@ -610,13 +599,12 @@ export class ArrApi = ArrNode> extends NodeApi { * @param length Number of elements to delete. * @returns Reference to itself. */ - public del(index: number, length: number): this { + public del(index: number, length: number): void { const {api, node} = this; const spans = node.findInterval(index, length); if (!spans) throw new Error('OUT_OF_BOUNDS'); api.builder.del(node.id, spans); api.apply(); - return this; } /** diff --git a/src/json-crdt/partial-edit/__tests__/scenario.spec.ts b/src/json-crdt/partial-edit/__tests__/scenario.spec.ts index f6d6cf6913..194b6d7a15 100644 --- a/src/json-crdt/partial-edit/__tests__/scenario.spec.ts +++ b/src/json-crdt/partial-edit/__tests__/scenario.spec.ts @@ -30,7 +30,9 @@ test('two concurrent users can apply partial updates', () => { }); const doc3 = decoder.decode(fields).fork(); - doc3.api.str('/about').del(2, 2).ins(2, 'was'); + const str = doc3.api.str('/about'); + str.del(2, 2); + str.ins(2, 'was'); doc3.api.obj([]).set({verified: true}); const patch3 = doc3.api.builder.flush(); expect(doc3.view()).toStrictEqual({