@loro-extended/change 5.2.0 → 5.4.0

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/dist/index.js CHANGED
@@ -99,6 +99,11 @@ function deriveValueShapePlaceholder(shape) {
99
99
  }
100
100
  }
101
101
 
102
+ // src/functional-helpers.ts
103
+ import {
104
+ LoroDoc as LoroDoc2
105
+ } from "loro-crdt";
106
+
102
107
  // src/loro.ts
103
108
  var LORO_SYMBOL = /* @__PURE__ */ Symbol.for("loro-extended:loro");
104
109
  function loro(refOrDoc) {
@@ -112,7 +117,9 @@ function loro(refOrDoc) {
112
117
  }
113
118
 
114
119
  // src/typed-doc.ts
115
- import { LoroDoc } from "loro-crdt";
120
+ import {
121
+ LoroDoc
122
+ } from "loro-crdt";
116
123
 
117
124
  // src/json-patch.ts
118
125
  function normalizePath(path) {
@@ -495,6 +502,7 @@ var BaseRefInternals = class {
495
502
  }
496
503
  cachedContainer;
497
504
  loroNamespace;
505
+ _suppressAutoCommit = false;
498
506
  /** Get the underlying Loro container (cached) */
499
507
  getContainer() {
500
508
  if (!this.cachedContainer) {
@@ -502,12 +510,23 @@ var BaseRefInternals = class {
502
510
  }
503
511
  return this.cachedContainer;
504
512
  }
505
- /** Commit changes if autoCommit is enabled */
513
+ /** Commit changes if autoCommit is enabled and not suppressed */
506
514
  commitIfAuto() {
507
- if (this.params.autoCommit) {
515
+ if (this.params.autoCommit && !this._suppressAutoCommit) {
508
516
  this.params.getDoc().commit();
509
517
  }
510
518
  }
519
+ /**
520
+ * Temporarily suppress auto-commit during batch operations.
521
+ * Used by assignPlainValueToTypedRef() to batch multiple property assignments.
522
+ */
523
+ setSuppressAutoCommit(suppress) {
524
+ this._suppressAutoCommit = suppress;
525
+ }
526
+ /** Check if auto-commit is currently suppressed */
527
+ isSuppressAutoCommit() {
528
+ return this._suppressAutoCommit;
529
+ }
511
530
  /** Get the shape for this ref */
512
531
  getShape() {
513
532
  return this.params.shape;
@@ -552,6 +571,10 @@ var BaseRefInternals = class {
552
571
  }
553
572
  return this.loroNamespace;
554
573
  }
574
+ /** Force materialization of the container and its nested containers */
575
+ materialize() {
576
+ this.getContainer();
577
+ }
555
578
  /** Create the loro() namespace object - subclasses override for specific types */
556
579
  createLoroNamespace() {
557
580
  const self = this;
@@ -722,17 +745,38 @@ function convertStructInput(value, shape) {
722
745
  return value;
723
746
  }
724
747
  const map = new LoroMap();
725
- for (const [k, v] of Object.entries(value)) {
748
+ for (const k of Object.keys(shape.shapes)) {
726
749
  const nestedSchema = shape.shapes[k];
727
- if (nestedSchema) {
750
+ const v = value[k];
751
+ if (v !== void 0) {
728
752
  const convertedValue = convertInputToRef(v, nestedSchema);
729
753
  if (isContainer(convertedValue)) {
730
754
  map.setContainer(k, convertedValue);
731
755
  } else {
732
756
  map.set(k, convertedValue);
733
757
  }
734
- } else {
735
- map.set(k, value);
758
+ } else if (isContainerShape(nestedSchema)) {
759
+ let emptyValue;
760
+ if (nestedSchema._type === "struct" || nestedSchema._type === "record") {
761
+ emptyValue = {};
762
+ } else if (nestedSchema._type === "list" || nestedSchema._type === "movableList") {
763
+ emptyValue = [];
764
+ } else if (nestedSchema._type === "text") {
765
+ emptyValue = "";
766
+ } else if (nestedSchema._type === "counter") {
767
+ emptyValue = 0;
768
+ }
769
+ if (emptyValue !== void 0) {
770
+ const convertedValue = convertInputToRef(emptyValue, nestedSchema);
771
+ if (isContainer(convertedValue)) {
772
+ map.setContainer(k, convertedValue);
773
+ }
774
+ }
775
+ }
776
+ }
777
+ for (const [k, v] of Object.entries(value)) {
778
+ if (!shape.shapes[k]) {
779
+ map.set(k, v);
736
780
  }
737
781
  }
738
782
  return map;
@@ -1368,7 +1412,6 @@ var RecordRefInternals = class extends BaseRefInternals {
1368
1412
  } else {
1369
1413
  const ref = this.getOrCreateRef(key);
1370
1414
  if (assignPlainValueToTypedRef(ref, value)) {
1371
- this.commitIfAuto();
1372
1415
  return;
1373
1416
  }
1374
1417
  throw new Error(
@@ -1383,6 +1426,80 @@ var RecordRefInternals = class extends BaseRefInternals {
1383
1426
  this.refCache.delete(key);
1384
1427
  this.commitIfAuto();
1385
1428
  }
1429
+ /**
1430
+ * Replace entire contents with new values.
1431
+ * Keys not in `values` are removed.
1432
+ */
1433
+ replace(values) {
1434
+ const container = this.getContainer();
1435
+ const currentKeys = new Set(container.keys());
1436
+ const newKeys = new Set(Object.keys(values));
1437
+ const wasSuppressed = this.isSuppressAutoCommit();
1438
+ if (!wasSuppressed) {
1439
+ this.setSuppressAutoCommit(true);
1440
+ }
1441
+ try {
1442
+ for (const key of currentKeys) {
1443
+ if (!newKeys.has(key)) {
1444
+ container.delete(key);
1445
+ this.refCache.delete(key);
1446
+ }
1447
+ }
1448
+ for (const key of newKeys) {
1449
+ this.set(key, values[key]);
1450
+ }
1451
+ } finally {
1452
+ if (!wasSuppressed) {
1453
+ this.setSuppressAutoCommit(false);
1454
+ }
1455
+ }
1456
+ this.commitIfAuto();
1457
+ }
1458
+ /**
1459
+ * Merge values into record.
1460
+ * Existing keys not in `values` are kept.
1461
+ */
1462
+ merge(values) {
1463
+ const wasSuppressed = this.isSuppressAutoCommit();
1464
+ if (!wasSuppressed) {
1465
+ this.setSuppressAutoCommit(true);
1466
+ }
1467
+ try {
1468
+ for (const key of Object.keys(values)) {
1469
+ this.set(key, values[key]);
1470
+ }
1471
+ } finally {
1472
+ if (!wasSuppressed) {
1473
+ this.setSuppressAutoCommit(false);
1474
+ }
1475
+ }
1476
+ this.commitIfAuto();
1477
+ }
1478
+ /**
1479
+ * Remove all entries from the record.
1480
+ */
1481
+ clear() {
1482
+ const container = this.getContainer();
1483
+ const keys = container.keys();
1484
+ if (keys.length === 0) {
1485
+ return;
1486
+ }
1487
+ const wasSuppressed = this.isSuppressAutoCommit();
1488
+ if (!wasSuppressed) {
1489
+ this.setSuppressAutoCommit(true);
1490
+ }
1491
+ try {
1492
+ for (const key of keys) {
1493
+ container.delete(key);
1494
+ this.refCache.delete(key);
1495
+ }
1496
+ } finally {
1497
+ if (!wasSuppressed) {
1498
+ this.setSuppressAutoCommit(false);
1499
+ }
1500
+ }
1501
+ this.commitIfAuto();
1502
+ }
1386
1503
  /** Absorb mutated plain values back into Loro containers */
1387
1504
  absorbPlainValues() {
1388
1505
  absorbCachedPlainValues(this.refCache, () => this.getContainer());
@@ -1447,14 +1564,77 @@ var RecordRef = class extends TypedRef {
1447
1564
  const container = this[INTERNAL_SYMBOL].getContainer();
1448
1565
  return container.keys();
1449
1566
  }
1567
+ /**
1568
+ * Returns an array of all values in the record.
1569
+ * For container-valued records, returns properly typed refs.
1570
+ */
1450
1571
  values() {
1451
- const container = this[INTERNAL_SYMBOL].getContainer();
1452
- return container.values();
1572
+ return this.keys().map(
1573
+ (key) => this.get(key)
1574
+ );
1575
+ }
1576
+ /**
1577
+ * Returns an array of [key, value] pairs.
1578
+ * For container-valued records, values are properly typed refs.
1579
+ */
1580
+ entries() {
1581
+ return this.keys().map((key) => [
1582
+ key,
1583
+ this.get(key)
1584
+ ]);
1453
1585
  }
1454
1586
  get size() {
1455
1587
  const container = this[INTERNAL_SYMBOL].getContainer();
1456
1588
  return container.size;
1457
1589
  }
1590
+ /**
1591
+ * Replace entire contents with new values.
1592
+ * Keys not in `values` are removed.
1593
+ *
1594
+ * @example
1595
+ * ```typescript
1596
+ * doc.change(draft => {
1597
+ * draft.players.replace({
1598
+ * alice: { score: 100 },
1599
+ * bob: { score: 50 }
1600
+ * })
1601
+ * })
1602
+ * ```
1603
+ */
1604
+ replace(values) {
1605
+ this[INTERNAL_SYMBOL].replace(values);
1606
+ }
1607
+ /**
1608
+ * Merge values into record.
1609
+ * Existing keys not in `values` are kept.
1610
+ *
1611
+ * @example
1612
+ * ```typescript
1613
+ * doc.change(draft => {
1614
+ * // Adds charlie, updates alice, keeps bob unchanged
1615
+ * draft.players.merge({
1616
+ * alice: { score: 150 },
1617
+ * charlie: { score: 25 }
1618
+ * })
1619
+ * })
1620
+ * ```
1621
+ */
1622
+ merge(values) {
1623
+ this[INTERNAL_SYMBOL].merge(values);
1624
+ }
1625
+ /**
1626
+ * Remove all entries from the record.
1627
+ *
1628
+ * @example
1629
+ * ```typescript
1630
+ * doc.change(draft => {
1631
+ * draft.players.clear()
1632
+ * })
1633
+ * ```
1634
+ */
1635
+ clear() {
1636
+ this[INTERNAL_SYMBOL].clear();
1637
+ }
1458
1638
  toJSON() {
1459
1639
  return serializeRefToJSON(this, this.keys());
1460
1640
  }
@@ -1543,7 +1723,6 @@ var StructRefInternals = class extends BaseRefInternals {
1543
1723
  } else {
1544
1724
  const ref = this.getOrCreateRef(key, shape);
1545
1725
  if (assignPlainValueToTypedRef(ref, value)) {
1546
- this.commitIfAuto();
1547
1726
  return;
1548
1727
  }
1549
1728
  throw new Error(
@@ -1565,6 +1744,18 @@ var StructRefInternals = class extends BaseRefInternals {
1565
1744
  () => this.getContainer()
1566
1745
  );
1567
1746
  }
1747
+ /** Force materialization of the container and its nested containers */
1748
+ materialize() {
1749
+ this.getContainer();
1750
+ const structShape = this.getShape();
1751
+ for (const key in structShape.shapes) {
1752
+ const shape = structShape.shapes[key];
1753
+ if (!isValueShape(shape)) {
1754
+ const ref = this.getOrCreateRef(key, shape);
1755
+ ref[INTERNAL_SYMBOL].materialize();
1756
+ }
1757
+ }
1758
+ }
1568
1759
  /** Create the loro namespace for struct */
1569
1760
  createLoroNamespace() {
1570
1761
  const self = this;
@@ -1920,6 +2111,11 @@ var TreeNodeRefInternals = class {
1920
2111
  this.dataRef[INTERNAL_SYMBOL].absorbPlainValues();
1921
2112
  }
1922
2113
  }
2114
+ /** Force materialization of the container and its nested containers */
2115
+ materialize() {
2116
+ const dataRef = this.getOrCreateDataRef();
2117
+ dataRef[INTERNAL_SYMBOL].materialize();
2118
+ }
1923
2119
  /** Get the loro namespace (cached) */
1924
2120
  getLoroNamespace() {
1925
2121
  if (!this.loroNamespace) {
@@ -2371,24 +2567,54 @@ function createContainerTypedRef(params) {
2371
2567
  );
2372
2568
  }
2373
2569
  }
2374
- function assignPlainValueToTypedRef(ref, value) {
2375
- const shape = ref[INTERNAL_SYMBOL]?.getShape?.() ?? ref.shape;
2570
+ function assignPlainValueToTypedRef(ref, value, skipCommit = false) {
2571
+ const internals = ref[INTERNAL_SYMBOL];
2572
+ if (internals) {
2573
+ internals.materialize();
2574
+ }
2575
+ const shape = internals?.getShape?.() ?? ref.shape;
2376
2576
  const shapeType = shape?._type;
2377
2577
  if (shapeType === "struct" || shapeType === "record") {
2378
- for (const k in value) {
2379
- ;
2380
- ref[k] = value[k];
2578
+ const wasSuppressed = internals?.isSuppressAutoCommit?.() ?? false;
2579
+ if (internals && !wasSuppressed) {
2580
+ internals.setSuppressAutoCommit(true);
2581
+ }
2582
+ try {
2583
+ for (const k in value) {
2584
+ ;
2585
+ ref[k] = value[k];
2586
+ }
2587
+ } finally {
2588
+ if (internals && !wasSuppressed) {
2589
+ internals.setSuppressAutoCommit(false);
2590
+ }
2591
+ }
2592
+ if (!skipCommit && internals?.getAutoCommit?.()) {
2593
+ internals.getDoc().commit();
2381
2594
  }
2382
2595
  return true;
2383
2596
  }
2384
2597
  if (shapeType === "list" || shapeType === "movableList") {
2385
2598
  if (Array.isArray(value)) {
2386
2599
  const listRef = ref;
2387
- if (listRef.length > 0) {
2388
- listRef.delete(0, listRef.length);
2600
+ const wasSuppressed = internals?.isSuppressAutoCommit?.() ?? false;
2601
+ if (internals && !wasSuppressed) {
2602
+ internals.setSuppressAutoCommit(true);
2603
+ }
2604
+ try {
2605
+ if (listRef.length > 0) {
2606
+ listRef.delete(0, listRef.length);
2607
+ }
2608
+ for (const item of value) {
2609
+ listRef.push(item);
2610
+ }
2611
+ } finally {
2612
+ if (internals && !wasSuppressed) {
2613
+ internals.setSuppressAutoCommit(false);
2614
+ }
2389
2615
  }
2390
- for (const item of value) {
2391
- listRef.push(item);
2616
+ if (!skipCommit && internals?.getAutoCommit?.()) {
2617
+ internals.getDoc().commit();
2392
2618
  }
2393
2619
  return true;
2394
2620
  }
@@ -2861,9 +3087,10 @@ function createTypedDoc(shape, existingDoc) {
2861
3087
  return true;
2862
3088
  return Reflect.has(target, prop);
2863
3089
  },
2864
- // Support Object.keys() - don't include change, forkAt, or LORO_SYMBOL in enumeration
3090
+ // Support Object.keys() - filter out Symbol properties to allow proxies to be used
3091
+ // in place of plain objects. This prevents React's "Object keys must be strings" error.
2865
3092
  ownKeys(target) {
2866
- return Reflect.ownKeys(target);
3093
+ return Reflect.ownKeys(target).filter((key) => typeof key === "string");
2867
3094
  },
2868
3095
  getOwnPropertyDescriptor(target, prop) {
2869
3096
  if (prop === "change") {
@@ -2927,12 +3154,34 @@ function getLoroDoc(docOrRef) {
2927
3154
  function getLoroContainer(ref) {
2928
3155
  return loro(ref).container;
2929
3156
  }
3157
+ function fork(doc, options) {
3158
+ const loroDoc = loro(doc).doc;
3159
+ const forkedLoroDoc = loroDoc.fork();
3160
+ const shape = loro(doc).docShape;
3161
+ if (options?.preservePeerId) {
3162
+ forkedLoroDoc.setPeerId(loroDoc.peerId);
3163
+ }
3164
+ return createTypedDoc(shape, forkedLoroDoc);
3165
+ }
2930
3166
  function forkAt(doc, frontiers) {
2931
3167
  const loroDoc = loro(doc).doc;
2932
3168
  const forkedLoroDoc = loroDoc.forkAt(frontiers);
2933
3169
  const shape = loro(doc).docShape;
2934
3170
  return createTypedDoc(shape, forkedLoroDoc);
2935
3171
  }
3172
+ function shallowForkAt(doc, frontiers, options) {
3173
+ const loroDoc = loro(doc).doc;
3174
+ const shape = loro(doc).docShape;
3175
+ const shallowBytes = loroDoc.export({
3176
+ mode: "shallow-snapshot",
3177
+ frontiers
3178
+ });
3179
+ const shallowLoroDoc = LoroDoc2.fromSnapshot(shallowBytes);
3180
+ if (options?.preservePeerId) {
3181
+ shallowLoroDoc.setPeerId(loroDoc.peerId);
3182
+ }
3183
+ return createTypedDoc(shape, shallowLoroDoc);
3184
+ }
2936
3185
 
2937
3186
  // src/path-builder.ts
2938
3187
  function createPathSelector(segments) {
@@ -3104,6 +3353,134 @@ function createPlaceholderProxy(target) {
3104
3353
  });
3105
3354
  }
3106
3355
 
3356
+ // src/replay-diff.ts
3357
+ function replayDiff(doc, diff) {
3358
+ const containerMap = /* @__PURE__ */ new Map();
3359
+ for (const [containerId, containerDiff] of diff) {
3360
+ let container = containerMap.get(containerId);
3361
+ if (!container) {
3362
+ container = doc.getContainerById(containerId);
3363
+ }
3364
+ if (!container) {
3365
+ continue;
3366
+ }
3367
+ switch (containerDiff.type) {
3368
+ case "text":
3369
+ replayTextDiff(container, containerDiff);
3370
+ break;
3371
+ case "list":
3372
+ replayListDiff(
3373
+ container,
3374
+ containerDiff,
3375
+ containerMap
3376
+ );
3377
+ break;
3378
+ case "map":
3379
+ replayMapDiff(container, containerDiff, containerMap);
3380
+ break;
3381
+ case "tree":
3382
+ replayTreeDiff(container, containerDiff);
3383
+ break;
3384
+ case "counter":
3385
+ replayCounterDiff(container, containerDiff);
3386
+ break;
3387
+ }
3388
+ }
3389
+ }
3390
+ function replayTextDiff(text, diff) {
3391
+ text.applyDelta(diff.diff);
3392
+ }
3393
+ function replayListDiff(list, diff, containerMap) {
3394
+ let index = 0;
3395
+ for (const delta of diff.diff) {
3396
+ if (delta.retain !== void 0) {
3397
+ index += delta.retain;
3398
+ } else if (delta.delete !== void 0) {
3399
+ list.delete(index, delta.delete);
3400
+ } else if (delta.insert !== void 0) {
3401
+ const values = delta.insert;
3402
+ for (let i = 0; i < values.length; i++) {
3403
+ const value = values[i];
3404
+ if (isContainer2(value)) {
3405
+ const newContainer = createContainerOfSameType(value);
3406
+ const insertedContainer = list.insertContainer(
3407
+ index + i,
3408
+ newContainer
3409
+ );
3410
+ containerMap.set(value.id, insertedContainer);
3411
+ } else {
3412
+ ;
3413
+ list.insert(
3414
+ index + i,
3415
+ value
3416
+ );
3417
+ }
3418
+ }
3419
+ index += values.length;
3420
+ }
3421
+ }
3422
+ }
3423
+ function replayMapDiff(map, diff, containerMap) {
3424
+ for (const [key, value] of Object.entries(diff.updated)) {
3425
+ if (value === void 0) {
3426
+ map.delete(key);
3427
+ } else if (isContainer2(value)) {
3428
+ const newContainer = createContainerOfSameType(value);
3429
+ const insertedContainer = map.setContainer(key, newContainer);
3430
+ containerMap.set(value.id, insertedContainer);
3431
+ } else {
3432
+ map.set(key, value);
3433
+ }
3434
+ }
3435
+ }
3436
+ function replayTreeDiff(tree, diff) {
3437
+ for (const item of diff.diff) {
3438
+ replayTreeDiffItem(tree, item);
3439
+ }
3440
+ }
3441
+ function replayTreeDiffItem(tree, item) {
3442
+ switch (item.action) {
3443
+ case "create":
3444
+ tree.createNode(item.parent, item.index);
3445
+ break;
3446
+ case "delete":
3447
+ tree.delete(item.target);
3448
+ break;
3449
+ case "move":
3450
+ tree.move(item.target, item.parent, item.index);
3451
+ break;
3452
+ }
3453
+ }
3454
+ function replayCounterDiff(counter, diff) {
3455
+ if (diff.increment > 0) {
3456
+ counter.increment(diff.increment);
3457
+ } else if (diff.increment < 0) {
3458
+ counter.decrement(-diff.increment);
3459
+ }
3460
+ }
3461
+ function isContainer2(value) {
3462
+ return value !== null && typeof value === "object" && "kind" in value && typeof value.kind === "function";
3463
+ }
3464
+ function createContainerOfSameType(container) {
3465
+ const kind = container.kind();
3466
+ switch (kind) {
3467
+ case "List":
3468
+ return new container.constructor();
3469
+ case "Map":
3470
+ return new container.constructor();
3471
+ case "Text":
3472
+ return new container.constructor();
3473
+ case "Tree":
3474
+ return new container.constructor();
3475
+ case "Counter":
3476
+ return new container.constructor();
3477
+ case "MovableList":
3478
+ return new container.constructor();
3479
+ default:
3480
+ throw new Error(`Unknown container kind: ${kind}`);
3481
+ }
3482
+ }
3483
+
3107
3484
  // src/shape.ts
3108
3485
  function makeNullable(shape) {
3109
3486
  const nullShape = {
@@ -3543,6 +3920,7 @@ export {
3543
3920
  deriveShapePlaceholder,
3544
3921
  evaluatePath,
3545
3922
  evaluatePathOnValue,
3923
+ fork,
3546
3924
  forkAt,
3547
3925
  getLoroContainer,
3548
3926
  getLoroDoc,
@@ -3550,6 +3928,8 @@ export {
3550
3928
  loro,
3551
3929
  mergeValue,
3552
3930
  overlayPlaceholder,
3931
+ replayDiff,
3932
+ shallowForkAt,
3553
3933
  validatePlaceholder
3554
3934
  };
3555
3935
  //# sourceMappingURL=index.js.map