Skip to content

Commit

Permalink
POC of typed extension views for arbitrary list of extensions.
Browse files Browse the repository at this point in the history
  • Loading branch information
brabeji committed Sep 1, 2024
1 parent 12a31f5 commit 8ae7ac0
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 14 deletions.
40 changes: 40 additions & 0 deletions src/json-crdt-extensions/__tests__/ext.spec.ts
Original file line number Diff line number Diff line change
@@ -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<ObjNode, FileView> {
public readonly extId = 101;

public name(): string {
return 'File' as const;
}

public view(): FileView {
return {type: 'File', content: {type: 'Content'}};
}
}
class FileApi extends NodeApi<FileNode> implements ExtApi<FileNode> {}
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;
});
});
23 changes: 16 additions & 7 deletions src/json-crdt/model/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -147,17 +148,21 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
* session ID generated by {@link Model.sid}.
* @returns A strictly typed model.
*/
public static readonly create = <S extends NodeBuilder>(
public static readonly create = <S extends NodeBuilder, E extends Extension<any, any, any, any, any, any>[]>(
schema?: S,
sidOrClock: clock.ClockVector | number = Model.sid(),
): Model<SchemaToJsonNode<S>> => {
options?: {extensions?: E},
): Model<SchemaToJsonNode<S, 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<SchemaToJsonNode<S>>(cl);
const model = new Model<SchemaToJsonNode<S, E>>(cl);
for (const extension of options?.extensions ?? []) {
model.ext.register(extension);
}
if (schema) model.setSchema(schema, true);
return model;
};
Expand Down Expand Up @@ -186,12 +191,16 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
* @param sid Session ID to set for the model.
* @returns An instance of a model.
*/
public static readonly load = <S extends NodeBuilder>(
public static readonly load = <S extends NodeBuilder, E extends Extension<any, any, any, any, any, any>[]>(
data: Uint8Array,
sid?: number,
schema?: S,
): Model<SchemaToJsonNode<S>> => {
const model = decoder.decode(data) as unknown as Model<SchemaToJsonNode<S>>;
options?: {extensions?: E},
): Model<SchemaToJsonNode<S, E>> => {
const model = decoder.decode(data) as unknown as Model<SchemaToJsonNode<S, E>>;
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;
Expand Down Expand Up @@ -537,7 +546,7 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
* session.
* @returns Strictly typed model.
*/
public setSchema<S extends NodeBuilder>(schema: S, useGlobalSession: boolean = true): Model<SchemaToJsonNode<S>> {
public setSchema<S extends NodeBuilder>(schema: S, useGlobalSession: boolean = true): Model<SchemaToJsonNode<S, []>> {
const c = this.clock;
const isNewDocument = c.time === 1;
if (isNewDocument) {
Expand Down
11 changes: 10 additions & 1 deletion src/json-crdt/nodes/types.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -44,4 +47,10 @@ export interface JsonNode<View = unknown> extends Identifiable {
api: undefined | unknown; // JsonNodeApi<this>;
}

export type JsonNodeView<N> = N extends JsonNode<infer V> ? V : {[K in keyof N]: JsonNodeView<N[K]>};
export type JsonNodeView<N> = N extends ExtNode<any, infer V>
? V
: N extends VecNode<ExtensionVecData<infer N2>>
? JsonNodeView<N2>
: N extends JsonNode<infer V>
? V
: {[K in keyof N]: JsonNodeView<N[K]>};
23 changes: 17 additions & 6 deletions src/json-crdt/schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = S extends builder.str<infer T>
export type SchemaToJsonNode<S, Extensions extends Extension<number, any, any, any, any>[] = []> = S extends builder.str<infer T>
? nodes.StrNode<T>
: S extends builder.bin
? nodes.BinNode
: S extends builder.con<infer T>
? nodes.ConNode<T>
: S extends builder.val<infer T>
? nodes.ValNode<SchemaToJsonNode<T>>
? nodes.ValNode<SchemaToJsonNode<T,Extensions>>
: S extends builder.vec<infer T>
? nodes.VecNode<{[K in keyof T]: SchemaToJsonNode<T[K]>}>
? nodes.VecNode<{[K in keyof T]: SchemaToJsonNode<T[K],Extensions>}>
: S extends builder.obj<infer T>
? nodes.ObjNode<{[K in keyof T]: SchemaToJsonNode<T[K]>}>
? nodes.ObjNode<{[K in keyof T]: SchemaToJsonNode<T[K],Extensions>}>
: S extends builder.arr<infer T>
? nodes.ArrNode<SchemaToJsonNode<T>>
? nodes.ArrNode<SchemaToJsonNode<T,Extensions>>
: S extends builder.ext<ExtensionId.peritext, any>
? nodes.VecNode<ExtensionVecData<PeritextNode>>
: S extends builder.ext<ExtensionId.quill, any>
? nodes.VecNode<ExtensionVecData<QuillDeltaNode>>
: S extends builder.ext<ExtensionId.mval, any>
? nodes.VecNode<ExtensionVecData<MvalNode>>
: nodes.JsonNode;
: S extends builder.ext<infer ExtId, any>
? Extensions extends (infer ExtsUnion)[]
? nodes.VecNode<
ExtensionVecData<
Extract<ExtsUnion, { readonly id: ExtId }> extends Extension<any, any, infer M, any, any, any>
? M extends ExtNode<any, any>?M:never
: never
>
>
: never
: nodes.JsonNode;

export type ExtensionVecData<EDataNode extends ExtNode<any, any>> = {__BRAND__: 'ExtVecData'} & [
header: nodes.ConNode<Uint8Array>,
Expand Down

0 comments on commit 8ae7ac0

Please sign in to comment.