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/dist/index.js
CHANGED
|
@@ -1,52 +1,49 @@
|
|
|
1
1
|
// src/OpId.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (!(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (!(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} else {
|
|
25
|
-
return opIdA.peerId.localeCompare(opIdB.peerId);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
static equals(opIdA, opIdB) {
|
|
29
|
-
if (opIdA === opIdB) {
|
|
30
|
-
return true;
|
|
31
|
-
} else if (!opIdA || !opIdB) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
return _OpId.compare(opIdA, opIdB) === 0;
|
|
35
|
-
}
|
|
36
|
-
static tryParseStr(opIdStr) {
|
|
37
|
-
const parts = opIdStr.split("@");
|
|
38
|
-
if (parts.length !== 2) {
|
|
39
|
-
throw new Error(`Invalid OpId string: ${opIdStr}`);
|
|
40
|
-
}
|
|
41
|
-
return new _OpId(parseInt(parts[0], 10), parts[1]);
|
|
2
|
+
function createOpId(counter, peerId) {
|
|
3
|
+
return { counter, peerId };
|
|
4
|
+
}
|
|
5
|
+
function compareOpId(opIdA, opIdB) {
|
|
6
|
+
if (typeof opIdA === "string") {
|
|
7
|
+
const parsedA = tryParseOpIdStr(opIdA);
|
|
8
|
+
if (!parsedA) throw new Error(`Invalid OpId string: ${opIdA}`);
|
|
9
|
+
opIdA = parsedA;
|
|
10
|
+
}
|
|
11
|
+
if (typeof opIdB === "string") {
|
|
12
|
+
const parsedB = tryParseOpIdStr(opIdB);
|
|
13
|
+
if (!parsedB) throw new Error(`Invalid OpId string: ${opIdB}`);
|
|
14
|
+
opIdB = parsedB;
|
|
15
|
+
}
|
|
16
|
+
const counterA = opIdA.counter;
|
|
17
|
+
const counterB = opIdB.counter;
|
|
18
|
+
if (counterA > counterB) {
|
|
19
|
+
return 1;
|
|
20
|
+
} else if (counterA < counterB) {
|
|
21
|
+
return -1;
|
|
22
|
+
} else {
|
|
23
|
+
return opIdA.peerId.localeCompare(opIdB.peerId);
|
|
42
24
|
}
|
|
43
|
-
|
|
44
|
-
|
|
25
|
+
}
|
|
26
|
+
function equalsOpId(opIdA, opIdB) {
|
|
27
|
+
if (opIdA === opIdB) {
|
|
28
|
+
return true;
|
|
29
|
+
} else if (!opIdA || !opIdB) {
|
|
30
|
+
return false;
|
|
45
31
|
}
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
return compareOpId(opIdA, opIdB) === 0;
|
|
33
|
+
}
|
|
34
|
+
function tryParseOpIdStr(opIdStr) {
|
|
35
|
+
const parts = opIdStr.split("@");
|
|
36
|
+
if (parts.length !== 2) {
|
|
37
|
+
throw new Error(`Invalid OpId string: ${opIdStr}`);
|
|
48
38
|
}
|
|
49
|
-
|
|
39
|
+
return createOpId(parseInt(parts[0], 10), parts[1]);
|
|
40
|
+
}
|
|
41
|
+
function isOpIdGreaterThan(opIdA, opIdB) {
|
|
42
|
+
return compareOpId(opIdA, opIdB) === 1;
|
|
43
|
+
}
|
|
44
|
+
function opIdToString(opId) {
|
|
45
|
+
return `${opId.counter}@${opId.peerId}`;
|
|
46
|
+
}
|
|
50
47
|
|
|
51
48
|
// src/operations.ts
|
|
52
49
|
function isMoveVertexOp(op) {
|
|
@@ -62,13 +59,13 @@ function isModifyPropertyOp(op) {
|
|
|
62
59
|
return "key" in op && "value" in op && typeof op.value === "object" && op.value !== null && "type" in op.value;
|
|
63
60
|
}
|
|
64
61
|
function newMoveVertexOp(clock, peerId, targetId, parentId) {
|
|
65
|
-
return { id:
|
|
62
|
+
return { id: createOpId(clock, peerId), targetId, parentId };
|
|
66
63
|
}
|
|
67
64
|
function newSetVertexPropertyOp(clock, peerId, targetId, key, value) {
|
|
68
|
-
return { id:
|
|
65
|
+
return { id: createOpId(clock, peerId), targetId, key, value, transient: false };
|
|
69
66
|
}
|
|
70
67
|
function newSetTransientVertexPropertyOp(clock, peerId, targetId, key, value) {
|
|
71
|
-
return { id:
|
|
68
|
+
return { id: createOpId(clock, peerId), targetId, key, value, transient: true };
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
// src/VertexState.ts
|
|
@@ -357,6 +354,57 @@ function uuid() {
|
|
|
357
354
|
return removeDashes(crypto.randomUUID());
|
|
358
355
|
}
|
|
359
356
|
|
|
357
|
+
// src/reactive.ts
|
|
358
|
+
function toObject(tree, id) {
|
|
359
|
+
const obj = {};
|
|
360
|
+
for (const { key, value } of tree.getVertexProperties(id)) obj[key] = value;
|
|
361
|
+
return obj;
|
|
362
|
+
}
|
|
363
|
+
function bindVertex(tree, id, schema) {
|
|
364
|
+
return new Proxy({}, {
|
|
365
|
+
get(_target, prop) {
|
|
366
|
+
if (typeof prop !== "string") return void 0;
|
|
367
|
+
return tree.getVertexProperty(id, prop);
|
|
368
|
+
},
|
|
369
|
+
set(_target, prop, value) {
|
|
370
|
+
if (typeof prop !== "string") return true;
|
|
371
|
+
if (schema?.shape && schema.shape[prop]) {
|
|
372
|
+
const field = schema.shape[prop];
|
|
373
|
+
if (field.safeParse) {
|
|
374
|
+
const res = field.safeParse(value);
|
|
375
|
+
if (!res.success) throw new Error(`Invalid value for ${String(prop)}`);
|
|
376
|
+
value = res.data ?? value;
|
|
377
|
+
}
|
|
378
|
+
} else if (schema?.safeParse) {
|
|
379
|
+
const next = { ...toObject(tree, id), [prop]: value };
|
|
380
|
+
const res = schema.safeParse(next);
|
|
381
|
+
if (!res.success) throw new Error(`Invalid value for ${String(prop)}`);
|
|
382
|
+
const parsed = res.data;
|
|
383
|
+
if (parsed && Object.prototype.hasOwnProperty.call(parsed, prop)) {
|
|
384
|
+
value = parsed[prop];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
tree.setVertexProperty(id, prop, value);
|
|
388
|
+
return true;
|
|
389
|
+
},
|
|
390
|
+
deleteProperty(_target, prop) {
|
|
391
|
+
if (typeof prop !== "string") return true;
|
|
392
|
+
tree.setVertexProperty(id, prop, void 0);
|
|
393
|
+
return true;
|
|
394
|
+
},
|
|
395
|
+
has(_target, prop) {
|
|
396
|
+
if (typeof prop !== "string") return false;
|
|
397
|
+
return !!schema?.shape && Object.prototype.hasOwnProperty.call(schema.shape, prop);
|
|
398
|
+
},
|
|
399
|
+
ownKeys() {
|
|
400
|
+
return Object.keys(schema?.shape ?? {});
|
|
401
|
+
},
|
|
402
|
+
getOwnPropertyDescriptor() {
|
|
403
|
+
return { enumerable: true, configurable: true };
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
360
408
|
// src/Vertex.ts
|
|
361
409
|
var Vertex = class {
|
|
362
410
|
constructor(tree, state) {
|
|
@@ -475,6 +523,10 @@ var Vertex = class {
|
|
|
475
523
|
moveTo(parent) {
|
|
476
524
|
this.tree.moveVertex(this.id, parent.id);
|
|
477
525
|
}
|
|
526
|
+
/** Returns a live reactive object bound to this vertex. Optional schema validates writes. */
|
|
527
|
+
bind(schema) {
|
|
528
|
+
return bindVertex(this.tree, this.id, schema);
|
|
529
|
+
}
|
|
478
530
|
};
|
|
479
531
|
|
|
480
532
|
// src/StateVector.ts
|
|
@@ -900,7 +952,7 @@ var _RepTree = class _RepTree {
|
|
|
900
952
|
}
|
|
901
953
|
applyOps(ops) {
|
|
902
954
|
for (const op of ops) {
|
|
903
|
-
if (this.knownOps.has(op.id
|
|
955
|
+
if (this.knownOps.has(opIdToString(op.id))) {
|
|
904
956
|
continue;
|
|
905
957
|
}
|
|
906
958
|
this.applyOperation(op);
|
|
@@ -908,16 +960,16 @@ var _RepTree = class _RepTree {
|
|
|
908
960
|
}
|
|
909
961
|
/** Applies operations in an optimized way, sorting move ops by OpId to avoid undo-do-redo cycles */
|
|
910
962
|
applyOpsOptimizedForLotsOfMoves(ops) {
|
|
911
|
-
const newMoveOps = ops.filter((op) => isMoveVertexOp(op) && !this.knownOps.has(op.id
|
|
963
|
+
const newMoveOps = ops.filter((op) => isMoveVertexOp(op) && !this.knownOps.has(opIdToString(op.id)));
|
|
912
964
|
if (newMoveOps.length > 0) {
|
|
913
965
|
const allMoveOps = [...this.moveOps, ...newMoveOps];
|
|
914
|
-
allMoveOps.sort((a, b) =>
|
|
966
|
+
allMoveOps.sort((a, b) => compareOpId(a.id, b.id));
|
|
915
967
|
for (let i = 0, len = allMoveOps.length; i < len; i++) {
|
|
916
968
|
const op = allMoveOps[i];
|
|
917
969
|
this.applyMove(op);
|
|
918
970
|
}
|
|
919
971
|
}
|
|
920
|
-
const propertyOps = ops.filter((op) => isAnyPropertyOp(op) && !this.knownOps.has(op.id
|
|
972
|
+
const propertyOps = ops.filter((op) => isAnyPropertyOp(op) && !this.knownOps.has(opIdToString(op.id)));
|
|
921
973
|
for (let i = 0, len = propertyOps.length; i < len; i++) {
|
|
922
974
|
const op = propertyOps[i];
|
|
923
975
|
this.applyProperty(op);
|
|
@@ -939,7 +991,7 @@ var _RepTree = class _RepTree {
|
|
|
939
991
|
return false;
|
|
940
992
|
}
|
|
941
993
|
for (let i = 0; i < movesA.length; i++) {
|
|
942
|
-
if (!
|
|
994
|
+
if (!equalsOpId(movesA[i].id, movesB[i].id)) {
|
|
943
995
|
return false;
|
|
944
996
|
}
|
|
945
997
|
}
|
|
@@ -1084,7 +1136,7 @@ var _RepTree = class _RepTree {
|
|
|
1084
1136
|
}
|
|
1085
1137
|
this.updateLamportClock(op);
|
|
1086
1138
|
const lastOp = this.moveOps.length > 0 ? this.moveOps[this.moveOps.length - 1] : null;
|
|
1087
|
-
if (lastOp === null || op.id
|
|
1139
|
+
if (lastOp === null || isOpIdGreaterThan(op.id, lastOp.id)) {
|
|
1088
1140
|
this.moveOps.push(op);
|
|
1089
1141
|
this.reportOpAsApplied(op);
|
|
1090
1142
|
this.tryToMove(op);
|
|
@@ -1093,7 +1145,7 @@ var _RepTree = class _RepTree {
|
|
|
1093
1145
|
for (let i = this.moveOps.length - 1; i >= 0; i--) {
|
|
1094
1146
|
const moveOp = this.moveOps[i];
|
|
1095
1147
|
targetIndex = i;
|
|
1096
|
-
if (op.id
|
|
1148
|
+
if (isOpIdGreaterThan(op.id, moveOp.id)) {
|
|
1097
1149
|
break;
|
|
1098
1150
|
} else {
|
|
1099
1151
|
this.undoMove(moveOp);
|
|
@@ -1176,17 +1228,17 @@ var _RepTree = class _RepTree {
|
|
|
1176
1228
|
const prevOpId = this.propertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
|
|
1177
1229
|
if (!op.transient) {
|
|
1178
1230
|
this.setPropertyOps.push(op);
|
|
1179
|
-
if (!prevOpId || op.id
|
|
1231
|
+
if (!prevOpId || isOpIdGreaterThan(op.id, prevOpId)) {
|
|
1180
1232
|
this.setLLWPropertyAndItsOpId(op);
|
|
1181
1233
|
} else {
|
|
1182
|
-
this.knownOps.add(op.id
|
|
1234
|
+
this.knownOps.add(opIdToString(op.id));
|
|
1183
1235
|
}
|
|
1184
|
-
if (prevTransientOpId && op.id
|
|
1236
|
+
if (prevTransientOpId && isOpIdGreaterThan(op.id, prevTransientOpId)) {
|
|
1185
1237
|
this.transientPropertiesAndTheirOpIds.delete(`${op.key}@${op.targetId}`);
|
|
1186
1238
|
targetVertex.removeTransientProperty(op.key);
|
|
1187
1239
|
}
|
|
1188
1240
|
} else {
|
|
1189
|
-
if (!prevTransientOpId || op.id
|
|
1241
|
+
if (!prevTransientOpId || isOpIdGreaterThan(op.id, prevTransientOpId)) {
|
|
1190
1242
|
this.setTransientPropertyAndItsOpId(op);
|
|
1191
1243
|
}
|
|
1192
1244
|
}
|
|
@@ -1221,7 +1273,7 @@ var _RepTree = class _RepTree {
|
|
|
1221
1273
|
}
|
|
1222
1274
|
}
|
|
1223
1275
|
reportOpAsApplied(op) {
|
|
1224
|
-
this.knownOps.add(op.id
|
|
1276
|
+
this.knownOps.add(opIdToString(op.id));
|
|
1225
1277
|
if (this._stateVectorEnabled) {
|
|
1226
1278
|
this.stateVector.updateFromOp(op);
|
|
1227
1279
|
}
|
|
@@ -1248,7 +1300,7 @@ var _RepTree = class _RepTree {
|
|
|
1248
1300
|
undoMove(op) {
|
|
1249
1301
|
const targetVertex = this.state.getVertex(op.targetId);
|
|
1250
1302
|
if (!targetVertex) {
|
|
1251
|
-
console.error(`An attempt to undo move operation ${op.id
|
|
1303
|
+
console.error(`An attempt to undo move operation ${opIdToString(op.id)} failed because the target vertex ${op.targetId} not found`);
|
|
1252
1304
|
return;
|
|
1253
1305
|
}
|
|
1254
1306
|
const prevParentId = this.parentIdBeforeMove.get(op.id);
|
|
@@ -1291,7 +1343,7 @@ var _RepTree = class _RepTree {
|
|
|
1291
1343
|
}
|
|
1292
1344
|
}
|
|
1293
1345
|
}
|
|
1294
|
-
missingOps.sort((a, b) =>
|
|
1346
|
+
missingOps.sort((a, b) => compareOpId(a.id, b.id));
|
|
1295
1347
|
return missingOps;
|
|
1296
1348
|
}
|
|
1297
1349
|
/**
|
|
@@ -1314,16 +1366,27 @@ var _RepTree = class _RepTree {
|
|
|
1314
1366
|
this.stateVector = new StateVector();
|
|
1315
1367
|
}
|
|
1316
1368
|
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Parses the vertex properties with a provided schema that has a `parse` method (e.g., Zod schema)
|
|
1371
|
+
*/
|
|
1372
|
+
parseVertex(vertexId, schema) {
|
|
1373
|
+
const propsArray = this.getVertexProperties(vertexId);
|
|
1374
|
+
const propsObject = {};
|
|
1375
|
+
for (const { key, value } of propsArray) {
|
|
1376
|
+
propsObject[key] = value;
|
|
1377
|
+
}
|
|
1378
|
+
return schema.parse(propsObject);
|
|
1379
|
+
}
|
|
1317
1380
|
};
|
|
1318
1381
|
_RepTree.NULL_VERTEX_ID = "0";
|
|
1319
1382
|
var RepTree = _RepTree;
|
|
1320
1383
|
export {
|
|
1321
|
-
OpId,
|
|
1322
1384
|
RepTree,
|
|
1323
1385
|
StateVector,
|
|
1324
1386
|
TreeState,
|
|
1325
1387
|
Vertex,
|
|
1326
1388
|
VertexState,
|
|
1389
|
+
bindVertex,
|
|
1327
1390
|
isAnyPropertyOp,
|
|
1328
1391
|
isLWWPropertyOp,
|
|
1329
1392
|
isModifyPropertyOp,
|