From 8ae7ac0795e8d81e3915a0e9bb886c6416948c04 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] 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..a65e2b195a 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,