Skip to content

Commit

Permalink
Merge pull request #465 from streamich/crdt-schema
Browse files Browse the repository at this point in the history
CRDT schema builder improvements
  • Loading branch information
streamich authored Nov 29, 2023
2 parents b02305f + 16658ed commit acdaff9
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 87 deletions.
1 change: 1 addition & 0 deletions src/json-crdt-patch/builder/Tuple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export class VectorDelayedValue<T extends unknown[]> {
/**
* @param slots
* @returns
* @deprecated Use `s.vec(...)` instead.
*/
export const vec = <T extends unknown[]>(...slots: T): VectorDelayedValue<T> => new VectorDelayedValue(slots);
155 changes: 155 additions & 0 deletions src/json-crdt-patch/builder/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,26 @@ import type {ITimestampStruct} from '../clock';
import {NodeBuilder} from './DelayedValueBuilder';

/* tslint:disable no-namespace class-name */

/**
* This namespace contains all the node builders. Each node builder is a
* schema for a specific node type. Each node builder has a `build` method
* that takes a {@link NodeBuilder} and returns the ID of the node.
*/
export namespace nodes {
/**
* The `con` class represents a "con" JSON CRDT node. As the generic type
* parameter, it takes the type of the raw value.
*
* Example:
*
* ```typescript
* s.con(0);
* s.con('');
* s.con<number>(123);
* s.con<0 | 1>(0);
* ```
*/
export class con<T extends unknown | ITimestampStruct> extends NodeBuilder {
public readonly type = 'con';

Expand All @@ -11,6 +30,19 @@ export namespace nodes {
}
}

/**
* The `str` class represents a "str" JSON CRDT node. As the generic type
* parameter, it takes the type of the raw value.
*
* Example:
*
* ```typescript
* s.str('');
* s.str('hello');
* s.str<string>('world');
* s.str<'' | 'hello' | 'world'>('hello');
* ```
*/
export class str<T extends string = string> extends NodeBuilder {
public readonly type = 'str';

Expand All @@ -19,6 +51,9 @@ export namespace nodes {
}
}

/**
* The `bin` class represents a "bin" JSON CRDT node.
*/
export class bin extends NodeBuilder {
public readonly type = 'bin';

Expand All @@ -27,6 +62,18 @@ export namespace nodes {
}
}

/**
* The `val` class represents a "val" JSON CRDT node. As the generic type
* parameter, it takes the type of the inner node builder.
*
* Example:
*
* ```typescript
* s.val(s.con(0));
* s.val(s.str(''));
* s.val(s.str('hello'));
* ```
*/
export class val<T extends NodeBuilder> extends NodeBuilder {
public readonly type = 'val';

Expand All @@ -40,6 +87,17 @@ export namespace nodes {
}
}

/**
* The `vec` class represents a "vec" JSON CRDT node. As the generic type
* parameter, it takes a tuple of node builders.
*
* Example:
*
* ```typescript
* s.vec(s.con(0), s.con(1));
* s.vec(s.str(''), s.str('hello'));
* ```
*/
export class vec<T extends NodeBuilder[]> extends NodeBuilder {
public readonly type = 'vec';

Expand All @@ -61,6 +119,20 @@ export namespace nodes {
}
}

/**
* The `obj` class represents a "obj" JSON CRDT node. As the generic type
* parameter, it takes a record of node builders. The optional generic type
* parameter is a record of optional keys.
*
* Example:
*
* ```typescript
* s.obj({
* name: s.str(''),
* age: s.con(0),
* });
* ```
*/
export class obj<
T extends Record<string, NodeBuilder>,
O extends Record<string, NodeBuilder> = {},
Expand Down Expand Up @@ -91,6 +163,29 @@ export namespace nodes {
}
}

/**
* A type alias for {@link obj}. It creates a "map" node schema, which is an
* object where a key can be any string and the value is of the same type.
*
* Example:
*
* ```typescript
* s.map<nodes.con<number>>
* ```
*/
export type map<R extends NodeBuilder> = obj<Record<string, R>, Record<string, R>>;

/**
* The `arr` class represents a "arr" JSON CRDT node. As the generic type
* parameter, it an array of node builders.
*
* Example:
*
* ```typescript
* s.arr([s.con(0), s.con(1)]);
* s.arr([s.str(''), s.str('hello')]);
* ```
*/
export class arr<T extends NodeBuilder> extends NodeBuilder {
public readonly type = 'arr';

Expand All @@ -110,15 +205,75 @@ export namespace nodes {
}
/* tslint:enable no-namespace class-name */

/**
* Schema builder. Use this to create a JSON CRDT model schema and the default
* value. Example:
*
* ```typescript
* const schema = s.obj({
* name: s.str(''),
* age: s.con(0),
* tags: s.arr<nodes.con<string>>([]),
* });
* ```
*/
export const schema = {
/**
* Creates a "con" node schema and the default value.
* @param raw Raw default value.
*/
con: <T extends unknown | ITimestampStruct>(raw: T) => new nodes.con<T>(raw),

/**
* Creates a "str" node schema and the default value.
* @param str Default value.
*/
str: <T extends string>(str: T) => new nodes.str<T>(str || ('' as T)),

/**
* Creates a "bin" node schema and the default value.
* @param bin Default value.
*/
bin: (bin: Uint8Array) => new nodes.bin(bin),

/**
* Creates a "val" node schema and the default value.
* @param val Default value.
*/
val: <T extends NodeBuilder>(val: T) => new nodes.val<T>(val),

/**
* Creates a "vec" node schema and the default value.
* @param vec Default value.
*/
vec: <T extends NodeBuilder[]>(...vec: T) => new nodes.vec<T>(vec),

/**
* Creates a "obj" node schema and the default value.
* @param obj Default value, required object keys.
* @param opt Default value of optional object keys.
*/
obj: <T extends Record<string, NodeBuilder>, O extends Record<string, NodeBuilder>>(obj: T, opt?: O) =>
new nodes.obj<T, O>(obj, opt),

/**
* This is an alias for {@link schema.obj}. It creates a "map" node schema,
* which is an object where a key can be any string and the value is of the
* same type.
* @param obj Default value.
*/
map: <R extends NodeBuilder>(obj: Record<string, R>): nodes.map<R> =>
schema.obj<Record<string, R>, Record<string, R>>(obj),

/**
* Creates an "arr" node schema and the default value.
* @param arr Default value.
*/
arr: <T extends NodeBuilder>(arr: T[]) => new nodes.arr<T>(arr),
};

/**
* Schema builder. Use this to create a JSON CRDT model schema and the default
* value. Alias for {@link schema}.
*/
export const s = schema;
55 changes: 28 additions & 27 deletions src/json-crdt/model/Model.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import * as operations from '../../json-crdt-patch/operations';
import * as clock from '../../json-crdt-patch/clock';
import {ConNode} from '../nodes/con/ConNode';
import {encoder, decoder} from '../codec/structural/binary/shared';
import {
ITimestampStruct,
Timestamp,
IClockVector,
ClockVector,
ServerClockVector,
compare,
toDisplayString,
} from '../../json-crdt-patch/clock';
import {JsonCrdtPatchOperation, Patch} from '../../json-crdt-patch/Patch';
import {ModelApi} from './api/ModelApi';
import {ORIGIN, SESSION, SYSTEM_SESSION_TIME} from '../../json-crdt-patch/constants';
Expand Down Expand Up @@ -48,12 +40,12 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
* @param clockOrSessionId Logical clock to use.
* @returns CRDT model.
*/
public static withLogicalClock(clockOrSessionId?: ClockVector | number): Model {
const clock =
public static withLogicalClock(clockOrSessionId?: clock.ClockVector | number): Model {
const clockVector =
typeof clockOrSessionId === 'number'
? new ClockVector(clockOrSessionId, 1)
: clockOrSessionId || new ClockVector(randomSessionId(), 1);
return new Model(clock);
? new clock.ClockVector(clockOrSessionId, 1)
: clockOrSessionId || new clock.ClockVector(randomSessionId(), 1);
return new Model(clockVector);
}

/**
Expand All @@ -67,8 +59,8 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
* @returns CRDT model.
*/
public static withServerClock(time: number = 0): Model {
const clock = new ServerClockVector(SESSION.SERVER, time);
return new Model(clock);
const clockVector = new clock.ServerClockVector(SESSION.SERVER, time);
return new Model(clockVector);
}

/**
Expand All @@ -92,15 +84,15 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
* Clock that keeps track of logical timestamps of the current editing session
* and logical clocks of all known peers.
*/
public clock: IClockVector;
public clock: clock.IClockVector;

/**
* Index of all known node objects (objects, array, strings, values)
* in this document.
*
* @ignore
*/
public index = new AvlMap<ITimestampStruct, JsonNode>(compare);
public index = new AvlMap<clock.ITimestampStruct, JsonNode>(clock.compare);

/**
* Extensions to the JSON CRDT protocol. Extensions are used to implement
Expand All @@ -110,9 +102,9 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
*/
public ext: Extensions = new Extensions();

public constructor(clock: IClockVector) {
this.clock = clock;
if (!clock.time) clock.time = 1;
public constructor(clockVector: clock.IClockVector) {
this.clock = clockVector;
if (!clockVector.time) clockVector.time = 1;
}

/** @ignore */
Expand All @@ -133,6 +125,14 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
return this.api.r.proxy();
}

/**
* Experimental node retrieval API using proxy objects. Returns a strictly
* typed proxy wrapper around the value of the root node.
*/
public get s() {
return this.api.r.proxy().val;
}

/**
* Tracks number of times the `applyPatch` was called.
*
Expand Down Expand Up @@ -243,7 +243,7 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
} else if (op instanceof operations.InsArrOp) {
const node = index.get(op.obj);
if (node instanceof ArrNode) {
const nodes: ITimestampStruct[] = [];
const nodes: clock.ITimestampStruct[] = [];
const data = op.data;
const length = data.length;
for (let i = 0; i < length; i++) {
Expand All @@ -262,7 +262,7 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
for (let i = 0; i < length; i++) {
const span = op.what[i];
for (let j = 0; j < span.span; j++) {
const id = node.getById(new Timestamp(span.sid, span.time + j));
const id = node.getById(new clock.Timestamp(span.sid, span.time + j));
if (id) this.deleteNodeTree(id);
}
}
Expand All @@ -287,7 +287,7 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
*
* @ignore
*/
protected deleteNodeTree(value: ITimestampStruct) {
protected deleteNodeTree(value: clock.ITimestampStruct) {
const isSystemNode = value.sid === SESSION.SYSTEM;
if (isSystemNode) return;
const node = this.index.get(value);
Expand All @@ -307,7 +307,8 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
*/
public fork(sessionId: number = randomSessionId()): Model<N> {
const copy = Model.fromBinary(this.toBinary()) as unknown as Model<N>;
if (copy.clock.sid !== sessionId && copy.clock instanceof ClockVector) copy.clock = copy.clock.fork(sessionId);
if (copy.clock.sid !== sessionId && copy.clock instanceof clock.ClockVector)
copy.clock = copy.clock.fork(sessionId);
copy.ext = this.ext;
return copy;
}
Expand All @@ -325,7 +326,7 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
* Resets the model to equivalent state of another model.
*/
public reset(to: Model<N>): void {
this.index = new AvlMap<ITimestampStruct, JsonNode>(compare);
this.index = new AvlMap<clock.ITimestampStruct, JsonNode>(clock.compare);
const blob = to.toBinary();
decoder.decode(blob, <any>this);
this.clock = to.clock.clone();
Expand Down Expand Up @@ -383,7 +384,7 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
(nodes.length
? printTree(
tab,
nodes.map((node) => (tab) => `${node.name()} ${toDisplayString(node.id)}`),
nodes.map((node) => (tab) => `${node.name()} ${clock.toDisplayString(node.id)}`),
)
: '')
);
Expand Down
Loading

0 comments on commit acdaff9

Please sign in to comment.