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 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
- var OpId = class _OpId {
52
- constructor(counter, peerId) {
53
- this.counter = counter;
54
- this.peerId = peerId;
55
- }
56
- static compare(opIdA, opIdB) {
57
- if (!(opIdA instanceof _OpId)) {
58
- const parsedA = _OpId.tryParseStr(opIdA);
59
- if (!parsedA) throw new Error(`Invalid OpId string: ${opIdA}`);
60
- opIdA = parsedA;
61
- }
62
- if (!(opIdB instanceof _OpId)) {
63
- const parsedB = _OpId.tryParseStr(opIdB);
64
- if (!parsedB) throw new Error(`Invalid OpId string: ${opIdB}`);
65
- opIdB = parsedB;
66
- }
67
- const counterA = opIdA.counter;
68
- const counterB = opIdB.counter;
69
- if (counterA > counterB) {
70
- return 1;
71
- } else if (counterA < counterB) {
72
- return -1;
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
- isGreaterThan(opId) {
93
- return _OpId.compare(this, opId) === 1;
74
+ }
75
+ function equalsOpId(opIdA, opIdB) {
76
+ if (opIdA === opIdB) {
77
+ return true;
78
+ } else if (!opIdA || !opIdB) {
79
+ return false;
94
80
  }
95
- toString() {
96
- return `${this.counter}@${this.peerId}`;
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: new OpId(clock, peerId), targetId, parentId };
111
+ return { id: createOpId(clock, peerId), targetId, parentId };
115
112
  }
116
113
  function newSetVertexPropertyOp(clock, peerId, targetId, key, value) {
117
- return { id: new OpId(clock, peerId), targetId, key, value, transient: false };
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: new OpId(clock, peerId), targetId, key, value, transient: true };
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.toString())) {
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.toString()));
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) => OpId.compare(a.id, b.id));
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.toString()));
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 (!OpId.equals(movesA[i].id, movesB[i].id)) {
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.isGreaterThan(lastOp.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.isGreaterThan(moveOp.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.isGreaterThan(prevOpId)) {
1280
+ if (!prevOpId || isOpIdGreaterThan(op.id, prevOpId)) {
1229
1281
  this.setLLWPropertyAndItsOpId(op);
1230
1282
  } else {
1231
- this.knownOps.add(op.id.toString());
1283
+ this.knownOps.add(opIdToString(op.id));
1232
1284
  }
1233
- if (prevTransientOpId && op.id.isGreaterThan(prevTransientOpId)) {
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.isGreaterThan(prevTransientOpId)) {
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.toString());
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.toString()} failed because the target vertex ${op.targetId} not found`);
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) => OpId.compare(a.id, b.id));
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,