@liveblocks/core 3.23.1 → 3.24.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
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
 
7
7
  // src/version.ts
8
8
  var PKG_NAME = "@liveblocks/core";
9
- var PKG_VERSION = "3.23.1";
9
+ var PKG_VERSION = "3.24.0";
10
10
  var PKG_FORMAT = "esm";
11
11
 
12
12
  // src/dupe-detection.ts
@@ -988,14 +988,15 @@ var OpCode = Object.freeze({
988
988
  DELETE_OBJECT_KEY: 6,
989
989
  CREATE_MAP: 7,
990
990
  CREATE_REGISTER: 8,
991
- // TODO: 9 and 10 are used by LiveText, wait until it's merged.
991
+ CREATE_TEXT: 9,
992
+ UPDATE_TEXT: 10,
992
993
  CREATE_FILE: 11
993
994
  });
994
995
  function isIgnoredOp(op) {
995
996
  return op.type === OpCode.DELETE_CRDT && op.id === "ACK";
996
997
  }
997
998
  function isCreateOp(op) {
998
- return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_FILE || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST;
999
+ return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_FILE || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST || op.type === OpCode.CREATE_TEXT;
999
1000
  }
1000
1001
 
1001
1002
  // src/protocol/StorageNode.ts
@@ -1004,7 +1005,7 @@ var CrdtType = Object.freeze({
1004
1005
  LIST: 1,
1005
1006
  MAP: 2,
1006
1007
  REGISTER: 3,
1007
- // TODO: 4 is used by LiveText, wait until it's merged.
1008
+ TEXT: 4,
1008
1009
  FILE: 5
1009
1010
  });
1010
1011
  function isRootStorageNode(node) {
@@ -1022,6 +1023,9 @@ function isMapStorageNode(node) {
1022
1023
  function isRegisterStorageNode(node) {
1023
1024
  return node[1].type === CrdtType.REGISTER;
1024
1025
  }
1026
+ function isTextStorageNode(node) {
1027
+ return node[1].type === CrdtType.TEXT;
1028
+ }
1025
1029
  function isFileStorageNode(node) {
1026
1030
  return node[1].type === CrdtType.FILE;
1027
1031
  }
@@ -1047,6 +1051,9 @@ function* compactNodesToNodeStream(compactNodes) {
1047
1051
  case CrdtType.REGISTER:
1048
1052
  yield [cnode[0], { type: CrdtType.REGISTER, parentId: cnode[2], parentKey: cnode[3], data: cnode[4] }];
1049
1053
  break;
1054
+ case CrdtType.TEXT:
1055
+ yield [cnode[0], { type: CrdtType.TEXT, parentId: cnode[2], parentKey: cnode[3], data: cnode[4], version: cnode[5] }];
1056
+ break;
1050
1057
  case CrdtType.FILE:
1051
1058
  yield [cnode[0], { type: CrdtType.FILE, parentId: cnode[2], parentKey: cnode[3], data: cnode[4] }];
1052
1059
  break;
@@ -1078,6 +1085,17 @@ function* nodeStreamToCompactNodes(nodes) {
1078
1085
  const id = node[0];
1079
1086
  const crdt = node[1];
1080
1087
  yield [id, CrdtType.REGISTER, crdt.parentId, crdt.parentKey, crdt.data];
1088
+ } else if (isTextStorageNode(node)) {
1089
+ const id = node[0];
1090
+ const crdt = node[1];
1091
+ yield [
1092
+ id,
1093
+ CrdtType.TEXT,
1094
+ crdt.parentId,
1095
+ crdt.parentKey,
1096
+ crdt.data,
1097
+ crdt.version
1098
+ ];
1081
1099
  } else if (isFileStorageNode(node)) {
1082
1100
  const id = node[0];
1083
1101
  const crdt = node[1];
@@ -1087,6 +1105,42 @@ function* nodeStreamToCompactNodes(nodes) {
1087
1105
  }
1088
1106
  }
1089
1107
 
1108
+ // src/internal.ts
1109
+ var kInternal = /* @__PURE__ */ Symbol();
1110
+
1111
+ // src/lib/fancy-console.ts
1112
+ var fancy_console_exports = {};
1113
+ __export(fancy_console_exports, {
1114
+ error: () => error2,
1115
+ errorWithTitle: () => errorWithTitle,
1116
+ warn: () => warn,
1117
+ warnWithTitle: () => warnWithTitle
1118
+ });
1119
+ var badge = "background:#0e0d12;border-radius:9999px;color:#fff;padding:3px 7px;font-family:sans-serif;font-weight:600;";
1120
+ var bold = "font-weight:600";
1121
+ function wrap(method) {
1122
+ return typeof window === "undefined" || process.env.NODE_ENV === "test" ? console[method] : (
1123
+ /* istanbul ignore next */
1124
+ (message, ...args) => console[method]("%cLiveblocks", badge, message, ...args)
1125
+ );
1126
+ }
1127
+ var warn = wrap("warn");
1128
+ var error2 = wrap("error");
1129
+ function wrapWithTitle(method) {
1130
+ return typeof window === "undefined" || process.env.NODE_ENV === "test" ? console[method] : (
1131
+ /* istanbul ignore next */
1132
+ (title, message, ...args) => console[method](
1133
+ `%cLiveblocks%c ${title}`,
1134
+ badge,
1135
+ bold,
1136
+ message,
1137
+ ...args
1138
+ )
1139
+ );
1140
+ }
1141
+ var warnWithTitle = wrapWithTitle("warn");
1142
+ var errorWithTitle = wrapWithTitle("error");
1143
+
1090
1144
  // src/lib/position.ts
1091
1145
  var MIN_CODE = 32;
1092
1146
  var MAX_CODE = 126;
@@ -1266,6 +1320,18 @@ function asPos(str) {
1266
1320
  return isPos(str) ? str : convertToPos(str);
1267
1321
  }
1268
1322
 
1323
+ // src/crdts/StorageUpdates.ts
1324
+ var REMOTE = freeze({ origin: "remote" });
1325
+ var LOCAL_EDIT = freeze({ origin: "local", via: "edit" });
1326
+ var LOCAL_UNDO = freeze({ origin: "local", via: "undo" });
1327
+ var LOCAL_REDO = freeze({ origin: "local", via: "redo" });
1328
+ function toUpdateSource(source) {
1329
+ return source.origin === "remote" ? source : (
1330
+ // Removes `optimistic` field, which is not public
1331
+ { origin: "local", via: source.via }
1332
+ );
1333
+ }
1334
+
1269
1335
  // src/crdts/UnacknowledgedOps.ts
1270
1336
  var UnacknowledgedOps = class {
1271
1337
  // opId -> op
@@ -1284,6 +1350,10 @@ ${parentKey}`;
1284
1350
  get size() {
1285
1351
  return this.#byOpId.size;
1286
1352
  }
1353
+ /** The still-unacknowledged op with the given opId, if any. */
1354
+ get(opId) {
1355
+ return this.#byOpId.get(opId);
1356
+ }
1287
1357
  /**
1288
1358
  * Mark the given Op as still unacknowledged.
1289
1359
  */
@@ -1366,6 +1436,8 @@ ${parentKey}`;
1366
1436
  };
1367
1437
 
1368
1438
  // src/crdts/AbstractCrdt.ts
1439
+ var warnedOrphanedNodes = /* @__PURE__ */ new WeakSet();
1440
+ var ORPHANED_NODE_WARNING = "Cannot sync changes made to this Live structure because it is no longer part of Storage. Retrieve the current value from its parent before mutating it.";
1369
1441
  function createManagedPool(options) {
1370
1442
  const {
1371
1443
  getCurrentConnectionId,
@@ -1383,8 +1455,8 @@ function createManagedPool(options) {
1383
1455
  deleteNode: (id) => void nodes.delete(id),
1384
1456
  generateId: () => `${getCurrentConnectionId()}:${clock++}`,
1385
1457
  generateOpId: () => `${getCurrentConnectionId()}:${opClock++}`,
1386
- dispatch(ops, reverse, storageUpdates) {
1387
- onDispatch?.(ops, reverse, storageUpdates);
1458
+ dispatch(ops, reverse, storageUpdates, options2) {
1459
+ onDispatch?.(ops, reverse, storageUpdates, options2);
1388
1460
  },
1389
1461
  assertStorageIsWritable: () => {
1390
1462
  if (!isStorageWritable()) {
@@ -1407,10 +1479,17 @@ function Orphaned(oldKey, oldPos = asPos(oldKey)) {
1407
1479
  return Object.freeze({ type: "Orphaned", oldKey, oldPos });
1408
1480
  }
1409
1481
  var AbstractCrdt = class {
1410
- // ^^^^^^^^^^^^ TODO: Make this an interface
1411
1482
  #pool;
1412
1483
  #id;
1413
1484
  #parent = NoParent;
1485
+ constructor() {
1486
+ Object.defineProperty(this, kInternal, {
1487
+ value: {
1488
+ getId: () => this.#id
1489
+ },
1490
+ enumerable: false
1491
+ });
1492
+ }
1414
1493
  /** @internal */
1415
1494
  _getParentKeyOrThrow() {
1416
1495
  switch (this.parent.type) {
@@ -1450,6 +1529,17 @@ var AbstractCrdt = class {
1450
1529
  return this.#parent;
1451
1530
  }
1452
1531
  /** @internal */
1532
+ _warnIfOrphaned() {
1533
+ const node = crdtAsLiveNode(this);
1534
+ if (this.parent.type === "Orphaned" && !warnedOrphanedNodes.has(node)) {
1535
+ warnedOrphanedNodes.add(node);
1536
+ warn(ORPHANED_NODE_WARNING, {
1537
+ type: node.constructor.name,
1538
+ formerParentKey: this.parent.oldKey
1539
+ });
1540
+ }
1541
+ }
1542
+ /** @internal */
1453
1543
  get _parentKey() {
1454
1544
  switch (this.parent.type) {
1455
1545
  case "HasParent":
@@ -1463,11 +1553,14 @@ var AbstractCrdt = class {
1463
1553
  }
1464
1554
  }
1465
1555
  /** @internal */
1466
- _apply(op, _isLocal) {
1556
+ _apply(op, source) {
1467
1557
  switch (op.type) {
1468
1558
  case OpCode.DELETE_CRDT: {
1469
1559
  if (this.parent.type === "HasParent") {
1470
- return this.parent.node._detachChild(crdtAsLiveNode(this));
1560
+ return this.parent.node._detachChild(
1561
+ crdtAsLiveNode(this),
1562
+ toUpdateSource(source)
1563
+ );
1471
1564
  }
1472
1565
  return { modified: false };
1473
1566
  }
@@ -1657,8 +1750,8 @@ var LiveFile = class _LiveFile extends AbstractCrdt {
1657
1750
  throw new Error("A LiveFile node cannot have children");
1658
1751
  }
1659
1752
  /** @internal */
1660
- _apply(op, isLocal) {
1661
- return super._apply(op, isLocal);
1753
+ _apply(op, source) {
1754
+ return super._apply(op, source);
1662
1755
  }
1663
1756
  /** @internal */
1664
1757
  _toTreeNode(key) {
@@ -1678,39 +1771,6 @@ var LiveFile = class _LiveFile extends AbstractCrdt {
1678
1771
  }
1679
1772
  };
1680
1773
 
1681
- // src/lib/fancy-console.ts
1682
- var fancy_console_exports = {};
1683
- __export(fancy_console_exports, {
1684
- error: () => error2,
1685
- errorWithTitle: () => errorWithTitle,
1686
- warn: () => warn,
1687
- warnWithTitle: () => warnWithTitle
1688
- });
1689
- var badge = "background:#0e0d12;border-radius:9999px;color:#fff;padding:3px 7px;font-family:sans-serif;font-weight:600;";
1690
- var bold = "font-weight:600";
1691
- function wrap(method) {
1692
- return typeof window === "undefined" || process.env.NODE_ENV === "test" ? console[method] : (
1693
- /* istanbul ignore next */
1694
- (message, ...args) => console[method]("%cLiveblocks", badge, message, ...args)
1695
- );
1696
- }
1697
- var warn = wrap("warn");
1698
- var error2 = wrap("error");
1699
- function wrapWithTitle(method) {
1700
- return typeof window === "undefined" || process.env.NODE_ENV === "test" ? console[method] : (
1701
- /* istanbul ignore next */
1702
- (title, message, ...args) => console[method](
1703
- `%cLiveblocks%c ${title}`,
1704
- badge,
1705
- bold,
1706
- message,
1707
- ...args
1708
- )
1709
- );
1710
- }
1711
- var warnWithTitle = wrapWithTitle("warn");
1712
- var errorWithTitle = wrapWithTitle("error");
1713
-
1714
1774
  // src/lib/guards.ts
1715
1775
  function isDefined(value) {
1716
1776
  return value !== null && value !== void 0;
@@ -4877,9 +4937,6 @@ var ManagedSocket = class {
4877
4937
  }
4878
4938
  };
4879
4939
 
4880
- // src/internal.ts
4881
- var kInternal = /* @__PURE__ */ Symbol();
4882
-
4883
4940
  // src/lib/IncrementalJsonParser.ts
4884
4941
  var EMPTY_OBJECT = Object.freeze({});
4885
4942
  var NULL_KEYWORD_CHARS = Array.from(new Set("null"));
@@ -6881,8 +6938,8 @@ var LiveRegister = class _LiveRegister extends AbstractCrdt {
6881
6938
  throw new Error("Method not implemented.");
6882
6939
  }
6883
6940
  /** @internal */
6884
- _apply(op, isLocal) {
6885
- return super._apply(op, isLocal);
6941
+ _apply(op, source) {
6942
+ return super._apply(op, source);
6886
6943
  }
6887
6944
  /** @internal */
6888
6945
  _toTreeNode(key) {
@@ -7043,7 +7100,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7043
7100
  item._detach();
7044
7101
  }
7045
7102
  }
7046
- #applySetRemote(op) {
7103
+ #applyRemoteSet(op) {
7047
7104
  if (this._pool === void 0) {
7048
7105
  throw new Error("Can't attach child if managed pool is not present");
7049
7106
  }
@@ -7061,9 +7118,11 @@ var LiveList = class _LiveList extends AbstractCrdt {
7061
7118
  itemWithSamePosition._detach();
7062
7119
  this.#items.add(child);
7063
7120
  return {
7064
- modified: makeUpdate(this, [
7065
- setDelta(indexOfItemWithSamePosition, child)
7066
- ]),
7121
+ modified: makeUpdate(
7122
+ this,
7123
+ [setDelta(indexOfItemWithSamePosition, child)],
7124
+ REMOTE
7125
+ ),
7067
7126
  reverse: []
7068
7127
  };
7069
7128
  } else {
@@ -7074,20 +7133,22 @@ var LiveList = class _LiveList extends AbstractCrdt {
7074
7133
  setDelta(indexOfItemWithSamePosition, child)
7075
7134
  ];
7076
7135
  const deleteDelta2 = this.#detachItemAssociatedToSetOperation(
7077
- op.deletedId
7136
+ op.deletedId,
7137
+ REMOTE
7078
7138
  );
7079
7139
  if (deleteDelta2) {
7080
7140
  delta.push(deleteDelta2);
7081
7141
  }
7082
7142
  return {
7083
- modified: makeUpdate(this, delta),
7143
+ modified: makeUpdate(this, delta, REMOTE),
7084
7144
  reverse: []
7085
7145
  };
7086
7146
  }
7087
7147
  } else {
7088
7148
  const updates = [];
7089
7149
  const deleteDelta2 = this.#detachItemAssociatedToSetOperation(
7090
- op.deletedId
7150
+ op.deletedId,
7151
+ REMOTE
7091
7152
  );
7092
7153
  if (deleteDelta2) {
7093
7154
  updates.push(deleteDelta2);
@@ -7096,29 +7157,32 @@ var LiveList = class _LiveList extends AbstractCrdt {
7096
7157
  updates.push(insertDelta(this._indexOfPosition(key), child));
7097
7158
  return {
7098
7159
  reverse: [],
7099
- modified: makeUpdate(this, updates)
7160
+ modified: makeUpdate(this, updates, REMOTE)
7100
7161
  };
7101
7162
  }
7102
7163
  }
7103
- #applySetAck(op) {
7164
+ #applySetAck(op, source) {
7104
7165
  if (this._pool === void 0) {
7105
7166
  throw new Error("Can't attach child if managed pool is not present");
7106
7167
  }
7107
7168
  const delta = [];
7108
- const deletedDelta = this.#detachItemAssociatedToSetOperation(op.deletedId);
7169
+ const deletedDelta = this.#detachItemAssociatedToSetOperation(
7170
+ op.deletedId,
7171
+ source
7172
+ );
7109
7173
  if (deletedDelta) {
7110
7174
  delta.push(deletedDelta);
7111
7175
  }
7112
7176
  const unacknowledgedOpId = this.#unacknowledgedSetOpIdAt(op.parentKey);
7113
7177
  if (unacknowledgedOpId !== void 0 && unacknowledgedOpId !== op.opId) {
7114
- return delta.length === 0 ? { modified: false } : { modified: makeUpdate(this, delta), reverse: [] };
7178
+ return delta.length === 0 ? { modified: false } : { modified: makeUpdate(this, delta, source), reverse: [] };
7115
7179
  }
7116
7180
  const indexOfItemWithSamePosition = this._indexOfPosition(op.parentKey);
7117
7181
  const existingItem = this.#items.find((item) => item._id === op.id);
7118
7182
  if (existingItem !== void 0) {
7119
7183
  if (existingItem._parentKey === op.parentKey) {
7120
7184
  return {
7121
- modified: delta.length > 0 ? makeUpdate(this, delta) : false,
7185
+ modified: delta.length > 0 ? makeUpdate(this, delta, source) : false,
7122
7186
  reverse: []
7123
7187
  };
7124
7188
  }
@@ -7136,7 +7200,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7136
7200
  delta.push(moveDelta(prevIndex, newIndex, existingItem));
7137
7201
  }
7138
7202
  return {
7139
- modified: delta.length > 0 ? makeUpdate(this, delta) : false,
7203
+ modified: delta.length > 0 ? makeUpdate(this, delta, source) : false,
7140
7204
  reverse: []
7141
7205
  };
7142
7206
  } else {
@@ -7146,11 +7210,15 @@ var LiveList = class _LiveList extends AbstractCrdt {
7146
7210
  this.#implicitlyDeletedItems.delete(orphan);
7147
7211
  const recreatedItemIndex = this.#insert(orphan);
7148
7212
  return {
7149
- modified: makeUpdate(this, [
7150
- // If there is an item at this position, update is a set, else it's an insert
7151
- indexOfItemWithSamePosition === -1 ? insertDelta(recreatedItemIndex, orphan) : setDelta(recreatedItemIndex, orphan),
7152
- ...delta
7153
- ]),
7213
+ modified: makeUpdate(
7214
+ this,
7215
+ [
7216
+ // If there is an item at this position, update is a set, else it's an insert
7217
+ indexOfItemWithSamePosition === -1 ? insertDelta(recreatedItemIndex, orphan) : setDelta(recreatedItemIndex, orphan),
7218
+ ...delta
7219
+ ],
7220
+ source
7221
+ ),
7154
7222
  reverse: []
7155
7223
  };
7156
7224
  } else {
@@ -7165,11 +7233,15 @@ var LiveList = class _LiveList extends AbstractCrdt {
7165
7233
  op.parentKey
7166
7234
  );
7167
7235
  return {
7168
- modified: makeUpdate(this, [
7169
- // If there is an item at this position, update is a set, else it's an insert
7170
- indexOfItemWithSamePosition === -1 ? insertDelta(newIndex, newItem) : setDelta(newIndex, newItem),
7171
- ...delta
7172
- ]),
7236
+ modified: makeUpdate(
7237
+ this,
7238
+ [
7239
+ // If there is an item at this position, update is a set, else it's an insert
7240
+ indexOfItemWithSamePosition === -1 ? insertDelta(newIndex, newItem) : setDelta(newIndex, newItem),
7241
+ ...delta
7242
+ ],
7243
+ source
7244
+ ),
7173
7245
  reverse: []
7174
7246
  };
7175
7247
  }
@@ -7178,7 +7250,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7178
7250
  /**
7179
7251
  * Returns the update delta of the deletion or null
7180
7252
  */
7181
- #detachItemAssociatedToSetOperation(deletedId) {
7253
+ #detachItemAssociatedToSetOperation(deletedId, source) {
7182
7254
  if (deletedId === void 0 || this._pool === void 0) {
7183
7255
  return null;
7184
7256
  }
@@ -7186,7 +7258,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7186
7258
  if (deletedItem === void 0) {
7187
7259
  return null;
7188
7260
  }
7189
- const result = this._detachChild(deletedItem);
7261
+ const result = this._detachChild(deletedItem, source);
7190
7262
  if (result.modified === false) {
7191
7263
  return null;
7192
7264
  }
@@ -7204,10 +7276,11 @@ var LiveList = class _LiveList extends AbstractCrdt {
7204
7276
  const { newItem, newIndex } = this.#createAttachItemAndSort(op, key);
7205
7277
  const bumpDeltas = this.#bumpUnackedPushesAbove(key);
7206
7278
  return {
7207
- modified: makeUpdate(this, [
7208
- insertDelta(newIndex, newItem),
7209
- ...bumpDeltas
7210
- ]),
7279
+ modified: makeUpdate(
7280
+ this,
7281
+ [insertDelta(newIndex, newItem), ...bumpDeltas],
7282
+ REMOTE
7283
+ ),
7211
7284
  reverse: []
7212
7285
  };
7213
7286
  }
@@ -7288,7 +7361,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7288
7361
  }
7289
7362
  return deltas;
7290
7363
  }
7291
- #applyInsertAck(op) {
7364
+ #applyInsertAck(op, source) {
7292
7365
  const existingItem = this.#items.find((item) => item._id === op.id);
7293
7366
  const key = asPos(op.parentKey);
7294
7367
  const itemIndexAtPosition = this._indexOfPosition(key);
@@ -7310,9 +7383,11 @@ var LiveList = class _LiveList extends AbstractCrdt {
7310
7383
  return { modified: false };
7311
7384
  }
7312
7385
  return {
7313
- modified: makeUpdate(this, [
7314
- moveDelta(oldPositionIndex, newIndex, existingItem)
7315
- ]),
7386
+ modified: makeUpdate(
7387
+ this,
7388
+ [moveDelta(oldPositionIndex, newIndex, existingItem)],
7389
+ source
7390
+ ),
7316
7391
  reverse: []
7317
7392
  };
7318
7393
  }
@@ -7324,7 +7399,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7324
7399
  this.#insert(orphan);
7325
7400
  const newIndex = this._indexOfPosition(key);
7326
7401
  return {
7327
- modified: makeUpdate(this, [insertDelta(newIndex, orphan)]),
7402
+ modified: makeUpdate(this, [insertDelta(newIndex, orphan)], source),
7328
7403
  reverse: []
7329
7404
  };
7330
7405
  } else {
@@ -7333,13 +7408,13 @@ var LiveList = class _LiveList extends AbstractCrdt {
7333
7408
  }
7334
7409
  const { newItem, newIndex } = this.#createAttachItemAndSort(op, key);
7335
7410
  return {
7336
- modified: makeUpdate(this, [insertDelta(newIndex, newItem)]),
7411
+ modified: makeUpdate(this, [insertDelta(newIndex, newItem)], source),
7337
7412
  reverse: []
7338
7413
  };
7339
7414
  }
7340
7415
  }
7341
7416
  }
7342
- #applyInsertUndoRedo(op) {
7417
+ #applyLocalInsert(op, source) {
7343
7418
  const { id, parentKey: key } = op;
7344
7419
  const child = creationOpToLiveNode(op);
7345
7420
  if (this._pool?.getNode(id) !== void 0) {
@@ -7358,11 +7433,11 @@ var LiveList = class _LiveList extends AbstractCrdt {
7358
7433
  this.#insert(child);
7359
7434
  const newIndex = this._indexOfPosition(newKey);
7360
7435
  return {
7361
- modified: makeUpdate(this, [insertDelta(newIndex, child)]),
7436
+ modified: makeUpdate(this, [insertDelta(newIndex, child)], source),
7362
7437
  reverse: [{ type: OpCode.DELETE_CRDT, id }]
7363
7438
  };
7364
7439
  }
7365
- #applySetUndoRedo(op) {
7440
+ #applyLocalSet(op, source) {
7366
7441
  const { id, parentKey: key } = op;
7367
7442
  const child = creationOpToLiveNode(op);
7368
7443
  if (this._pool?.getNode(id) !== void 0) {
@@ -7384,46 +7459,48 @@ var LiveList = class _LiveList extends AbstractCrdt {
7384
7459
  );
7385
7460
  const delta = [setDelta(indexOfItemWithSameKey, child)];
7386
7461
  const deletedDelta = this.#detachItemAssociatedToSetOperation(
7387
- op.deletedId
7462
+ op.deletedId,
7463
+ source
7388
7464
  );
7389
7465
  if (deletedDelta) {
7390
7466
  delta.push(deletedDelta);
7391
7467
  }
7392
7468
  return {
7393
- modified: makeUpdate(this, delta),
7469
+ modified: makeUpdate(this, delta, source),
7394
7470
  reverse
7395
7471
  };
7396
7472
  } else {
7397
7473
  this.#insert(child);
7398
- this.#detachItemAssociatedToSetOperation(op.deletedId);
7474
+ this.#detachItemAssociatedToSetOperation(op.deletedId, source);
7399
7475
  const newIndex = this._indexOfPosition(newKey);
7400
7476
  return {
7401
7477
  reverse: [{ type: OpCode.DELETE_CRDT, id }],
7402
- modified: makeUpdate(this, [insertDelta(newIndex, child)])
7478
+ modified: makeUpdate(this, [insertDelta(newIndex, child)], source)
7403
7479
  };
7404
7480
  }
7405
7481
  }
7406
7482
  /** @internal */
7407
- _attachChild(op, source) {
7483
+ _attachChild(op, opSource) {
7484
+ const source = toUpdateSource(opSource);
7408
7485
  if (this._pool === void 0) {
7409
7486
  throw new Error("Can't attach child if managed pool is not present");
7410
7487
  }
7411
7488
  let result;
7412
7489
  if (op.intent === "set") {
7413
- if (source === 1 /* THEIRS */) {
7414
- result = this.#applySetRemote(op);
7415
- } else if (source === 2 /* OURS */) {
7416
- result = this.#applySetAck(op);
7490
+ if (opSource.origin === "remote") {
7491
+ result = this.#applyRemoteSet(op);
7492
+ } else if (!opSource.optimistic) {
7493
+ result = this.#applySetAck(op, source);
7417
7494
  } else {
7418
- result = this.#applySetUndoRedo(op);
7495
+ result = this.#applyLocalSet(op, source);
7419
7496
  }
7420
7497
  } else {
7421
- if (source === 1 /* THEIRS */) {
7498
+ if (opSource.origin === "remote") {
7422
7499
  result = this.#applyRemoteInsert(op);
7423
- } else if (source === 2 /* OURS */) {
7424
- result = this.#applyInsertAck(op);
7500
+ } else if (!opSource.optimistic) {
7501
+ result = this.#applyInsertAck(op, source);
7425
7502
  } else {
7426
- result = this.#applyInsertUndoRedo(op);
7503
+ result = this.#applyLocalInsert(op, source);
7427
7504
  }
7428
7505
  }
7429
7506
  if (result.modified !== false) {
@@ -7432,7 +7509,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7432
7509
  return result;
7433
7510
  }
7434
7511
  /** @internal */
7435
- _detachChild(child) {
7512
+ _detachChild(child, source) {
7436
7513
  if (child) {
7437
7514
  const parentKey = nn(child._parentKey);
7438
7515
  const reverse = child._toOps(nn(this._id), parentKey);
@@ -7447,19 +7524,23 @@ var LiveList = class _LiveList extends AbstractCrdt {
7447
7524
  this.invalidate();
7448
7525
  child._detach();
7449
7526
  return {
7450
- modified: makeUpdate(this, [deleteDelta(indexToDelete, previousNode)]),
7527
+ modified: makeUpdate(
7528
+ this,
7529
+ [deleteDelta(indexToDelete, previousNode)],
7530
+ source
7531
+ ),
7451
7532
  reverse
7452
7533
  };
7453
7534
  }
7454
7535
  return { modified: false };
7455
7536
  }
7456
- #applySetChildKeyRemote(newKey, child) {
7537
+ #applyRemoteSetChildKey(newKey, child) {
7457
7538
  if (this.#implicitlyDeletedItems.has(child)) {
7458
7539
  this.#implicitlyDeletedItems.delete(child);
7459
7540
  child._setParentLink(this, newKey);
7460
7541
  const newIndex = this.#insert(child);
7461
7542
  return {
7462
- modified: makeUpdate(this, [insertDelta(newIndex, child)]),
7543
+ modified: makeUpdate(this, [insertDelta(newIndex, child)], REMOTE),
7463
7544
  reverse: []
7464
7545
  };
7465
7546
  }
@@ -7480,7 +7561,11 @@ var LiveList = class _LiveList extends AbstractCrdt {
7480
7561
  };
7481
7562
  }
7482
7563
  return {
7483
- modified: makeUpdate(this, [moveDelta(previousIndex, newIndex, child)]),
7564
+ modified: makeUpdate(
7565
+ this,
7566
+ [moveDelta(previousIndex, newIndex, child)],
7567
+ REMOTE
7568
+ ),
7484
7569
  reverse: []
7485
7570
  };
7486
7571
  } else {
@@ -7497,12 +7582,16 @@ var LiveList = class _LiveList extends AbstractCrdt {
7497
7582
  };
7498
7583
  }
7499
7584
  return {
7500
- modified: makeUpdate(this, [moveDelta(previousIndex, newIndex, child)]),
7585
+ modified: makeUpdate(
7586
+ this,
7587
+ [moveDelta(previousIndex, newIndex, child)],
7588
+ REMOTE
7589
+ ),
7501
7590
  reverse: []
7502
7591
  };
7503
7592
  }
7504
7593
  }
7505
- #applySetChildKeyAck(newKey, child) {
7594
+ #applySetChildKeyAck(newKey, child, source) {
7506
7595
  const previousKey = nn(child._parentKey);
7507
7596
  if (this.#implicitlyDeletedItems.has(child)) {
7508
7597
  const existingItemIndex = this._indexOfPosition(newKey);
@@ -7521,7 +7610,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7521
7610
  child._setParentLink(this, newKey);
7522
7611
  const newIndex = this.#insert(child);
7523
7612
  return {
7524
- modified: makeUpdate(this, [insertDelta(newIndex, child)]),
7613
+ modified: makeUpdate(this, [insertDelta(newIndex, child)], source),
7525
7614
  reverse: []
7526
7615
  };
7527
7616
  } else {
@@ -7549,15 +7638,17 @@ var LiveList = class _LiveList extends AbstractCrdt {
7549
7638
  };
7550
7639
  } else {
7551
7640
  return {
7552
- modified: makeUpdate(this, [
7553
- moveDelta(previousIndex, newIndex, child)
7554
- ]),
7641
+ modified: makeUpdate(
7642
+ this,
7643
+ [moveDelta(previousIndex, newIndex, child)],
7644
+ source
7645
+ ),
7555
7646
  reverse: []
7556
7647
  };
7557
7648
  }
7558
7649
  }
7559
7650
  }
7560
- #applySetChildKeyUndoRedo(newKey, child) {
7651
+ #applyLocalSetChildKey(newKey, child, source) {
7561
7652
  const previousKey = nn(child._parentKey);
7562
7653
  const previousIndex = this.#items.findIndex((item) => item === child);
7563
7654
  const existingItemIndex = this._indexOfPosition(newKey);
@@ -7576,7 +7667,11 @@ var LiveList = class _LiveList extends AbstractCrdt {
7576
7667
  };
7577
7668
  }
7578
7669
  return {
7579
- modified: makeUpdate(this, [moveDelta(previousIndex, newIndex, child)]),
7670
+ modified: makeUpdate(
7671
+ this,
7672
+ [moveDelta(previousIndex, newIndex, child)],
7673
+ source
7674
+ ),
7580
7675
  reverse: [
7581
7676
  {
7582
7677
  type: OpCode.SET_PARENT_KEY,
@@ -7587,18 +7682,19 @@ var LiveList = class _LiveList extends AbstractCrdt {
7587
7682
  };
7588
7683
  }
7589
7684
  /** @internal */
7590
- _setChildKey(newKey, child, source) {
7591
- if (source === 1 /* THEIRS */) {
7592
- return this.#applySetChildKeyRemote(newKey, child);
7593
- } else if (source === 2 /* OURS */) {
7594
- return this.#applySetChildKeyAck(newKey, child);
7685
+ _setChildKey(newKey, child, opSource) {
7686
+ const source = toUpdateSource(opSource);
7687
+ if (opSource.origin === "remote") {
7688
+ return this.#applyRemoteSetChildKey(newKey, child);
7689
+ } else if (!opSource.optimistic) {
7690
+ return this.#applySetChildKeyAck(newKey, child, source);
7595
7691
  } else {
7596
- return this.#applySetChildKeyUndoRedo(newKey, child);
7692
+ return this.#applyLocalSetChildKey(newKey, child, source);
7597
7693
  }
7598
7694
  }
7599
7695
  /** @internal */
7600
- _apply(op, isLocal) {
7601
- return super._apply(op, isLocal);
7696
+ _apply(op, source) {
7697
+ return super._apply(op, source);
7602
7698
  }
7603
7699
  /** @internal */
7604
7700
  _serialize() {
@@ -7639,6 +7735,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7639
7735
  * instead of resolving its position against the client's stale view.
7640
7736
  */
7641
7737
  #injectAt(element, index, intent) {
7738
+ this._warnIfOrphaned();
7642
7739
  this._pool?.assertStorageIsWritable();
7643
7740
  if (index < 0 || index > this.#items.length) {
7644
7741
  throw new Error(
@@ -7659,7 +7756,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7659
7756
  intent === "push" ? addIntentToRootOp(ops, "push") : ops,
7660
7757
  [{ type: OpCode.DELETE_CRDT, id }],
7661
7758
  /* @__PURE__ */ new Map([
7662
- [this._id, makeUpdate(this, [insertDelta(index, value)])]
7759
+ [this._id, makeUpdate(this, [insertDelta(index, value)], LOCAL_EDIT)]
7663
7760
  ])
7664
7761
  );
7665
7762
  }
@@ -7670,6 +7767,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7670
7767
  * @param targetIndex The index where the element should be after moving.
7671
7768
  */
7672
7769
  move(index, targetIndex) {
7770
+ this._warnIfOrphaned();
7673
7771
  this._pool?.assertStorageIsWritable();
7674
7772
  if (targetIndex < 0) {
7675
7773
  throw new Error("targetIndex cannot be less than 0");
@@ -7700,7 +7798,10 @@ var LiveList = class _LiveList extends AbstractCrdt {
7700
7798
  this.#updateItemPositionAt(index, position);
7701
7799
  if (this._pool && this._id) {
7702
7800
  const storageUpdates = /* @__PURE__ */ new Map([
7703
- [this._id, makeUpdate(this, [moveDelta(index, targetIndex, item)])]
7801
+ [
7802
+ this._id,
7803
+ makeUpdate(this, [moveDelta(index, targetIndex, item)], LOCAL_EDIT)
7804
+ ]
7704
7805
  ]);
7705
7806
  this._pool.dispatch(
7706
7807
  [
@@ -7727,6 +7828,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7727
7828
  * @param index The index of the element to delete
7728
7829
  */
7729
7830
  delete(index) {
7831
+ this._warnIfOrphaned();
7730
7832
  this._pool?.assertStorageIsWritable();
7731
7833
  if (index < 0 || index >= this.#items.length) {
7732
7834
  throw new Error(
@@ -7743,7 +7845,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7743
7845
  const storageUpdates = /* @__PURE__ */ new Map();
7744
7846
  storageUpdates.set(
7745
7847
  nn(this._id),
7746
- makeUpdate(this, [deleteDelta(index, item)])
7848
+ makeUpdate(this, [deleteDelta(index, item)], LOCAL_EDIT)
7747
7849
  );
7748
7850
  this._pool.dispatch(
7749
7851
  [
@@ -7760,6 +7862,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7760
7862
  }
7761
7863
  }
7762
7864
  clear() {
7865
+ this._warnIfOrphaned();
7763
7866
  this._pool?.assertStorageIsWritable();
7764
7867
  if (this._pool) {
7765
7868
  const ops = [];
@@ -7783,7 +7886,10 @@ var LiveList = class _LiveList extends AbstractCrdt {
7783
7886
  this.#items.clear();
7784
7887
  this.invalidate();
7785
7888
  const storageUpdates = /* @__PURE__ */ new Map();
7786
- storageUpdates.set(nn(this._id), makeUpdate(this, updateDelta));
7889
+ storageUpdates.set(
7890
+ nn(this._id),
7891
+ makeUpdate(this, updateDelta, LOCAL_EDIT)
7892
+ );
7787
7893
  this._pool.dispatch(ops, reverseOps, storageUpdates);
7788
7894
  } else {
7789
7895
  for (const item of this.#items) {
@@ -7794,6 +7900,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
7794
7900
  }
7795
7901
  }
7796
7902
  set(index, item) {
7903
+ this._warnIfOrphaned();
7797
7904
  this._pool?.assertStorageIsWritable();
7798
7905
  if (index < 0 || index >= this.#items.length) {
7799
7906
  throw new Error(
@@ -7813,7 +7920,10 @@ var LiveList = class _LiveList extends AbstractCrdt {
7813
7920
  const id = this._pool.generateId();
7814
7921
  value._attach(id, this._pool);
7815
7922
  const storageUpdates = /* @__PURE__ */ new Map();
7816
- storageUpdates.set(this._id, makeUpdate(this, [setDelta(index, value)]));
7923
+ storageUpdates.set(
7924
+ this._id,
7925
+ makeUpdate(this, [setDelta(index, value)], LOCAL_EDIT)
7926
+ );
7817
7927
  const ops = addIntentToRootOp(
7818
7928
  value._toOpsWithOpId(this._id, position, this._pool),
7819
7929
  "set",
@@ -7986,11 +8096,12 @@ var LiveList = class _LiveList extends AbstractCrdt {
7986
8096
  );
7987
8097
  }
7988
8098
  };
7989
- function makeUpdate(liveList, deltaUpdates) {
8099
+ function makeUpdate(liveList, deltaUpdates, source) {
7990
8100
  return {
7991
8101
  node: liveList,
7992
8102
  type: "LiveList",
7993
- updates: deltaUpdates
8103
+ updates: deltaUpdates,
8104
+ source
7994
8105
  };
7995
8106
  }
7996
8107
  function setDelta(index, item) {
@@ -8109,7 +8220,9 @@ var LiveMap = class _LiveMap extends AbstractCrdt {
8109
8220
  if (this._pool.getNode(id) !== void 0) {
8110
8221
  return { modified: false };
8111
8222
  }
8112
- if (source === 2 /* OURS */) {
8223
+ if (source.origin === "remote") {
8224
+ this.#unacknowledgedSet.delete(key);
8225
+ } else if (!source.optimistic) {
8113
8226
  const lastUpdateOpId = this.#unacknowledgedSet.get(key);
8114
8227
  if (lastUpdateOpId === opId) {
8115
8228
  this.#unacknowledgedSet.delete(key);
@@ -8117,8 +8230,6 @@ var LiveMap = class _LiveMap extends AbstractCrdt {
8117
8230
  } else if (lastUpdateOpId !== void 0) {
8118
8231
  return { modified: false };
8119
8232
  }
8120
- } else if (source === 1 /* THEIRS */) {
8121
- this.#unacknowledgedSet.delete(key);
8122
8233
  }
8123
8234
  const previousValue = this.#map.get(key);
8124
8235
  let reverse;
@@ -8137,7 +8248,8 @@ var LiveMap = class _LiveMap extends AbstractCrdt {
8137
8248
  modified: {
8138
8249
  node: this,
8139
8250
  type: "LiveMap",
8140
- updates: { [key]: { type: "update" } }
8251
+ updates: { [key]: { type: "update" } },
8252
+ source: toUpdateSource(source)
8141
8253
  },
8142
8254
  reverse
8143
8255
  };
@@ -8150,7 +8262,7 @@ var LiveMap = class _LiveMap extends AbstractCrdt {
8150
8262
  }
8151
8263
  }
8152
8264
  /** @internal */
8153
- _detachChild(child) {
8265
+ _detachChild(child, source) {
8154
8266
  const id = nn(this._id);
8155
8267
  const parentKey = nn(child._parentKey);
8156
8268
  const reverse = child._toOps(id, parentKey);
@@ -8169,7 +8281,8 @@ var LiveMap = class _LiveMap extends AbstractCrdt {
8169
8281
  type: "delete",
8170
8282
  deletedItem: liveNodeToLson(child)
8171
8283
  }
8172
- }
8284
+ },
8285
+ source
8173
8286
  };
8174
8287
  return { modified: storageUpdate, reverse };
8175
8288
  }
@@ -8202,6 +8315,7 @@ var LiveMap = class _LiveMap extends AbstractCrdt {
8202
8315
  * @param value The value of the element to add. Should be serializable to JSON.
8203
8316
  */
8204
8317
  set(key, value) {
8318
+ this._warnIfOrphaned();
8205
8319
  this._pool?.assertStorageIsWritable();
8206
8320
  const oldValue = this.#map.get(key);
8207
8321
  if (oldValue) {
@@ -8218,7 +8332,8 @@ var LiveMap = class _LiveMap extends AbstractCrdt {
8218
8332
  storageUpdates.set(this._id, {
8219
8333
  node: this,
8220
8334
  type: "LiveMap",
8221
- updates: { [key]: { type: "update" } }
8335
+ updates: { [key]: { type: "update" } },
8336
+ source: LOCAL_EDIT
8222
8337
  });
8223
8338
  const ops = item._toOpsWithOpId(this._id, key, this._pool);
8224
8339
  this.#unacknowledgedSet.set(key, nn(ops[0].opId));
@@ -8248,6 +8363,7 @@ var LiveMap = class _LiveMap extends AbstractCrdt {
8248
8363
  * @returns true if an element existed and has been removed, or false if the element does not exist.
8249
8364
  */
8250
8365
  delete(key) {
8366
+ this._warnIfOrphaned();
8251
8367
  this._pool?.assertStorageIsWritable();
8252
8368
  const item = this.#map.get(key);
8253
8369
  if (item === void 0) {
@@ -8267,7 +8383,8 @@ var LiveMap = class _LiveMap extends AbstractCrdt {
8267
8383
  type: "delete",
8268
8384
  deletedItem: liveNodeToLson(item)
8269
8385
  }
8270
- }
8386
+ },
8387
+ source: LOCAL_EDIT
8271
8388
  });
8272
8389
  this._pool.dispatch(
8273
8390
  [
@@ -8632,7 +8749,7 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8632
8749
  }
8633
8750
  return { modified: false };
8634
8751
  }
8635
- if (source === 0 /* LOCAL */) {
8752
+ if (source.origin === "local" && source.optimistic) {
8636
8753
  this.#unackedOpsByKey.set(key, nn(opId));
8637
8754
  } else if (this.#unackedOpsByKey.get(key) === void 0) {
8638
8755
  } else if (this.#unackedOpsByKey.get(key) === opId) {
@@ -8670,12 +8787,13 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8670
8787
  modified: {
8671
8788
  node: this,
8672
8789
  type: "LiveObject",
8673
- updates: { [key]: { type: "update" } }
8790
+ updates: { [key]: { type: "update" } },
8791
+ source: toUpdateSource(source)
8674
8792
  }
8675
8793
  };
8676
8794
  }
8677
8795
  /** @internal */
8678
- _detachChild(child) {
8796
+ _detachChild(child, source) {
8679
8797
  if (child) {
8680
8798
  const id = nn(this._id);
8681
8799
  const parentKey = nn(child._parentKey);
@@ -8693,7 +8811,8 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8693
8811
  type: "LiveObject",
8694
8812
  updates: {
8695
8813
  [parentKey]: { type: "delete", deletedItem }
8696
- }
8814
+ },
8815
+ source
8697
8816
  };
8698
8817
  return { modified: storageUpdate, reverse };
8699
8818
  }
@@ -8709,13 +8828,13 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8709
8828
  }
8710
8829
  }
8711
8830
  /** @internal */
8712
- _apply(op, isLocal) {
8831
+ _apply(op, source) {
8713
8832
  if (op.type === OpCode.UPDATE_OBJECT) {
8714
- return this.#applyUpdate(op, isLocal);
8833
+ return this.#applyUpdate(op, source);
8715
8834
  } else if (op.type === OpCode.DELETE_OBJECT_KEY) {
8716
- return this.#applyDeleteObjectKey(op, isLocal);
8835
+ return this.#applyDeleteObjectKey(op, source);
8717
8836
  }
8718
- return super._apply(op, isLocal);
8837
+ return super._apply(op, source);
8719
8838
  }
8720
8839
  /** @internal */
8721
8840
  _serialize() {
@@ -8739,7 +8858,7 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8739
8858
  };
8740
8859
  }
8741
8860
  }
8742
- #applyUpdate(op, isLocal) {
8861
+ #applyUpdate(op, source) {
8743
8862
  let isModified = false;
8744
8863
  const id = nn(this._id);
8745
8864
  const reverse = [];
@@ -8767,7 +8886,7 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8767
8886
  if (value === void 0) {
8768
8887
  continue;
8769
8888
  }
8770
- if (isLocal) {
8889
+ if (source.origin === "local" && source.optimistic) {
8771
8890
  this.#unackedOpsByKey.set(key, nn(op.opId));
8772
8891
  } else if (this.#unackedOpsByKey.get(key) === void 0) {
8773
8892
  isModified = true;
@@ -8794,18 +8913,19 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8794
8913
  modified: {
8795
8914
  node: this,
8796
8915
  type: "LiveObject",
8797
- updates: updateDelta
8916
+ updates: updateDelta,
8917
+ source: toUpdateSource(source)
8798
8918
  },
8799
8919
  reverse
8800
8920
  } : { modified: false };
8801
8921
  }
8802
- #applyDeleteObjectKey(op, isLocal) {
8922
+ #applyDeleteObjectKey(op, source) {
8803
8923
  const key = op.key;
8804
8924
  const oldValue = this.#synced.get(key);
8805
8925
  if (oldValue === void 0) {
8806
8926
  return { modified: false };
8807
8927
  }
8808
- if (!isLocal && this.#unackedOpsByKey.get(key) !== void 0) {
8928
+ if (!(source.origin === "local" && source.optimistic) && this.#unackedOpsByKey.get(key) !== void 0) {
8809
8929
  return { modified: false };
8810
8930
  }
8811
8931
  const id = nn(this._id);
@@ -8831,7 +8951,8 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8831
8951
  type: "LiveObject",
8832
8952
  updates: {
8833
8953
  [op.key]: { type: "delete", deletedItem: oldValue }
8834
- }
8954
+ },
8955
+ source: toUpdateSource(source)
8835
8956
  },
8836
8957
  reverse
8837
8958
  };
@@ -8862,6 +8983,7 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8862
8983
  * Caveat: this method will not add changes to the undo/redo stack.
8863
8984
  */
8864
8985
  setLocal(key, value) {
8986
+ this._warnIfOrphaned();
8865
8987
  this._pool?.assertStorageIsWritable();
8866
8988
  const deleteResult = this.#prepareDelete(key);
8867
8989
  this.#local.set(key, value);
@@ -8877,7 +8999,8 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8877
8999
  updates: {
8878
9000
  ...existing?.updates,
8879
9001
  [key]: { type: "update" }
8880
- }
9002
+ },
9003
+ source: LOCAL_EDIT
8881
9004
  });
8882
9005
  this._pool.dispatch(ops, reverse, storageUpdates);
8883
9006
  }
@@ -8895,6 +9018,7 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8895
9018
  * #synced or pool/id are unavailable. Does NOT dispatch.
8896
9019
  */
8897
9020
  #prepareDelete(key) {
9021
+ this._warnIfOrphaned();
8898
9022
  this._pool?.assertStorageIsWritable();
8899
9023
  const k = key;
8900
9024
  if (this.#local.has(k) && !this.#synced.has(k)) {
@@ -8911,7 +9035,8 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8911
9035
  type: "delete",
8912
9036
  deletedItem: oldValue2
8913
9037
  }
8914
- }
9038
+ },
9039
+ source: LOCAL_EDIT
8915
9040
  });
8916
9041
  return [[], [], storageUpdates2];
8917
9042
  }
@@ -8959,7 +9084,8 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8959
9084
  type: "LiveObject",
8960
9085
  updates: {
8961
9086
  [key]: { type: "delete", deletedItem: oldValue }
8962
- }
9087
+ },
9088
+ source: LOCAL_EDIT
8963
9089
  });
8964
9090
  return [ops, reverse, storageUpdates];
8965
9091
  }
@@ -8979,6 +9105,7 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8979
9105
  * @param patch The object used to overrides properties
8980
9106
  */
8981
9107
  update(patch) {
9108
+ this._warnIfOrphaned();
8982
9109
  this._pool?.assertStorageIsWritable();
8983
9110
  if (_LiveObject.detectLargeObjects) {
8984
9111
  const data = {};
@@ -9097,7 +9224,8 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
9097
9224
  storageUpdates.set(this._id, {
9098
9225
  node: this,
9099
9226
  type: "LiveObject",
9100
- updates: updateDelta
9227
+ updates: updateDelta,
9228
+ source: LOCAL_EDIT
9101
9229
  });
9102
9230
  this._pool.dispatch(ops, reverseOps, storageUpdates);
9103
9231
  }
@@ -9179,123 +9307,1313 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
9179
9307
  }
9180
9308
  };
9181
9309
 
9182
- // src/crdts/liveblocks-helpers.ts
9183
- function creationOpToLiveNode(op) {
9184
- return lsonToLiveNode(creationOpToLson(op));
9185
- }
9186
- function creationOpToLson(op) {
9187
- switch (op.type) {
9188
- case OpCode.CREATE_FILE:
9189
- return new LiveFile(op.data);
9190
- case OpCode.CREATE_REGISTER:
9191
- return op.data;
9192
- case OpCode.CREATE_OBJECT:
9193
- return new LiveObject(op.data);
9194
- case OpCode.CREATE_MAP:
9195
- return new LiveMap();
9196
- case OpCode.CREATE_LIST:
9197
- return new LiveList([]);
9198
- default:
9199
- return assertNever(op, "Unknown creation Op");
9200
- }
9201
- }
9202
- function isSameNodeOrChildOf(node, parent) {
9203
- if (node === parent) {
9310
+ // src/crdts/liveTextOps.ts
9311
+ function attributesEqual(left, right) {
9312
+ if (left === right) {
9204
9313
  return true;
9205
9314
  }
9206
- if (node.parent.type === "HasParent") {
9207
- return isSameNodeOrChildOf(node.parent.node, parent);
9315
+ if (left === void 0 || right === void 0) {
9316
+ return false;
9208
9317
  }
9209
- return false;
9210
- }
9211
- function liveObjectFromNodeStream(nodes) {
9212
- const pool = createManagedPool({
9213
- getCurrentConnectionId: () => {
9214
- throw new Error(
9215
- "Cannot mutate a historic storage version: it is a read-only snapshot"
9216
- );
9318
+ const leftKeys = Object.keys(left);
9319
+ const rightKeys = Object.keys(right);
9320
+ if (leftKeys.length !== rightKeys.length) {
9321
+ return false;
9322
+ }
9323
+ for (const key of leftKeys) {
9324
+ if (left[key] !== right[key]) {
9325
+ return false;
9217
9326
  }
9218
- });
9219
- return LiveObject._fromItems(nodes, pool);
9220
- }
9221
- function deserialize(node, parentToChildren, pool) {
9222
- if (isObjectStorageNode(node)) {
9223
- return LiveObject._deserialize(node, parentToChildren, pool);
9224
- } else if (isListStorageNode(node)) {
9225
- return LiveList._deserialize(node, parentToChildren, pool);
9226
- } else if (isMapStorageNode(node)) {
9227
- return LiveMap._deserialize(node, parentToChildren, pool);
9228
- } else if (isRegisterStorageNode(node)) {
9229
- return LiveRegister._deserialize(node, parentToChildren, pool);
9230
- } else if (isFileStorageNode(node)) {
9231
- return LiveFile._deserialize(node, parentToChildren, pool);
9232
- } else {
9233
- throw new Error("Unexpected CRDT type");
9234
9327
  }
9328
+ return true;
9235
9329
  }
9236
- function deserializeToLson(node, parentToChildren, pool) {
9237
- if (isObjectStorageNode(node)) {
9238
- return LiveObject._deserialize(node, parentToChildren, pool);
9239
- } else if (isListStorageNode(node)) {
9240
- return LiveList._deserialize(node, parentToChildren, pool);
9241
- } else if (isMapStorageNode(node)) {
9242
- return LiveMap._deserialize(node, parentToChildren, pool);
9243
- } else if (isRegisterStorageNode(node)) {
9244
- return node[1].data;
9245
- } else if (isFileStorageNode(node)) {
9246
- return LiveFile._deserialize(node, parentToChildren, pool);
9247
- } else {
9248
- throw new Error("Unexpected CRDT type");
9330
+ function cloneAttributes(attributes) {
9331
+ return attributes === void 0 ? void 0 : freeze({ ...attributes });
9332
+ }
9333
+ function normalizeSegments(segments) {
9334
+ const normalized = [];
9335
+ for (const segment of segments) {
9336
+ if (segment.text.length === 0) {
9337
+ continue;
9338
+ }
9339
+ const last = normalized.at(-1);
9340
+ const attributes = cloneAttributes(segment.attributes);
9341
+ if (last !== void 0 && attributesEqual(last.attributes, attributes)) {
9342
+ last.text += segment.text;
9343
+ } else {
9344
+ normalized.push({ text: segment.text, attributes });
9345
+ }
9249
9346
  }
9347
+ return normalized;
9250
9348
  }
9251
- function isLiveStructure(value) {
9252
- return isLiveList(value) || isLiveMap(value) || isLiveObject(value) || isLiveFile(value);
9349
+ function dataToSegments(data) {
9350
+ return normalizeSegments(
9351
+ data.map(([text, attributes]) => ({
9352
+ text,
9353
+ attributes
9354
+ }))
9355
+ );
9253
9356
  }
9254
- function isLiveNode(value) {
9255
- return isLiveStructure(value) || isLiveRegister(value);
9357
+ function segmentsToData(segments) {
9358
+ return segments.map(
9359
+ (segment) => segment.attributes === void 0 ? [segment.text] : [segment.text, { ...segment.attributes }]
9360
+ );
9256
9361
  }
9257
- function isLiveList(value) {
9258
- return value instanceof LiveList;
9362
+ function textLength(segments) {
9363
+ return segments.reduce((sum, segment) => sum + segment.text.length, 0);
9259
9364
  }
9260
- function isLiveMap(value) {
9261
- return value instanceof LiveMap;
9365
+ function splitSegmentsAt(segments, index) {
9366
+ const result = [];
9367
+ let offset = 0;
9368
+ for (const segment of segments) {
9369
+ const end = offset + segment.text.length;
9370
+ if (index > offset && index < end) {
9371
+ const before2 = segment.text.slice(0, index - offset);
9372
+ const after2 = segment.text.slice(index - offset);
9373
+ result.push({ text: before2, attributes: segment.attributes });
9374
+ result.push({ text: after2, attributes: segment.attributes });
9375
+ } else {
9376
+ result.push({ text: segment.text, attributes: segment.attributes });
9377
+ }
9378
+ offset = end;
9379
+ }
9380
+ return result;
9262
9381
  }
9263
- function isLiveObject(value) {
9264
- return value instanceof LiveObject;
9382
+ function clipRange(index, length, contentLength) {
9383
+ const clippedIndex = Math.max(0, Math.min(index, contentLength));
9384
+ const clippedEnd = Math.max(
9385
+ clippedIndex,
9386
+ Math.min(index + length, contentLength)
9387
+ );
9388
+ return { index: clippedIndex, length: clippedEnd - clippedIndex };
9265
9389
  }
9266
- function isLiveFile(value) {
9267
- return value instanceof LiveFile;
9390
+ function isInSurrogatePair(text, index) {
9391
+ const previous = text.charCodeAt(index - 1);
9392
+ const next = text.charCodeAt(index);
9393
+ return previous >= 55296 && previous <= 56319 && next >= 56320 && next <= 57343;
9268
9394
  }
9269
- function isLiveRegister(value) {
9270
- return value instanceof LiveRegister;
9395
+ function clipIndexToCodePointBoundary(text, index) {
9396
+ const clippedIndex = Math.max(0, Math.min(index, text.length));
9397
+ return isInSurrogatePair(text, clippedIndex) ? clippedIndex - 1 : clippedIndex;
9271
9398
  }
9272
- function cloneLson(value) {
9273
- return value === void 0 ? void 0 : isLiveStructure(value) ? value.clone() : deepClone(value);
9399
+ function clipRangeToCodePointBoundaries(text, index, length) {
9400
+ const clipped = clipRange(index, length, text.length);
9401
+ if (clipped.length === 0) {
9402
+ return {
9403
+ index: clipIndexToCodePointBoundary(text, clipped.index),
9404
+ length: 0
9405
+ };
9406
+ }
9407
+ const clippedEnd = clipped.index + clipped.length;
9408
+ const normalizedIndex = isInSurrogatePair(text, clipped.index) ? clipped.index - 1 : clipped.index;
9409
+ const normalizedEnd = isInSurrogatePair(text, clippedEnd) ? clippedEnd + 1 : clippedEnd;
9410
+ return {
9411
+ index: normalizedIndex,
9412
+ length: normalizedEnd - normalizedIndex
9413
+ };
9274
9414
  }
9275
- function liveNodeToLson(obj) {
9276
- if (obj instanceof LiveRegister) {
9277
- return obj.data;
9278
- } else if (obj instanceof LiveList || obj instanceof LiveMap || obj instanceof LiveObject || obj instanceof LiveFile) {
9279
- return obj;
9280
- } else {
9281
- return assertNever(obj, "Unknown AbstractCrdt");
9415
+ function applyInsert(segments, index, text, attributes) {
9416
+ if (text.length === 0) {
9417
+ return normalizeSegments(segments);
9418
+ }
9419
+ const split = splitSegmentsAt(segments, index);
9420
+ const result = [];
9421
+ let offset = 0;
9422
+ let inserted = false;
9423
+ for (const segment of split) {
9424
+ if (!inserted && offset === index) {
9425
+ result.push({ text, attributes });
9426
+ inserted = true;
9427
+ }
9428
+ result.push(segment);
9429
+ offset += segment.text.length;
9282
9430
  }
9431
+ if (!inserted) {
9432
+ result.push({ text, attributes });
9433
+ }
9434
+ return normalizeSegments(result);
9283
9435
  }
9284
- function lsonToLiveNode(value) {
9285
- if (value instanceof LiveObject || value instanceof LiveMap || value instanceof LiveList || value instanceof LiveFile) {
9286
- return value;
9287
- } else {
9288
- return new LiveRegister(value);
9436
+ function extractDeletedSegments(segments, index, length) {
9437
+ const split = splitSegmentsAt(
9438
+ splitSegmentsAt(segments, index),
9439
+ index + length
9440
+ );
9441
+ const deleted = [];
9442
+ let offset = 0;
9443
+ for (const segment of split) {
9444
+ const end = offset + segment.text.length;
9445
+ if (offset >= index && end <= index + length) {
9446
+ deleted.push({
9447
+ text: segment.text,
9448
+ attributes: segment.attributes
9449
+ });
9450
+ }
9451
+ offset = end;
9289
9452
  }
9453
+ return normalizeSegments(deleted);
9290
9454
  }
9291
- function dumpPool(pool) {
9292
- const rows = Array.from(pool.nodes.values(), (node) => {
9293
- const parent = node.parent;
9294
- const parentId = parent.type === "HasParent" ? parent.node._id ?? "?" : parent.type === "Orphaned" ? "<orphaned>" : "-";
9295
- let value;
9296
- if (node instanceof LiveRegister) {
9297
- value = stringifyOrLog(node.data);
9298
- } else if (node instanceof LiveList) {
9455
+ function applyDelete(segments, index, length) {
9456
+ const deletedSegments = extractDeletedSegments(segments, index, length);
9457
+ const split = splitSegmentsAt(
9458
+ splitSegmentsAt(segments, index),
9459
+ index + length
9460
+ );
9461
+ const result = [];
9462
+ let offset = 0;
9463
+ let deletedText = "";
9464
+ for (const segment of split) {
9465
+ const end = offset + segment.text.length;
9466
+ if (offset >= index && end <= index + length) {
9467
+ deletedText += segment.text;
9468
+ } else {
9469
+ result.push(segment);
9470
+ }
9471
+ offset = end;
9472
+ }
9473
+ return {
9474
+ segments: normalizeSegments(result),
9475
+ deletedText,
9476
+ deletedSegments
9477
+ };
9478
+ }
9479
+ function applyFormat(segments, index, length, attributes) {
9480
+ const split = splitSegmentsAt(
9481
+ splitSegmentsAt(segments, index),
9482
+ index + length
9483
+ );
9484
+ const result = [];
9485
+ let offset = 0;
9486
+ for (const segment of split) {
9487
+ const end = offset + segment.text.length;
9488
+ if (offset >= index && end <= index + length) {
9489
+ const nextAttributes = {
9490
+ ...segment.attributes ?? {}
9491
+ };
9492
+ for (const [key, value] of Object.entries(attributes)) {
9493
+ if (value === null) {
9494
+ delete nextAttributes[key];
9495
+ } else {
9496
+ nextAttributes[key] = value;
9497
+ }
9498
+ }
9499
+ result.push({
9500
+ text: segment.text,
9501
+ attributes: Object.keys(nextAttributes).length === 0 ? void 0 : freeze(nextAttributes)
9502
+ });
9503
+ } else {
9504
+ result.push(segment);
9505
+ }
9506
+ offset = end;
9507
+ }
9508
+ return normalizeSegments(result);
9509
+ }
9510
+ function formatReverseOperations(segments, index, length, patch) {
9511
+ const split = splitSegmentsAt(
9512
+ splitSegmentsAt(segments, index),
9513
+ index + length
9514
+ );
9515
+ const result = [];
9516
+ let offset = 0;
9517
+ for (const segment of split) {
9518
+ const end = offset + segment.text.length;
9519
+ if (offset >= index && end <= index + length) {
9520
+ const attributes = {};
9521
+ const current = segment.attributes ?? {};
9522
+ for (const key of Object.keys(patch)) {
9523
+ const value = Object.hasOwn(current, key) ? current[key] : void 0;
9524
+ attributes[key] = value ?? null;
9525
+ }
9526
+ result.push({
9527
+ type: "format",
9528
+ index: offset,
9529
+ length: segment.text.length,
9530
+ attributes
9531
+ });
9532
+ }
9533
+ offset = end;
9534
+ }
9535
+ return result;
9536
+ }
9537
+ function mapIndexThroughOperation(index, op) {
9538
+ if (op.type === "insert") {
9539
+ return op.index <= index ? index + op.text.length : index;
9540
+ } else if (op.type === "delete") {
9541
+ if (op.index >= index) {
9542
+ return index;
9543
+ }
9544
+ return Math.max(op.index, index - op.length);
9545
+ } else {
9546
+ return index;
9547
+ }
9548
+ }
9549
+ function mapTextIndexThroughOperations(index, ops) {
9550
+ let mapped = index;
9551
+ for (const op of ops) {
9552
+ mapped = mapIndexThroughOperation(mapped, op);
9553
+ }
9554
+ return mapped;
9555
+ }
9556
+ function inverseMapIndexThroughOperation(index, op) {
9557
+ if (op.type === "insert") {
9558
+ if (index <= op.index) {
9559
+ return index;
9560
+ }
9561
+ return Math.max(op.index, index - op.text.length);
9562
+ } else if (op.type === "delete") {
9563
+ return op.index <= index ? index + op.length : index;
9564
+ } else {
9565
+ return index;
9566
+ }
9567
+ }
9568
+ function inverseMapTextIndexThroughOperations(index, ops) {
9569
+ let mapped = index;
9570
+ for (let i = ops.length - 1; i >= 0; i--) {
9571
+ mapped = inverseMapIndexThroughOperation(mapped, ops[i]);
9572
+ }
9573
+ return mapped;
9574
+ }
9575
+ function oppositeOrder(order) {
9576
+ return order === "before" ? "after" : "before";
9577
+ }
9578
+ function mapIndexOverDelete(index, deleteIndex, deleteLength) {
9579
+ if (deleteIndex >= index) {
9580
+ return index;
9581
+ }
9582
+ return Math.max(deleteIndex, index - deleteLength);
9583
+ }
9584
+ function transformInsert(op, over, order) {
9585
+ if (over.type === "insert") {
9586
+ const shifts = over.index < op.index || over.index === op.index && order === "after";
9587
+ return [shifts ? { ...op, index: op.index + over.text.length } : { ...op }];
9588
+ } else if (over.type === "delete") {
9589
+ return [
9590
+ { ...op, index: mapIndexOverDelete(op.index, over.index, over.length) }
9591
+ ];
9592
+ } else {
9593
+ return [{ ...op }];
9594
+ }
9595
+ }
9596
+ function transformDelete(op, over) {
9597
+ const start = op.index;
9598
+ const end = op.index + op.length;
9599
+ if (over.type === "insert") {
9600
+ const at = over.index;
9601
+ const len = over.text.length;
9602
+ if (at <= start) {
9603
+ return [{ ...op, index: start + len }];
9604
+ }
9605
+ if (at >= end) {
9606
+ return [{ ...op }];
9607
+ }
9608
+ return [
9609
+ { type: "delete", index: start, length: at - start },
9610
+ { type: "delete", index: start + len, length: end - at }
9611
+ ];
9612
+ } else if (over.type === "delete") {
9613
+ const newStart = mapIndexOverDelete(start, over.index, over.length);
9614
+ const newEnd = mapIndexOverDelete(end, over.index, over.length);
9615
+ return newEnd - newStart > 0 ? [{ type: "delete", index: newStart, length: newEnd - newStart }] : [];
9616
+ } else {
9617
+ return [{ ...op }];
9618
+ }
9619
+ }
9620
+ function transformFormat(op, over, order) {
9621
+ const start = op.index;
9622
+ const end = op.index + op.length;
9623
+ if (over.type === "insert") {
9624
+ const at = over.index;
9625
+ const len = over.text.length;
9626
+ if (at <= start) {
9627
+ return [{ ...op, index: start + len }];
9628
+ }
9629
+ if (at >= end) {
9630
+ return [{ ...op }];
9631
+ }
9632
+ return [
9633
+ {
9634
+ type: "format",
9635
+ index: start,
9636
+ length: at - start,
9637
+ attributes: op.attributes
9638
+ },
9639
+ {
9640
+ type: "format",
9641
+ index: at + len,
9642
+ length: end - at,
9643
+ attributes: op.attributes
9644
+ }
9645
+ ];
9646
+ } else if (over.type === "delete") {
9647
+ const newStart = mapIndexOverDelete(start, over.index, over.length);
9648
+ const newEnd = mapIndexOverDelete(end, over.index, over.length);
9649
+ return newEnd - newStart > 0 ? [
9650
+ {
9651
+ type: "format",
9652
+ index: newStart,
9653
+ length: newEnd - newStart,
9654
+ attributes: op.attributes
9655
+ }
9656
+ ] : [];
9657
+ } else {
9658
+ if (order === "after") {
9659
+ return [{ ...op }];
9660
+ }
9661
+ const overlapStart = Math.max(start, over.index);
9662
+ const overlapEnd = Math.min(end, over.index + over.length);
9663
+ if (overlapStart >= overlapEnd) {
9664
+ return [{ ...op }];
9665
+ }
9666
+ const hasConflict = Object.keys(op.attributes).some(
9667
+ (key) => Object.hasOwn(over.attributes, key)
9668
+ );
9669
+ if (!hasConflict) {
9670
+ return [{ ...op }];
9671
+ }
9672
+ const reduced = {};
9673
+ for (const [key, value] of Object.entries(op.attributes)) {
9674
+ if (!Object.hasOwn(over.attributes, key)) {
9675
+ reduced[key] = value;
9676
+ }
9677
+ }
9678
+ const pieces = [];
9679
+ if (start < overlapStart) {
9680
+ pieces.push({
9681
+ type: "format",
9682
+ index: start,
9683
+ length: overlapStart - start,
9684
+ attributes: op.attributes
9685
+ });
9686
+ }
9687
+ if (Object.keys(reduced).length > 0) {
9688
+ pieces.push({
9689
+ type: "format",
9690
+ index: overlapStart,
9691
+ length: overlapEnd - overlapStart,
9692
+ attributes: reduced
9693
+ });
9694
+ }
9695
+ if (overlapEnd < end) {
9696
+ pieces.push({
9697
+ type: "format",
9698
+ index: overlapEnd,
9699
+ length: end - overlapEnd,
9700
+ attributes: op.attributes
9701
+ });
9702
+ }
9703
+ return pieces;
9704
+ }
9705
+ }
9706
+ function transformSingle(op, over, order) {
9707
+ switch (op.type) {
9708
+ case "insert":
9709
+ return transformInsert(op, over, order);
9710
+ case "delete":
9711
+ return transformDelete(op, over);
9712
+ case "format":
9713
+ return transformFormat(op, over, order);
9714
+ }
9715
+ }
9716
+ function transformTextOperationsX(a, b, order) {
9717
+ if (a.length === 0 || b.length === 0) {
9718
+ return [[...a], [...b]];
9719
+ }
9720
+ if (a.length === 1 && b.length === 1) {
9721
+ return [
9722
+ transformSingle(a[0], b[0], order),
9723
+ transformSingle(b[0], a[0], oppositeOrder(order))
9724
+ ];
9725
+ }
9726
+ if (a.length > 1) {
9727
+ const [headA1, b1] = transformTextOperationsX([a[0]], b, order);
9728
+ const [restA1, b2] = transformTextOperationsX(a.slice(1), b1, order);
9729
+ return [[...headA1, ...restA1], b2];
9730
+ }
9731
+ const [a1, headB1] = transformTextOperationsX(a, [b[0]], order);
9732
+ const [a2, restB1] = transformTextOperationsX(a1, b.slice(1), order);
9733
+ return [a2, [...headB1, ...restB1]];
9734
+ }
9735
+ function transformTextOperations(ops, over, order) {
9736
+ return transformTextOperationsX(ops, over, order)[0];
9737
+ }
9738
+ function textOperationsEqual(a, b) {
9739
+ return a === b || stableStringify(a) === stableStringify(b);
9740
+ }
9741
+ function applyTextOperationsToSegments(segments, ops) {
9742
+ let next = [...segments];
9743
+ for (const op of ops) {
9744
+ if (op.type === "insert") {
9745
+ const index = Math.max(0, Math.min(op.index, textLength(next)));
9746
+ next = applyInsert(next, index, op.text, op.attributes);
9747
+ } else if (op.type === "delete") {
9748
+ const index = Math.max(0, Math.min(op.index, textLength(next)));
9749
+ const clipped = clipRange(index, op.length, textLength(next));
9750
+ next = applyDelete(next, clipped.index, clipped.length).segments;
9751
+ } else {
9752
+ const index = Math.max(0, Math.min(op.index, textLength(next)));
9753
+ const clipped = clipRange(index, op.length, textLength(next));
9754
+ next = applyFormat(next, clipped.index, clipped.length, op.attributes);
9755
+ }
9756
+ }
9757
+ return next;
9758
+ }
9759
+ function applyLiveTextOperations(data, ops) {
9760
+ return segmentsToData(
9761
+ applyTextOperationsToSegments(dataToSegments(data), ops)
9762
+ );
9763
+ }
9764
+ function normalizeLiveTextOperations(data, operations) {
9765
+ let shadow = dataToSegments(data);
9766
+ const normalized = [];
9767
+ for (const operation of operations) {
9768
+ const text = shadow.map((segment) => segment.text).join("");
9769
+ let normalizedOperation;
9770
+ if (operation.type === "insert") {
9771
+ normalizedOperation = {
9772
+ ...operation,
9773
+ index: clipIndexToCodePointBoundary(text, operation.index)
9774
+ };
9775
+ } else {
9776
+ const range = clipRangeToCodePointBoundaries(
9777
+ text,
9778
+ operation.index,
9779
+ operation.length
9780
+ );
9781
+ normalizedOperation = {
9782
+ ...operation,
9783
+ index: range.index,
9784
+ length: range.length
9785
+ };
9786
+ }
9787
+ normalized.push(normalizedOperation);
9788
+ shadow = applyTextOperationsToSegments(shadow, [normalizedOperation]);
9789
+ }
9790
+ return normalized;
9791
+ }
9792
+ function invertTextOperations(segments, ops) {
9793
+ let shadow = [...segments];
9794
+ const reverse = [];
9795
+ for (const op of ops) {
9796
+ if (op.type === "insert") {
9797
+ shadow = applyInsert(shadow, op.index, op.text, op.attributes);
9798
+ reverse.unshift({
9799
+ type: "delete",
9800
+ index: op.index,
9801
+ length: op.text.length
9802
+ });
9803
+ } else if (op.type === "delete") {
9804
+ const deletedSegments = extractDeletedSegments(
9805
+ shadow,
9806
+ op.index,
9807
+ op.length
9808
+ );
9809
+ shadow = applyDelete(shadow, op.index, op.length).segments;
9810
+ const inserts = [];
9811
+ let insertIndex = op.index;
9812
+ for (const segment of deletedSegments) {
9813
+ inserts.push({
9814
+ type: "insert",
9815
+ index: insertIndex,
9816
+ text: segment.text,
9817
+ attributes: segment.attributes
9818
+ });
9819
+ insertIndex += segment.text.length;
9820
+ }
9821
+ for (let index = inserts.length - 1; index >= 0; index--) {
9822
+ reverse.unshift(inserts[index]);
9823
+ }
9824
+ } else {
9825
+ const inverse = formatReverseOperations(
9826
+ shadow,
9827
+ op.index,
9828
+ op.length,
9829
+ op.attributes
9830
+ );
9831
+ shadow = applyFormat(shadow, op.index, op.length, op.attributes);
9832
+ reverse.unshift(...inverse.reverse());
9833
+ }
9834
+ }
9835
+ return reverse;
9836
+ }
9837
+
9838
+ // src/crdts/LiveText.ts
9839
+ var ACCEPTED_OPS_HISTORY_LIMIT = 1e3;
9840
+ var LiveText = class _LiveText extends AbstractCrdt {
9841
+ /** The local document: #confirmed ⊕ #inFlightOps ⊕ #queuedOps. */
9842
+ #segments;
9843
+ /** The server-confirmed document (only authoritative ops applied). */
9844
+ #confirmed;
9845
+ #version;
9846
+ /** The op currently awaiting server acknowledgement (at most one). */
9847
+ #inFlightOpId;
9848
+ /** Its ops, continuously re-expressed against current server state. */
9849
+ #inFlightOps = [];
9850
+ /** Local edits made while an op is in flight; sent after the ack. */
9851
+ #queuedOps = [];
9852
+ #acceptedOps = [];
9853
+ /**
9854
+ * Creates a new LiveText document.
9855
+ *
9856
+ * @param textOrData Initial plain text, or an array of `[text]` /
9857
+ * `[text, attributes]` segments. Defaults to an empty document.
9858
+ *
9859
+ * @example
9860
+ * new LiveText();
9861
+ * new LiveText("Hello world");
9862
+ * new LiveText([["Hello ", { bold: true }], ["world"]]);
9863
+ */
9864
+ constructor(textOrData = "", version = 0) {
9865
+ super();
9866
+ this.#segments = typeof textOrData === "string" ? textOrData.length === 0 ? [] : [{ text: textOrData }] : dataToSegments(textOrData);
9867
+ this.#confirmed = [...this.#segments];
9868
+ this.#version = version;
9869
+ Object.assign(this[kInternal], {
9870
+ encodeIndex: (localIndex) => this.#encodeIndex(localIndex),
9871
+ decodeIndex: (index, fromVersion) => this.#decodeIndex(index, fromVersion)
9872
+ });
9873
+ }
9874
+ get version() {
9875
+ return this.#version;
9876
+ }
9877
+ get length() {
9878
+ return textLength(this.#segments);
9879
+ }
9880
+ /** @internal */
9881
+ static _deserialize([id, item], _parentToChildren, pool) {
9882
+ const text = new _LiveText(item.data, item.version);
9883
+ text._attach(id, pool);
9884
+ return text;
9885
+ }
9886
+ /** @internal */
9887
+ _toOps(parentId, parentKey) {
9888
+ if (this._id === void 0) {
9889
+ throw new Error("Cannot serialize LiveText if it is not attached");
9890
+ }
9891
+ return [
9892
+ {
9893
+ type: OpCode.CREATE_TEXT,
9894
+ id: this._id,
9895
+ parentId,
9896
+ parentKey,
9897
+ data: this.toJSON(),
9898
+ version: this.#version
9899
+ }
9900
+ ];
9901
+ }
9902
+ /** @internal */
9903
+ _serialize() {
9904
+ if (this.parent.type !== "HasParent") {
9905
+ throw new Error("Cannot serialize LiveText if parent is missing");
9906
+ }
9907
+ return {
9908
+ type: CrdtType.TEXT,
9909
+ parentId: nn(this.parent.node._id, "Parent node expected to have ID"),
9910
+ parentKey: this.parent.key,
9911
+ data: this.toJSON(),
9912
+ version: this.#version
9913
+ };
9914
+ }
9915
+ /** @internal */
9916
+ _attachChild(_op) {
9917
+ throw new Error("LiveText cannot contain child nodes");
9918
+ }
9919
+ /** @internal */
9920
+ _detachChild(_crdt) {
9921
+ throw new Error("LiveText cannot contain child nodes");
9922
+ }
9923
+ /** @internal */
9924
+ _apply(op, source) {
9925
+ if (op.type !== OpCode.UPDATE_TEXT) {
9926
+ return super._apply(op, source);
9927
+ }
9928
+ if (source.origin === "local" && source.optimistic) {
9929
+ return this.#applyLocal(op, toUpdateSource(source));
9930
+ }
9931
+ if (op.opId !== void 0 && op.opId === this.#inFlightOpId) {
9932
+ return this.#applyAck(op, toUpdateSource(source));
9933
+ }
9934
+ if (op.opId !== void 0 && this.#acceptedOps.some((entry) => entry.opId === op.opId)) {
9935
+ this.#version = Math.max(this.#version, op.version ?? op.baseVersion + 1);
9936
+ return { modified: false };
9937
+ }
9938
+ return this.#applyRemote(op, toUpdateSource(source));
9939
+ }
9940
+ /**
9941
+ * Inserts text at the given index.
9942
+ *
9943
+ * @param index Character index at which to insert. Values outside the
9944
+ * document range are clipped.
9945
+ * @param text Text to insert.
9946
+ * @param attributes Optional inline attributes for the inserted text.
9947
+ *
9948
+ * @example
9949
+ * const text = new LiveText("Hello");
9950
+ * text.insert(5, " world");
9951
+ * text.insert(0, "Say: ", { italic: true });
9952
+ */
9953
+ insert(index, text, attributes) {
9954
+ const clippedIndex = clipIndexToCodePointBoundary(this.toString(), index);
9955
+ this.#dispatch([{ type: "insert", index: clippedIndex, text, attributes }]);
9956
+ }
9957
+ /**
9958
+ * Deletes `length` characters starting at `index`.
9959
+ *
9960
+ * @example
9961
+ * const text = new LiveText("Hello world");
9962
+ * text.delete(5, 6); // "Hello"
9963
+ */
9964
+ delete(index, length) {
9965
+ const clipped = clipRangeToCodePointBoundaries(
9966
+ this.toString(),
9967
+ index,
9968
+ length
9969
+ );
9970
+ if (clipped.length === 0) {
9971
+ return;
9972
+ }
9973
+ this.#dispatch([
9974
+ { type: "delete", index: clipped.index, length: clipped.length }
9975
+ ]);
9976
+ }
9977
+ /**
9978
+ * Replaces a range of text with new text.
9979
+ *
9980
+ * @example
9981
+ * const text = new LiveText("Hello world");
9982
+ * text.replace(0, 5, "Hi"); // "Hi world"
9983
+ */
9984
+ replace(index, length, text, attributes) {
9985
+ const clipped = clipRangeToCodePointBoundaries(
9986
+ this.toString(),
9987
+ index,
9988
+ length
9989
+ );
9990
+ const ops = [];
9991
+ if (clipped.length > 0) {
9992
+ ops.push({
9993
+ type: "delete",
9994
+ index: clipped.index,
9995
+ length: clipped.length
9996
+ });
9997
+ }
9998
+ if (text.length > 0) {
9999
+ ops.push({ type: "insert", index: clipped.index, text, attributes });
10000
+ }
10001
+ this.#dispatch(ops);
10002
+ }
10003
+ /**
10004
+ * Encode a local-document index (an offset into this LiveText's current
10005
+ * #segments, which CodeMirror or any consumer mirrors as its document)
10006
+ * into server-confirmed coordinates suitable for broadcasting to peers via
10007
+ * presence or any other side channel.
10008
+ *
10009
+ * The returned index is in this LiveText's current #confirmed coordinates
10010
+ * — that is, with this client's local pending ops inverse-mapped out.
10011
+ * Pair it with the current {@link LiveText.version} when sending so the
10012
+ * receiver can call {@link PrivateLiveTextApi.decodeIndex} to land the
10013
+ * position in their own local document coordinates regardless of their
10014
+ * private pending ops.
10015
+ *
10016
+ * Index ambiguity at boundaries is resolved by an inverse-of-forward
10017
+ * convention: a position at or before a local insertion is reported as
10018
+ * the position right before the insertion in #confirmed; a position past
10019
+ * the insertion shifts left by the insertion's length. Positions inside
10020
+ * an own-pending insertion collapse to the insertion point.
10021
+ */
10022
+ #encodeIndex(localIndex) {
10023
+ let mapped = Math.max(0, Math.min(localIndex, this.length));
10024
+ mapped = inverseMapTextIndexThroughOperations(mapped, this.#queuedOps);
10025
+ mapped = inverseMapTextIndexThroughOperations(mapped, this.#inFlightOps);
10026
+ return mapped;
10027
+ }
10028
+ /**
10029
+ * Decode an `(index, fromVersion)` pair produced by
10030
+ * {@link PrivateLiveTextApi.encodeIndex} — typically on a peer — into an
10031
+ * offset in this LiveText's current local document (an index suitable for
10032
+ * placing a CodeMirror marker, an annotation anchor, or anything else that
10033
+ * lives over #segments).
10034
+ *
10035
+ * Composes the accepted ops applied since `fromVersion` (drawn from
10036
+ * #acceptedOps in locally-applied form) with this client's own local
10037
+ * pending ops, in that order. The result is in current #segments
10038
+ * coordinates.
10039
+ *
10040
+ * Returns `null` when the position cannot be decoded against the current
10041
+ * state:
10042
+ * - `fromVersion` is greater than this LiveText's current version: the
10043
+ * peer is ahead of us. The caller should park the message and retry
10044
+ * after more accepted ops arrive.
10045
+ * - `fromVersion` falls outside the retained accepted-ops history. This
10046
+ * only happens after very long-lived disconnections; the caller can
10047
+ * fall back to using the raw index and letting subsequent local
10048
+ * transactions map it (with bounded drift).
10049
+ */
10050
+ #decodeIndex(index, fromVersion) {
10051
+ if (fromVersion > this.#version) {
10052
+ return null;
10053
+ }
10054
+ if (fromVersion < this.#version) {
10055
+ const oldest = this.#acceptedOps[0]?.version;
10056
+ if (oldest === void 0 || oldest > fromVersion + 1) {
10057
+ return null;
10058
+ }
10059
+ }
10060
+ let mapped = index;
10061
+ for (const entry of this.#acceptedOps) {
10062
+ if (entry.version <= fromVersion) continue;
10063
+ if (entry.version > this.#version) break;
10064
+ if (entry.ops.length === 0) continue;
10065
+ mapped = mapTextIndexThroughOperations(mapped, entry.ops);
10066
+ }
10067
+ mapped = mapTextIndexThroughOperations(mapped, this.#inFlightOps);
10068
+ mapped = mapTextIndexThroughOperations(mapped, this.#queuedOps);
10069
+ return Math.max(0, Math.min(mapped, this.length));
10070
+ }
10071
+ /**
10072
+ * Applies or removes inline attributes on a range of text.
10073
+ *
10074
+ * Set an attribute to `null` to remove it from the range.
10075
+ *
10076
+ * @example
10077
+ * const text = new LiveText("Hello world");
10078
+ * text.format(0, 5, { bold: true });
10079
+ * text.format(0, 5, { bold: null });
10080
+ */
10081
+ format(index, length, attributes) {
10082
+ const clipped = clipRangeToCodePointBoundaries(
10083
+ this.toString(),
10084
+ index,
10085
+ length
10086
+ );
10087
+ if (clipped.length === 0) {
10088
+ return;
10089
+ }
10090
+ this.#dispatch([
10091
+ {
10092
+ type: "format",
10093
+ index: clipped.index,
10094
+ length: clipped.length,
10095
+ attributes
10096
+ }
10097
+ ]);
10098
+ }
10099
+ /** Local edits made through the public API. */
10100
+ #dispatch(ops) {
10101
+ if (ops.length === 0) {
10102
+ return;
10103
+ }
10104
+ this._warnIfOrphaned();
10105
+ this._pool?.assertStorageIsWritable();
10106
+ const attached = this._pool !== void 0 && this._id !== void 0;
10107
+ const reverse = attached ? this.#invertOperations(ops) : [];
10108
+ const changes = this.#applyOperationsLocally(ops);
10109
+ if (!attached) {
10110
+ return;
10111
+ }
10112
+ const pool = nn(this._pool);
10113
+ const id = nn(this._id);
10114
+ const updates = /* @__PURE__ */ new Map([
10115
+ [
10116
+ id,
10117
+ {
10118
+ type: "LiveText",
10119
+ node: this,
10120
+ version: this.#version,
10121
+ updates: changes,
10122
+ source: LOCAL_EDIT
10123
+ }
10124
+ ]
10125
+ ]);
10126
+ if (this.#inFlightOpId === void 0) {
10127
+ const opId = pool.generateOpId();
10128
+ this.#inFlightOpId = opId;
10129
+ this.#inFlightOps = [...ops];
10130
+ pool.dispatch(
10131
+ [
10132
+ {
10133
+ type: OpCode.UPDATE_TEXT,
10134
+ id,
10135
+ opId,
10136
+ baseVersion: this.#version,
10137
+ ops: [...ops]
10138
+ }
10139
+ ],
10140
+ reverse,
10141
+ updates
10142
+ );
10143
+ } else {
10144
+ this.#queuedOps.push(...ops);
10145
+ pool.dispatch([], reverse, updates, { clearRedoStack: true });
10146
+ }
10147
+ }
10148
+ /**
10149
+ * A local replay of an existing wire op: an undo/redo frame, or an
10150
+ * unacknowledged op re-sent after a reconnect.
10151
+ */
10152
+ #applyLocal(op, source) {
10153
+ const mutableOp = op;
10154
+ if (op.opId !== void 0 && op.opId === this.#inFlightOpId) {
10155
+ this.#inFlightOps = [...this.#inFlightOps, ...this.#queuedOps];
10156
+ this.#queuedOps = [];
10157
+ mutableOp.baseVersion = this.#version;
10158
+ mutableOp.ops = [...this.#inFlightOps];
10159
+ return { modified: false };
10160
+ }
10161
+ let ops = op.ops;
10162
+ for (const entry of this.#acceptedOps) {
10163
+ if (entry.version > op.baseVersion && entry.ops.length > 0) {
10164
+ ops = transformTextOperations(ops, entry.ops, "after");
10165
+ }
10166
+ }
10167
+ const reverse = this.#invertOperations(ops);
10168
+ const changes = this.#applyOperationsLocally(ops);
10169
+ if (this.#inFlightOpId === void 0 && ops.length > 0) {
10170
+ this.#inFlightOpId = nn(op.opId, "Local ops must have an opId");
10171
+ this.#inFlightOps = [...ops];
10172
+ mutableOp.baseVersion = this.#version;
10173
+ mutableOp.ops = [...ops];
10174
+ } else {
10175
+ this.#queuedOps.push(...ops);
10176
+ mutableOp.baseVersion = this.#version;
10177
+ mutableOp.ops = [];
10178
+ }
10179
+ if (changes.length === 0) {
10180
+ return { modified: false };
10181
+ }
10182
+ return {
10183
+ reverse,
10184
+ modified: {
10185
+ type: "LiveText",
10186
+ node: this,
10187
+ version: this.#version,
10188
+ updates: changes,
10189
+ source
10190
+ }
10191
+ };
10192
+ }
10193
+ /** Server acknowledgement of our in-flight op. */
10194
+ #applyAck(op, source) {
10195
+ const ackedVersion = op.version ?? Math.max(this.#version, op.baseVersion + 1);
10196
+ const predicted = this.#inFlightOps;
10197
+ const opId = this.#inFlightOpId;
10198
+ this.#confirmed = applyTextOperationsToSegments(this.#confirmed, op.ops);
10199
+ this.#inFlightOpId = void 0;
10200
+ this.#inFlightOps = [];
10201
+ let appliedOps = [];
10202
+ let result = { modified: false };
10203
+ if (!textOperationsEqual(op.ops, predicted)) {
10204
+ error2(
10205
+ "LiveText: acknowledgement did not match the local prediction; resynchronizing"
10206
+ );
10207
+ const rebuilt = this.#rebuildLocalFromConfirmed();
10208
+ appliedOps = rebuilt.appliedOps;
10209
+ if (rebuilt.changes.length > 0) {
10210
+ result = {
10211
+ reverse: [],
10212
+ modified: {
10213
+ type: "LiveText",
10214
+ node: this,
10215
+ version: ackedVersion,
10216
+ updates: rebuilt.changes,
10217
+ source
10218
+ }
10219
+ };
10220
+ }
10221
+ }
10222
+ this.#version = Math.max(this.#version, ackedVersion);
10223
+ this.#recordAccepted(ackedVersion, appliedOps, opId);
10224
+ this.#flushQueued();
10225
+ return result;
10226
+ }
10227
+ /** An accepted op from another client (or a server-fabricated fix op). */
10228
+ #applyRemote(op, source) {
10229
+ const version = op.version ?? this.#version + 1;
10230
+ this.#confirmed = applyTextOperationsToSegments(this.#confirmed, op.ops);
10231
+ const [overInFlight, inFlight] = transformTextOperationsX(
10232
+ op.ops,
10233
+ this.#inFlightOps,
10234
+ "before"
10235
+ );
10236
+ const [applied, queued] = transformTextOperationsX(
10237
+ overInFlight,
10238
+ this.#queuedOps,
10239
+ "before"
10240
+ );
10241
+ this.#inFlightOps = inFlight;
10242
+ this.#queuedOps = queued;
10243
+ this.#recordAccepted(version, applied, op.opId);
10244
+ if (applied.length === 0) {
10245
+ this.#version = Math.max(this.#version, version);
10246
+ return { modified: false };
10247
+ }
10248
+ const reverse = this.#invertOperations(applied);
10249
+ const changes = this.#applyOperationsLocally(applied);
10250
+ this.#version = Math.max(this.#version, version);
10251
+ return {
10252
+ reverse,
10253
+ modified: {
10254
+ type: "LiveText",
10255
+ node: this,
10256
+ version: this.#version,
10257
+ updates: changes,
10258
+ source
10259
+ }
10260
+ };
10261
+ }
10262
+ /** Send the queued ops as the next in-flight op (after an ack). */
10263
+ #flushQueued() {
10264
+ if (this.#queuedOps.length === 0 || this._pool === void 0 || this._id === void 0) {
10265
+ return;
10266
+ }
10267
+ const opId = this._pool.generateOpId();
10268
+ this.#inFlightOpId = opId;
10269
+ this.#inFlightOps = this.#queuedOps;
10270
+ this.#queuedOps = [];
10271
+ this._pool.dispatch(
10272
+ [
10273
+ {
10274
+ type: OpCode.UPDATE_TEXT,
10275
+ id: this._id,
10276
+ opId,
10277
+ baseVersion: this.#version,
10278
+ ops: [...this.#inFlightOps]
10279
+ }
10280
+ ],
10281
+ [],
10282
+ /* @__PURE__ */ new Map(),
10283
+ // The local content was already applied (and made undoable) when the
10284
+ // edits happened; this is purely an outbound flush.
10285
+ { clearRedoStack: false }
10286
+ );
10287
+ }
10288
+ /**
10289
+ * Rebuild the local document as confirmed ⊕ queued ops, returning the
10290
+ * coarse delta that was applied. Only used by defensive recovery paths.
10291
+ */
10292
+ #rebuildLocalFromConfirmed() {
10293
+ const before2 = this.#segments;
10294
+ const after2 = applyTextOperationsToSegments(this.#confirmed, [
10295
+ ...this.#inFlightOps,
10296
+ ...this.#queuedOps
10297
+ ]);
10298
+ if (stableStringify(segmentsToData(before2)) === stableStringify(segmentsToData(after2))) {
10299
+ this.#segments = after2;
10300
+ return { appliedOps: [], changes: [] };
10301
+ }
10302
+ const beforeText = before2.map((segment) => segment.text).join("");
10303
+ this.#segments = after2;
10304
+ this.invalidate();
10305
+ const appliedOps = [];
10306
+ const changes = [];
10307
+ if (beforeText.length > 0) {
10308
+ appliedOps.push({ type: "delete", index: 0, length: beforeText.length });
10309
+ changes.push({
10310
+ type: "delete",
10311
+ index: 0,
10312
+ length: beforeText.length,
10313
+ deletedText: beforeText
10314
+ });
10315
+ }
10316
+ let index = 0;
10317
+ for (const segment of after2) {
10318
+ appliedOps.push({
10319
+ type: "insert",
10320
+ index,
10321
+ text: segment.text,
10322
+ attributes: segment.attributes
10323
+ });
10324
+ changes.push({
10325
+ type: "insert",
10326
+ index,
10327
+ text: segment.text,
10328
+ attributes: segment.attributes
10329
+ });
10330
+ index += segment.text.length;
10331
+ }
10332
+ return { appliedOps, changes };
10333
+ }
10334
+ /**
10335
+ * Reconcile this node against an authoritative storage snapshot (e.g.
10336
+ * after a reconnect). The confirmed state and version are replaced by the
10337
+ * snapshot's; pending (in-flight + queued) ops are preserved on top and
10338
+ * will be re-sent by the offline-ops replay.
10339
+ *
10340
+ * @internal
10341
+ */
10342
+ _resyncText(data, version, source) {
10343
+ this.#confirmed = dataToSegments(data);
10344
+ this.#version = version;
10345
+ this.#acceptedOps = [];
10346
+ const rebuilt = this.#rebuildLocalFromConfirmed();
10347
+ if (rebuilt.changes.length === 0) {
10348
+ return void 0;
10349
+ }
10350
+ return {
10351
+ type: "LiveText",
10352
+ node: this,
10353
+ version: this.#version,
10354
+ updates: rebuilt.changes,
10355
+ source
10356
+ };
10357
+ }
10358
+ /**
10359
+ * Called when the server rejected one of our ops. Drops all pending state
10360
+ * for this node (edits queued behind a rejected op cannot be trusted
10361
+ * either); the room follows up with a storage resync.
10362
+ *
10363
+ * @internal
10364
+ */
10365
+ _rejectPendingOp(opId) {
10366
+ if (opId !== this.#inFlightOpId) {
10367
+ return;
10368
+ }
10369
+ this.#inFlightOpId = void 0;
10370
+ this.#inFlightOps = [];
10371
+ this.#queuedOps = [];
10372
+ }
10373
+ #recordAccepted(version, ops, opId) {
10374
+ if (this.#acceptedOps.some((entry) => entry.version === version)) {
10375
+ return;
10376
+ }
10377
+ this.#acceptedOps.push({ version, opId, ops: [...ops] });
10378
+ this.#acceptedOps.sort((left, right) => left.version - right.version);
10379
+ if (this.#acceptedOps.length > ACCEPTED_OPS_HISTORY_LIMIT) {
10380
+ this.#acceptedOps.splice(
10381
+ 0,
10382
+ this.#acceptedOps.length - ACCEPTED_OPS_HISTORY_LIMIT
10383
+ );
10384
+ }
10385
+ }
10386
+ #applyOperationsLocally(ops) {
10387
+ const changes = [];
10388
+ for (const op of ops) {
10389
+ if (op.type === "insert") {
10390
+ this.#segments = applyInsert(
10391
+ this.#segments,
10392
+ op.index,
10393
+ op.text,
10394
+ op.attributes
10395
+ );
10396
+ changes.push({
10397
+ type: "insert",
10398
+ index: op.index,
10399
+ text: op.text,
10400
+ attributes: op.attributes
10401
+ });
10402
+ } else if (op.type === "delete") {
10403
+ const result = applyDelete(this.#segments, op.index, op.length);
10404
+ this.#segments = result.segments;
10405
+ changes.push({
10406
+ type: "delete",
10407
+ index: op.index,
10408
+ length: op.length,
10409
+ deletedText: result.deletedText
10410
+ });
10411
+ } else {
10412
+ this.#segments = applyFormat(
10413
+ this.#segments,
10414
+ op.index,
10415
+ op.length,
10416
+ op.attributes
10417
+ );
10418
+ changes.push({
10419
+ type: "format",
10420
+ index: op.index,
10421
+ length: op.length,
10422
+ attributes: op.attributes
10423
+ });
10424
+ }
10425
+ }
10426
+ this.invalidate();
10427
+ return changes;
10428
+ }
10429
+ #invertOperations(ops) {
10430
+ return [
10431
+ {
10432
+ type: OpCode.UPDATE_TEXT,
10433
+ id: nn(this._id),
10434
+ baseVersion: this.#version,
10435
+ ops: invertTextOperations(this.#segments, ops)
10436
+ }
10437
+ ];
10438
+ }
10439
+ /** Returns the plain text content without attributes. Equivalent to joining the text from each segment in {@link LiveText.toJSON}. */
10440
+ toString() {
10441
+ return this.#segments.map((segment) => segment.text).join("");
10442
+ }
10443
+ /**
10444
+ * Returns a JSON-compatible snapshot of the document as a {@link LiveTextData}
10445
+ * array.
10446
+ *
10447
+ * @example
10448
+ * new LiveText([["Hello ", { bold: true }], ["world"]]).toJSON();
10449
+ * // [["Hello ", { bold: true }], ["world"]]
10450
+ */
10451
+ toJSON() {
10452
+ return super.toJSON();
10453
+ }
10454
+ /** @internal */
10455
+ _toJSON() {
10456
+ return segmentsToData(this.#segments);
10457
+ }
10458
+ /** @internal */
10459
+ toTreeNode(key) {
10460
+ return super.toTreeNode(key);
10461
+ }
10462
+ /** @internal */
10463
+ _toTreeNode(key) {
10464
+ const nodeId = this._id ?? nanoid();
10465
+ const payload = this.toJSON().map(
10466
+ (segment, index) => ({
10467
+ type: "Json",
10468
+ id: `${nodeId}:${index}`,
10469
+ key: String(index),
10470
+ payload: segment
10471
+ })
10472
+ );
10473
+ payload.push({
10474
+ type: "Json",
10475
+ id: `${nodeId}:version`,
10476
+ key: "version",
10477
+ payload: this.version
10478
+ });
10479
+ return {
10480
+ type: "LiveText",
10481
+ id: nodeId,
10482
+ key,
10483
+ payload
10484
+ };
10485
+ }
10486
+ clone() {
10487
+ return new _LiveText(this.toJSON(), this.#version);
10488
+ }
10489
+ };
10490
+
10491
+ // src/crdts/liveblocks-helpers.ts
10492
+ function creationOpToLiveNode(op) {
10493
+ return lsonToLiveNode(creationOpToLson(op));
10494
+ }
10495
+ function creationOpToLson(op) {
10496
+ switch (op.type) {
10497
+ case OpCode.CREATE_FILE:
10498
+ return new LiveFile(op.data);
10499
+ case OpCode.CREATE_REGISTER:
10500
+ return op.data;
10501
+ case OpCode.CREATE_OBJECT:
10502
+ return new LiveObject(op.data);
10503
+ case OpCode.CREATE_MAP:
10504
+ return new LiveMap();
10505
+ case OpCode.CREATE_LIST:
10506
+ return new LiveList([]);
10507
+ case OpCode.CREATE_TEXT:
10508
+ return new LiveText(op.data, op.version);
10509
+ default:
10510
+ return assertNever(op, "Unknown creation Op");
10511
+ }
10512
+ }
10513
+ function isSameNodeOrChildOf(node, parent) {
10514
+ if (node === parent) {
10515
+ return true;
10516
+ }
10517
+ if (node.parent.type === "HasParent") {
10518
+ return isSameNodeOrChildOf(node.parent.node, parent);
10519
+ }
10520
+ return false;
10521
+ }
10522
+ function liveObjectFromNodeStream(nodes) {
10523
+ const pool = createManagedPool({
10524
+ getCurrentConnectionId: () => {
10525
+ throw new Error(
10526
+ "Cannot mutate a historic storage version: it is a read-only snapshot"
10527
+ );
10528
+ }
10529
+ });
10530
+ return LiveObject._fromItems(nodes, pool);
10531
+ }
10532
+ function deserialize(node, parentToChildren, pool) {
10533
+ if (isObjectStorageNode(node)) {
10534
+ return LiveObject._deserialize(node, parentToChildren, pool);
10535
+ } else if (isListStorageNode(node)) {
10536
+ return LiveList._deserialize(node, parentToChildren, pool);
10537
+ } else if (isMapStorageNode(node)) {
10538
+ return LiveMap._deserialize(node, parentToChildren, pool);
10539
+ } else if (isRegisterStorageNode(node)) {
10540
+ return LiveRegister._deserialize(node, parentToChildren, pool);
10541
+ } else if (isTextStorageNode(node)) {
10542
+ return LiveText._deserialize(node, parentToChildren, pool);
10543
+ } else if (isFileStorageNode(node)) {
10544
+ return LiveFile._deserialize(node, parentToChildren, pool);
10545
+ } else {
10546
+ throw new Error("Unexpected CRDT type");
10547
+ }
10548
+ }
10549
+ function deserializeToLson(node, parentToChildren, pool) {
10550
+ if (isObjectStorageNode(node)) {
10551
+ return LiveObject._deserialize(node, parentToChildren, pool);
10552
+ } else if (isListStorageNode(node)) {
10553
+ return LiveList._deserialize(node, parentToChildren, pool);
10554
+ } else if (isMapStorageNode(node)) {
10555
+ return LiveMap._deserialize(node, parentToChildren, pool);
10556
+ } else if (isRegisterStorageNode(node)) {
10557
+ return node[1].data;
10558
+ } else if (isTextStorageNode(node)) {
10559
+ return LiveText._deserialize(node, parentToChildren, pool);
10560
+ } else if (isFileStorageNode(node)) {
10561
+ return LiveFile._deserialize(node, parentToChildren, pool);
10562
+ } else {
10563
+ throw new Error("Unexpected CRDT type");
10564
+ }
10565
+ }
10566
+ function isLiveStructure(value) {
10567
+ return isLiveList(value) || isLiveMap(value) || isLiveObject(value) || isLiveText(value) || isLiveFile(value);
10568
+ }
10569
+ function isLiveNode(value) {
10570
+ return isLiveStructure(value) || isLiveRegister(value);
10571
+ }
10572
+ function isLiveList(value) {
10573
+ return value instanceof LiveList;
10574
+ }
10575
+ function isLiveMap(value) {
10576
+ return value instanceof LiveMap;
10577
+ }
10578
+ function isLiveObject(value) {
10579
+ return value instanceof LiveObject;
10580
+ }
10581
+ function isLiveText(value) {
10582
+ return value instanceof LiveText;
10583
+ }
10584
+ function isLiveFile(value) {
10585
+ return value instanceof LiveFile;
10586
+ }
10587
+ function isLiveRegister(value) {
10588
+ return value instanceof LiveRegister;
10589
+ }
10590
+ function cloneLson(value) {
10591
+ return value === void 0 ? void 0 : isLiveStructure(value) ? value.clone() : deepClone(value);
10592
+ }
10593
+ function liveNodeToLson(obj) {
10594
+ if (obj instanceof LiveRegister) {
10595
+ return obj.data;
10596
+ } else if (obj instanceof LiveList || obj instanceof LiveMap || obj instanceof LiveObject || obj instanceof LiveText || obj instanceof LiveFile) {
10597
+ return obj;
10598
+ } else {
10599
+ return assertNever(obj, "Unknown AbstractCrdt");
10600
+ }
10601
+ }
10602
+ function lsonToLiveNode(value) {
10603
+ if (value instanceof LiveObject || value instanceof LiveMap || value instanceof LiveList || value instanceof LiveText || value instanceof LiveFile) {
10604
+ return value;
10605
+ } else {
10606
+ return new LiveRegister(value);
10607
+ }
10608
+ }
10609
+ function dumpPool(pool) {
10610
+ const rows = Array.from(pool.nodes.values(), (node) => {
10611
+ const parent = node.parent;
10612
+ const parentId = parent.type === "HasParent" ? parent.node._id ?? "?" : parent.type === "Orphaned" ? "<orphaned>" : "-";
10613
+ let value;
10614
+ if (node instanceof LiveRegister) {
10615
+ value = stringifyOrLog(node.data);
10616
+ } else if (node instanceof LiveList) {
9299
10617
  value = "<LiveList>";
9300
10618
  } else if (node instanceof LiveMap) {
9301
10619
  value = "<LiveMap>";
@@ -9344,7 +10662,26 @@ function isJsonEq(a, b) {
9344
10662
  }
9345
10663
  return true;
9346
10664
  }
9347
- function diffNodeMap(prev, next) {
10665
+ function liveTextDataToReplaceOps(before2, after2) {
10666
+ const ops = [];
10667
+ const beforeLength = before2.reduce(
10668
+ (length, [text]) => length + text.length,
10669
+ 0
10670
+ );
10671
+ if (beforeLength > 0) {
10672
+ ops.push({ type: "delete", index: 0, length: beforeLength });
10673
+ }
10674
+ let index = 0;
10675
+ for (const [text, attributes] of after2) {
10676
+ if (text.length === 0) {
10677
+ continue;
10678
+ }
10679
+ ops.push({ type: "insert", index, text, attributes });
10680
+ index += text.length;
10681
+ }
10682
+ return ops;
10683
+ }
10684
+ function diffNodeMap(prev, next, options) {
9348
10685
  const ops = [];
9349
10686
  const idsToRecreate = /* @__PURE__ */ new Set();
9350
10687
  next.forEach((nextCrdt, id) => {
@@ -9436,6 +10773,16 @@ function diffNodeMap(prev, next) {
9436
10773
  parentKey: crdt.parentKey
9437
10774
  });
9438
10775
  break;
10776
+ case CrdtType.TEXT:
10777
+ ops.push({
10778
+ type: OpCode.CREATE_TEXT,
10779
+ id,
10780
+ parentId: crdt.parentId,
10781
+ parentKey: crdt.parentKey,
10782
+ data: crdt.data,
10783
+ version: crdt.version
10784
+ });
10785
+ break;
9439
10786
  }
9440
10787
  }
9441
10788
  next.forEach((crdt, id) => {
@@ -9462,6 +10809,17 @@ function diffNodeMap(prev, next) {
9462
10809
  }
9463
10810
  }
9464
10811
  }
10812
+ if (options?.includeLiveTextUpdates === true && crdt.type === CrdtType.TEXT && currentCrdt.type === CrdtType.TEXT && !isJsonEq(crdt.data, currentCrdt.data)) {
10813
+ ops.push({
10814
+ type: OpCode.UPDATE_TEXT,
10815
+ id,
10816
+ // A restore is a new edit in the current timeline. The version from
10817
+ // the historic snapshot describes its old timeline and must not move
10818
+ // this node's current version backwards.
10819
+ baseVersion: currentCrdt.version,
10820
+ ops: liveTextDataToReplaceOps(currentCrdt.data, crdt.data)
10821
+ });
10822
+ }
9465
10823
  if (crdt.parentKey !== currentCrdt.parentKey) {
9466
10824
  ops.push({
9467
10825
  type: OpCode.SET_PARENT_KEY,
@@ -9502,19 +10860,36 @@ function mergeListStorageUpdates(first, second) {
9502
10860
  updates: updates.concat(second.updates)
9503
10861
  };
9504
10862
  }
10863
+ function mergeTextStorageUpdates(first, second) {
10864
+ return {
10865
+ ...second,
10866
+ updates: first.updates.concat(second.updates)
10867
+ };
10868
+ }
10869
+ function mergeUpdateSources(first, second) {
10870
+ if (first.origin === "remote" || second.origin === "remote") {
10871
+ return REMOTE;
10872
+ }
10873
+ if (second.via !== "edit") return second;
10874
+ if (first.via !== "edit") return first;
10875
+ return LOCAL_EDIT;
10876
+ }
9505
10877
  function mergeStorageUpdates(first, second) {
9506
10878
  if (first === void 0) {
9507
10879
  return second;
9508
10880
  }
10881
+ const source = mergeUpdateSources(first.source, second.source);
9509
10882
  if (first.type === "LiveObject" && second.type === "LiveObject") {
9510
- return mergeObjectStorageUpdates(first, second);
10883
+ return { ...mergeObjectStorageUpdates(first, second), source };
9511
10884
  } else if (first.type === "LiveMap" && second.type === "LiveMap") {
9512
- return mergeMapStorageUpdates(first, second);
10885
+ return { ...mergeMapStorageUpdates(first, second), source };
9513
10886
  } else if (first.type === "LiveList" && second.type === "LiveList") {
9514
- return mergeListStorageUpdates(first, second);
10887
+ return { ...mergeListStorageUpdates(first, second), source };
10888
+ } else if (first.type === "LiveText" && second.type === "LiveText") {
10889
+ return { ...mergeTextStorageUpdates(first, second), source };
9515
10890
  } else {
10891
+ return { ...second, source };
9516
10892
  }
9517
- return second;
9518
10893
  }
9519
10894
 
9520
10895
  // src/devtools/bridge.ts
@@ -10471,6 +11846,8 @@ function createRoom(options, config) {
10471
11846
  activeBatch: null,
10472
11847
  unacknowledgedOps
10473
11848
  };
11849
+ let nextHistoryItemId = 0;
11850
+ let historyDisabled = 0;
10474
11851
  const nodeMapBuffer = makeNodeMapBuffer();
10475
11852
  const stopwatch = config.enableDebugLogging ? makeStopWatch() : void 0;
10476
11853
  let lastTokenKey;
@@ -10555,7 +11932,7 @@ function createRoom(options, config) {
10555
11932
  }
10556
11933
  }
10557
11934
  });
10558
- function onDispatch(ops, reverse, storageUpdates) {
11935
+ function onDispatch(ops, reverse, storageUpdates, options2) {
10559
11936
  if (context.activeBatch) {
10560
11937
  for (const op of ops) {
10561
11938
  context.activeBatch.ops.push(op);
@@ -10570,12 +11947,17 @@ function createRoom(options, config) {
10570
11947
  );
10571
11948
  }
10572
11949
  context.activeBatch.reverseOps.pushLeft(reverse);
11950
+ if (options2?.clearRedoStack) {
11951
+ context.activeBatch.clearRedoStack = true;
11952
+ }
10573
11953
  } else {
10574
11954
  if (reverse.length > 0) {
10575
11955
  addToUndoStack(reverse);
10576
11956
  }
11957
+ if (options2?.clearRedoStack ?? ops.length > 0) {
11958
+ clearRedoStack();
11959
+ }
10577
11960
  if (ops.length > 0) {
10578
- context.redoStack.length = 0;
10579
11961
  dispatchOps(ops);
10580
11962
  }
10581
11963
  notify({ storageUpdates });
@@ -10595,6 +11977,7 @@ function createRoom(options, config) {
10595
11977
  others: makeEventSource(),
10596
11978
  storageBatch: makeEventSource(),
10597
11979
  history: makeEventSource(),
11980
+ privateHistory: makeEventSource(),
10598
11981
  storageDidLoad: makeEventSource(),
10599
11982
  storageStatus: makeEventSource(),
10600
11983
  ydoc: makeEventSource(),
@@ -10682,12 +12065,12 @@ function createRoom(options, config) {
10682
12065
  self,
10683
12066
  (me) => me !== null ? userToTreeNode("Me", me) : null
10684
12067
  );
10685
- function diffCurrentStorageAgainst(target) {
12068
+ function diffCurrentStorageAgainst(target, options2) {
10686
12069
  const current = /* @__PURE__ */ new Map();
10687
12070
  for (const [id, crdt] of context.pool.nodes) {
10688
12071
  current.set(id, crdt._serialize());
10689
12072
  }
10690
- return diffNodeMap(current, target);
12073
+ return diffNodeMap(current, target, options2);
10691
12074
  }
10692
12075
  function createOrUpdateRootFromMessage(nodes) {
10693
12076
  if (nodes.size === 0) {
@@ -10695,6 +12078,23 @@ function createRoom(options, config) {
10695
12078
  }
10696
12079
  if (context.root !== void 0) {
10697
12080
  const result = applyRemoteOps(diffCurrentStorageAgainst(nodes));
12081
+ for (const [id, crdt] of nodes) {
12082
+ if (crdt.type === CrdtType.TEXT) {
12083
+ const node = context.pool.nodes.get(id);
12084
+ if (node !== void 0 && isLiveText(node)) {
12085
+ const update = node._resyncText(crdt.data, crdt.version, REMOTE);
12086
+ if (update !== void 0) {
12087
+ result.updates.storageUpdates.set(
12088
+ id,
12089
+ mergeStorageUpdates(
12090
+ result.updates.storageUpdates.get(id),
12091
+ update
12092
+ )
12093
+ );
12094
+ }
12095
+ }
12096
+ }
12097
+ }
10698
12098
  notify(result.updates);
10699
12099
  } else {
10700
12100
  context.root = LiveObject._fromItems(
@@ -10719,12 +12119,23 @@ function createRoom(options, config) {
10719
12119
  }
10720
12120
  });
10721
12121
  }
12122
+ function notifyPrivateHistory(event) {
12123
+ if (historyDisabled > 0) return;
12124
+ eventHub.privateHistory.notify(event);
12125
+ }
12126
+ function clearRedoStack() {
12127
+ if (context.redoStack.length === 0) return;
12128
+ const ids = context.redoStack.map((item) => item.id);
12129
+ context.redoStack.length = 0;
12130
+ notifyPrivateHistory({ action: "discard", ids });
12131
+ }
10722
12132
  function reconcileStorageWithNodes(nodes) {
10723
12133
  if (context.root === void 0) {
10724
12134
  throw new Error("Cannot reconcile storage before it is loaded");
10725
12135
  }
10726
12136
  const ops = diffCurrentStorageAgainst(
10727
- new Map(nodes)
12137
+ new Map(nodes),
12138
+ { includeLiveTextUpdates: true }
10728
12139
  );
10729
12140
  if (ops.length === 0) {
10730
12141
  return;
@@ -10743,9 +12154,14 @@ function createRoom(options, config) {
10743
12154
  }
10744
12155
  function _addToRealUndoStack(frames) {
10745
12156
  if (context.undoStack.length >= 50) {
10746
- context.undoStack.shift();
12157
+ const evicted = context.undoStack.shift();
12158
+ if (evicted !== void 0) {
12159
+ notifyPrivateHistory({ action: "discard", ids: [evicted.id] });
12160
+ }
10747
12161
  }
10748
- context.undoStack.push(frames);
12162
+ const id = nextHistoryItemId++;
12163
+ context.undoStack.push({ id, frames });
12164
+ notifyPrivateHistory({ action: "push", id });
10749
12165
  onHistoryChange();
10750
12166
  }
10751
12167
  function addToUndoStack(frames) {
@@ -10769,7 +12185,10 @@ function createRoom(options, config) {
10769
12185
  eventHub.myPresence.notify(context.myPresence.get());
10770
12186
  }
10771
12187
  if (storageUpdates !== void 0 && storageUpdates.size > 0) {
10772
- const updates2 = Array.from(storageUpdates.values());
12188
+ const updates2 = Array.from(storageUpdates.values(), (update) => ({
12189
+ ...update,
12190
+ source: toUpdateSource(update.source)
12191
+ }));
10773
12192
  eventHub.storageBatch.notify(updates2);
10774
12193
  }
10775
12194
  notifyStorageStatus();
@@ -10783,19 +12202,69 @@ function createRoom(options, config) {
10783
12202
  "Internal. Tried to get connection id but connection was never open"
10784
12203
  );
10785
12204
  }
10786
- function applyLocalOps(frames) {
12205
+ const viaByOpId = /* @__PURE__ */ new Map();
12206
+ function viaOfAckedOp(opId) {
12207
+ const via = viaByOpId.get(opId);
12208
+ if (via === void 0) {
12209
+ return "edit";
12210
+ }
12211
+ viaByOpId.delete(opId);
12212
+ return via;
12213
+ }
12214
+ function applyLocalOps(frames, localSource = LOCAL_EDIT) {
10787
12215
  const [pframes, ops] = partition(
10788
12216
  frames,
10789
12217
  (f) => f.type === "presence"
10790
12218
  );
10791
- const opsWithOpIds = ops.map(
12219
+ const restoredTextIds = /* @__PURE__ */ new Map();
12220
+ for (const op of ops) {
12221
+ if (op.type === OpCode.CREATE_TEXT && op.opId === void 0 && context.pool.nodes.get(op.id) === void 0 && !restoredTextIds.has(op.id)) {
12222
+ restoredTextIds.set(op.id, context.pool.generateId());
12223
+ }
12224
+ }
12225
+ const remappedOps = restoredTextIds.size === 0 ? ops : ops.map((op) => {
12226
+ if (op.opId !== void 0) {
12227
+ return op;
12228
+ }
12229
+ const id = restoredTextIds.get(op.id);
12230
+ if (isCreateOp(op)) {
12231
+ const parentId = restoredTextIds.get(op.parentId);
12232
+ const deletedId = op.deletedId === void 0 ? void 0 : restoredTextIds.get(op.deletedId);
12233
+ if (id === void 0 && parentId === void 0 && deletedId === void 0) {
12234
+ return op;
12235
+ }
12236
+ if (op.type === OpCode.CREATE_TEXT && id !== void 0) {
12237
+ return {
12238
+ ...op,
12239
+ id,
12240
+ version: 0,
12241
+ ...parentId === void 0 ? {} : { parentId },
12242
+ ...deletedId === void 0 ? {} : { deletedId }
12243
+ };
12244
+ }
12245
+ return {
12246
+ ...op,
12247
+ ...id === void 0 ? {} : { id },
12248
+ ...parentId === void 0 ? {} : { parentId },
12249
+ ...deletedId === void 0 ? {} : { deletedId }
12250
+ };
12251
+ }
12252
+ return id === void 0 ? op : { ...op, id };
12253
+ });
12254
+ const opsWithOpIds = remappedOps.map(
10792
12255
  (op) => op.opId === void 0 ? { ...op, opId: context.pool.generateOpId() } : op
10793
12256
  );
12257
+ if (localSource.via !== "edit") {
12258
+ for (const op of opsWithOpIds) {
12259
+ viaByOpId.set(op.opId, localSource.via);
12260
+ }
12261
+ }
10794
12262
  const { reverse, updates } = applyOps(
10795
12263
  pframes,
10796
12264
  opsWithOpIds,
10797
12265
  /* isLocal */
10798
- true
12266
+ true,
12267
+ localSource
10799
12268
  );
10800
12269
  return { opsToEmit: opsWithOpIds, reverse, updates };
10801
12270
  }
@@ -10807,7 +12276,7 @@ function createRoom(options, config) {
10807
12276
  false
10808
12277
  );
10809
12278
  }
10810
- function applyOps(pframes, ops, isLocal) {
12279
+ function applyOps(pframes, ops, isLocal, localSource = LOCAL_EDIT) {
10811
12280
  const output = {
10812
12281
  reverse: new Deque(),
10813
12282
  storageUpdates: /* @__PURE__ */ new Map(),
@@ -10836,12 +12305,16 @@ function createRoom(options, config) {
10836
12305
  for (const op of ops) {
10837
12306
  let source;
10838
12307
  if (isLocal) {
10839
- source = 0 /* LOCAL */;
12308
+ source = { ...localSource, optimistic: true };
10840
12309
  } else if (op.opId !== void 0) {
10841
12310
  context.unacknowledgedOps.delete(op.opId);
10842
- source = 2 /* OURS */;
12311
+ source = {
12312
+ origin: "local",
12313
+ via: viaOfAckedOp(op.opId),
12314
+ optimistic: false
12315
+ };
10843
12316
  } else {
10844
- source = 1 /* THEIRS */;
12317
+ source = REMOTE;
10845
12318
  }
10846
12319
  const applyOpResult = applyOp(op, source);
10847
12320
  if (applyOpResult.modified) {
@@ -10856,7 +12329,7 @@ function createRoom(options, config) {
10856
12329
  );
10857
12330
  output.reverse.pushLeft(applyOpResult.reverse);
10858
12331
  }
10859
- if (op.type === OpCode.CREATE_LIST || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_FILE) {
12332
+ if (op.type === OpCode.CREATE_LIST || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_TEXT || op.type === OpCode.CREATE_FILE) {
10860
12333
  createdNodeIds.add(op.id);
10861
12334
  }
10862
12335
  }
@@ -10876,12 +12349,13 @@ function createRoom(options, config) {
10876
12349
  switch (op.type) {
10877
12350
  case OpCode.DELETE_OBJECT_KEY:
10878
12351
  case OpCode.UPDATE_OBJECT:
12352
+ case OpCode.UPDATE_TEXT:
10879
12353
  case OpCode.DELETE_CRDT: {
10880
12354
  const node = context.pool.nodes.get(op.id);
10881
12355
  if (node === void 0) {
10882
12356
  return { modified: false };
10883
12357
  }
10884
- return node._apply(op, source === 0 /* LOCAL */);
12358
+ return node._apply(op, source);
10885
12359
  }
10886
12360
  case OpCode.SET_PARENT_KEY: {
10887
12361
  const node = context.pool.nodes.get(op.id);
@@ -10900,6 +12374,7 @@ function createRoom(options, config) {
10900
12374
  case OpCode.CREATE_OBJECT:
10901
12375
  case OpCode.CREATE_LIST:
10902
12376
  case OpCode.CREATE_MAP:
12377
+ case OpCode.CREATE_TEXT:
10903
12378
  case OpCode.CREATE_FILE:
10904
12379
  case OpCode.CREATE_REGISTER: {
10905
12380
  if (op.parentId === void 0) {
@@ -11154,16 +12629,38 @@ function createRoom(options, config) {
11154
12629
  }
11155
12630
  break;
11156
12631
  }
11157
- // Receiving a RejectedOps message in the client means that the server is no
11158
- // longer in sync with the client. Trying to synchronize the client again by
11159
- // rolling back particular Ops may be hard/impossible. It's fine to not try and
11160
- // accept the out-of-sync reality and throw an error.
12632
+ // Receiving a RejectedOps message means the server refused some of
12633
+ // our ops, so our optimistic local state is out of sync with the
12634
+ // server. For LiveText ops this is a normal (if rare) situation
12635
+ // e.g. a client that was offline long enough to fall outside the
12636
+ // server's retained history window — and we can recover: drop the
12637
+ // rejected pending state and re-fetch the authoritative storage
12638
+ // snapshot. For other ops (e.g. permission rejections), rolling back
12639
+ // particular Ops is hard/impossible, so we keep the old behavior of
12640
+ // accepting the out-of-sync reality and surfacing an error.
11161
12641
  case ServerMsgCode.REJECT_STORAGE_OP: {
11162
12642
  errorWithTitle(
11163
12643
  "Storage mutation rejection error",
11164
12644
  message.reason
11165
12645
  );
11166
- if (process.env.NODE_ENV !== "production") {
12646
+ let needsStorageResync = false;
12647
+ for (const opId of message.opIds) {
12648
+ const rejectedOp = context.unacknowledgedOps.get(opId);
12649
+ context.unacknowledgedOps.delete(opId);
12650
+ context.buffer.storageOperations = context.buffer.storageOperations.filter((op) => op.opId !== opId);
12651
+ viaByOpId.delete(opId);
12652
+ if (rejectedOp !== void 0 && rejectedOp.type === OpCode.UPDATE_TEXT) {
12653
+ const node = context.pool.nodes.get(rejectedOp.id);
12654
+ if (node !== void 0 && isLiveText(node)) {
12655
+ node._rejectPendingOp(opId);
12656
+ needsStorageResync = true;
12657
+ }
12658
+ }
12659
+ }
12660
+ if (needsStorageResync) {
12661
+ refreshStorage();
12662
+ flushNowOrSoon();
12663
+ } else if (process.env.NODE_ENV !== "production") {
11167
12664
  throw new Error(
11168
12665
  `Storage mutations rejected by server: ${message.reason}`
11169
12666
  );
@@ -11705,14 +13202,15 @@ function createRoom(options, config) {
11705
13202
  if (context.activeBatch) {
11706
13203
  throw new Error("undo is not allowed during a batch");
11707
13204
  }
11708
- const frames = context.undoStack.pop();
11709
- if (frames === void 0) {
13205
+ const item = context.undoStack.pop();
13206
+ if (item === void 0) {
11710
13207
  return;
11711
13208
  }
11712
13209
  context.pausedHistory = null;
11713
- const result = applyLocalOps(frames);
13210
+ const result = applyLocalOps(item.frames, LOCAL_UNDO);
13211
+ context.redoStack.push({ id: item.id, frames: result.reverse });
13212
+ notifyPrivateHistory({ action: "undo", id: item.id });
11714
13213
  notify(result.updates);
11715
- context.redoStack.push(result.reverse);
11716
13214
  onHistoryChange();
11717
13215
  for (const op of result.opsToEmit) {
11718
13216
  context.buffer.storageOperations.push(op);
@@ -11723,14 +13221,15 @@ function createRoom(options, config) {
11723
13221
  if (context.activeBatch) {
11724
13222
  throw new Error("redo is not allowed during a batch");
11725
13223
  }
11726
- const frames = context.redoStack.pop();
11727
- if (frames === void 0) {
13224
+ const item = context.redoStack.pop();
13225
+ if (item === void 0) {
11728
13226
  return;
11729
13227
  }
11730
13228
  context.pausedHistory = null;
11731
- const result = applyLocalOps(frames);
13229
+ const result = applyLocalOps(item.frames, LOCAL_REDO);
13230
+ context.undoStack.push({ id: item.id, frames: result.reverse });
13231
+ notifyPrivateHistory({ action: "redo", id: item.id });
11732
13232
  notify(result.updates);
11733
- context.undoStack.push(result.reverse);
11734
13233
  onHistoryChange();
11735
13234
  for (const op of result.opsToEmit) {
11736
13235
  context.buffer.storageOperations.push(op);
@@ -11740,6 +13239,8 @@ function createRoom(options, config) {
11740
13239
  function clear() {
11741
13240
  context.undoStack.length = 0;
11742
13241
  context.redoStack.length = 0;
13242
+ notifyPrivateHistory({ action: "clear" });
13243
+ onHistoryChange();
11743
13244
  }
11744
13245
  function batch2(callback) {
11745
13246
  if (context.activeBatch) {
@@ -11767,8 +13268,8 @@ function createRoom(options, config) {
11767
13268
  if (currentBatch.scheduleHistoryResume) {
11768
13269
  commitPausedHistoryToUndoStack();
11769
13270
  }
11770
- if (currentBatch.ops.length > 0) {
11771
- context.redoStack.length = 0;
13271
+ if (currentBatch.ops.length > 0 || currentBatch.clearRedoStack) {
13272
+ clearRedoStack();
11772
13273
  }
11773
13274
  if (currentBatch.ops.length > 0) {
11774
13275
  dispatchOps(currentBatch.ops);
@@ -11797,7 +13298,6 @@ function createRoom(options, config) {
11797
13298
  }
11798
13299
  commitPausedHistoryToUndoStack();
11799
13300
  }
11800
- let historyDisabled = 0;
11801
13301
  function disableHistory(fn) {
11802
13302
  const origUndo = context.undoStack;
11803
13303
  const origRedo = context.redoStack;
@@ -12047,13 +13547,28 @@ function createRoom(options, config) {
12047
13547
  },
12048
13548
  // prettier-ignore
12049
13549
  get undoStack() {
12050
- return deepClone(context.undoStack);
13550
+ return structuredClone(
13551
+ context.undoStack.map((item) => ({
13552
+ id: item.id,
13553
+ frames: item.frames
13554
+ }))
13555
+ );
13556
+ },
13557
+ // prettier-ignore
13558
+ get redoStack() {
13559
+ return structuredClone(
13560
+ context.redoStack.map((item) => ({
13561
+ id: item.id,
13562
+ frames: item.frames
13563
+ }))
13564
+ );
12051
13565
  },
12052
13566
  // prettier-ignore
12053
13567
  get nodeCount() {
12054
13568
  return context.pool.nodes.size;
12055
13569
  },
12056
13570
  // prettier-ignore
13571
+ history: eventHub.privateHistory.observable,
12057
13572
  getYjsProvider() {
12058
13573
  return context.yjsProvider;
12059
13574
  },
@@ -13084,6 +14599,12 @@ function toPlainLson(lson) {
13084
14599
  liveblocksType: "LiveList",
13085
14600
  data: [...lson].map((item) => toPlainLson(item))
13086
14601
  };
14602
+ } else if (lson instanceof LiveText) {
14603
+ return {
14604
+ liveblocksType: "LiveText",
14605
+ data: lson.toJSON(),
14606
+ version: lson.version
14607
+ };
13087
14608
  } else if (lson instanceof LiveFile) {
13088
14609
  return {
13089
14610
  liveblocksType: "LiveFile",
@@ -13271,6 +14792,7 @@ export {
13271
14792
  LiveList,
13272
14793
  LiveMap,
13273
14794
  LiveObject,
14795
+ LiveText,
13274
14796
  LiveblocksError,
13275
14797
  MENTION_CHARACTER,
13276
14798
  MutableSignal,
@@ -13282,6 +14804,7 @@ export {
13282
14804
  SortedList,
13283
14805
  TextEditorType,
13284
14806
  WebsocketCloseCodes,
14807
+ applyLiveTextOperations,
13285
14808
  asPos,
13286
14809
  assert,
13287
14810
  assertNever,
@@ -13342,6 +14865,7 @@ export {
13342
14865
  isRegisterStorageNode,
13343
14866
  isRootStorageNode,
13344
14867
  isStartsWithOperator,
14868
+ isTextStorageNode,
13345
14869
  isUrl,
13346
14870
  kInternal,
13347
14871
  keys,
@@ -13355,6 +14879,7 @@ export {
13355
14879
  nanoid,
13356
14880
  nn,
13357
14881
  nodeStreamToCompactNodes,
14882
+ normalizeLiveTextOperations,
13358
14883
  normalizeRoomAccesses,
13359
14884
  normalizeRoomPermissions,
13360
14885
  normalizeUpdateRoomAccesses,
@@ -13370,6 +14895,7 @@ export {
13370
14895
  stringifyCommentBody,
13371
14896
  throwUsageError,
13372
14897
  toPlainLson,
14898
+ transformTextOperations,
13373
14899
  tryParseJson,
13374
14900
  url,
13375
14901
  urljoin,