reptree 0.2.2 → 0.2.4

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,43 @@ 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
+ #### Aliases for internal fields
49
+
50
+ - `name` ↔ `_n`
51
+ - `createdAt` ↔ `_c` (Date exposed, ISO stored)
52
+
53
+ These aliases are applied by default when using `bindVertex` or `vertex.bind()`.
54
+
55
+ ```ts
56
+ person.name = 'Alice'; // writes _n
57
+ person.createdAt = new Date(); // writes _c (ISO)
58
+ console.log(person.createdAt instanceof Date); // true
59
+ ```
60
+
61
+ For more, see `docs/reactive-vertices.md`.
62
+
26
63
  ```typescript
27
64
  import { RepTree } from 'reptree';
28
65
 
@@ -74,6 +111,15 @@ const ops = tree.getAllOps();
74
111
  otherTree.merge(ops);
75
112
  ```
76
113
 
114
+ ### Creating children with normalized props
115
+
116
+ `vertex.newChild(props)` and `vertex.newNamedChild(name, props)` accept plain objects. RepTree will:
117
+
118
+ - Map `name` → `_n`, `createdAt` (Date) → `_c` (ISO)
119
+ - Filter unsupported types (non-primitive objects except Y.Doc)
120
+ - Ignore `props.name` if `newNamedChild` has an explicit `name`
121
+ - Forbid nested children in props for now
122
+
77
123
  ## Yjs Integration
78
124
 
79
125
  RepTree supports [Yjs](https://github.com/yjs/yjs) documents as vertex properties, enabling real-time collaborative editing with a variety of shared data types:
package/dist/index.cjs CHANGED
@@ -35,6 +35,8 @@ __export(index_exports, {
35
35
  TreeState: () => TreeState,
36
36
  Vertex: () => Vertex,
37
37
  VertexState: () => VertexState,
38
+ bindVertex: () => bindVertex,
39
+ defaultAliases: () => defaultAliases,
38
40
  isAnyPropertyOp: () => isAnyPropertyOp,
39
41
  isLWWPropertyOp: () => isLWWPropertyOp,
40
42
  isModifyPropertyOp: () => isModifyPropertyOp,
@@ -402,8 +404,211 @@ function uuid() {
402
404
  return removeDashes(crypto.randomUUID());
403
405
  }
404
406
 
407
+ // src/reactive.ts
408
+ var defaultAliases = [
409
+ { publicKey: "name", internalKey: "_n" },
410
+ {
411
+ publicKey: "createdAt",
412
+ internalKey: "_c",
413
+ toPublic: (v) => typeof v === "string" ? new Date(v) : v,
414
+ toInternal: (v) => v instanceof Date ? v.toISOString() : v
415
+ }
416
+ ];
417
+ function buildAliasMaps(aliases) {
418
+ const publicToInternal = /* @__PURE__ */ new Map();
419
+ const internalToPublic = /* @__PURE__ */ new Map();
420
+ for (const rule of aliases) {
421
+ publicToInternal.set(rule.publicKey, rule);
422
+ internalToPublic.set(rule.internalKey, rule);
423
+ }
424
+ return { publicToInternal, internalToPublic };
425
+ }
426
+ function toPublicObject(tree, id, internalToPublic) {
427
+ const obj = {};
428
+ for (const { key, value } of tree.getVertexProperties(id)) {
429
+ const rule = internalToPublic.get(key);
430
+ if (rule) {
431
+ const converted = rule.toPublic ? rule.toPublic(value) : value;
432
+ obj[rule.publicKey] = converted;
433
+ } else {
434
+ obj[key] = value;
435
+ }
436
+ }
437
+ return obj;
438
+ }
439
+ var RESERVED_METHOD_USE_TRANSIENT = "useTransient";
440
+ var RESERVED_METHOD_COMMIT_TRANSIENTS = "commitTransients";
441
+ function bindVertex(tree, id, schemaOrOptions) {
442
+ const isOptions = typeof schemaOrOptions === "object" && schemaOrOptions !== null && (Object.prototype.hasOwnProperty.call(schemaOrOptions, "aliases") || Object.prototype.hasOwnProperty.call(schemaOrOptions, "includeInternalKeys") || Object.prototype.hasOwnProperty.call(schemaOrOptions, "schema"));
443
+ const options = isOptions ? schemaOrOptions : { schema: schemaOrOptions };
444
+ const schema = options.schema;
445
+ const aliases = options.aliases ?? defaultAliases;
446
+ const includeInternalKeys = options.includeInternalKeys ?? false;
447
+ const { publicToInternal, internalToPublic } = buildAliasMaps(aliases);
448
+ return new Proxy({}, {
449
+ get(_target, prop) {
450
+ if (prop === RESERVED_METHOD_USE_TRANSIENT) {
451
+ return (fn) => {
452
+ const transientProxy = new Proxy({}, {
453
+ get(_t, p) {
454
+ if (typeof p !== "string") return void 0;
455
+ const rule2 = publicToInternal.get(p);
456
+ if (rule2) {
457
+ const raw = tree.getVertexProperty(id, rule2.internalKey);
458
+ return rule2.toPublic ? rule2.toPublic(raw) : raw;
459
+ }
460
+ return tree.getVertexProperty(id, p);
461
+ },
462
+ set(_t, p, value) {
463
+ if (typeof p !== "string") return true;
464
+ if (schema?.shape && schema.shape[p]) {
465
+ const field = schema.shape[p];
466
+ if (field.safeParse) {
467
+ const res = field.safeParse(value);
468
+ if (!res.success) throw new Error(`Invalid value for ${String(p)}`);
469
+ value = res.data ?? value;
470
+ }
471
+ } else if (schema?.safeParse) {
472
+ const next = { ...toPublicObject(tree, id, internalToPublic), [p]: value };
473
+ const res = schema.safeParse(next);
474
+ if (!res.success) throw new Error(`Invalid value for ${String(p)}`);
475
+ const parsed = res.data;
476
+ if (parsed && Object.prototype.hasOwnProperty.call(parsed, p)) {
477
+ value = parsed[p];
478
+ }
479
+ }
480
+ const rule2 = publicToInternal.get(p);
481
+ if (rule2) {
482
+ const converted = rule2.toInternal ? rule2.toInternal(value) : value;
483
+ tree.setTransientVertexProperty(id, rule2.internalKey, converted);
484
+ return true;
485
+ }
486
+ tree.setTransientVertexProperty(id, p, value);
487
+ return true;
488
+ },
489
+ deleteProperty(_t, p) {
490
+ if (typeof p !== "string") return true;
491
+ const rule2 = publicToInternal.get(p);
492
+ if (rule2) {
493
+ tree.setTransientVertexProperty(id, rule2.internalKey, void 0);
494
+ return true;
495
+ }
496
+ tree.setTransientVertexProperty(id, p, void 0);
497
+ return true;
498
+ }
499
+ });
500
+ fn(transientProxy);
501
+ };
502
+ }
503
+ if (prop === RESERVED_METHOD_COMMIT_TRANSIENTS) {
504
+ return () => {
505
+ const entries = tree.getVertexProperties(id);
506
+ const currentPublic = schema?.safeParse ? toPublicObject(tree, id, internalToPublic) : void 0;
507
+ for (const { key: internalKey, value: overlayValue } of entries) {
508
+ const persistentValue = tree.getVertexProperty(id, internalKey, false);
509
+ if (overlayValue === persistentValue) continue;
510
+ const aliasRule = internalToPublic.get(internalKey);
511
+ const publicKey = aliasRule ? aliasRule.publicKey : internalKey;
512
+ const publicValue = aliasRule && aliasRule.toPublic ? aliasRule.toPublic(overlayValue) : overlayValue;
513
+ let valueToPersistInternal = overlayValue;
514
+ if (schema?.shape && schema.shape[publicKey]) {
515
+ const field = schema.shape[publicKey];
516
+ if (field.safeParse) {
517
+ const res = field.safeParse(publicValue);
518
+ if (!res.success) throw new Error(`Invalid value for ${String(publicKey)}`);
519
+ const coerced = res.data;
520
+ const maybeInternal = aliasRule && aliasRule.toInternal ? aliasRule.toInternal(coerced) : coerced;
521
+ valueToPersistInternal = maybeInternal;
522
+ }
523
+ } else if (schema?.safeParse && currentPublic) {
524
+ const nextPublic = { ...currentPublic, [publicKey]: publicValue };
525
+ const res = schema.safeParse(nextPublic);
526
+ if (!res.success) throw new Error("Invalid values for commitTransients");
527
+ const parsed = res.data;
528
+ const parsedPublic = Object.prototype.hasOwnProperty.call(parsed, publicKey) ? parsed[publicKey] : publicValue;
529
+ const maybeInternal = aliasRule && aliasRule.toInternal ? aliasRule.toInternal(parsedPublic) : parsedPublic;
530
+ valueToPersistInternal = maybeInternal;
531
+ }
532
+ tree.setVertexProperty(id, internalKey, valueToPersistInternal);
533
+ }
534
+ };
535
+ }
536
+ if (typeof prop !== "string") return void 0;
537
+ const rule = publicToInternal.get(prop);
538
+ if (rule) {
539
+ const raw = tree.getVertexProperty(id, rule.internalKey);
540
+ return rule.toPublic ? rule.toPublic(raw) : raw;
541
+ }
542
+ return tree.getVertexProperty(id, prop);
543
+ },
544
+ set(_target, prop, value) {
545
+ if (typeof prop !== "string") return true;
546
+ if (prop === RESERVED_METHOD_USE_TRANSIENT || prop === RESERVED_METHOD_COMMIT_TRANSIENTS) {
547
+ return true;
548
+ }
549
+ if (schema?.shape && schema.shape[prop]) {
550
+ const field = schema.shape[prop];
551
+ if (field.safeParse) {
552
+ const res = field.safeParse(value);
553
+ if (!res.success) throw new Error(`Invalid value for ${String(prop)}`);
554
+ value = res.data ?? value;
555
+ }
556
+ } else if (schema?.safeParse) {
557
+ const next = { ...toPublicObject(tree, id, internalToPublic), [prop]: value };
558
+ const res = schema.safeParse(next);
559
+ if (!res.success) throw new Error(`Invalid value for ${String(prop)}`);
560
+ const parsed = res.data;
561
+ if (parsed && Object.prototype.hasOwnProperty.call(parsed, prop)) {
562
+ value = parsed[prop];
563
+ }
564
+ }
565
+ const rule = publicToInternal.get(prop);
566
+ if (rule) {
567
+ const converted = rule.toInternal ? rule.toInternal(value) : value;
568
+ tree.setVertexProperty(id, rule.internalKey, converted);
569
+ return true;
570
+ }
571
+ tree.setVertexProperty(id, prop, value);
572
+ return true;
573
+ },
574
+ deleteProperty(_target, prop) {
575
+ if (typeof prop !== "string") return true;
576
+ if (prop === RESERVED_METHOD_USE_TRANSIENT || prop === RESERVED_METHOD_COMMIT_TRANSIENTS) {
577
+ return true;
578
+ }
579
+ const rule = publicToInternal.get(prop);
580
+ if (rule) {
581
+ tree.setVertexProperty(id, rule.internalKey, void 0);
582
+ return true;
583
+ }
584
+ tree.setVertexProperty(id, prop, void 0);
585
+ return true;
586
+ },
587
+ has(_target, prop) {
588
+ if (typeof prop !== "string") return false;
589
+ if (schema?.shape && Object.prototype.hasOwnProperty.call(schema.shape, prop)) return true;
590
+ if (includeInternalKeys) {
591
+ return publicToInternal.has(prop) || internalToPublic.has(prop);
592
+ }
593
+ return false;
594
+ },
595
+ ownKeys() {
596
+ const keys = /* @__PURE__ */ new Set();
597
+ for (const k of Object.keys(schema?.shape ?? {})) keys.add(k);
598
+ if (includeInternalKeys) {
599
+ for (const rule of aliases) keys.add(rule.internalKey);
600
+ }
601
+ return Array.from(keys);
602
+ },
603
+ getOwnPropertyDescriptor() {
604
+ return { enumerable: true, configurable: true };
605
+ }
606
+ });
607
+ }
608
+
405
609
  // src/Vertex.ts
406
- var Vertex = class {
610
+ var Y = __toESM(require("yjs"), 1);
611
+ var Vertex = class _Vertex {
407
612
  constructor(tree, state) {
408
613
  this.tree = tree;
409
614
  this.state = state;
@@ -449,12 +654,18 @@ var Vertex = class {
449
654
  return this.children.map((v) => v.getAsTypedObject());
450
655
  }
451
656
  newChild(props) {
452
- const typedProps = props;
453
- return this.tree.newVertex(this.id, typedProps);
657
+ if (props && typeof props === "object" && "children" in props) {
658
+ throw new Error("Passing children inside props is not supported at the moment");
659
+ }
660
+ const normalized = _Vertex.normalizePropsForCreation(props);
661
+ return this.tree.newVertex(this.id, normalized);
454
662
  }
455
663
  newNamedChild(name, props) {
456
- const typedProps = props;
457
- return this.tree.newNamedVertex(this.id, name, typedProps);
664
+ if (props && typeof props === "object" && "children" in props) {
665
+ throw new Error("Passing children inside props is not supported at the moment");
666
+ }
667
+ const normalized = _Vertex.normalizePropsForCreation(props, name);
668
+ return this.tree.newNamedVertex(this.id, name, normalized);
458
669
  }
459
670
  setProperty(key, value) {
460
671
  const existingValue = this.getProperty(key, false);
@@ -520,6 +731,68 @@ var Vertex = class {
520
731
  moveTo(parent) {
521
732
  this.tree.moveVertex(this.id, parent.id);
522
733
  }
734
+ /** Returns a live reactive object bound to this vertex. Accepts schema or options. */
735
+ bind(schemaOrOptions) {
736
+ return bindVertex(this.tree, this.id, schemaOrOptions);
737
+ }
738
+ /**
739
+ * Normalizes an input props object for vertex creation:
740
+ * - Aliases name -> _n, createdAt -> _c (Date -> ISO string)
741
+ * - Filters unsupported field types with a console warning
742
+ * - When a name param is provided to newNamedChild, ignores conflicting name in props
743
+ */
744
+ static normalizePropsForCreation(props, explicitName) {
745
+ if (!props) return null;
746
+ const input = props;
747
+ const out = {};
748
+ const skipped = [];
749
+ for (const [rawKey, rawValue] of Object.entries(input)) {
750
+ if (rawValue === void 0) {
751
+ continue;
752
+ }
753
+ if (rawKey === "children") continue;
754
+ let key = rawKey;
755
+ if (rawKey === "name") {
756
+ if (explicitName !== void 0) {
757
+ console.warn('newNamedChild: "name" in props is ignored because a name argument was provided');
758
+ continue;
759
+ }
760
+ key = "_n";
761
+ } else if (rawKey === "createdAt") {
762
+ key = "_c";
763
+ }
764
+ let value = rawValue;
765
+ if (key === "_c") {
766
+ if (value instanceof Date) {
767
+ value = value.toISOString();
768
+ } else if (typeof value === "string") {
769
+ } else {
770
+ skipped.push(rawKey);
771
+ continue;
772
+ }
773
+ }
774
+ const isPrimitive = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
775
+ if (Array.isArray(value)) {
776
+ if (!value.every(isPrimitive)) {
777
+ skipped.push(rawKey);
778
+ continue;
779
+ }
780
+ } else if (typeof value === "object" && value !== null) {
781
+ if (!(value instanceof Y.Doc)) {
782
+ skipped.push(rawKey);
783
+ continue;
784
+ }
785
+ } else if (!isPrimitive(value)) {
786
+ skipped.push(rawKey);
787
+ continue;
788
+ }
789
+ out[key] = value;
790
+ }
791
+ if (skipped.length > 0) {
792
+ console.warn(`Some fields were skipped due to unsupported types: ${skipped.join(", ")}`);
793
+ }
794
+ return Object.keys(out).length > 0 ? out : null;
795
+ }
523
796
  };
524
797
 
525
798
  // src/StateVector.ts
@@ -708,7 +981,7 @@ var StateVector = class _StateVector {
708
981
  };
709
982
 
710
983
  // src/RepTree.ts
711
- var Y = __toESM(require("yjs"), 1);
984
+ var Y2 = __toESM(require("yjs"), 1);
712
985
  var _RepTree = class _RepTree {
713
986
  /**
714
987
  * @param peerId - The peer ID of the current client. Should be unique across all peers.
@@ -870,8 +1143,8 @@ var _RepTree = class _RepTree {
870
1143
  setTransientVertexProperty(vertexId, key, value) {
871
1144
  this.lamportClock++;
872
1145
  let opValue;
873
- if (value instanceof Y.Doc) {
874
- const state = Y.encodeStateAsUpdate(value);
1146
+ if (value instanceof Y2.Doc) {
1147
+ const state = Y2.encodeStateAsUpdate(value);
875
1148
  opValue = {
876
1149
  type: "yjs",
877
1150
  value: state
@@ -887,8 +1160,8 @@ var _RepTree = class _RepTree {
887
1160
  setVertexProperty(vertexId, key, value) {
888
1161
  this.lamportClock++;
889
1162
  let opValue;
890
- if (value instanceof Y.Doc) {
891
- const state = Y.encodeStateAsUpdate(value);
1163
+ if (value instanceof Y2.Doc) {
1164
+ const state = Y2.encodeStateAsUpdate(value);
892
1165
  opValue = {
893
1166
  type: "yjs",
894
1167
  value: state
@@ -1060,10 +1333,10 @@ var _RepTree = class _RepTree {
1060
1333
  if (!propB) {
1061
1334
  return false;
1062
1335
  }
1063
- if (propA.value instanceof Y.Doc && propB.value instanceof Y.Doc) {
1064
- const snapshotA = Y.snapshot(propA.value);
1065
- const snapshotB = Y.snapshot(propB.value);
1066
- if (!Y.equalSnapshots(snapshotA, snapshotB)) {
1336
+ if (propA.value instanceof Y2.Doc && propB.value instanceof Y2.Doc) {
1337
+ const snapshotA = Y2.snapshot(propA.value);
1338
+ const snapshotB = Y2.snapshot(propB.value);
1339
+ if (!Y2.equalSnapshots(snapshotA, snapshotB)) {
1067
1340
  return false;
1068
1341
  }
1069
1342
  } else if (propA.value !== propB.value) {
@@ -1167,7 +1440,7 @@ var _RepTree = class _RepTree {
1167
1440
  const propertyKey = `${key}@${vertexId}`;
1168
1441
  if (this.yjsObservers.has(propertyKey)) {
1169
1442
  const existingDoc = this.getVertexProperty(vertexId, key);
1170
- if (existingDoc instanceof Y.Doc) {
1443
+ if (existingDoc instanceof Y2.Doc) {
1171
1444
  existingDoc.off("update", this.yjsObservers.get(propertyKey));
1172
1445
  }
1173
1446
  this.yjsObservers.delete(propertyKey);
@@ -1247,13 +1520,13 @@ var _RepTree = class _RepTree {
1247
1520
  throw new Error("Unknown CRDT type");
1248
1521
  }
1249
1522
  const ydoc = targetVertex.getProperty(op.key);
1250
- if (ydoc instanceof Y.Doc) {
1251
- Y.applyUpdate(ydoc, crdtValue.value);
1523
+ if (ydoc instanceof Y2.Doc) {
1524
+ Y2.applyUpdate(ydoc, crdtValue.value);
1252
1525
  } else {
1253
- const newDoc = new Y.Doc();
1526
+ const newDoc = new Y2.Doc();
1254
1527
  this.setupYjsObserver(newDoc, op.targetId, op.key);
1255
1528
  this.state.setProperty(op.targetId, op.key, newDoc);
1256
- Y.applyUpdate(newDoc, crdtValue.value);
1529
+ Y2.applyUpdate(newDoc, crdtValue.value);
1257
1530
  }
1258
1531
  this.propertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
1259
1532
  this.reportOpAsApplied(op);
@@ -1359,6 +1632,17 @@ var _RepTree = class _RepTree {
1359
1632
  this.stateVector = new StateVector();
1360
1633
  }
1361
1634
  }
1635
+ /**
1636
+ * Parses the vertex properties with a provided schema that has a `parse` method (e.g., Zod schema)
1637
+ */
1638
+ parseVertex(vertexId, schema) {
1639
+ const propsArray = this.getVertexProperties(vertexId);
1640
+ const propsObject = {};
1641
+ for (const { key, value } of propsArray) {
1642
+ propsObject[key] = value;
1643
+ }
1644
+ return schema.parse(propsObject);
1645
+ }
1362
1646
  };
1363
1647
  _RepTree.NULL_VERTEX_ID = "0";
1364
1648
  var RepTree = _RepTree;
@@ -1369,6 +1653,8 @@ var RepTree = _RepTree;
1369
1653
  TreeState,
1370
1654
  Vertex,
1371
1655
  VertexState,
1656
+ bindVertex,
1657
+ defaultAliases,
1372
1658
  isAnyPropertyOp,
1373
1659
  isLWWPropertyOp,
1374
1660
  isModifyPropertyOp,