Skip to content

Commit e6015e2

Browse files
committed
POC of typed extension views for arbitrary list of extensions.
1 parent 12a31f5 commit e6015e2

File tree

4 files changed

+83
-14
lines changed

4 files changed

+83
-14
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {s} from '../../json-crdt-patch';
2+
import {Extension} from '../../json-crdt/extensions/Extension';
3+
import {ExtNode} from '../../json-crdt/extensions/ExtNode';
4+
import {ExtApi} from '../../json-crdt/extensions/types';
5+
import * as ext from '../../json-crdt-extensions/ext';
6+
import {NodeApi} from '../../json-crdt/model/api/nodes';
7+
import {Model} from '../../json-crdt/model/Model';
8+
import {ObjNode} from '../../json-crdt/nodes';
9+
10+
export type FileView = {type: 'File'; content: {type: 'Content'}};
11+
class FileNode extends ExtNode<ObjNode, FileView> {
12+
public readonly extId = 101;
13+
14+
public name(): string {
15+
return 'File' as const;
16+
}
17+
18+
public view(): FileView {
19+
return {type: 'File', content: {type: 'Content'}};
20+
}
21+
}
22+
class FileApi extends NodeApi<FileNode> implements ExtApi<FileNode> {}
23+
export const File = new Extension(101, 'File', FileNode, FileApi, () =>
24+
s.obj({type: s.con('File'), content: s.map({})}),
25+
);
26+
27+
describe('Extensions', () => {
28+
test('type inference', () => {
29+
const schema = s.obj({
30+
field1: File.new(),
31+
field2: ext.cnt.new(1),
32+
field3: s.con('a'),
33+
field4: ext.mval.new(1),
34+
});
35+
const model = Model.create(schema, 1, {extensions: [File, ext.cnt, ext.mval]});
36+
const v = model.view();
37+
// now typed correctly
38+
type ModelView = typeof v;
39+
});
40+
});

src/json-crdt/model/Model.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {JsonNode, JsonNodeView} from '../nodes/types';
1515
import type {Printable} from 'tree-dump/lib/types';
1616
import type {NodeBuilder} from '../../json-crdt-patch';
1717
import type {NodeApi} from './api/nodes';
18+
import {Extension} from '../extensions/Extension';
1819

1920
export const UNDEFINED = new ConNode(ORIGIN, undefined);
2021

@@ -147,17 +148,21 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
147148
* session ID generated by {@link Model.sid}.
148149
* @returns A strictly typed model.
149150
*/
150-
public static readonly create = <S extends NodeBuilder>(
151+
public static readonly create = <S extends NodeBuilder, E extends Extension<any, any, any, any, any, any>[] = []>(
151152
schema?: S,
152153
sidOrClock: clock.ClockVector | number = Model.sid(),
153-
): Model<SchemaToJsonNode<S>> => {
154+
options?: {extensions?: E},
155+
): Model<SchemaToJsonNode<S, E>> => {
154156
const cl =
155157
typeof sidOrClock === 'number'
156158
? sidOrClock === SESSION.SERVER
157159
? new clock.ServerClockVector(SESSION.SERVER, 1)
158160
: new clock.ClockVector(sidOrClock, 1)
159161
: sidOrClock;
160-
const model = new Model<SchemaToJsonNode<S>>(cl);
162+
const model = new Model<SchemaToJsonNode<S, E>>(cl);
163+
for (const extension of options?.extensions ?? []) {
164+
model.ext.register(extension);
165+
}
161166
if (schema) model.setSchema(schema, true);
162167
return model;
163168
};
@@ -186,12 +191,16 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
186191
* @param sid Session ID to set for the model.
187192
* @returns An instance of a model.
188193
*/
189-
public static readonly load = <S extends NodeBuilder>(
194+
public static readonly load = <S extends NodeBuilder, E extends Extension<any, any, any, any, any, any>[]>(
190195
data: Uint8Array,
191196
sid?: number,
192197
schema?: S,
193-
): Model<SchemaToJsonNode<S>> => {
194-
const model = decoder.decode(data) as unknown as Model<SchemaToJsonNode<S>>;
198+
options?: {extensions?: E},
199+
): Model<SchemaToJsonNode<S, E>> => {
200+
const model = decoder.decode(data) as unknown as Model<SchemaToJsonNode<S, E>>;
201+
for (const extension of options?.extensions ?? []) {
202+
model.ext.register(extension);
203+
}
195204
if (schema) model.setSchema(schema, true);
196205
if (typeof sid === 'number') model.setSid(sid);
197206
return model;
@@ -537,7 +546,7 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
537546
* session.
538547
* @returns Strictly typed model.
539548
*/
540-
public setSchema<S extends NodeBuilder>(schema: S, useGlobalSession: boolean = true): Model<SchemaToJsonNode<S>> {
549+
public setSchema<S extends NodeBuilder>(schema: S, useGlobalSession: boolean = true): Model<SchemaToJsonNode<S, []>> {
541550
const c = this.clock;
542551
const isNewDocument = c.time === 1;
543552
if (isNewDocument) {

src/json-crdt/nodes/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import type {Identifiable} from '../../json-crdt-patch/types';
2+
import {ExtNode} from '../extensions/ExtNode';
3+
import {ExtensionVecData} from '../schema/types';
4+
import {VecNode} from './vec/VecNode';
25

36
/**
47
* Each JsonNode represents a structural unit of a JSON document. It is like an
@@ -44,4 +47,10 @@ export interface JsonNode<View = unknown> extends Identifiable {
4447
api: undefined | unknown; // JsonNodeApi<this>;
4548
}
4649

47-
export type JsonNodeView<N> = N extends JsonNode<infer V> ? V : {[K in keyof N]: JsonNodeView<N[K]>};
50+
export type JsonNodeView<N> = N extends ExtNode<any, infer V>
51+
? V
52+
: N extends VecNode<ExtensionVecData<infer N2>>
53+
? JsonNodeView<N2>
54+
: N extends JsonNode<infer V>
55+
? V
56+
: {[K in keyof N]: JsonNodeView<N[K]>};

src/json-crdt/schema/types.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,40 @@ import type {PeritextNode, QuillDeltaNode} from '../../json-crdt-extensions';
44
import type {nodes as builder} from '../../json-crdt-patch';
55
import type {ExtNode} from '../extensions/ExtNode';
66
import type * as nodes from '../nodes';
7+
import {Extension} from '../extensions/Extension';
78

89
// prettier-ignore
9-
export type SchemaToJsonNode<S> = S extends builder.str<infer T>
10+
export type SchemaToJsonNode<S, Extensions extends Extension<number, any, any, any, any>[] = []> = S extends builder.str<infer T>
1011
? nodes.StrNode<T>
1112
: S extends builder.bin
1213
? nodes.BinNode
1314
: S extends builder.con<infer T>
1415
? nodes.ConNode<T>
1516
: S extends builder.val<infer T>
16-
? nodes.ValNode<SchemaToJsonNode<T>>
17+
? nodes.ValNode<SchemaToJsonNode<T,Extensions>>
1718
: S extends builder.vec<infer T>
18-
? nodes.VecNode<{[K in keyof T]: SchemaToJsonNode<T[K]>}>
19+
? nodes.VecNode<{[K in keyof T]: SchemaToJsonNode<T[K],Extensions>}>
1920
: S extends builder.obj<infer T>
20-
? nodes.ObjNode<{[K in keyof T]: SchemaToJsonNode<T[K]>}>
21+
? nodes.ObjNode<{[K in keyof T]: SchemaToJsonNode<T[K],Extensions>}>
2122
: S extends builder.arr<infer T>
22-
? nodes.ArrNode<SchemaToJsonNode<T>>
23+
? nodes.ArrNode<SchemaToJsonNode<T,Extensions>>
2324
: S extends builder.ext<ExtensionId.peritext, any>
2425
? nodes.VecNode<ExtensionVecData<PeritextNode>>
2526
: S extends builder.ext<ExtensionId.quill, any>
2627
? nodes.VecNode<ExtensionVecData<QuillDeltaNode>>
2728
: S extends builder.ext<ExtensionId.mval, any>
2829
? nodes.VecNode<ExtensionVecData<MvalNode>>
29-
: nodes.JsonNode;
30+
: S extends builder.ext<infer ExtId, any>
31+
? Extensions extends (infer ExtsUnion)[]
32+
? nodes.VecNode<
33+
ExtensionVecData<
34+
Extract<ExtsUnion, { readonly id: ExtId }> extends Extension<any, any, infer M, any, any, any>
35+
? M extends ExtNode<any, any>?M:never
36+
: never
37+
>
38+
>
39+
: never
40+
: nodes.JsonNode;
3041

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

0 commit comments

Comments
 (0)