reptree 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/index.cjs +127 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -8
- package/dist/index.d.ts +36 -8
- package/dist/index.js +126 -63
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -23,6 +23,30 @@ npm install reptree
|
|
|
23
23
|
|
|
24
24
|
## Usage
|
|
25
25
|
|
|
26
|
+
### Reactive vertex with Zod (optional)
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { RepTree, bindVertex } from 'reptree';
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
|
|
32
|
+
const tree = new RepTree('peer1');
|
|
33
|
+
const root = tree.createRoot();
|
|
34
|
+
const v = root.newChild();
|
|
35
|
+
|
|
36
|
+
const Person = z.object({ name: z.string(), age: z.number().int().min(0) });
|
|
37
|
+
|
|
38
|
+
// Helper function form
|
|
39
|
+
const person = bindVertex(tree, v.id, Person);
|
|
40
|
+
|
|
41
|
+
// Or via instance method on Vertex
|
|
42
|
+
// const person = v.bind(Person);
|
|
43
|
+
|
|
44
|
+
person.name = 'Alice'; // validated and persisted
|
|
45
|
+
person.age = 33; // validated and persisted
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For more, see `docs/reactive-vertices.md`.
|
|
49
|
+
|
|
26
50
|
```typescript
|
|
27
51
|
import { RepTree } from 'reptree';
|
|
28
52
|
|
package/dist/index.cjs
CHANGED
|
@@ -30,12 +30,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
OpId: () => OpId,
|
|
34
33
|
RepTree: () => RepTree,
|
|
35
34
|
StateVector: () => StateVector,
|
|
36
35
|
TreeState: () => TreeState,
|
|
37
36
|
Vertex: () => Vertex,
|
|
38
37
|
VertexState: () => VertexState,
|
|
38
|
+
bindVertex: () => bindVertex,
|
|
39
39
|
isAnyPropertyOp: () => isAnyPropertyOp,
|
|
40
40
|
isLWWPropertyOp: () => isLWWPropertyOp,
|
|
41
41
|
isModifyPropertyOp: () => isModifyPropertyOp,
|
|
@@ -48,54 +48,51 @@ __export(index_exports, {
|
|
|
48
48
|
module.exports = __toCommonJS(index_exports);
|
|
49
49
|
|
|
50
50
|
// src/OpId.ts
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
} else {
|
|
74
|
-
return opIdA.peerId.localeCompare(opIdB.peerId);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
static equals(opIdA, opIdB) {
|
|
78
|
-
if (opIdA === opIdB) {
|
|
79
|
-
return true;
|
|
80
|
-
} else if (!opIdA || !opIdB) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
return _OpId.compare(opIdA, opIdB) === 0;
|
|
84
|
-
}
|
|
85
|
-
static tryParseStr(opIdStr) {
|
|
86
|
-
const parts = opIdStr.split("@");
|
|
87
|
-
if (parts.length !== 2) {
|
|
88
|
-
throw new Error(`Invalid OpId string: ${opIdStr}`);
|
|
89
|
-
}
|
|
90
|
-
return new _OpId(parseInt(parts[0], 10), parts[1]);
|
|
51
|
+
function createOpId(counter, peerId) {
|
|
52
|
+
return { counter, peerId };
|
|
53
|
+
}
|
|
54
|
+
function compareOpId(opIdA, opIdB) {
|
|
55
|
+
if (typeof opIdA === "string") {
|
|
56
|
+
const parsedA = tryParseOpIdStr(opIdA);
|
|
57
|
+
if (!parsedA) throw new Error(`Invalid OpId string: ${opIdA}`);
|
|
58
|
+
opIdA = parsedA;
|
|
59
|
+
}
|
|
60
|
+
if (typeof opIdB === "string") {
|
|
61
|
+
const parsedB = tryParseOpIdStr(opIdB);
|
|
62
|
+
if (!parsedB) throw new Error(`Invalid OpId string: ${opIdB}`);
|
|
63
|
+
opIdB = parsedB;
|
|
64
|
+
}
|
|
65
|
+
const counterA = opIdA.counter;
|
|
66
|
+
const counterB = opIdB.counter;
|
|
67
|
+
if (counterA > counterB) {
|
|
68
|
+
return 1;
|
|
69
|
+
} else if (counterA < counterB) {
|
|
70
|
+
return -1;
|
|
71
|
+
} else {
|
|
72
|
+
return opIdA.peerId.localeCompare(opIdB.peerId);
|
|
91
73
|
}
|
|
92
|
-
|
|
93
|
-
|
|
74
|
+
}
|
|
75
|
+
function equalsOpId(opIdA, opIdB) {
|
|
76
|
+
if (opIdA === opIdB) {
|
|
77
|
+
return true;
|
|
78
|
+
} else if (!opIdA || !opIdB) {
|
|
79
|
+
return false;
|
|
94
80
|
}
|
|
95
|
-
|
|
96
|
-
|
|
81
|
+
return compareOpId(opIdA, opIdB) === 0;
|
|
82
|
+
}
|
|
83
|
+
function tryParseOpIdStr(opIdStr) {
|
|
84
|
+
const parts = opIdStr.split("@");
|
|
85
|
+
if (parts.length !== 2) {
|
|
86
|
+
throw new Error(`Invalid OpId string: ${opIdStr}`);
|
|
97
87
|
}
|
|
98
|
-
|
|
88
|
+
return createOpId(parseInt(parts[0], 10), parts[1]);
|
|
89
|
+
}
|
|
90
|
+
function isOpIdGreaterThan(opIdA, opIdB) {
|
|
91
|
+
return compareOpId(opIdA, opIdB) === 1;
|
|
92
|
+
}
|
|
93
|
+
function opIdToString(opId) {
|
|
94
|
+
return `${opId.counter}@${opId.peerId}`;
|
|
95
|
+
}
|
|
99
96
|
|
|
100
97
|
// src/operations.ts
|
|
101
98
|
function isMoveVertexOp(op) {
|
|
@@ -111,13 +108,13 @@ function isModifyPropertyOp(op) {
|
|
|
111
108
|
return "key" in op && "value" in op && typeof op.value === "object" && op.value !== null && "type" in op.value;
|
|
112
109
|
}
|
|
113
110
|
function newMoveVertexOp(clock, peerId, targetId, parentId) {
|
|
114
|
-
return { id:
|
|
111
|
+
return { id: createOpId(clock, peerId), targetId, parentId };
|
|
115
112
|
}
|
|
116
113
|
function newSetVertexPropertyOp(clock, peerId, targetId, key, value) {
|
|
117
|
-
return { id:
|
|
114
|
+
return { id: createOpId(clock, peerId), targetId, key, value, transient: false };
|
|
118
115
|
}
|
|
119
116
|
function newSetTransientVertexPropertyOp(clock, peerId, targetId, key, value) {
|
|
120
|
-
return { id:
|
|
117
|
+
return { id: createOpId(clock, peerId), targetId, key, value, transient: true };
|
|
121
118
|
}
|
|
122
119
|
|
|
123
120
|
// src/VertexState.ts
|
|
@@ -406,6 +403,57 @@ function uuid() {
|
|
|
406
403
|
return removeDashes(crypto.randomUUID());
|
|
407
404
|
}
|
|
408
405
|
|
|
406
|
+
// src/reactive.ts
|
|
407
|
+
function toObject(tree, id) {
|
|
408
|
+
const obj = {};
|
|
409
|
+
for (const { key, value } of tree.getVertexProperties(id)) obj[key] = value;
|
|
410
|
+
return obj;
|
|
411
|
+
}
|
|
412
|
+
function bindVertex(tree, id, schema) {
|
|
413
|
+
return new Proxy({}, {
|
|
414
|
+
get(_target, prop) {
|
|
415
|
+
if (typeof prop !== "string") return void 0;
|
|
416
|
+
return tree.getVertexProperty(id, prop);
|
|
417
|
+
},
|
|
418
|
+
set(_target, prop, value) {
|
|
419
|
+
if (typeof prop !== "string") return true;
|
|
420
|
+
if (schema?.shape && schema.shape[prop]) {
|
|
421
|
+
const field = schema.shape[prop];
|
|
422
|
+
if (field.safeParse) {
|
|
423
|
+
const res = field.safeParse(value);
|
|
424
|
+
if (!res.success) throw new Error(`Invalid value for ${String(prop)}`);
|
|
425
|
+
value = res.data ?? value;
|
|
426
|
+
}
|
|
427
|
+
} else if (schema?.safeParse) {
|
|
428
|
+
const next = { ...toObject(tree, id), [prop]: value };
|
|
429
|
+
const res = schema.safeParse(next);
|
|
430
|
+
if (!res.success) throw new Error(`Invalid value for ${String(prop)}`);
|
|
431
|
+
const parsed = res.data;
|
|
432
|
+
if (parsed && Object.prototype.hasOwnProperty.call(parsed, prop)) {
|
|
433
|
+
value = parsed[prop];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
tree.setVertexProperty(id, prop, value);
|
|
437
|
+
return true;
|
|
438
|
+
},
|
|
439
|
+
deleteProperty(_target, prop) {
|
|
440
|
+
if (typeof prop !== "string") return true;
|
|
441
|
+
tree.setVertexProperty(id, prop, void 0);
|
|
442
|
+
return true;
|
|
443
|
+
},
|
|
444
|
+
has(_target, prop) {
|
|
445
|
+
if (typeof prop !== "string") return false;
|
|
446
|
+
return !!schema?.shape && Object.prototype.hasOwnProperty.call(schema.shape, prop);
|
|
447
|
+
},
|
|
448
|
+
ownKeys() {
|
|
449
|
+
return Object.keys(schema?.shape ?? {});
|
|
450
|
+
},
|
|
451
|
+
getOwnPropertyDescriptor() {
|
|
452
|
+
return { enumerable: true, configurable: true };
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
409
457
|
// src/Vertex.ts
|
|
410
458
|
var Vertex = class {
|
|
411
459
|
constructor(tree, state) {
|
|
@@ -524,6 +572,10 @@ var Vertex = class {
|
|
|
524
572
|
moveTo(parent) {
|
|
525
573
|
this.tree.moveVertex(this.id, parent.id);
|
|
526
574
|
}
|
|
575
|
+
/** Returns a live reactive object bound to this vertex. Optional schema validates writes. */
|
|
576
|
+
bind(schema) {
|
|
577
|
+
return bindVertex(this.tree, this.id, schema);
|
|
578
|
+
}
|
|
527
579
|
};
|
|
528
580
|
|
|
529
581
|
// src/StateVector.ts
|
|
@@ -949,7 +1001,7 @@ var _RepTree = class _RepTree {
|
|
|
949
1001
|
}
|
|
950
1002
|
applyOps(ops) {
|
|
951
1003
|
for (const op of ops) {
|
|
952
|
-
if (this.knownOps.has(op.id
|
|
1004
|
+
if (this.knownOps.has(opIdToString(op.id))) {
|
|
953
1005
|
continue;
|
|
954
1006
|
}
|
|
955
1007
|
this.applyOperation(op);
|
|
@@ -957,16 +1009,16 @@ var _RepTree = class _RepTree {
|
|
|
957
1009
|
}
|
|
958
1010
|
/** Applies operations in an optimized way, sorting move ops by OpId to avoid undo-do-redo cycles */
|
|
959
1011
|
applyOpsOptimizedForLotsOfMoves(ops) {
|
|
960
|
-
const newMoveOps = ops.filter((op) => isMoveVertexOp(op) && !this.knownOps.has(op.id
|
|
1012
|
+
const newMoveOps = ops.filter((op) => isMoveVertexOp(op) && !this.knownOps.has(opIdToString(op.id)));
|
|
961
1013
|
if (newMoveOps.length > 0) {
|
|
962
1014
|
const allMoveOps = [...this.moveOps, ...newMoveOps];
|
|
963
|
-
allMoveOps.sort((a, b) =>
|
|
1015
|
+
allMoveOps.sort((a, b) => compareOpId(a.id, b.id));
|
|
964
1016
|
for (let i = 0, len = allMoveOps.length; i < len; i++) {
|
|
965
1017
|
const op = allMoveOps[i];
|
|
966
1018
|
this.applyMove(op);
|
|
967
1019
|
}
|
|
968
1020
|
}
|
|
969
|
-
const propertyOps = ops.filter((op) => isAnyPropertyOp(op) && !this.knownOps.has(op.id
|
|
1021
|
+
const propertyOps = ops.filter((op) => isAnyPropertyOp(op) && !this.knownOps.has(opIdToString(op.id)));
|
|
970
1022
|
for (let i = 0, len = propertyOps.length; i < len; i++) {
|
|
971
1023
|
const op = propertyOps[i];
|
|
972
1024
|
this.applyProperty(op);
|
|
@@ -988,7 +1040,7 @@ var _RepTree = class _RepTree {
|
|
|
988
1040
|
return false;
|
|
989
1041
|
}
|
|
990
1042
|
for (let i = 0; i < movesA.length; i++) {
|
|
991
|
-
if (!
|
|
1043
|
+
if (!equalsOpId(movesA[i].id, movesB[i].id)) {
|
|
992
1044
|
return false;
|
|
993
1045
|
}
|
|
994
1046
|
}
|
|
@@ -1133,7 +1185,7 @@ var _RepTree = class _RepTree {
|
|
|
1133
1185
|
}
|
|
1134
1186
|
this.updateLamportClock(op);
|
|
1135
1187
|
const lastOp = this.moveOps.length > 0 ? this.moveOps[this.moveOps.length - 1] : null;
|
|
1136
|
-
if (lastOp === null || op.id
|
|
1188
|
+
if (lastOp === null || isOpIdGreaterThan(op.id, lastOp.id)) {
|
|
1137
1189
|
this.moveOps.push(op);
|
|
1138
1190
|
this.reportOpAsApplied(op);
|
|
1139
1191
|
this.tryToMove(op);
|
|
@@ -1142,7 +1194,7 @@ var _RepTree = class _RepTree {
|
|
|
1142
1194
|
for (let i = this.moveOps.length - 1; i >= 0; i--) {
|
|
1143
1195
|
const moveOp = this.moveOps[i];
|
|
1144
1196
|
targetIndex = i;
|
|
1145
|
-
if (op.id
|
|
1197
|
+
if (isOpIdGreaterThan(op.id, moveOp.id)) {
|
|
1146
1198
|
break;
|
|
1147
1199
|
} else {
|
|
1148
1200
|
this.undoMove(moveOp);
|
|
@@ -1225,17 +1277,17 @@ var _RepTree = class _RepTree {
|
|
|
1225
1277
|
const prevOpId = this.propertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
|
|
1226
1278
|
if (!op.transient) {
|
|
1227
1279
|
this.setPropertyOps.push(op);
|
|
1228
|
-
if (!prevOpId || op.id
|
|
1280
|
+
if (!prevOpId || isOpIdGreaterThan(op.id, prevOpId)) {
|
|
1229
1281
|
this.setLLWPropertyAndItsOpId(op);
|
|
1230
1282
|
} else {
|
|
1231
|
-
this.knownOps.add(op.id
|
|
1283
|
+
this.knownOps.add(opIdToString(op.id));
|
|
1232
1284
|
}
|
|
1233
|
-
if (prevTransientOpId && op.id
|
|
1285
|
+
if (prevTransientOpId && isOpIdGreaterThan(op.id, prevTransientOpId)) {
|
|
1234
1286
|
this.transientPropertiesAndTheirOpIds.delete(`${op.key}@${op.targetId}`);
|
|
1235
1287
|
targetVertex.removeTransientProperty(op.key);
|
|
1236
1288
|
}
|
|
1237
1289
|
} else {
|
|
1238
|
-
if (!prevTransientOpId || op.id
|
|
1290
|
+
if (!prevTransientOpId || isOpIdGreaterThan(op.id, prevTransientOpId)) {
|
|
1239
1291
|
this.setTransientPropertyAndItsOpId(op);
|
|
1240
1292
|
}
|
|
1241
1293
|
}
|
|
@@ -1270,7 +1322,7 @@ var _RepTree = class _RepTree {
|
|
|
1270
1322
|
}
|
|
1271
1323
|
}
|
|
1272
1324
|
reportOpAsApplied(op) {
|
|
1273
|
-
this.knownOps.add(op.id
|
|
1325
|
+
this.knownOps.add(opIdToString(op.id));
|
|
1274
1326
|
if (this._stateVectorEnabled) {
|
|
1275
1327
|
this.stateVector.updateFromOp(op);
|
|
1276
1328
|
}
|
|
@@ -1297,7 +1349,7 @@ var _RepTree = class _RepTree {
|
|
|
1297
1349
|
undoMove(op) {
|
|
1298
1350
|
const targetVertex = this.state.getVertex(op.targetId);
|
|
1299
1351
|
if (!targetVertex) {
|
|
1300
|
-
console.error(`An attempt to undo move operation ${op.id
|
|
1352
|
+
console.error(`An attempt to undo move operation ${opIdToString(op.id)} failed because the target vertex ${op.targetId} not found`);
|
|
1301
1353
|
return;
|
|
1302
1354
|
}
|
|
1303
1355
|
const prevParentId = this.parentIdBeforeMove.get(op.id);
|
|
@@ -1340,7 +1392,7 @@ var _RepTree = class _RepTree {
|
|
|
1340
1392
|
}
|
|
1341
1393
|
}
|
|
1342
1394
|
}
|
|
1343
|
-
missingOps.sort((a, b) =>
|
|
1395
|
+
missingOps.sort((a, b) => compareOpId(a.id, b.id));
|
|
1344
1396
|
return missingOps;
|
|
1345
1397
|
}
|
|
1346
1398
|
/**
|
|
@@ -1363,17 +1415,28 @@ var _RepTree = class _RepTree {
|
|
|
1363
1415
|
this.stateVector = new StateVector();
|
|
1364
1416
|
}
|
|
1365
1417
|
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Parses the vertex properties with a provided schema that has a `parse` method (e.g., Zod schema)
|
|
1420
|
+
*/
|
|
1421
|
+
parseVertex(vertexId, schema) {
|
|
1422
|
+
const propsArray = this.getVertexProperties(vertexId);
|
|
1423
|
+
const propsObject = {};
|
|
1424
|
+
for (const { key, value } of propsArray) {
|
|
1425
|
+
propsObject[key] = value;
|
|
1426
|
+
}
|
|
1427
|
+
return schema.parse(propsObject);
|
|
1428
|
+
}
|
|
1366
1429
|
};
|
|
1367
1430
|
_RepTree.NULL_VERTEX_ID = "0";
|
|
1368
1431
|
var RepTree = _RepTree;
|
|
1369
1432
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1370
1433
|
0 && (module.exports = {
|
|
1371
|
-
OpId,
|
|
1372
1434
|
RepTree,
|
|
1373
1435
|
StateVector,
|
|
1374
1436
|
TreeState,
|
|
1375
1437
|
Vertex,
|
|
1376
1438
|
VertexState,
|
|
1439
|
+
bindVertex,
|
|
1377
1440
|
isAnyPropertyOp,
|
|
1378
1441
|
isLWWPropertyOp,
|
|
1379
1442
|
isModifyPropertyOp,
|