From e6015e2bb509f740f955a17ffd6d0c7d49115ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Brabec?= Date: Sun, 1 Sep 2024 21:45:40 +0200 Subject: [PATCH 1/2] POC of typed extension views for arbitrary list of extensions. --- .../__tests__/ext.spec.ts | 40 +++++++++++++++++++ src/json-crdt/model/Model.ts | 23 +++++++---- src/json-crdt/nodes/types.ts | 11 ++++- src/json-crdt/schema/types.ts | 23 ++++++++--- 4 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 src/json-crdt-extensions/__tests__/ext.spec.ts diff --git a/src/json-crdt-extensions/__tests__/ext.spec.ts b/src/json-crdt-extensions/__tests__/ext.spec.ts new file mode 100644 index 0000000000..35f451e6c9 --- /dev/null +++ b/src/json-crdt-extensions/__tests__/ext.spec.ts @@ -0,0 +1,40 @@ +import {s} from '../../json-crdt-patch'; +import {Extension} from '../../json-crdt/extensions/Extension'; +import {ExtNode} from '../../json-crdt/extensions/ExtNode'; +import {ExtApi} from '../../json-crdt/extensions/types'; +import * as ext from '../../json-crdt-extensions/ext'; +import {NodeApi} from '../../json-crdt/model/api/nodes'; +import {Model} from '../../json-crdt/model/Model'; +import {ObjNode} from '../../json-crdt/nodes'; + +export type FileView = {type: 'File'; content: {type: 'Content'}}; +class FileNode extends ExtNode { + public readonly extId = 101; + + public name(): string { + return 'File' as const; + } + + public view(): FileView { + return {type: 'File', content: {type: 'Content'}}; + } +} +class FileApi extends NodeApi implements ExtApi {} +export const File = new Extension(101, 'File', FileNode, FileApi, () => + s.obj({type: s.con('File'), content: s.map({})}), +); + +describe('Extensions', () => { + test('type inference', () => { + const schema = s.obj({ + field1: File.new(), + field2: ext.cnt.new(1), + field3: s.con('a'), + field4: ext.mval.new(1), + }); + const model = Model.create(schema, 1, {extensions: [File, ext.cnt, ext.mval]}); + const v = model.view(); + // now typed correctly + type ModelView = typeof v; + }); +}); diff --git a/src/json-crdt/model/Model.ts b/src/json-crdt/model/Model.ts index d07406c178..b74615e2f8 100644 --- a/src/json-crdt/model/Model.ts +++ b/src/json-crdt/model/Model.ts @@ -15,6 +15,7 @@ import type {JsonNode, JsonNodeView} from '../nodes/types'; import type {Printable} from 'tree-dump/lib/types'; import type {NodeBuilder} from '../../json-crdt-patch'; import type {NodeApi} from './api/nodes'; +import {Extension} from '../extensions/Extension'; export const UNDEFINED = new ConNode(ORIGIN, undefined); @@ -147,17 +148,21 @@ export class Model> implements Printable { * session ID generated by {@link Model.sid}. * @returns A strictly typed model. */ - public static readonly create = ( + public static readonly create = [] = []>( schema?: S, sidOrClock: clock.ClockVector | number = Model.sid(), - ): Model> => { + options?: {extensions?: E}, + ): Model> => { const cl = typeof sidOrClock === 'number' ? sidOrClock === SESSION.SERVER ? new clock.ServerClockVector(SESSION.SERVER, 1) : new clock.ClockVector(sidOrClock, 1) : sidOrClock; - const model = new Model>(cl); + const model = new Model>(cl); + for (const extension of options?.extensions ?? []) { + model.ext.register(extension); + } if (schema) model.setSchema(schema, true); return model; }; @@ -186,12 +191,16 @@ export class Model> implements Printable { * @param sid Session ID to set for the model. * @returns An instance of a model. */ - public static readonly load = ( + public static readonly load = []>( data: Uint8Array, sid?: number, schema?: S, - ): Model> => { - const model = decoder.decode(data) as unknown as Model>; + options?: {extensions?: E}, + ): Model> => { + const model = decoder.decode(data) as unknown as Model>; + for (const extension of options?.extensions ?? []) { + model.ext.register(extension); + } if (schema) model.setSchema(schema, true); if (typeof sid === 'number') model.setSid(sid); return model; @@ -537,7 +546,7 @@ export class Model> implements Printable { * session. * @returns Strictly typed model. */ - public setSchema(schema: S, useGlobalSession: boolean = true): Model> { + public setSchema(schema: S, useGlobalSession: boolean = true): Model> { const c = this.clock; const isNewDocument = c.time === 1; if (isNewDocument) { diff --git a/src/json-crdt/nodes/types.ts b/src/json-crdt/nodes/types.ts index fb56284524..f3902f97eb 100644 --- a/src/json-crdt/nodes/types.ts +++ b/src/json-crdt/nodes/types.ts @@ -1,4 +1,7 @@ import type {Identifiable} from '../../json-crdt-patch/types'; +import {ExtNode} from '../extensions/ExtNode'; +import {ExtensionVecData} from '../schema/types'; +import {VecNode} from './vec/VecNode'; /** * Each JsonNode represents a structural unit of a JSON document. It is like an @@ -44,4 +47,10 @@ export interface JsonNode extends Identifiable { api: undefined | unknown; // JsonNodeApi; } -export type JsonNodeView = N extends JsonNode ? V : {[K in keyof N]: JsonNodeView}; +export type JsonNodeView = N extends ExtNode + ? V + : N extends VecNode> + ? JsonNodeView + : N extends JsonNode + ? V + : {[K in keyof N]: JsonNodeView}; diff --git a/src/json-crdt/schema/types.ts b/src/json-crdt/schema/types.ts index bf2df60013..b1782b390f 100644 --- a/src/json-crdt/schema/types.ts +++ b/src/json-crdt/schema/types.ts @@ -4,29 +4,40 @@ import type {PeritextNode, QuillDeltaNode} from '../../json-crdt-extensions'; import type {nodes as builder} from '../../json-crdt-patch'; import type {ExtNode} from '../extensions/ExtNode'; import type * as nodes from '../nodes'; +import {Extension} from '../extensions/Extension'; // prettier-ignore -export type SchemaToJsonNode = S extends builder.str +export type SchemaToJsonNode[] = []> = S extends builder.str ? nodes.StrNode : S extends builder.bin ? nodes.BinNode : S extends builder.con ? nodes.ConNode : S extends builder.val - ? nodes.ValNode> + ? nodes.ValNode> : S extends builder.vec - ? nodes.VecNode<{[K in keyof T]: SchemaToJsonNode}> + ? nodes.VecNode<{[K in keyof T]: SchemaToJsonNode}> : S extends builder.obj - ? nodes.ObjNode<{[K in keyof T]: SchemaToJsonNode}> + ? nodes.ObjNode<{[K in keyof T]: SchemaToJsonNode}> : S extends builder.arr - ? nodes.ArrNode> + ? nodes.ArrNode> : S extends builder.ext ? nodes.VecNode> : S extends builder.ext ? nodes.VecNode> : S extends builder.ext ? nodes.VecNode> - : nodes.JsonNode; + : S extends builder.ext + ? Extensions extends (infer ExtsUnion)[] + ? nodes.VecNode< + ExtensionVecData< + Extract extends Extension + ? M extends ExtNode?M:never + : never + > + > + : never + : nodes.JsonNode; export type ExtensionVecData> = {__BRAND__: 'ExtVecData'} & [ header: nodes.ConNode, From c4e9d57f8112897f57d1fee902f3a7070d049de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Brabec?= Date: Thu, 5 Sep 2024 19:18:19 +0200 Subject: [PATCH 2/2] POC of typed node apis for arbitrary list of extensions. --- .../__tests__/ext.spec.ts | 6 ++- src/json-crdt/model/Model.ts | 22 ++++---- src/json-crdt/model/api/ModelApi.ts | 9 ++-- src/json-crdt/model/api/nodes.ts | 39 +++++++++----- src/json-crdt/model/api/types.ts | 51 +++++++++++-------- src/json-crdt/schema/types.ts | 10 ++-- 6 files changed, 81 insertions(+), 56 deletions(-) diff --git a/src/json-crdt-extensions/__tests__/ext.spec.ts b/src/json-crdt-extensions/__tests__/ext.spec.ts index 35f451e6c9..78818522c4 100644 --- a/src/json-crdt-extensions/__tests__/ext.spec.ts +++ b/src/json-crdt-extensions/__tests__/ext.spec.ts @@ -19,7 +19,9 @@ class FileNode extends ExtNode { return {type: 'File', content: {type: 'Content'}}; } } -class FileApi extends NodeApi implements ExtApi {} +class FileApi extends NodeApi implements ExtApi { + rename() {} +} export const File = new Extension(101, 'File', FileNode, FileApi, () => s.obj({type: s.con('File'), content: s.map({})}), ); @@ -34,7 +36,9 @@ describe('Extensions', () => { }); const model = Model.create(schema, 1, {extensions: [File, ext.cnt, ext.mval]}); const v = model.view(); + const f1 = model.api.node.get('field1'); // now typed correctly type ModelView = typeof v; + type NodeApi = typeof f1; }); }); diff --git a/src/json-crdt/model/Model.ts b/src/json-crdt/model/Model.ts index b74615e2f8..1853cae5e6 100644 --- a/src/json-crdt/model/Model.ts +++ b/src/json-crdt/model/Model.ts @@ -23,7 +23,9 @@ export const UNDEFINED = new ConNode(ORIGIN, undefined); * In instance of Model class represents the underlying data structure, * i.e. model, of the JSON CRDT document. */ -export class Model> implements Printable { +export class Model, Exts extends Extension[] = []> + implements Printable +{ /** * Generates a random session ID. Use this method to generate a session ID * for a new user. Store the session ID in the user's browser or device once @@ -152,14 +154,14 @@ export class Model> implements Printable { schema?: S, sidOrClock: clock.ClockVector | number = Model.sid(), options?: {extensions?: E}, - ): Model> => { + ): Model, E> => { const cl = typeof sidOrClock === 'number' ? sidOrClock === SESSION.SERVER ? new clock.ServerClockVector(SESSION.SERVER, 1) : new clock.ClockVector(sidOrClock, 1) : sidOrClock; - const model = new Model>(cl); + const model = new Model, E>(cl); for (const extension of options?.extensions ?? []) { model.ext.register(extension); } @@ -196,8 +198,8 @@ export class Model> implements Printable { sid?: number, schema?: S, options?: {extensions?: E}, - ): Model> => { - const model = decoder.decode(data) as unknown as Model>; + ): Model,E> => { + const model = decoder.decode(data) as unknown as Model,E>; for (const extension of options?.extensions ?? []) { model.ext.register(extension); } @@ -261,13 +263,13 @@ export class Model> implements Printable { } /** @ignore */ - private _api?: ModelApi; + private _api?: ModelApi; /** * API for applying local changes to the current document. */ - public get api(): ModelApi { - if (!this._api) this._api = new ModelApi(this); + public get api(): ModelApi { + if (!this._api) this._api = new ModelApi(this); return this._api; } @@ -476,7 +478,7 @@ export class Model> implements Printable { * * @returns A copy of this model with the same session ID. */ - public clone(): Model { + public clone(): Model { return this.fork(this.clock.sid); } @@ -493,7 +495,7 @@ export class Model> implements Printable { /** * Resets the model to equivalent state of another model. */ - public reset(to: Model): void { + public reset(to: Model): void { this.onbeforereset?.(); const index = this.index; this.index = new AvlMap(clock.compare); diff --git a/src/json-crdt/model/api/ModelApi.ts b/src/json-crdt/model/api/ModelApi.ts index e92d658244..43045aeb84 100644 --- a/src/json-crdt/model/api/ModelApi.ts +++ b/src/json-crdt/model/api/ModelApi.ts @@ -7,7 +7,8 @@ import {SyncStore} from '../../../util/events/sync-store'; import {MergeFanOut, MicrotaskBufferFanOut} from './fanout'; import {ExtNode} from '../../extensions/ExtNode'; import type {Model} from '../Model'; -import type {JsonNode, JsonNodeView} from '../../nodes'; +import type {JsonNode, JsonNodeView, RootNode} from '../../nodes'; +import {Extension} from '../../extensions/Extension'; /** * Local changes API for a JSON CRDT model. This class is the main entry point @@ -15,7 +16,9 @@ import type {JsonNode, JsonNodeView} from '../../nodes'; * * @category Local API */ -export class ModelApi implements SyncStore> { +export class ModelApi[] = []> + implements SyncStore> +{ /** * Patch builder for the local changes. */ @@ -99,7 +102,7 @@ export class ModelApi implements SyncStore, Extensions>(this.model.root, this); } /** @ignore */ diff --git a/src/json-crdt/model/api/nodes.ts b/src/json-crdt/model/api/nodes.ts index f64dbb22ed..65eaa46159 100644 --- a/src/json-crdt/model/api/nodes.ts +++ b/src/json-crdt/model/api/nodes.ts @@ -21,11 +21,10 @@ export type ApiPath = string | number | Path | void; * * @category Local API */ -export class NodeApi implements Printable { - constructor( - public node: N, - public readonly api: ModelApi, - ) {} +export class NodeApi[] = []> + implements Printable +{ + constructor(public node: N, public readonly api: ModelApi) {} /** @ignore */ private ev: undefined | NodeEvents = undefined; @@ -122,7 +121,7 @@ export class NodeApi implements Printable { * @param ext Extension of the node * @returns API of the extension */ - public asExt(): JsonNodeApi> | ExtApi | undefined; + public asExt(): JsonNodeApi, Extensions> | ExtApi | undefined; public asExt, EApi extends ExtApi>( ext: Extension, ): EApi; @@ -206,12 +205,15 @@ export class ConApi = ConNode> extends NodeApi { * * @category Local API */ -export class ValApi = ValNode> extends NodeApi { +export class ValApi< + N extends ValNode = ValNode, + Extensions extends Extension[] = [], +> extends NodeApi { /** * Get API instance of the inner node. * @returns Inner node API. */ - public get(): JsonNodeApi ? T : JsonNode> { + public get(): JsonNodeApi ? T : JsonNode, Extensions> { return this.in() as any; } @@ -254,14 +256,17 @@ type UnVecNode = N extends VecNode ? T : never; * * @category Local API */ -export class VecApi = VecNode> extends NodeApi { +export class VecApi< + N extends VecNode = VecNode, + Extensions extends Extension[] = [], +> extends NodeApi { /** * Get API instance of a child node. * * @param key Object key to get. * @returns A specified child node API. */ - public get>(key: K): JsonNodeApi[K]> { + public get>(key: K): JsonNodeApi[K], Extensions> { return this.in(key as string) as any; } @@ -326,14 +331,17 @@ type UnObjNode = N extends ObjNode ? T : never; * * @category Local API */ -export class ObjApi = ObjNode> extends NodeApi { +export class ObjApi< + N extends ObjNode = ObjNode, + Extensions extends Extension[] = [], +> extends NodeApi { /** * Get API instance of a child node. * * @param key Object key to get. * @returns A specified child node API. */ - public get>(key: K): JsonNodeApi[K]> { + public get>(key: K): JsonNodeApi[K], Extensions> { return this.in(key as string) as any; } @@ -560,14 +568,17 @@ type UnArrNode = N extends ArrNode ? T : never; * * @category Local API */ -export class ArrApi = ArrNode> extends NodeApi { +export class ArrApi< + N extends ArrNode = ArrNode, + Extensions extends Extension[] = [], +> extends NodeApi { /** * Get API instance of a child node. * * @param index Index of the element to get. * @returns Child node API for the element at the given index. */ - public get(index: number): JsonNodeApi> { + public get(index: number): JsonNodeApi, Extensions> { return this.in(index) as any; } diff --git a/src/json-crdt/model/api/types.ts b/src/json-crdt/model/api/types.ts index 78d4b283d4..e9727ae2be 100644 --- a/src/json-crdt/model/api/types.ts +++ b/src/json-crdt/model/api/types.ts @@ -1,26 +1,35 @@ import type {PeritextNode, PeritextApi, QuillDeltaNode, QuillDeltaApi} from '../../../json-crdt-extensions'; +import { Extension } from '../../extensions/Extension'; +import { ExtNode } from '../../extensions/ExtNode'; import type * as types from '../../nodes'; import type * as nodes from './nodes'; // prettier-ignore -export type JsonNodeApi = N extends types.ConNode - ? nodes.ConApi - : N extends types.RootNode - ? nodes.ValApi - : N extends types.ValNode - ? nodes.ValApi - : N extends types.StrNode - ? nodes.StrApi - : N extends types.BinNode - ? nodes.BinApi - : N extends types.ArrNode - ? nodes.ArrApi - : N extends types.ObjNode - ? nodes.ObjApi - : N extends types.VecNode - ? nodes.VecApi - : N extends PeritextNode - ? PeritextApi - : N extends QuillDeltaNode - ? QuillDeltaApi - : never; +export type JsonNodeApi[]> = +N extends ExtNode + ? Extensions extends (infer ExtsUnion)[] + ? Extract extends Extension + ? Api + : never + : never + : N extends types.ConNode + ? nodes.ConApi + : N extends types.RootNode + ? nodes.ValApi + : N extends types.ValNode + ? nodes.ValApi + : N extends types.StrNode + ? nodes.StrApi + : N extends types.BinNode + ? nodes.BinApi + : N extends types.ArrNode + ? nodes.ArrApi + : N extends types.ObjNode + ? nodes.ObjApi + : N extends types.VecNode + ? nodes.VecApi + : N extends PeritextNode + ? PeritextApi + : N extends QuillDeltaNode + ? QuillDeltaApi + : never; diff --git a/src/json-crdt/schema/types.ts b/src/json-crdt/schema/types.ts index b1782b390f..3b20951bd1 100644 --- a/src/json-crdt/schema/types.ts +++ b/src/json-crdt/schema/types.ts @@ -29,13 +29,9 @@ export type SchemaToJsonNode> : S extends builder.ext ? Extensions extends (infer ExtsUnion)[] - ? nodes.VecNode< - ExtensionVecData< - Extract extends Extension - ? M extends ExtNode?M:never - : never - > - > + ? Extract extends Extension + ? M + : never : never : nodes.JsonNode;