Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC of typed extension views for arbitrary list of extensions. #700

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/json-crdt-extensions/__tests__/ext.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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> {
rename() {}
}
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();
const f1 = model.api.node.get('field1');
// now typed correctly
type ModelView = typeof v;
type NodeApi = typeof f1;
});
});
37 changes: 24 additions & 13 deletions src/json-crdt/model/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ 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);

/**
* In instance of Model class represents the underlying data structure,
* i.e. model, of the JSON CRDT document.
*/
export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
export class Model<N extends JsonNode = JsonNode<any>, Exts extends Extension<number, any, any, any, any>[] = []>
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
Expand Down Expand Up @@ -147,17 +150,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>, 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>, 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 +193,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>,E> => {
const model = decoder.decode(data) as unknown as Model<SchemaToJsonNode<S, E>,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 @@ -252,13 +263,13 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
}

/** @ignore */
private _api?: ModelApi<N>;
private _api?: ModelApi<N, Exts>;

/**
* API for applying local changes to the current document.
*/
public get api(): ModelApi<N> {
if (!this._api) this._api = new ModelApi<N>(this);
public get api(): ModelApi<N, Exts> {
if (!this._api) this._api = new ModelApi<N, Exts>(this);
return this._api;
}

Expand Down Expand Up @@ -467,7 +478,7 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
*
* @returns A copy of this model with the same session ID.
*/
public clone(): Model<N> {
public clone(): Model<N, Exts> {
return this.fork(this.clock.sid);
}

Expand All @@ -484,7 +495,7 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
/**
* Resets the model to equivalent state of another model.
*/
public reset(to: Model<N>): void {
public reset(to: Model<N, Exts>): void {
this.onbeforereset?.();
const index = this.index;
this.index = new AvlMap<clock.ITimestampStruct, JsonNode>(clock.compare);
Expand Down Expand Up @@ -537,7 +548,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
9 changes: 6 additions & 3 deletions src/json-crdt/model/api/ModelApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ 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
* for executing local user actions on a JSON CRDT document.
*
* @category Local API
*/
export class ModelApi<N extends JsonNode = JsonNode> implements SyncStore<JsonNodeView<N>> {
export class ModelApi<N extends JsonNode = JsonNode, Extensions extends Extension<number, any, any, any, any>[] = []>
implements SyncStore<JsonNodeView<N>>
{
/**
* Patch builder for the local changes.
*/
Expand Down Expand Up @@ -99,7 +102,7 @@ export class ModelApi<N extends JsonNode = JsonNode> implements SyncStore<JsonNo
* Local changes API for the root node.
*/
public get r() {
return new ValApi(this.model.root, this);
return new ValApi<RootNode<N>, Extensions>(this.model.root, this);
}

/** @ignore */
Expand Down
39 changes: 25 additions & 14 deletions src/json-crdt/model/api/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ export type ApiPath = string | number | Path | void;
*
* @category Local API
*/
export class NodeApi<N extends JsonNode = JsonNode> implements Printable {
constructor(
public node: N,
public readonly api: ModelApi<any>,
) {}
export class NodeApi<N extends JsonNode = JsonNode, Extensions extends Extension<number, any, any, any, any>[] = []>
implements Printable
{
constructor(public node: N, public readonly api: ModelApi<any>) {}

/** @ignore */
private ev: undefined | NodeEvents<N> = undefined;
Expand Down Expand Up @@ -122,7 +121,7 @@ export class NodeApi<N extends JsonNode = JsonNode> implements Printable {
* @param ext Extension of the node
* @returns API of the extension
*/
public asExt(): JsonNodeApi<VecNodeExtensionData<N>> | ExtApi<any> | undefined;
public asExt(): JsonNodeApi<VecNodeExtensionData<N>, Extensions> | ExtApi<any> | undefined;
public asExt<EN extends ExtNode<any, any>, EApi extends ExtApi<EN>>(
ext: Extension<any, any, EN, EApi, any, any>,
): EApi;
Expand Down Expand Up @@ -206,12 +205,15 @@ export class ConApi<N extends ConNode<any> = ConNode<any>> extends NodeApi<N> {
*
* @category Local API
*/
export class ValApi<N extends ValNode<any> = ValNode<any>> extends NodeApi<N> {
export class ValApi<
N extends ValNode<any> = ValNode<any>,
Extensions extends Extension<number, any, any, any, any>[] = [],
> extends NodeApi<N> {
/**
* Get API instance of the inner node.
* @returns Inner node API.
*/
public get(): JsonNodeApi<N extends ValNode<infer T> ? T : JsonNode> {
public get(): JsonNodeApi<N extends ValNode<infer T> ? T : JsonNode, Extensions> {
return this.in() as any;
}

Expand Down Expand Up @@ -254,14 +256,17 @@ type UnVecNode<N> = N extends VecNode<infer T> ? T : never;
*
* @category Local API
*/
export class VecApi<N extends VecNode<any> = VecNode<any>> extends NodeApi<N> {
export class VecApi<
N extends VecNode<any> = VecNode<any>,
Extensions extends Extension<number, any, any, any, any>[] = [],
> extends NodeApi<N> {
/**
* Get API instance of a child node.
*
* @param key Object key to get.
* @returns A specified child node API.
*/
public get<K extends keyof UnVecNode<N>>(key: K): JsonNodeApi<UnVecNode<N>[K]> {
public get<K extends keyof UnVecNode<N>>(key: K): JsonNodeApi<UnVecNode<N>[K], Extensions> {
return this.in(key as string) as any;
}

Expand Down Expand Up @@ -326,14 +331,17 @@ type UnObjNode<N> = N extends ObjNode<infer T> ? T : never;
*
* @category Local API
*/
export class ObjApi<N extends ObjNode<any> = ObjNode<any>> extends NodeApi<N> {
export class ObjApi<
N extends ObjNode<any> = ObjNode<any>,
Extensions extends Extension<number, any, any, any, any>[] = [],
> extends NodeApi<N> {
/**
* Get API instance of a child node.
*
* @param key Object key to get.
* @returns A specified child node API.
*/
public get<K extends keyof UnObjNode<N>>(key: K): JsonNodeApi<UnObjNode<N>[K]> {
public get<K extends keyof UnObjNode<N>>(key: K): JsonNodeApi<UnObjNode<N>[K], Extensions> {
return this.in(key as string) as any;
}

Expand Down Expand Up @@ -560,14 +568,17 @@ type UnArrNode<N> = N extends ArrNode<infer T> ? T : never;
*
* @category Local API
*/
export class ArrApi<N extends ArrNode<any> = ArrNode<any>> extends NodeApi<N> {
export class ArrApi<
N extends ArrNode<any> = ArrNode<any>,
Extensions extends Extension<number, any, any, any, any>[] = [],
> extends NodeApi<N> {
/**
* 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<UnArrNode<N>> {
public get(index: number): JsonNodeApi<UnArrNode<N>, Extensions> {
return this.in(index) as any;
}

Expand Down
51 changes: 30 additions & 21 deletions src/json-crdt/model/api/types.ts
Original file line number Diff line number Diff line change
@@ -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> = N extends types.ConNode<any>
? nodes.ConApi<N>
: N extends types.RootNode<any>
? nodes.ValApi<N>
: N extends types.ValNode<any>
? nodes.ValApi<N>
: N extends types.StrNode
? nodes.StrApi
: N extends types.BinNode
? nodes.BinApi
: N extends types.ArrNode<any>
? nodes.ArrApi<N>
: N extends types.ObjNode<any>
? nodes.ObjApi<N>
: N extends types.VecNode<any>
? nodes.VecApi<N>
: N extends PeritextNode
? PeritextApi
: N extends QuillDeltaNode
? QuillDeltaApi
: never;
export type JsonNodeApi<N, Extensions extends Extension<number, any, any, any, any>[]> =
N extends ExtNode<any>
? Extensions extends (infer ExtsUnion)[]
? Extract<ExtsUnion, { readonly id: N['extId'] }> extends Extension<number, any, any, infer Api, any, any>
? Api
: never
: never
: N extends types.ConNode<any>
? nodes.ConApi<N>
: N extends types.RootNode<any>
? nodes.ValApi<N>
: N extends types.ValNode<any>
? nodes.ValApi<N, Extensions>
: N extends types.StrNode
? nodes.StrApi
: N extends types.BinNode
? nodes.BinApi
: N extends types.ArrNode<any>
? nodes.ArrApi<N, Extensions>
: N extends types.ObjNode<any>
? nodes.ObjApi<N, Extensions>
: N extends types.VecNode<any>
? nodes.VecApi<N, Extensions>
: N extends PeritextNode
? PeritextApi
: N extends QuillDeltaNode
? QuillDeltaApi
: never;
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>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if the first branch is necessary

? V
: N extends VecNode<ExtensionVecData<infer N2>>
? JsonNodeView<N2>
: N extends JsonNode<infer V>
? V
: {[K in keyof N]: JsonNodeView<N[K]>};
Loading