Skip to content

Commit acdaff9

Browse files
authored
Merge pull request #465 from streamich/crdt-schema
CRDT schema builder improvements
2 parents b02305f + 16658ed commit acdaff9

File tree

8 files changed

+283
-87
lines changed

8 files changed

+283
-87
lines changed

src/json-crdt-patch/builder/Tuple.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ export class VectorDelayedValue<T extends unknown[]> {
88
/**
99
* @param slots
1010
* @returns
11+
* @deprecated Use `s.vec(...)` instead.
1112
*/
1213
export const vec = <T extends unknown[]>(...slots: T): VectorDelayedValue<T> => new VectorDelayedValue(slots);

src/json-crdt-patch/builder/schema.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,26 @@ import type {ITimestampStruct} from '../clock';
22
import {NodeBuilder} from './DelayedValueBuilder';
33

44
/* tslint:disable no-namespace class-name */
5+
6+
/**
7+
* This namespace contains all the node builders. Each node builder is a
8+
* schema for a specific node type. Each node builder has a `build` method
9+
* that takes a {@link NodeBuilder} and returns the ID of the node.
10+
*/
511
export namespace nodes {
12+
/**
13+
* The `con` class represents a "con" JSON CRDT node. As the generic type
14+
* parameter, it takes the type of the raw value.
15+
*
16+
* Example:
17+
*
18+
* ```typescript
19+
* s.con(0);
20+
* s.con('');
21+
* s.con<number>(123);
22+
* s.con<0 | 1>(0);
23+
* ```
24+
*/
625
export class con<T extends unknown | ITimestampStruct> extends NodeBuilder {
726
public readonly type = 'con';
827

@@ -11,6 +30,19 @@ export namespace nodes {
1130
}
1231
}
1332

33+
/**
34+
* The `str` class represents a "str" JSON CRDT node. As the generic type
35+
* parameter, it takes the type of the raw value.
36+
*
37+
* Example:
38+
*
39+
* ```typescript
40+
* s.str('');
41+
* s.str('hello');
42+
* s.str<string>('world');
43+
* s.str<'' | 'hello' | 'world'>('hello');
44+
* ```
45+
*/
1446
export class str<T extends string = string> extends NodeBuilder {
1547
public readonly type = 'str';
1648

@@ -19,6 +51,9 @@ export namespace nodes {
1951
}
2052
}
2153

54+
/**
55+
* The `bin` class represents a "bin" JSON CRDT node.
56+
*/
2257
export class bin extends NodeBuilder {
2358
public readonly type = 'bin';
2459

@@ -27,6 +62,18 @@ export namespace nodes {
2762
}
2863
}
2964

65+
/**
66+
* The `val` class represents a "val" JSON CRDT node. As the generic type
67+
* parameter, it takes the type of the inner node builder.
68+
*
69+
* Example:
70+
*
71+
* ```typescript
72+
* s.val(s.con(0));
73+
* s.val(s.str(''));
74+
* s.val(s.str('hello'));
75+
* ```
76+
*/
3077
export class val<T extends NodeBuilder> extends NodeBuilder {
3178
public readonly type = 'val';
3279

@@ -40,6 +87,17 @@ export namespace nodes {
4087
}
4188
}
4289

90+
/**
91+
* The `vec` class represents a "vec" JSON CRDT node. As the generic type
92+
* parameter, it takes a tuple of node builders.
93+
*
94+
* Example:
95+
*
96+
* ```typescript
97+
* s.vec(s.con(0), s.con(1));
98+
* s.vec(s.str(''), s.str('hello'));
99+
* ```
100+
*/
43101
export class vec<T extends NodeBuilder[]> extends NodeBuilder {
44102
public readonly type = 'vec';
45103

@@ -61,6 +119,20 @@ export namespace nodes {
61119
}
62120
}
63121

122+
/**
123+
* The `obj` class represents a "obj" JSON CRDT node. As the generic type
124+
* parameter, it takes a record of node builders. The optional generic type
125+
* parameter is a record of optional keys.
126+
*
127+
* Example:
128+
*
129+
* ```typescript
130+
* s.obj({
131+
* name: s.str(''),
132+
* age: s.con(0),
133+
* });
134+
* ```
135+
*/
64136
export class obj<
65137
T extends Record<string, NodeBuilder>,
66138
O extends Record<string, NodeBuilder> = {},
@@ -91,6 +163,29 @@ export namespace nodes {
91163
}
92164
}
93165

166+
/**
167+
* A type alias for {@link obj}. It creates a "map" node schema, which is an
168+
* object where a key can be any string and the value is of the same type.
169+
*
170+
* Example:
171+
*
172+
* ```typescript
173+
* s.map<nodes.con<number>>
174+
* ```
175+
*/
176+
export type map<R extends NodeBuilder> = obj<Record<string, R>, Record<string, R>>;
177+
178+
/**
179+
* The `arr` class represents a "arr" JSON CRDT node. As the generic type
180+
* parameter, it an array of node builders.
181+
*
182+
* Example:
183+
*
184+
* ```typescript
185+
* s.arr([s.con(0), s.con(1)]);
186+
* s.arr([s.str(''), s.str('hello')]);
187+
* ```
188+
*/
94189
export class arr<T extends NodeBuilder> extends NodeBuilder {
95190
public readonly type = 'arr';
96191

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

208+
/**
209+
* Schema builder. Use this to create a JSON CRDT model schema and the default
210+
* value. Example:
211+
*
212+
* ```typescript
213+
* const schema = s.obj({
214+
* name: s.str(''),
215+
* age: s.con(0),
216+
* tags: s.arr<nodes.con<string>>([]),
217+
* });
218+
* ```
219+
*/
113220
export const schema = {
221+
/**
222+
* Creates a "con" node schema and the default value.
223+
* @param raw Raw default value.
224+
*/
114225
con: <T extends unknown | ITimestampStruct>(raw: T) => new nodes.con<T>(raw),
226+
227+
/**
228+
* Creates a "str" node schema and the default value.
229+
* @param str Default value.
230+
*/
115231
str: <T extends string>(str: T) => new nodes.str<T>(str || ('' as T)),
232+
233+
/**
234+
* Creates a "bin" node schema and the default value.
235+
* @param bin Default value.
236+
*/
116237
bin: (bin: Uint8Array) => new nodes.bin(bin),
238+
239+
/**
240+
* Creates a "val" node schema and the default value.
241+
* @param val Default value.
242+
*/
117243
val: <T extends NodeBuilder>(val: T) => new nodes.val<T>(val),
244+
245+
/**
246+
* Creates a "vec" node schema and the default value.
247+
* @param vec Default value.
248+
*/
118249
vec: <T extends NodeBuilder[]>(...vec: T) => new nodes.vec<T>(vec),
250+
251+
/**
252+
* Creates a "obj" node schema and the default value.
253+
* @param obj Default value, required object keys.
254+
* @param opt Default value of optional object keys.
255+
*/
119256
obj: <T extends Record<string, NodeBuilder>, O extends Record<string, NodeBuilder>>(obj: T, opt?: O) =>
120257
new nodes.obj<T, O>(obj, opt),
258+
259+
/**
260+
* This is an alias for {@link schema.obj}. It creates a "map" node schema,
261+
* which is an object where a key can be any string and the value is of the
262+
* same type.
263+
* @param obj Default value.
264+
*/
265+
map: <R extends NodeBuilder>(obj: Record<string, R>): nodes.map<R> =>
266+
schema.obj<Record<string, R>, Record<string, R>>(obj),
267+
268+
/**
269+
* Creates an "arr" node schema and the default value.
270+
* @param arr Default value.
271+
*/
121272
arr: <T extends NodeBuilder>(arr: T[]) => new nodes.arr<T>(arr),
122273
};
123274

275+
/**
276+
* Schema builder. Use this to create a JSON CRDT model schema and the default
277+
* value. Alias for {@link schema}.
278+
*/
124279
export const s = schema;

src/json-crdt/model/Model.ts

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
import * as operations from '../../json-crdt-patch/operations';
2+
import * as clock from '../../json-crdt-patch/clock';
23
import {ConNode} from '../nodes/con/ConNode';
34
import {encoder, decoder} from '../codec/structural/binary/shared';
4-
import {
5-
ITimestampStruct,
6-
Timestamp,
7-
IClockVector,
8-
ClockVector,
9-
ServerClockVector,
10-
compare,
11-
toDisplayString,
12-
} from '../../json-crdt-patch/clock';
135
import {JsonCrdtPatchOperation, Patch} from '../../json-crdt-patch/Patch';
146
import {ModelApi} from './api/ModelApi';
157
import {ORIGIN, SESSION, SYSTEM_SESSION_TIME} from '../../json-crdt-patch/constants';
@@ -48,12 +40,12 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
4840
* @param clockOrSessionId Logical clock to use.
4941
* @returns CRDT model.
5042
*/
51-
public static withLogicalClock(clockOrSessionId?: ClockVector | number): Model {
52-
const clock =
43+
public static withLogicalClock(clockOrSessionId?: clock.ClockVector | number): Model {
44+
const clockVector =
5345
typeof clockOrSessionId === 'number'
54-
? new ClockVector(clockOrSessionId, 1)
55-
: clockOrSessionId || new ClockVector(randomSessionId(), 1);
56-
return new Model(clock);
46+
? new clock.ClockVector(clockOrSessionId, 1)
47+
: clockOrSessionId || new clock.ClockVector(randomSessionId(), 1);
48+
return new Model(clockVector);
5749
}
5850

5951
/**
@@ -67,8 +59,8 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
6759
* @returns CRDT model.
6860
*/
6961
public static withServerClock(time: number = 0): Model {
70-
const clock = new ServerClockVector(SESSION.SERVER, time);
71-
return new Model(clock);
62+
const clockVector = new clock.ServerClockVector(SESSION.SERVER, time);
63+
return new Model(clockVector);
7264
}
7365

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

9789
/**
9890
* Index of all known node objects (objects, array, strings, values)
9991
* in this document.
10092
*
10193
* @ignore
10294
*/
103-
public index = new AvlMap<ITimestampStruct, JsonNode>(compare);
95+
public index = new AvlMap<clock.ITimestampStruct, JsonNode>(clock.compare);
10496

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

113-
public constructor(clock: IClockVector) {
114-
this.clock = clock;
115-
if (!clock.time) clock.time = 1;
105+
public constructor(clockVector: clock.IClockVector) {
106+
this.clock = clockVector;
107+
if (!clockVector.time) clockVector.time = 1;
116108
}
117109

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

128+
/**
129+
* Experimental node retrieval API using proxy objects. Returns a strictly
130+
* typed proxy wrapper around the value of the root node.
131+
*/
132+
public get s() {
133+
return this.api.r.proxy().val;
134+
}
135+
136136
/**
137137
* Tracks number of times the `applyPatch` was called.
138138
*
@@ -243,7 +243,7 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
243243
} else if (op instanceof operations.InsArrOp) {
244244
const node = index.get(op.obj);
245245
if (node instanceof ArrNode) {
246-
const nodes: ITimestampStruct[] = [];
246+
const nodes: clock.ITimestampStruct[] = [];
247247
const data = op.data;
248248
const length = data.length;
249249
for (let i = 0; i < length; i++) {
@@ -262,7 +262,7 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
262262
for (let i = 0; i < length; i++) {
263263
const span = op.what[i];
264264
for (let j = 0; j < span.span; j++) {
265-
const id = node.getById(new Timestamp(span.sid, span.time + j));
265+
const id = node.getById(new clock.Timestamp(span.sid, span.time + j));
266266
if (id) this.deleteNodeTree(id);
267267
}
268268
}
@@ -287,7 +287,7 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
287287
*
288288
* @ignore
289289
*/
290-
protected deleteNodeTree(value: ITimestampStruct) {
290+
protected deleteNodeTree(value: clock.ITimestampStruct) {
291291
const isSystemNode = value.sid === SESSION.SYSTEM;
292292
if (isSystemNode) return;
293293
const node = this.index.get(value);
@@ -307,7 +307,8 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
307307
*/
308308
public fork(sessionId: number = randomSessionId()): Model<N> {
309309
const copy = Model.fromBinary(this.toBinary()) as unknown as Model<N>;
310-
if (copy.clock.sid !== sessionId && copy.clock instanceof ClockVector) copy.clock = copy.clock.fork(sessionId);
310+
if (copy.clock.sid !== sessionId && copy.clock instanceof clock.ClockVector)
311+
copy.clock = copy.clock.fork(sessionId);
311312
copy.ext = this.ext;
312313
return copy;
313314
}
@@ -325,7 +326,7 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
325326
* Resets the model to equivalent state of another model.
326327
*/
327328
public reset(to: Model<N>): void {
328-
this.index = new AvlMap<ITimestampStruct, JsonNode>(compare);
329+
this.index = new AvlMap<clock.ITimestampStruct, JsonNode>(clock.compare);
329330
const blob = to.toBinary();
330331
decoder.decode(blob, <any>this);
331332
this.clock = to.clock.clone();
@@ -383,7 +384,7 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
383384
(nodes.length
384385
? printTree(
385386
tab,
386-
nodes.map((node) => (tab) => `${node.name()} ${toDisplayString(node.id)}`),
387+
nodes.map((node) => (tab) => `${node.name()} ${clock.toDisplayString(node.id)}`),
387388
)
388389
: '')
389390
);

0 commit comments

Comments
 (0)