@liveblocks/core 3.22.0-rc1 → 3.23.0-exp1

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.22.0-rc1";
9
+ var PKG_VERSION = "3.23.0-exp1";
10
10
  var PKG_FORMAT = "esm";
11
11
 
12
12
  // src/dupe-detection.ts
@@ -1930,7 +1930,7 @@ function createApiClient({
1930
1930
  if (attachment.size <= ATTACHMENT_PART_SIZE) {
1931
1931
  return autoRetry(
1932
1932
  async () => httpClient.putBlob(
1933
- url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/upload/${encodeURIComponent(attachment.name)}`,
1933
+ url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/upload/${attachment.name}`,
1934
1934
  await authManager.getAuthValue({
1935
1935
  roomId,
1936
1936
  resource: "comments",
@@ -1949,7 +1949,7 @@ function createApiClient({
1949
1949
  const uploadedParts = [];
1950
1950
  const createMultiPartUpload = await autoRetry(
1951
1951
  async () => httpClient.post(
1952
- url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/multipart/${encodeURIComponent(attachment.name)}`,
1952
+ url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/multipart/${attachment.name}`,
1953
1953
  await authManager.getAuthValue({
1954
1954
  roomId,
1955
1955
  resource: "comments",
@@ -2132,7 +2132,17 @@ function createApiClient({
2132
2132
  })
2133
2133
  );
2134
2134
  }
2135
- async function getYjsHistoryVersion(options) {
2135
+ async function fetchStorageHistoryVersion(options) {
2136
+ return httpClient.rawGet(
2137
+ url`/v2/c/rooms/${options.roomId}/versions/${options.versionId}/storage`,
2138
+ await authManager.getAuthValue({
2139
+ roomId: options.roomId,
2140
+ resource: "storage",
2141
+ access: "read"
2142
+ })
2143
+ );
2144
+ }
2145
+ async function fetchYjsHistoryVersion(options) {
2136
2146
  return httpClient.rawGet(
2137
2147
  url`/v2/c/rooms/${options.roomId}/versions/${options.versionId}/yjs`,
2138
2148
  await authManager.getAuthValue({
@@ -2152,6 +2162,16 @@ function createApiClient({
2152
2162
  })
2153
2163
  );
2154
2164
  }
2165
+ async function deleteHistoryVersion(options) {
2166
+ await httpClient.delete(
2167
+ url`/v2/c/rooms/${options.roomId}/versions/${options.versionId}`,
2168
+ await authManager.getAuthValue({
2169
+ roomId: options.roomId,
2170
+ resource: "storage",
2171
+ access: "write"
2172
+ })
2173
+ );
2174
+ }
2155
2175
  async function reportTextEditor(options) {
2156
2176
  await httpClient.rawPost(
2157
2177
  url`/v2/c/rooms/${options.roomId}/text-metadata`,
@@ -2475,8 +2495,10 @@ function createApiClient({
2475
2495
  // Room text editor
2476
2496
  createTextMention,
2477
2497
  deleteTextMention,
2478
- getYjsHistoryVersion,
2498
+ fetchStorageHistoryVersion,
2499
+ fetchYjsHistoryVersion,
2479
2500
  createVersionHistorySnapshot,
2501
+ deleteHistoryVersion,
2480
2502
  reportTextEditor,
2481
2503
  listHistoryVersions,
2482
2504
  listHistoryVersionsSince,
@@ -3857,6 +3879,7 @@ var ManagedSocket = class {
3857
3879
 
3858
3880
  // src/internal.ts
3859
3881
  var kInternal = /* @__PURE__ */ Symbol();
3882
+ var kStorageUpdateSource = /* @__PURE__ */ Symbol();
3860
3883
 
3861
3884
  // src/lib/IncrementalJsonParser.ts
3862
3885
  var EMPTY_OBJECT = Object.freeze({});
@@ -5815,13 +5838,15 @@ var OpCode = Object.freeze({
5815
5838
  DELETE_CRDT: 5,
5816
5839
  DELETE_OBJECT_KEY: 6,
5817
5840
  CREATE_MAP: 7,
5818
- CREATE_REGISTER: 8
5841
+ CREATE_REGISTER: 8,
5842
+ CREATE_TEXT: 9,
5843
+ UPDATE_TEXT: 10
5819
5844
  });
5820
5845
  function isIgnoredOp(op) {
5821
5846
  return op.type === OpCode.DELETE_CRDT && op.id === "ACK";
5822
5847
  }
5823
5848
  function isCreateOp(op) {
5824
- return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST;
5849
+ return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST || op.type === OpCode.CREATE_TEXT;
5825
5850
  }
5826
5851
 
5827
5852
  // src/protocol/StorageNode.ts
@@ -5829,7 +5854,8 @@ var CrdtType = Object.freeze({
5829
5854
  OBJECT: 0,
5830
5855
  LIST: 1,
5831
5856
  MAP: 2,
5832
- REGISTER: 3
5857
+ REGISTER: 3,
5858
+ TEXT: 4
5833
5859
  });
5834
5860
  function isRootStorageNode(node) {
5835
5861
  return node[0] === "root";
@@ -5846,6 +5872,9 @@ function isMapStorageNode(node) {
5846
5872
  function isRegisterStorageNode(node) {
5847
5873
  return node[1].type === CrdtType.REGISTER;
5848
5874
  }
5875
+ function isTextStorageNode(node) {
5876
+ return node[1].type === CrdtType.TEXT;
5877
+ }
5849
5878
  function isCompactRootNode(node) {
5850
5879
  return node[0] === "root";
5851
5880
  }
@@ -5868,6 +5897,9 @@ function* compactNodesToNodeStream(compactNodes) {
5868
5897
  case CrdtType.REGISTER:
5869
5898
  yield [cnode[0], { type: CrdtType.REGISTER, parentId: cnode[2], parentKey: cnode[3], data: cnode[4] }];
5870
5899
  break;
5900
+ case CrdtType.TEXT:
5901
+ yield [cnode[0], { type: CrdtType.TEXT, parentId: cnode[2], parentKey: cnode[3], data: cnode[4], version: cnode[5] }];
5902
+ break;
5871
5903
  default:
5872
5904
  }
5873
5905
  }
@@ -5896,6 +5928,17 @@ function* nodeStreamToCompactNodes(nodes) {
5896
5928
  const id = node[0];
5897
5929
  const crdt = node[1];
5898
5930
  yield [id, CrdtType.REGISTER, crdt.parentId, crdt.parentKey, crdt.data];
5931
+ } else if (isTextStorageNode(node)) {
5932
+ const id = node[0];
5933
+ const crdt = node[1];
5934
+ yield [
5935
+ id,
5936
+ CrdtType.TEXT,
5937
+ crdt.parentId,
5938
+ crdt.parentKey,
5939
+ crdt.data,
5940
+ crdt.version
5941
+ ];
5899
5942
  } else {
5900
5943
  }
5901
5944
  }
@@ -6098,6 +6141,10 @@ ${parentKey}`;
6098
6141
  get size() {
6099
6142
  return this.#byOpId.size;
6100
6143
  }
6144
+ /** The still-unacknowledged op with the given opId, if any. */
6145
+ get(opId) {
6146
+ return this.#byOpId.get(opId);
6147
+ }
6101
6148
  /**
6102
6149
  * Mark the given Op as still unacknowledged.
6103
6150
  */
@@ -6180,7 +6227,7 @@ ${parentKey}`;
6180
6227
  };
6181
6228
 
6182
6229
  // src/crdts/AbstractCrdt.ts
6183
- function createManagedPool(roomId, options) {
6230
+ function createManagedPool(options) {
6184
6231
  const {
6185
6232
  getCurrentConnectionId,
6186
6233
  onDispatch,
@@ -6191,15 +6238,14 @@ function createManagedPool(roomId, options) {
6191
6238
  let opClock = 0;
6192
6239
  const nodes = /* @__PURE__ */ new Map();
6193
6240
  return {
6194
- roomId,
6195
6241
  nodes,
6196
6242
  getNode: (id) => nodes.get(id),
6197
6243
  addNode: (id, node) => void nodes.set(id, node),
6198
6244
  deleteNode: (id) => void nodes.delete(id),
6199
6245
  generateId: () => `${getCurrentConnectionId()}:${clock++}`,
6200
6246
  generateOpId: () => `${getCurrentConnectionId()}:${opClock++}`,
6201
- dispatch(ops, reverse, storageUpdates) {
6202
- onDispatch?.(ops, reverse, storageUpdates);
6247
+ dispatch(ops, reverse, storageUpdates, options2) {
6248
+ onDispatch?.(ops, reverse, storageUpdates, options2);
6203
6249
  },
6204
6250
  assertStorageIsWritable: () => {
6205
6251
  if (!isStorageWritable()) {
@@ -6222,10 +6268,17 @@ function Orphaned(oldKey, oldPos = asPos(oldKey)) {
6222
6268
  return Object.freeze({ type: "Orphaned", oldKey, oldPos });
6223
6269
  }
6224
6270
  var AbstractCrdt = class {
6225
- // ^^^^^^^^^^^^ TODO: Make this an interface
6226
6271
  #pool;
6227
6272
  #id;
6228
6273
  #parent = NoParent;
6274
+ constructor() {
6275
+ Object.defineProperty(this, kInternal, {
6276
+ value: {
6277
+ getId: () => this.#id
6278
+ },
6279
+ enumerable: false
6280
+ });
6281
+ }
6229
6282
  /** @internal */
6230
6283
  _getParentKeyOrThrow() {
6231
6284
  switch (this.parent.type) {
@@ -6256,9 +6309,6 @@ var AbstractCrdt = class {
6256
6309
  get _pool() {
6257
6310
  return this.#pool;
6258
6311
  }
6259
- get roomId() {
6260
- return this.#pool ? this.#pool.roomId : null;
6261
- }
6262
6312
  /** @internal */
6263
6313
  get _id() {
6264
6314
  return this.#id;
@@ -8758,157 +8808,1349 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8758
8808
  }
8759
8809
  };
8760
8810
 
8761
- // src/crdts/liveblocks-helpers.ts
8762
- function creationOpToLiveNode(op) {
8763
- return lsonToLiveNode(creationOpToLson(op));
8811
+ // src/crdts/liveTextOps.ts
8812
+ function attributesEqual(left, right) {
8813
+ if (left === right) {
8814
+ return true;
8815
+ }
8816
+ if (left === void 0 || right === void 0) {
8817
+ return false;
8818
+ }
8819
+ const leftKeys = Object.keys(left);
8820
+ const rightKeys = Object.keys(right);
8821
+ if (leftKeys.length !== rightKeys.length) {
8822
+ return false;
8823
+ }
8824
+ for (const key of leftKeys) {
8825
+ if (left[key] !== right[key]) {
8826
+ return false;
8827
+ }
8828
+ }
8829
+ return true;
8764
8830
  }
8765
- function creationOpToLson(op) {
8766
- switch (op.type) {
8767
- case OpCode.CREATE_REGISTER:
8768
- return op.data;
8769
- case OpCode.CREATE_OBJECT:
8770
- return new LiveObject(op.data);
8771
- case OpCode.CREATE_MAP:
8772
- return new LiveMap();
8773
- case OpCode.CREATE_LIST:
8774
- return new LiveList([]);
8775
- default:
8776
- return assertNever(op, "Unknown creation Op");
8831
+ function cloneAttributes(attributes) {
8832
+ return attributes === void 0 ? void 0 : freeze({ ...attributes });
8833
+ }
8834
+ function normalizeSegments(segments) {
8835
+ const normalized = [];
8836
+ for (const segment of segments) {
8837
+ if (segment.text.length === 0) {
8838
+ continue;
8839
+ }
8840
+ const last = normalized.at(-1);
8841
+ const attributes = cloneAttributes(segment.attributes);
8842
+ if (last !== void 0 && attributesEqual(last.attributes, attributes)) {
8843
+ last.text += segment.text;
8844
+ } else {
8845
+ normalized.push({ text: segment.text, attributes });
8846
+ }
8777
8847
  }
8848
+ return normalized;
8778
8849
  }
8779
- function isSameNodeOrChildOf(node, parent) {
8780
- if (node === parent) {
8781
- return true;
8850
+ function dataToSegments(data) {
8851
+ return normalizeSegments(
8852
+ data.map(([text, attributes]) => ({
8853
+ text,
8854
+ attributes
8855
+ }))
8856
+ );
8857
+ }
8858
+ function segmentsToData(segments) {
8859
+ return segments.map(
8860
+ (segment) => segment.attributes === void 0 ? [segment.text] : [segment.text, { ...segment.attributes }]
8861
+ );
8862
+ }
8863
+ function textLength(segments) {
8864
+ return segments.reduce((sum, segment) => sum + segment.text.length, 0);
8865
+ }
8866
+ function splitSegmentsAt(segments, index) {
8867
+ const result = [];
8868
+ let offset = 0;
8869
+ for (const segment of segments) {
8870
+ const end = offset + segment.text.length;
8871
+ if (index > offset && index < end) {
8872
+ const before2 = segment.text.slice(0, index - offset);
8873
+ const after2 = segment.text.slice(index - offset);
8874
+ result.push({ text: before2, attributes: segment.attributes });
8875
+ result.push({ text: after2, attributes: segment.attributes });
8876
+ } else {
8877
+ result.push({ text: segment.text, attributes: segment.attributes });
8878
+ }
8879
+ offset = end;
8782
8880
  }
8783
- if (node.parent.type === "HasParent") {
8784
- return isSameNodeOrChildOf(node.parent.node, parent);
8881
+ return result;
8882
+ }
8883
+ function clipRange(index, length, contentLength) {
8884
+ const clippedIndex = Math.max(0, Math.min(index, contentLength));
8885
+ const clippedEnd = Math.max(
8886
+ clippedIndex,
8887
+ Math.min(index + length, contentLength)
8888
+ );
8889
+ return { index: clippedIndex, length: clippedEnd - clippedIndex };
8890
+ }
8891
+ function applyInsert(segments, index, text, attributes) {
8892
+ if (text.length === 0) {
8893
+ return normalizeSegments(segments);
8785
8894
  }
8786
- return false;
8895
+ const split = splitSegmentsAt(segments, index);
8896
+ const result = [];
8897
+ let offset = 0;
8898
+ let inserted = false;
8899
+ for (const segment of split) {
8900
+ if (!inserted && offset === index) {
8901
+ result.push({ text, attributes });
8902
+ inserted = true;
8903
+ }
8904
+ result.push(segment);
8905
+ offset += segment.text.length;
8906
+ }
8907
+ if (!inserted) {
8908
+ result.push({ text, attributes });
8909
+ }
8910
+ return normalizeSegments(result);
8787
8911
  }
8788
- function deserialize(node, parentToChildren, pool) {
8789
- if (isObjectStorageNode(node)) {
8790
- return LiveObject._deserialize(node, parentToChildren, pool);
8791
- } else if (isListStorageNode(node)) {
8792
- return LiveList._deserialize(node, parentToChildren, pool);
8793
- } else if (isMapStorageNode(node)) {
8794
- return LiveMap._deserialize(node, parentToChildren, pool);
8795
- } else if (isRegisterStorageNode(node)) {
8796
- return LiveRegister._deserialize(node, parentToChildren, pool);
8797
- } else {
8798
- throw new Error("Unexpected CRDT type");
8912
+ function extractDeletedSegments(segments, index, length) {
8913
+ const split = splitSegmentsAt(
8914
+ splitSegmentsAt(segments, index),
8915
+ index + length
8916
+ );
8917
+ const deleted = [];
8918
+ let offset = 0;
8919
+ for (const segment of split) {
8920
+ const end = offset + segment.text.length;
8921
+ if (offset >= index && end <= index + length) {
8922
+ deleted.push({
8923
+ text: segment.text,
8924
+ attributes: segment.attributes
8925
+ });
8926
+ }
8927
+ offset = end;
8799
8928
  }
8929
+ return normalizeSegments(deleted);
8800
8930
  }
8801
- function deserializeToLson(node, parentToChildren, pool) {
8802
- if (isObjectStorageNode(node)) {
8803
- return LiveObject._deserialize(node, parentToChildren, pool);
8804
- } else if (isListStorageNode(node)) {
8805
- return LiveList._deserialize(node, parentToChildren, pool);
8806
- } else if (isMapStorageNode(node)) {
8807
- return LiveMap._deserialize(node, parentToChildren, pool);
8808
- } else if (isRegisterStorageNode(node)) {
8809
- return node[1].data;
8810
- } else {
8811
- throw new Error("Unexpected CRDT type");
8931
+ function applyDelete(segments, index, length) {
8932
+ const deletedSegments = extractDeletedSegments(segments, index, length);
8933
+ const split = splitSegmentsAt(
8934
+ splitSegmentsAt(segments, index),
8935
+ index + length
8936
+ );
8937
+ const result = [];
8938
+ let offset = 0;
8939
+ let deletedText = "";
8940
+ for (const segment of split) {
8941
+ const end = offset + segment.text.length;
8942
+ if (offset >= index && end <= index + length) {
8943
+ deletedText += segment.text;
8944
+ } else {
8945
+ result.push(segment);
8946
+ }
8947
+ offset = end;
8812
8948
  }
8949
+ return {
8950
+ segments: normalizeSegments(result),
8951
+ deletedText,
8952
+ deletedSegments
8953
+ };
8813
8954
  }
8814
- function isLiveStructure(value) {
8815
- return isLiveList(value) || isLiveMap(value) || isLiveObject(value);
8955
+ function applyFormat(segments, index, length, attributes) {
8956
+ const split = splitSegmentsAt(
8957
+ splitSegmentsAt(segments, index),
8958
+ index + length
8959
+ );
8960
+ const result = [];
8961
+ let offset = 0;
8962
+ for (const segment of split) {
8963
+ const end = offset + segment.text.length;
8964
+ if (offset >= index && end <= index + length) {
8965
+ const nextAttributes = {
8966
+ ...segment.attributes ?? {}
8967
+ };
8968
+ for (const [key, value] of Object.entries(attributes)) {
8969
+ if (value === null) {
8970
+ delete nextAttributes[key];
8971
+ } else {
8972
+ nextAttributes[key] = value;
8973
+ }
8974
+ }
8975
+ result.push({
8976
+ text: segment.text,
8977
+ attributes: Object.keys(nextAttributes).length === 0 ? void 0 : freeze(nextAttributes)
8978
+ });
8979
+ } else {
8980
+ result.push(segment);
8981
+ }
8982
+ offset = end;
8983
+ }
8984
+ return normalizeSegments(result);
8816
8985
  }
8817
- function isLiveNode(value) {
8818
- return isLiveStructure(value) || isLiveRegister(value);
8986
+ function formatReverseOperations(segments, index, length, patch) {
8987
+ const split = splitSegmentsAt(
8988
+ splitSegmentsAt(segments, index),
8989
+ index + length
8990
+ );
8991
+ const result = [];
8992
+ let offset = 0;
8993
+ for (const segment of split) {
8994
+ const end = offset + segment.text.length;
8995
+ if (offset >= index && end <= index + length) {
8996
+ const attributes = {};
8997
+ for (const key of Object.keys(patch)) {
8998
+ attributes[key] = segment.attributes?.[key] ?? null;
8999
+ }
9000
+ result.push({
9001
+ type: "format",
9002
+ index: offset,
9003
+ length: segment.text.length,
9004
+ attributes
9005
+ });
9006
+ }
9007
+ offset = end;
9008
+ }
9009
+ return result;
8819
9010
  }
8820
- function isLiveList(value) {
8821
- return value instanceof LiveList;
9011
+ function mapIndexThroughOperation(index, op) {
9012
+ if (op.type === "insert") {
9013
+ return op.index <= index ? index + op.text.length : index;
9014
+ } else if (op.type === "delete") {
9015
+ if (op.index >= index) {
9016
+ return index;
9017
+ }
9018
+ return Math.max(op.index, index - op.length);
9019
+ } else {
9020
+ return index;
9021
+ }
8822
9022
  }
8823
- function isLiveMap(value) {
8824
- return value instanceof LiveMap;
9023
+ function mapTextIndexThroughOperations(index, ops) {
9024
+ let mapped = index;
9025
+ for (const op of ops) {
9026
+ mapped = mapIndexThroughOperation(mapped, op);
9027
+ }
9028
+ return mapped;
8825
9029
  }
8826
- function isLiveObject(value) {
8827
- return value instanceof LiveObject;
9030
+ function inverseMapIndexThroughOperation(index, op) {
9031
+ if (op.type === "insert") {
9032
+ if (index <= op.index) {
9033
+ return index;
9034
+ }
9035
+ return Math.max(op.index, index - op.text.length);
9036
+ } else if (op.type === "delete") {
9037
+ return op.index <= index ? index + op.length : index;
9038
+ } else {
9039
+ return index;
9040
+ }
8828
9041
  }
8829
- function isLiveRegister(value) {
8830
- return value instanceof LiveRegister;
9042
+ function inverseMapTextIndexThroughOperations(index, ops) {
9043
+ let mapped = index;
9044
+ for (let i = ops.length - 1; i >= 0; i--) {
9045
+ mapped = inverseMapIndexThroughOperation(mapped, ops[i]);
9046
+ }
9047
+ return mapped;
8831
9048
  }
8832
- function cloneLson(value) {
8833
- return value === void 0 ? void 0 : isLiveStructure(value) ? value.clone() : deepClone(value);
9049
+ function oppositeOrder(order) {
9050
+ return order === "before" ? "after" : "before";
8834
9051
  }
8835
- function liveNodeToLson(obj) {
8836
- if (obj instanceof LiveRegister) {
8837
- return obj.data;
8838
- } else if (obj instanceof LiveList || obj instanceof LiveMap || obj instanceof LiveObject) {
8839
- return obj;
8840
- } else {
8841
- return assertNever(obj, "Unknown AbstractCrdt");
9052
+ function mapIndexOverDelete(index, deleteIndex, deleteLength) {
9053
+ if (deleteIndex >= index) {
9054
+ return index;
8842
9055
  }
9056
+ return Math.max(deleteIndex, index - deleteLength);
8843
9057
  }
8844
- function lsonToLiveNode(value) {
8845
- if (value instanceof LiveObject || value instanceof LiveMap || value instanceof LiveList) {
8846
- return value;
9058
+ function transformInsert(op, over, order) {
9059
+ if (over.type === "insert") {
9060
+ const shifts = over.index < op.index || over.index === op.index && order === "after";
9061
+ return [shifts ? { ...op, index: op.index + over.text.length } : { ...op }];
9062
+ } else if (over.type === "delete") {
9063
+ return [
9064
+ { ...op, index: mapIndexOverDelete(op.index, over.index, over.length) }
9065
+ ];
8847
9066
  } else {
8848
- return new LiveRegister(value);
9067
+ return [{ ...op }];
8849
9068
  }
8850
9069
  }
8851
- function dumpPool(pool) {
8852
- const rows = Array.from(pool.nodes.values(), (node) => {
8853
- const parent = node.parent;
8854
- const parentId = parent.type === "HasParent" ? parent.node._id ?? "?" : parent.type === "Orphaned" ? "<orphaned>" : "-";
8855
- let value;
8856
- if (node instanceof LiveRegister) {
8857
- value = stringifyOrLog(node.data);
8858
- } else if (node instanceof LiveList) {
8859
- value = "<LiveList>";
8860
- } else if (node instanceof LiveMap) {
8861
- value = "<LiveMap>";
8862
- } else {
8863
- value = "<LiveObject>";
9070
+ function transformDelete(op, over) {
9071
+ const start = op.index;
9072
+ const end = op.index + op.length;
9073
+ if (over.type === "insert") {
9074
+ const at = over.index;
9075
+ const len = over.text.length;
9076
+ if (at <= start) {
9077
+ return [{ ...op, index: start + len }];
8864
9078
  }
8865
- return { id: nn(node._id), parentId, key: node._parentKey ?? "", value };
8866
- });
8867
- rows.sort((a, b) => {
8868
- if (a.parentId !== b.parentId) return a.parentId < b.parentId ? -1 : 1;
8869
- if (a.key !== b.key) return a.key < b.key ? -1 : 1;
8870
- return 0;
8871
- });
8872
- return rows.map(
8873
- (r) => ` ${r.id} parent=${r.parentId} key=${r.key || "\u2014"} ${r.value}`
8874
- ).join("\n");
8875
- }
8876
- function isJsonEq(a, b) {
8877
- if (a === b) {
8878
- return true;
8879
- }
8880
- if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
8881
- return false;
9079
+ if (at >= end) {
9080
+ return [{ ...op }];
9081
+ }
9082
+ return [
9083
+ { type: "delete", index: start, length: at - start },
9084
+ { type: "delete", index: start + len, length: end - at }
9085
+ ];
9086
+ } else if (over.type === "delete") {
9087
+ const newStart = mapIndexOverDelete(start, over.index, over.length);
9088
+ const newEnd = mapIndexOverDelete(end, over.index, over.length);
9089
+ return newEnd - newStart > 0 ? [{ type: "delete", index: newStart, length: newEnd - newStart }] : [];
9090
+ } else {
9091
+ return [{ ...op }];
8882
9092
  }
8883
- if (Array.isArray(a) || Array.isArray(b)) {
8884
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
8885
- return false;
9093
+ }
9094
+ function transformFormat(op, over, order) {
9095
+ const start = op.index;
9096
+ const end = op.index + op.length;
9097
+ if (over.type === "insert") {
9098
+ const at = over.index;
9099
+ const len = over.text.length;
9100
+ if (at <= start) {
9101
+ return [{ ...op, index: start + len }];
8886
9102
  }
8887
- for (let i = 0; i < a.length; i++) {
8888
- if (!isJsonEq(a[i], b[i])) {
8889
- return false;
9103
+ if (at >= end) {
9104
+ return [{ ...op }];
9105
+ }
9106
+ return [
9107
+ {
9108
+ type: "format",
9109
+ index: start,
9110
+ length: at - start,
9111
+ attributes: op.attributes
9112
+ },
9113
+ {
9114
+ type: "format",
9115
+ index: at + len,
9116
+ length: end - at,
9117
+ attributes: op.attributes
9118
+ }
9119
+ ];
9120
+ } else if (over.type === "delete") {
9121
+ const newStart = mapIndexOverDelete(start, over.index, over.length);
9122
+ const newEnd = mapIndexOverDelete(end, over.index, over.length);
9123
+ return newEnd - newStart > 0 ? [
9124
+ {
9125
+ type: "format",
9126
+ index: newStart,
9127
+ length: newEnd - newStart,
9128
+ attributes: op.attributes
8890
9129
  }
9130
+ ] : [];
9131
+ } else {
9132
+ if (order === "after") {
9133
+ return [{ ...op }];
8891
9134
  }
8892
- return true;
9135
+ const overlapStart = Math.max(start, over.index);
9136
+ const overlapEnd = Math.min(end, over.index + over.length);
9137
+ if (overlapStart >= overlapEnd) {
9138
+ return [{ ...op }];
9139
+ }
9140
+ const hasConflict = Object.keys(op.attributes).some(
9141
+ (key) => key in over.attributes
9142
+ );
9143
+ if (!hasConflict) {
9144
+ return [{ ...op }];
9145
+ }
9146
+ const reduced = {};
9147
+ for (const [key, value] of Object.entries(op.attributes)) {
9148
+ if (!(key in over.attributes)) {
9149
+ reduced[key] = value;
9150
+ }
9151
+ }
9152
+ const pieces = [];
9153
+ if (start < overlapStart) {
9154
+ pieces.push({
9155
+ type: "format",
9156
+ index: start,
9157
+ length: overlapStart - start,
9158
+ attributes: op.attributes
9159
+ });
9160
+ }
9161
+ if (Object.keys(reduced).length > 0) {
9162
+ pieces.push({
9163
+ type: "format",
9164
+ index: overlapStart,
9165
+ length: overlapEnd - overlapStart,
9166
+ attributes: reduced
9167
+ });
9168
+ }
9169
+ if (overlapEnd < end) {
9170
+ pieces.push({
9171
+ type: "format",
9172
+ index: overlapEnd,
9173
+ length: end - overlapEnd,
9174
+ attributes: op.attributes
9175
+ });
9176
+ }
9177
+ return pieces;
8893
9178
  }
8894
- const aKeys = Object.keys(a);
8895
- if (aKeys.length !== Object.keys(b).length) {
8896
- return false;
9179
+ }
9180
+ function transformSingle(op, over, order) {
9181
+ switch (op.type) {
9182
+ case "insert":
9183
+ return transformInsert(op, over, order);
9184
+ case "delete":
9185
+ return transformDelete(op, over);
9186
+ case "format":
9187
+ return transformFormat(op, over, order);
8897
9188
  }
8898
- for (const key of aKeys) {
8899
- if (!isJsonEq(a[key], b[key])) {
8900
- return false;
9189
+ }
9190
+ function transformTextOperationsX(a, b, order) {
9191
+ if (a.length === 0 || b.length === 0) {
9192
+ return [[...a], [...b]];
9193
+ }
9194
+ if (a.length === 1 && b.length === 1) {
9195
+ return [
9196
+ transformSingle(a[0], b[0], order),
9197
+ transformSingle(b[0], a[0], oppositeOrder(order))
9198
+ ];
9199
+ }
9200
+ if (a.length > 1) {
9201
+ const [headA1, b1] = transformTextOperationsX([a[0]], b, order);
9202
+ const [restA1, b2] = transformTextOperationsX(a.slice(1), b1, order);
9203
+ return [[...headA1, ...restA1], b2];
9204
+ }
9205
+ const [a1, headB1] = transformTextOperationsX(a, [b[0]], order);
9206
+ const [a2, restB1] = transformTextOperationsX(a1, b.slice(1), order);
9207
+ return [a2, [...headB1, ...restB1]];
9208
+ }
9209
+ function transformTextOperations(ops, over, order) {
9210
+ return transformTextOperationsX(ops, over, order)[0];
9211
+ }
9212
+ function textOperationsEqual(a, b) {
9213
+ return a === b || stableStringify(a) === stableStringify(b);
9214
+ }
9215
+ function applyTextOperationsToSegments(segments, ops) {
9216
+ let next = [...segments];
9217
+ for (const op of ops) {
9218
+ if (op.type === "insert") {
9219
+ const index = Math.max(0, Math.min(op.index, textLength(next)));
9220
+ next = applyInsert(next, index, op.text, op.attributes);
9221
+ } else if (op.type === "delete") {
9222
+ const index = Math.max(0, Math.min(op.index, textLength(next)));
9223
+ const clipped = clipRange(index, op.length, textLength(next));
9224
+ next = applyDelete(next, clipped.index, clipped.length).segments;
9225
+ } else {
9226
+ const index = Math.max(0, Math.min(op.index, textLength(next)));
9227
+ const clipped = clipRange(index, op.length, textLength(next));
9228
+ next = applyFormat(next, clipped.index, clipped.length, op.attributes);
8901
9229
  }
8902
9230
  }
8903
- return true;
9231
+ return next;
8904
9232
  }
8905
- function diffNodeMap(prev, next) {
8906
- const ops = [];
8907
- prev.forEach((_, id) => {
8908
- if (!next.get(id)) {
8909
- ops.push({ type: OpCode.DELETE_CRDT, id });
9233
+ function applyLiveTextOperations(data, ops) {
9234
+ return segmentsToData(
9235
+ applyTextOperationsToSegments(dataToSegments(data), ops)
9236
+ );
9237
+ }
9238
+ function invertTextOperations(segments, ops) {
9239
+ let shadow = [...segments];
9240
+ const reverse = [];
9241
+ for (const op of ops) {
9242
+ if (op.type === "insert") {
9243
+ shadow = applyInsert(shadow, op.index, op.text, op.attributes);
9244
+ reverse.unshift({
9245
+ type: "delete",
9246
+ index: op.index,
9247
+ length: op.text.length
9248
+ });
9249
+ } else if (op.type === "delete") {
9250
+ const deletedSegments = extractDeletedSegments(
9251
+ shadow,
9252
+ op.index,
9253
+ op.length
9254
+ );
9255
+ shadow = applyDelete(shadow, op.index, op.length).segments;
9256
+ const inserts = [];
9257
+ let insertIndex = op.index;
9258
+ for (const segment of deletedSegments) {
9259
+ inserts.push({
9260
+ type: "insert",
9261
+ index: insertIndex,
9262
+ text: segment.text,
9263
+ attributes: segment.attributes
9264
+ });
9265
+ insertIndex += segment.text.length;
9266
+ }
9267
+ for (let index = inserts.length - 1; index >= 0; index--) {
9268
+ reverse.unshift(inserts[index]);
9269
+ }
9270
+ } else {
9271
+ const inverse = formatReverseOperations(
9272
+ shadow,
9273
+ op.index,
9274
+ op.length,
9275
+ op.attributes
9276
+ );
9277
+ shadow = applyFormat(shadow, op.index, op.length, op.attributes);
9278
+ reverse.unshift(...inverse.reverse());
8910
9279
  }
8911
- });
9280
+ }
9281
+ return reverse;
9282
+ }
9283
+
9284
+ // src/crdts/LiveText.ts
9285
+ var ACCEPTED_OPS_HISTORY_LIMIT = 1e3;
9286
+ var LiveText = class _LiveText extends AbstractCrdt {
9287
+ /** The local document: #confirmed ⊕ #inFlightOps ⊕ #queuedOps. */
9288
+ #segments;
9289
+ /** The server-confirmed document (only authoritative ops applied). */
9290
+ #confirmed;
9291
+ #version;
9292
+ /** The op currently awaiting server acknowledgement (at most one). */
9293
+ #inFlightOpId;
9294
+ /** Its ops, continuously re-expressed against current server state. */
9295
+ #inFlightOps = [];
9296
+ /** Local edits made while an op is in flight; sent after the ack. */
9297
+ #queuedOps = [];
9298
+ #acceptedOps = [];
9299
+ /**
9300
+ * Creates a new LiveText document.
9301
+ *
9302
+ * @param textOrData Initial plain text, or an array of `[text]` /
9303
+ * `[text, attributes]` segments. Defaults to an empty document.
9304
+ *
9305
+ * @example
9306
+ * new LiveText();
9307
+ * new LiveText("Hello world");
9308
+ * new LiveText([["Hello ", { bold: true }], ["world"]]);
9309
+ */
9310
+ constructor(textOrData = "", version = 0) {
9311
+ super();
9312
+ this.#segments = typeof textOrData === "string" ? textOrData.length === 0 ? [] : [{ text: textOrData }] : dataToSegments(textOrData);
9313
+ this.#confirmed = [...this.#segments];
9314
+ this.#version = version;
9315
+ Object.assign(this[kInternal], {
9316
+ encodeIndex: (localIndex) => this.#encodeIndex(localIndex),
9317
+ decodeIndex: (index, fromVersion) => this.#decodeIndex(index, fromVersion)
9318
+ });
9319
+ }
9320
+ get version() {
9321
+ return this.#version;
9322
+ }
9323
+ get length() {
9324
+ return textLength(this.#segments);
9325
+ }
9326
+ /** @internal */
9327
+ static _deserialize([id, item], _parentToChildren, pool) {
9328
+ const text = new _LiveText(item.data, item.version);
9329
+ text._attach(id, pool);
9330
+ return text;
9331
+ }
9332
+ /** @internal */
9333
+ _toOps(parentId, parentKey) {
9334
+ if (this._id === void 0) {
9335
+ throw new Error("Cannot serialize LiveText if it is not attached");
9336
+ }
9337
+ return [
9338
+ {
9339
+ type: OpCode.CREATE_TEXT,
9340
+ id: this._id,
9341
+ parentId,
9342
+ parentKey,
9343
+ data: this.toJSON(),
9344
+ version: this.#version
9345
+ }
9346
+ ];
9347
+ }
9348
+ /** @internal */
9349
+ _serialize() {
9350
+ if (this.parent.type !== "HasParent") {
9351
+ throw new Error("Cannot serialize LiveText if parent is missing");
9352
+ }
9353
+ return {
9354
+ type: CrdtType.TEXT,
9355
+ parentId: nn(this.parent.node._id, "Parent node expected to have ID"),
9356
+ parentKey: this.parent.key,
9357
+ data: this.toJSON(),
9358
+ version: this.#version
9359
+ };
9360
+ }
9361
+ /** @internal */
9362
+ _attachChild(_op) {
9363
+ throw new Error("LiveText cannot contain child nodes");
9364
+ }
9365
+ /** @internal */
9366
+ _detachChild(_crdt) {
9367
+ throw new Error("LiveText cannot contain child nodes");
9368
+ }
9369
+ /** @internal */
9370
+ _apply(op, isLocal) {
9371
+ if (op.type !== OpCode.UPDATE_TEXT) {
9372
+ return super._apply(op, isLocal);
9373
+ }
9374
+ if (isLocal) {
9375
+ return this.#applyLocal(op);
9376
+ }
9377
+ if (op.opId !== void 0 && op.opId === this.#inFlightOpId) {
9378
+ return this.#applyAck(op);
9379
+ }
9380
+ if (op.opId !== void 0 && this.#acceptedOps.some((entry) => entry.opId === op.opId)) {
9381
+ this.#version = Math.max(this.#version, op.version ?? op.baseVersion + 1);
9382
+ return { modified: false };
9383
+ }
9384
+ return this.#applyRemote(op);
9385
+ }
9386
+ /**
9387
+ * Inserts text at the given index.
9388
+ *
9389
+ * @param index Character index at which to insert. Values outside the
9390
+ * document range are clipped.
9391
+ * @param text Text to insert.
9392
+ * @param attributes Optional inline attributes for the inserted text.
9393
+ *
9394
+ * @example
9395
+ * const text = new LiveText("Hello");
9396
+ * text.insert(5, " world");
9397
+ * text.insert(0, "Say: ", { italic: true });
9398
+ */
9399
+ insert(index, text, attributes) {
9400
+ const clippedIndex = Math.max(0, Math.min(index, this.length));
9401
+ this.#dispatch([{ type: "insert", index: clippedIndex, text, attributes }]);
9402
+ }
9403
+ /**
9404
+ * Deletes `length` characters starting at `index`.
9405
+ *
9406
+ * @example
9407
+ * const text = new LiveText("Hello world");
9408
+ * text.delete(5, 6); // "Hello"
9409
+ */
9410
+ delete(index, length) {
9411
+ const clipped = clipRange(index, length, this.length);
9412
+ if (clipped.length === 0) {
9413
+ return;
9414
+ }
9415
+ this.#dispatch([
9416
+ { type: "delete", index: clipped.index, length: clipped.length }
9417
+ ]);
9418
+ }
9419
+ /**
9420
+ * Replaces a range of text with new text.
9421
+ *
9422
+ * @example
9423
+ * const text = new LiveText("Hello world");
9424
+ * text.replace(0, 5, "Hi"); // "Hi world"
9425
+ */
9426
+ replace(index, length, text, attributes) {
9427
+ const clipped = clipRange(index, length, this.length);
9428
+ const ops = [];
9429
+ if (clipped.length > 0) {
9430
+ ops.push({
9431
+ type: "delete",
9432
+ index: clipped.index,
9433
+ length: clipped.length
9434
+ });
9435
+ }
9436
+ if (text.length > 0) {
9437
+ ops.push({ type: "insert", index: clipped.index, text, attributes });
9438
+ }
9439
+ this.#dispatch(ops);
9440
+ }
9441
+ /**
9442
+ * Encode a local-document index (an offset into this LiveText's current
9443
+ * #segments, which CodeMirror or any consumer mirrors as its document)
9444
+ * into server-confirmed coordinates suitable for broadcasting to peers via
9445
+ * presence or any other side channel.
9446
+ *
9447
+ * The returned index is in this LiveText's current #confirmed coordinates
9448
+ * — that is, with this client's local pending ops inverse-mapped out.
9449
+ * Pair it with the current {@link LiveText.version} when sending so the
9450
+ * receiver can call {@link PrivateLiveTextApi.decodeIndex} to land the
9451
+ * position in their own local document coordinates regardless of their
9452
+ * private pending ops.
9453
+ *
9454
+ * Index ambiguity at boundaries is resolved by an inverse-of-forward
9455
+ * convention: a position at or before a local insertion is reported as
9456
+ * the position right before the insertion in #confirmed; a position past
9457
+ * the insertion shifts left by the insertion's length. Positions inside
9458
+ * an own-pending insertion collapse to the insertion point.
9459
+ */
9460
+ #encodeIndex(localIndex) {
9461
+ let mapped = Math.max(0, Math.min(localIndex, this.length));
9462
+ mapped = inverseMapTextIndexThroughOperations(mapped, this.#queuedOps);
9463
+ mapped = inverseMapTextIndexThroughOperations(mapped, this.#inFlightOps);
9464
+ return mapped;
9465
+ }
9466
+ /**
9467
+ * Decode an `(index, fromVersion)` pair produced by
9468
+ * {@link PrivateLiveTextApi.encodeIndex} — typically on a peer — into an
9469
+ * offset in this LiveText's current local document (an index suitable for
9470
+ * placing a CodeMirror marker, an annotation anchor, or anything else that
9471
+ * lives over #segments).
9472
+ *
9473
+ * Composes the accepted ops applied since `fromVersion` (drawn from
9474
+ * #acceptedOps in locally-applied form) with this client's own local
9475
+ * pending ops, in that order. The result is in current #segments
9476
+ * coordinates.
9477
+ *
9478
+ * Returns `null` when the position cannot be decoded against the current
9479
+ * state:
9480
+ * - `fromVersion` is greater than this LiveText's current version: the
9481
+ * peer is ahead of us. The caller should park the message and retry
9482
+ * after more accepted ops arrive.
9483
+ * - `fromVersion` falls outside the retained accepted-ops history. This
9484
+ * only happens after very long-lived disconnections; the caller can
9485
+ * fall back to using the raw index and letting subsequent local
9486
+ * transactions map it (with bounded drift).
9487
+ */
9488
+ #decodeIndex(index, fromVersion) {
9489
+ if (fromVersion > this.#version) {
9490
+ return null;
9491
+ }
9492
+ if (fromVersion < this.#version) {
9493
+ const oldest = this.#acceptedOps[0]?.version;
9494
+ if (oldest === void 0 || oldest > fromVersion + 1) {
9495
+ return null;
9496
+ }
9497
+ }
9498
+ let mapped = index;
9499
+ for (const entry of this.#acceptedOps) {
9500
+ if (entry.version <= fromVersion) continue;
9501
+ if (entry.version > this.#version) break;
9502
+ if (entry.ops.length === 0) continue;
9503
+ mapped = mapTextIndexThroughOperations(mapped, entry.ops);
9504
+ }
9505
+ mapped = mapTextIndexThroughOperations(mapped, this.#inFlightOps);
9506
+ mapped = mapTextIndexThroughOperations(mapped, this.#queuedOps);
9507
+ return Math.max(0, Math.min(mapped, this.length));
9508
+ }
9509
+ /**
9510
+ * Applies or removes inline attributes on a range of text.
9511
+ *
9512
+ * Set an attribute to `null` to remove it from the range.
9513
+ *
9514
+ * @example
9515
+ * const text = new LiveText("Hello world");
9516
+ * text.format(0, 5, { bold: true });
9517
+ * text.format(0, 5, { bold: null });
9518
+ */
9519
+ format(index, length, attributes) {
9520
+ const clipped = clipRange(index, length, this.length);
9521
+ if (clipped.length === 0) {
9522
+ return;
9523
+ }
9524
+ this.#dispatch([
9525
+ {
9526
+ type: "format",
9527
+ index: clipped.index,
9528
+ length: clipped.length,
9529
+ attributes
9530
+ }
9531
+ ]);
9532
+ }
9533
+ /** Local edits made through the public API. */
9534
+ #dispatch(ops) {
9535
+ if (ops.length === 0) {
9536
+ return;
9537
+ }
9538
+ this._pool?.assertStorageIsWritable();
9539
+ const attached = this._pool !== void 0 && this._id !== void 0;
9540
+ const reverse = attached ? this.#invertOperations(ops) : [];
9541
+ const changes = this.#applyOperationsLocally(ops);
9542
+ if (!attached) {
9543
+ return;
9544
+ }
9545
+ const pool = nn(this._pool);
9546
+ const id = nn(this._id);
9547
+ const updates = /* @__PURE__ */ new Map([
9548
+ [
9549
+ id,
9550
+ {
9551
+ type: "LiveText",
9552
+ node: this,
9553
+ version: this.#version,
9554
+ updates: changes
9555
+ }
9556
+ ]
9557
+ ]);
9558
+ if (this.#inFlightOpId === void 0) {
9559
+ const opId = pool.generateOpId();
9560
+ this.#inFlightOpId = opId;
9561
+ this.#inFlightOps = [...ops];
9562
+ pool.dispatch(
9563
+ [
9564
+ {
9565
+ type: OpCode.UPDATE_TEXT,
9566
+ id,
9567
+ opId,
9568
+ baseVersion: this.#version,
9569
+ ops: [...ops]
9570
+ }
9571
+ ],
9572
+ reverse,
9573
+ updates
9574
+ );
9575
+ } else {
9576
+ this.#queuedOps.push(...ops);
9577
+ pool.dispatch([], reverse, updates, { clearRedoStack: true });
9578
+ }
9579
+ }
9580
+ /**
9581
+ * A local replay of an existing wire op: an undo/redo frame, or an
9582
+ * unacknowledged op re-sent after a reconnect.
9583
+ */
9584
+ #applyLocal(op) {
9585
+ const mutableOp = op;
9586
+ if (op.opId !== void 0 && op.opId === this.#inFlightOpId) {
9587
+ this.#inFlightOps = [...this.#inFlightOps, ...this.#queuedOps];
9588
+ this.#queuedOps = [];
9589
+ mutableOp.baseVersion = this.#version;
9590
+ mutableOp.ops = [...this.#inFlightOps];
9591
+ return { modified: false };
9592
+ }
9593
+ let ops = op.ops;
9594
+ for (const entry of this.#acceptedOps) {
9595
+ if (entry.version > op.baseVersion && entry.ops.length > 0) {
9596
+ ops = transformTextOperations(ops, entry.ops, "after");
9597
+ }
9598
+ }
9599
+ const reverse = this.#invertOperations(ops);
9600
+ const changes = this.#applyOperationsLocally(ops);
9601
+ if (this.#inFlightOpId === void 0 && ops.length > 0) {
9602
+ this.#inFlightOpId = nn(op.opId, "Local ops must have an opId");
9603
+ this.#inFlightOps = [...ops];
9604
+ mutableOp.baseVersion = this.#version;
9605
+ mutableOp.ops = [...ops];
9606
+ } else {
9607
+ this.#queuedOps.push(...ops);
9608
+ mutableOp.baseVersion = this.#version;
9609
+ mutableOp.ops = [];
9610
+ }
9611
+ if (changes.length === 0) {
9612
+ return { modified: false };
9613
+ }
9614
+ return {
9615
+ reverse,
9616
+ modified: {
9617
+ type: "LiveText",
9618
+ node: this,
9619
+ version: this.#version,
9620
+ updates: changes
9621
+ }
9622
+ };
9623
+ }
9624
+ /** Server acknowledgement of our in-flight op. */
9625
+ #applyAck(op) {
9626
+ const ackedVersion = op.version ?? Math.max(this.#version, op.baseVersion + 1);
9627
+ const predicted = this.#inFlightOps;
9628
+ const opId = this.#inFlightOpId;
9629
+ this.#confirmed = applyTextOperationsToSegments(this.#confirmed, op.ops);
9630
+ this.#inFlightOpId = void 0;
9631
+ this.#inFlightOps = [];
9632
+ let appliedOps = [];
9633
+ let result = { modified: false };
9634
+ if (!textOperationsEqual(op.ops, predicted)) {
9635
+ error2(
9636
+ "LiveText: acknowledgement did not match the local prediction; resynchronizing"
9637
+ );
9638
+ const rebuilt = this.#rebuildLocalFromConfirmed();
9639
+ appliedOps = rebuilt.appliedOps;
9640
+ if (rebuilt.changes.length > 0) {
9641
+ result = {
9642
+ reverse: [],
9643
+ modified: {
9644
+ type: "LiveText",
9645
+ node: this,
9646
+ version: ackedVersion,
9647
+ updates: rebuilt.changes
9648
+ }
9649
+ };
9650
+ }
9651
+ }
9652
+ this.#version = Math.max(this.#version, ackedVersion);
9653
+ this.#recordAccepted(ackedVersion, appliedOps, opId);
9654
+ this.#flushQueued();
9655
+ return result;
9656
+ }
9657
+ /** An accepted op from another client (or a server-fabricated fix op). */
9658
+ #applyRemote(op) {
9659
+ const version = op.version ?? this.#version + 1;
9660
+ this.#confirmed = applyTextOperationsToSegments(this.#confirmed, op.ops);
9661
+ const [overInFlight, inFlight] = transformTextOperationsX(
9662
+ op.ops,
9663
+ this.#inFlightOps,
9664
+ "before"
9665
+ );
9666
+ const [applied, queued] = transformTextOperationsX(
9667
+ overInFlight,
9668
+ this.#queuedOps,
9669
+ "before"
9670
+ );
9671
+ this.#inFlightOps = inFlight;
9672
+ this.#queuedOps = queued;
9673
+ this.#recordAccepted(version, applied, op.opId);
9674
+ if (applied.length === 0) {
9675
+ this.#version = Math.max(this.#version, version);
9676
+ return { modified: false };
9677
+ }
9678
+ const reverse = this.#invertOperations(applied);
9679
+ const changes = this.#applyOperationsLocally(applied);
9680
+ this.#version = Math.max(this.#version, version);
9681
+ return {
9682
+ reverse,
9683
+ modified: {
9684
+ type: "LiveText",
9685
+ node: this,
9686
+ version: this.#version,
9687
+ updates: changes
9688
+ }
9689
+ };
9690
+ }
9691
+ /** Send the queued ops as the next in-flight op (after an ack). */
9692
+ #flushQueued() {
9693
+ if (this.#queuedOps.length === 0 || this._pool === void 0 || this._id === void 0) {
9694
+ return;
9695
+ }
9696
+ const opId = this._pool.generateOpId();
9697
+ this.#inFlightOpId = opId;
9698
+ this.#inFlightOps = this.#queuedOps;
9699
+ this.#queuedOps = [];
9700
+ this._pool.dispatch(
9701
+ [
9702
+ {
9703
+ type: OpCode.UPDATE_TEXT,
9704
+ id: this._id,
9705
+ opId,
9706
+ baseVersion: this.#version,
9707
+ ops: [...this.#inFlightOps]
9708
+ }
9709
+ ],
9710
+ [],
9711
+ /* @__PURE__ */ new Map(),
9712
+ // The local content was already applied (and made undoable) when the
9713
+ // edits happened; this is purely an outbound flush.
9714
+ { clearRedoStack: false }
9715
+ );
9716
+ }
9717
+ /**
9718
+ * Rebuild the local document as confirmed ⊕ queued ops, returning the
9719
+ * coarse delta that was applied. Only used by defensive recovery paths.
9720
+ */
9721
+ #rebuildLocalFromConfirmed() {
9722
+ const before2 = this.#segments;
9723
+ const after2 = applyTextOperationsToSegments(this.#confirmed, [
9724
+ ...this.#inFlightOps,
9725
+ ...this.#queuedOps
9726
+ ]);
9727
+ if (stableStringify(segmentsToData(before2)) === stableStringify(segmentsToData(after2))) {
9728
+ this.#segments = after2;
9729
+ return { appliedOps: [], changes: [] };
9730
+ }
9731
+ const beforeText = before2.map((segment) => segment.text).join("");
9732
+ this.#segments = after2;
9733
+ this.invalidate();
9734
+ const appliedOps = [];
9735
+ const changes = [];
9736
+ if (beforeText.length > 0) {
9737
+ appliedOps.push({ type: "delete", index: 0, length: beforeText.length });
9738
+ changes.push({
9739
+ type: "delete",
9740
+ index: 0,
9741
+ length: beforeText.length,
9742
+ deletedText: beforeText
9743
+ });
9744
+ }
9745
+ let index = 0;
9746
+ for (const segment of after2) {
9747
+ appliedOps.push({
9748
+ type: "insert",
9749
+ index,
9750
+ text: segment.text,
9751
+ attributes: segment.attributes
9752
+ });
9753
+ changes.push({
9754
+ type: "insert",
9755
+ index,
9756
+ text: segment.text,
9757
+ attributes: segment.attributes
9758
+ });
9759
+ index += segment.text.length;
9760
+ }
9761
+ return { appliedOps, changes };
9762
+ }
9763
+ /**
9764
+ * Reconcile this node against an authoritative storage snapshot (e.g.
9765
+ * after a reconnect). The confirmed state and version are replaced by the
9766
+ * snapshot's; pending (in-flight + queued) ops are preserved on top and
9767
+ * will be re-sent by the offline-ops replay.
9768
+ *
9769
+ * @internal
9770
+ */
9771
+ _resyncText(data, version) {
9772
+ this.#confirmed = dataToSegments(data);
9773
+ this.#version = version;
9774
+ this.#acceptedOps = [];
9775
+ const rebuilt = this.#rebuildLocalFromConfirmed();
9776
+ if (rebuilt.changes.length === 0) {
9777
+ return void 0;
9778
+ }
9779
+ return {
9780
+ type: "LiveText",
9781
+ node: this,
9782
+ version: this.#version,
9783
+ updates: rebuilt.changes
9784
+ };
9785
+ }
9786
+ /**
9787
+ * Called when the server rejected one of our ops. Drops all pending state
9788
+ * for this node (edits queued behind a rejected op cannot be trusted
9789
+ * either); the room follows up with a storage resync.
9790
+ *
9791
+ * @internal
9792
+ */
9793
+ _rejectPendingOp(opId) {
9794
+ if (opId !== this.#inFlightOpId) {
9795
+ return;
9796
+ }
9797
+ this.#inFlightOpId = void 0;
9798
+ this.#inFlightOps = [];
9799
+ this.#queuedOps = [];
9800
+ }
9801
+ #recordAccepted(version, ops, opId) {
9802
+ if (this.#acceptedOps.some((entry) => entry.version === version)) {
9803
+ return;
9804
+ }
9805
+ this.#acceptedOps.push({ version, opId, ops: [...ops] });
9806
+ this.#acceptedOps.sort((left, right) => left.version - right.version);
9807
+ if (this.#acceptedOps.length > ACCEPTED_OPS_HISTORY_LIMIT) {
9808
+ this.#acceptedOps.splice(
9809
+ 0,
9810
+ this.#acceptedOps.length - ACCEPTED_OPS_HISTORY_LIMIT
9811
+ );
9812
+ }
9813
+ }
9814
+ #applyOperationsLocally(ops) {
9815
+ const changes = [];
9816
+ for (const op of ops) {
9817
+ if (op.type === "insert") {
9818
+ this.#segments = applyInsert(
9819
+ this.#segments,
9820
+ op.index,
9821
+ op.text,
9822
+ op.attributes
9823
+ );
9824
+ changes.push({
9825
+ type: "insert",
9826
+ index: op.index,
9827
+ text: op.text,
9828
+ attributes: op.attributes
9829
+ });
9830
+ } else if (op.type === "delete") {
9831
+ const result = applyDelete(this.#segments, op.index, op.length);
9832
+ this.#segments = result.segments;
9833
+ changes.push({
9834
+ type: "delete",
9835
+ index: op.index,
9836
+ length: op.length,
9837
+ deletedText: result.deletedText
9838
+ });
9839
+ } else {
9840
+ this.#segments = applyFormat(
9841
+ this.#segments,
9842
+ op.index,
9843
+ op.length,
9844
+ op.attributes
9845
+ );
9846
+ changes.push({
9847
+ type: "format",
9848
+ index: op.index,
9849
+ length: op.length,
9850
+ attributes: op.attributes
9851
+ });
9852
+ }
9853
+ }
9854
+ this.invalidate();
9855
+ return changes;
9856
+ }
9857
+ #invertOperations(ops) {
9858
+ return [
9859
+ {
9860
+ type: OpCode.UPDATE_TEXT,
9861
+ id: nn(this._id),
9862
+ baseVersion: this.#version,
9863
+ ops: invertTextOperations(this.#segments, ops)
9864
+ }
9865
+ ];
9866
+ }
9867
+ /** Returns the plain text content without attributes. Equivalent to joining the text from each segment in {@link LiveText.toJSON}. */
9868
+ toString() {
9869
+ return this.#segments.map((segment) => segment.text).join("");
9870
+ }
9871
+ /**
9872
+ * Returns a JSON-compatible snapshot of the document as a {@link LiveTextData}
9873
+ * array.
9874
+ *
9875
+ * @example
9876
+ * new LiveText([["Hello ", { bold: true }], ["world"]]).toJSON();
9877
+ * // [["Hello ", { bold: true }], ["world"]]
9878
+ */
9879
+ toJSON() {
9880
+ return super.toJSON();
9881
+ }
9882
+ /** @internal */
9883
+ _toJSON() {
9884
+ return segmentsToData(this.#segments);
9885
+ }
9886
+ /** @internal */
9887
+ toTreeNode(key) {
9888
+ return super.toTreeNode(key);
9889
+ }
9890
+ /** @internal */
9891
+ _toTreeNode(key) {
9892
+ const nodeId = this._id ?? nanoid();
9893
+ const payload = this.toJSON().map(
9894
+ (segment, index) => ({
9895
+ type: "Json",
9896
+ id: `${nodeId}:${index}`,
9897
+ key: String(index),
9898
+ payload: segment
9899
+ })
9900
+ );
9901
+ payload.push({
9902
+ type: "Json",
9903
+ id: `${nodeId}:version`,
9904
+ key: "version",
9905
+ payload: this.version
9906
+ });
9907
+ return {
9908
+ type: "LiveText",
9909
+ id: nodeId,
9910
+ key,
9911
+ payload
9912
+ };
9913
+ }
9914
+ clone() {
9915
+ return new _LiveText(this.toJSON(), this.#version);
9916
+ }
9917
+ };
9918
+
9919
+ // src/crdts/liveblocks-helpers.ts
9920
+ function creationOpToLiveNode(op) {
9921
+ return lsonToLiveNode(creationOpToLson(op));
9922
+ }
9923
+ function creationOpToLson(op) {
9924
+ switch (op.type) {
9925
+ case OpCode.CREATE_REGISTER:
9926
+ return op.data;
9927
+ case OpCode.CREATE_OBJECT:
9928
+ return new LiveObject(op.data);
9929
+ case OpCode.CREATE_MAP:
9930
+ return new LiveMap();
9931
+ case OpCode.CREATE_LIST:
9932
+ return new LiveList([]);
9933
+ case OpCode.CREATE_TEXT:
9934
+ return new LiveText(op.data, op.version);
9935
+ default:
9936
+ return assertNever(op, "Unknown creation Op");
9937
+ }
9938
+ }
9939
+ function isSameNodeOrChildOf(node, parent) {
9940
+ if (node === parent) {
9941
+ return true;
9942
+ }
9943
+ if (node.parent.type === "HasParent") {
9944
+ return isSameNodeOrChildOf(node.parent.node, parent);
9945
+ }
9946
+ return false;
9947
+ }
9948
+ function liveObjectFromNodeStream(nodes) {
9949
+ const pool = createManagedPool({
9950
+ getCurrentConnectionId: () => {
9951
+ throw new Error(
9952
+ "Cannot mutate a historic storage version: it is a read-only snapshot"
9953
+ );
9954
+ }
9955
+ });
9956
+ return LiveObject._fromItems(nodes, pool);
9957
+ }
9958
+ function deserialize(node, parentToChildren, pool) {
9959
+ if (isObjectStorageNode(node)) {
9960
+ return LiveObject._deserialize(node, parentToChildren, pool);
9961
+ } else if (isListStorageNode(node)) {
9962
+ return LiveList._deserialize(node, parentToChildren, pool);
9963
+ } else if (isMapStorageNode(node)) {
9964
+ return LiveMap._deserialize(node, parentToChildren, pool);
9965
+ } else if (isRegisterStorageNode(node)) {
9966
+ return LiveRegister._deserialize(node, parentToChildren, pool);
9967
+ } else if (isTextStorageNode(node)) {
9968
+ return LiveText._deserialize(node, parentToChildren, pool);
9969
+ } else {
9970
+ throw new Error("Unexpected CRDT type");
9971
+ }
9972
+ }
9973
+ function deserializeToLson(node, parentToChildren, pool) {
9974
+ if (isObjectStorageNode(node)) {
9975
+ return LiveObject._deserialize(node, parentToChildren, pool);
9976
+ } else if (isListStorageNode(node)) {
9977
+ return LiveList._deserialize(node, parentToChildren, pool);
9978
+ } else if (isMapStorageNode(node)) {
9979
+ return LiveMap._deserialize(node, parentToChildren, pool);
9980
+ } else if (isRegisterStorageNode(node)) {
9981
+ return node[1].data;
9982
+ } else if (isTextStorageNode(node)) {
9983
+ return LiveText._deserialize(node, parentToChildren, pool);
9984
+ } else {
9985
+ throw new Error("Unexpected CRDT type");
9986
+ }
9987
+ }
9988
+ function isLiveStructure(value) {
9989
+ return isLiveList(value) || isLiveMap(value) || isLiveObject(value) || isLiveText(value);
9990
+ }
9991
+ function isLiveNode(value) {
9992
+ return isLiveStructure(value) || isLiveRegister(value);
9993
+ }
9994
+ function isLiveList(value) {
9995
+ return value instanceof LiveList;
9996
+ }
9997
+ function isLiveMap(value) {
9998
+ return value instanceof LiveMap;
9999
+ }
10000
+ function isLiveObject(value) {
10001
+ return value instanceof LiveObject;
10002
+ }
10003
+ function isLiveText(value) {
10004
+ return value instanceof LiveText;
10005
+ }
10006
+ function isLiveRegister(value) {
10007
+ return value instanceof LiveRegister;
10008
+ }
10009
+ function cloneLson(value) {
10010
+ return value === void 0 ? void 0 : isLiveStructure(value) ? value.clone() : deepClone(value);
10011
+ }
10012
+ function liveNodeToLson(obj) {
10013
+ if (obj instanceof LiveRegister) {
10014
+ return obj.data;
10015
+ } else if (obj instanceof LiveList || obj instanceof LiveMap || obj instanceof LiveObject || obj instanceof LiveText) {
10016
+ return obj;
10017
+ } else {
10018
+ return assertNever(obj, "Unknown AbstractCrdt");
10019
+ }
10020
+ }
10021
+ function lsonToLiveNode(value) {
10022
+ if (value instanceof LiveObject || value instanceof LiveMap || value instanceof LiveList || value instanceof LiveText) {
10023
+ return value;
10024
+ } else {
10025
+ return new LiveRegister(value);
10026
+ }
10027
+ }
10028
+ function dumpPool(pool) {
10029
+ const rows = Array.from(pool.nodes.values(), (node) => {
10030
+ const parent = node.parent;
10031
+ const parentId = parent.type === "HasParent" ? parent.node._id ?? "?" : parent.type === "Orphaned" ? "<orphaned>" : "-";
10032
+ let value;
10033
+ if (node instanceof LiveRegister) {
10034
+ value = stringifyOrLog(node.data);
10035
+ } else if (node instanceof LiveList) {
10036
+ value = "<LiveList>";
10037
+ } else if (node instanceof LiveMap) {
10038
+ value = "<LiveMap>";
10039
+ } else {
10040
+ value = "<LiveObject>";
10041
+ }
10042
+ return { id: nn(node._id), parentId, key: node._parentKey ?? "", value };
10043
+ });
10044
+ rows.sort((a, b) => {
10045
+ if (a.parentId !== b.parentId) return a.parentId < b.parentId ? -1 : 1;
10046
+ if (a.key !== b.key) return a.key < b.key ? -1 : 1;
10047
+ return 0;
10048
+ });
10049
+ return rows.map(
10050
+ (r) => ` ${r.id} parent=${r.parentId} key=${r.key || "\u2014"} ${r.value}`
10051
+ ).join("\n");
10052
+ }
10053
+ function isJsonEq(a, b) {
10054
+ if (a === b) {
10055
+ return true;
10056
+ }
10057
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
10058
+ return false;
10059
+ }
10060
+ if (Array.isArray(a) || Array.isArray(b)) {
10061
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
10062
+ return false;
10063
+ }
10064
+ for (let i = 0; i < a.length; i++) {
10065
+ if (!isJsonEq(a[i], b[i])) {
10066
+ return false;
10067
+ }
10068
+ }
10069
+ return true;
10070
+ }
10071
+ const aKeys = Object.keys(a);
10072
+ if (aKeys.length !== Object.keys(b).length) {
10073
+ return false;
10074
+ }
10075
+ for (const key of aKeys) {
10076
+ if (!isJsonEq(a[key], b[key])) {
10077
+ return false;
10078
+ }
10079
+ }
10080
+ return true;
10081
+ }
10082
+ function diffNodeMap(prev, next) {
10083
+ const ops = [];
10084
+ prev.forEach((_, id) => {
10085
+ if (!next.get(id)) {
10086
+ ops.push({ type: OpCode.DELETE_CRDT, id });
10087
+ }
10088
+ });
10089
+ const emitted = /* @__PURE__ */ new Set();
10090
+ function emitCreate(id, crdt) {
10091
+ if (emitted.has(id)) {
10092
+ return;
10093
+ }
10094
+ emitted.add(id);
10095
+ const parentId = crdt.parentId;
10096
+ if (parentId !== void 0 && !prev.has(parentId)) {
10097
+ const parentCrdt = next.get(parentId);
10098
+ if (parentCrdt !== void 0) {
10099
+ emitCreate(parentId, parentCrdt);
10100
+ }
10101
+ }
10102
+ switch (crdt.type) {
10103
+ case CrdtType.REGISTER:
10104
+ ops.push({
10105
+ type: OpCode.CREATE_REGISTER,
10106
+ id,
10107
+ parentId: crdt.parentId,
10108
+ parentKey: crdt.parentKey,
10109
+ data: crdt.data
10110
+ });
10111
+ break;
10112
+ case CrdtType.LIST:
10113
+ ops.push({
10114
+ type: OpCode.CREATE_LIST,
10115
+ id,
10116
+ parentId: crdt.parentId,
10117
+ parentKey: crdt.parentKey
10118
+ });
10119
+ break;
10120
+ case CrdtType.OBJECT:
10121
+ if (crdt.parentId === void 0 || crdt.parentKey === void 0) {
10122
+ throw new Error(
10123
+ "Internal error. Cannot serialize storage root into an operation"
10124
+ );
10125
+ }
10126
+ ops.push({
10127
+ type: OpCode.CREATE_OBJECT,
10128
+ id,
10129
+ parentId: crdt.parentId,
10130
+ parentKey: crdt.parentKey,
10131
+ data: crdt.data
10132
+ });
10133
+ break;
10134
+ case CrdtType.MAP:
10135
+ ops.push({
10136
+ type: OpCode.CREATE_MAP,
10137
+ id,
10138
+ parentId: crdt.parentId,
10139
+ parentKey: crdt.parentKey
10140
+ });
10141
+ break;
10142
+ case CrdtType.TEXT:
10143
+ ops.push({
10144
+ type: OpCode.CREATE_TEXT,
10145
+ id,
10146
+ parentId: crdt.parentId,
10147
+ parentKey: crdt.parentKey,
10148
+ data: crdt.data,
10149
+ version: crdt.version
10150
+ });
10151
+ break;
10152
+ }
10153
+ }
8912
10154
  next.forEach((crdt, id) => {
8913
10155
  const currentCrdt = prev.get(id);
8914
10156
  if (currentCrdt) {
@@ -8945,47 +10187,7 @@ function diffNodeMap(prev, next) {
8945
10187
  });
8946
10188
  }
8947
10189
  } else {
8948
- switch (crdt.type) {
8949
- case CrdtType.REGISTER:
8950
- ops.push({
8951
- type: OpCode.CREATE_REGISTER,
8952
- id,
8953
- parentId: crdt.parentId,
8954
- parentKey: crdt.parentKey,
8955
- data: crdt.data
8956
- });
8957
- break;
8958
- case CrdtType.LIST:
8959
- ops.push({
8960
- type: OpCode.CREATE_LIST,
8961
- id,
8962
- parentId: crdt.parentId,
8963
- parentKey: crdt.parentKey
8964
- });
8965
- break;
8966
- case CrdtType.OBJECT:
8967
- if (crdt.parentId === void 0 || crdt.parentKey === void 0) {
8968
- throw new Error(
8969
- "Internal error. Cannot serialize storage root into an operation"
8970
- );
8971
- }
8972
- ops.push({
8973
- type: OpCode.CREATE_OBJECT,
8974
- id,
8975
- parentId: crdt.parentId,
8976
- parentKey: crdt.parentKey,
8977
- data: crdt.data
8978
- });
8979
- break;
8980
- case CrdtType.MAP:
8981
- ops.push({
8982
- type: OpCode.CREATE_MAP,
8983
- id,
8984
- parentId: crdt.parentId,
8985
- parentKey: crdt.parentKey
8986
- });
8987
- break;
8988
- }
10190
+ emitCreate(id, crdt);
8989
10191
  }
8990
10192
  });
8991
10193
  return ops;
@@ -9017,19 +10219,43 @@ function mergeListStorageUpdates(first, second) {
9017
10219
  updates: updates.concat(second.updates)
9018
10220
  };
9019
10221
  }
10222
+ function mergeTextStorageUpdates(first, second) {
10223
+ return {
10224
+ ...second,
10225
+ updates: first.updates.concat(second.updates)
10226
+ };
10227
+ }
9020
10228
  function mergeStorageUpdates(first, second) {
9021
10229
  if (first === void 0) {
9022
10230
  return second;
9023
10231
  }
10232
+ let merged;
9024
10233
  if (first.type === "LiveObject" && second.type === "LiveObject") {
9025
- return mergeObjectStorageUpdates(first, second);
10234
+ merged = mergeObjectStorageUpdates(first, second);
9026
10235
  } else if (first.type === "LiveMap" && second.type === "LiveMap") {
9027
- return mergeMapStorageUpdates(first, second);
10236
+ merged = mergeMapStorageUpdates(first, second);
9028
10237
  } else if (first.type === "LiveList" && second.type === "LiveList") {
9029
- return mergeListStorageUpdates(first, second);
10238
+ merged = mergeListStorageUpdates(first, second);
10239
+ } else if (first.type === "LiveText" && second.type === "LiveText") {
10240
+ merged = mergeTextStorageUpdates(first, second);
9030
10241
  } else {
10242
+ merged = second;
10243
+ }
10244
+ const sa = first[kStorageUpdateSource];
10245
+ const sb = second[kStorageUpdateSource];
10246
+ if (sa !== void 0 || sb !== void 0) {
10247
+ if (sa?.origin === "remote" || sb?.origin === "remote") {
10248
+ merged[kStorageUpdateSource] = { origin: "remote" };
10249
+ } else if (sa?.via === "history" || sb?.via === "history") {
10250
+ const historySource = sb?.via === "history" ? sb : sa?.via === "history" ? sa : void 0;
10251
+ if (historySource?.via === "history") {
10252
+ merged[kStorageUpdateSource] = historySource;
10253
+ }
10254
+ } else {
10255
+ merged[kStorageUpdateSource] = { origin: "local", via: "mutation" };
10256
+ }
9031
10257
  }
9032
- return second;
10258
+ return merged;
9033
10259
  }
9034
10260
 
9035
10261
  // src/devtools/bridge.ts
@@ -9973,7 +11199,7 @@ function createRoom(options, config) {
9973
11199
  yjsProvider: void 0,
9974
11200
  yjsProviderDidChange: makeEventSource(),
9975
11201
  // Storage
9976
- pool: createManagedPool(roomId, {
11202
+ pool: createManagedPool({
9977
11203
  getCurrentConnectionId,
9978
11204
  onDispatch,
9979
11205
  isStorageWritable,
@@ -9986,6 +11212,8 @@ function createRoom(options, config) {
9986
11212
  activeBatch: null,
9987
11213
  unacknowledgedOps
9988
11214
  };
11215
+ let nextHistoryItemId = 0;
11216
+ let historyDisabled = 0;
9989
11217
  const nodeMapBuffer = makeNodeMapBuffer();
9990
11218
  const stopwatch = config.enableDebugLogging ? makeStopWatch() : void 0;
9991
11219
  let lastTokenKey;
@@ -10070,7 +11298,10 @@ function createRoom(options, config) {
10070
11298
  }
10071
11299
  }
10072
11300
  });
10073
- function onDispatch(ops, reverse, storageUpdates) {
11301
+ function onDispatch(ops, reverse, storageUpdates, options2) {
11302
+ for (const value of storageUpdates.values()) {
11303
+ value[kStorageUpdateSource] = { origin: "local", via: "mutation" };
11304
+ }
10074
11305
  if (context.activeBatch) {
10075
11306
  for (const op of ops) {
10076
11307
  context.activeBatch.ops.push(op);
@@ -10089,8 +11320,10 @@ function createRoom(options, config) {
10089
11320
  if (reverse.length > 0) {
10090
11321
  addToUndoStack(reverse);
10091
11322
  }
11323
+ if (options2?.clearRedoStack ?? ops.length > 0) {
11324
+ clearRedoStack();
11325
+ }
10092
11326
  if (ops.length > 0) {
10093
- context.redoStack.length = 0;
10094
11327
  dispatchOps(ops);
10095
11328
  }
10096
11329
  notify({ storageUpdates });
@@ -10110,6 +11343,7 @@ function createRoom(options, config) {
10110
11343
  others: makeEventSource(),
10111
11344
  storageBatch: makeEventSource(),
10112
11345
  history: makeEventSource(),
11346
+ privateHistory: makeEventSource(),
10113
11347
  storageDidLoad: makeEventSource(),
10114
11348
  storageStatus: makeEventSource(),
10115
11349
  ydoc: makeEventSource(),
@@ -10136,12 +11370,18 @@ function createRoom(options, config) {
10136
11370
  signal: options2.signal
10137
11371
  });
10138
11372
  }
10139
- async function getYjsHistoryVersion(versionId) {
10140
- return httpClient.getYjsHistoryVersion({ roomId, versionId });
11373
+ async function fetchStorageHistoryVersion(versionId) {
11374
+ return httpClient.fetchStorageHistoryVersion({ roomId, versionId });
11375
+ }
11376
+ async function fetchYjsHistoryVersion(versionId) {
11377
+ return httpClient.fetchYjsHistoryVersion({ roomId, versionId });
10141
11378
  }
10142
11379
  async function createVersionHistorySnapshot() {
10143
11380
  return httpClient.createVersionHistorySnapshot({ roomId });
10144
11381
  }
11382
+ async function deleteHistoryVersion(versionId) {
11383
+ return httpClient.deleteHistoryVersion({ roomId, versionId });
11384
+ }
10145
11385
  async function executeContextualPrompt(options2) {
10146
11386
  return httpClient.executeContextualPrompt({
10147
11387
  roomId,
@@ -10191,17 +11431,36 @@ function createRoom(options, config) {
10191
11431
  self,
10192
11432
  (me) => me !== null ? userToTreeNode("Me", me) : null
10193
11433
  );
11434
+ function diffCurrentStorageAgainst(target) {
11435
+ const current = /* @__PURE__ */ new Map();
11436
+ for (const [id, crdt] of context.pool.nodes) {
11437
+ current.set(id, crdt._serialize());
11438
+ }
11439
+ return diffNodeMap(current, target);
11440
+ }
10194
11441
  function createOrUpdateRootFromMessage(nodes) {
10195
11442
  if (nodes.size === 0) {
10196
11443
  throw new Error("Internal error: cannot load storage without items");
10197
11444
  }
10198
11445
  if (context.root !== void 0) {
10199
- const currentItems = /* @__PURE__ */ new Map();
10200
- for (const [id, crdt] of context.pool.nodes) {
10201
- currentItems.set(id, crdt._serialize());
11446
+ const result = applyRemoteOps(diffCurrentStorageAgainst(nodes));
11447
+ for (const [id, crdt] of nodes) {
11448
+ if (crdt.type === CrdtType.TEXT) {
11449
+ const node = context.pool.nodes.get(id);
11450
+ if (node !== void 0 && isLiveText(node)) {
11451
+ const update = node._resyncText(crdt.data, crdt.version);
11452
+ if (update !== void 0) {
11453
+ result.updates.storageUpdates.set(
11454
+ id,
11455
+ mergeStorageUpdates(
11456
+ result.updates.storageUpdates.get(id),
11457
+ update
11458
+ )
11459
+ );
11460
+ }
11461
+ }
11462
+ }
10202
11463
  }
10203
- const ops = diffNodeMap(currentItems, nodes);
10204
- const result = applyRemoteOps(ops);
10205
11464
  notify(result.updates);
10206
11465
  } else {
10207
11466
  context.root = LiveObject._fromItems(
@@ -10226,11 +11485,48 @@ function createRoom(options, config) {
10226
11485
  }
10227
11486
  });
10228
11487
  }
11488
+ function notifyPrivateHistory(event) {
11489
+ if (historyDisabled > 0) return;
11490
+ eventHub.privateHistory.notify(event);
11491
+ }
11492
+ function clearRedoStack() {
11493
+ if (context.redoStack.length === 0) return;
11494
+ const ids = context.redoStack.map((item) => item.id);
11495
+ context.redoStack.length = 0;
11496
+ notifyPrivateHistory({ action: "discard", ids });
11497
+ }
11498
+ function reconcileStorageWithNodes(nodes) {
11499
+ if (context.root === void 0) {
11500
+ throw new Error("Cannot reconcile storage before it is loaded");
11501
+ }
11502
+ const ops = diffCurrentStorageAgainst(
11503
+ new Map(nodes)
11504
+ );
11505
+ if (ops.length === 0) {
11506
+ return;
11507
+ }
11508
+ const result = applyLocalOps(ops);
11509
+ if (result.reverse.length > 0) {
11510
+ addToUndoStack(result.reverse);
11511
+ }
11512
+ context.redoStack.length = 0;
11513
+ for (const op of result.opsToEmit) {
11514
+ context.buffer.storageOperations.push(op);
11515
+ }
11516
+ notify(result.updates);
11517
+ onHistoryChange();
11518
+ flushNowOrSoon();
11519
+ }
10229
11520
  function _addToRealUndoStack(frames) {
10230
11521
  if (context.undoStack.length >= 50) {
10231
- context.undoStack.shift();
11522
+ const evicted = context.undoStack.shift();
11523
+ if (evicted !== void 0) {
11524
+ notifyPrivateHistory({ action: "discard", ids: [evicted.id] });
11525
+ }
10232
11526
  }
10233
- context.undoStack.push(frames);
11527
+ const id = nextHistoryItemId++;
11528
+ context.undoStack.push({ id, frames });
11529
+ notifyPrivateHistory({ action: "push", id });
10234
11530
  onHistoryChange();
10235
11531
  }
10236
11532
  function addToUndoStack(frames) {
@@ -10268,7 +11564,7 @@ function createRoom(options, config) {
10268
11564
  "Internal. Tried to get connection id but connection was never open"
10269
11565
  );
10270
11566
  }
10271
- function applyLocalOps(frames) {
11567
+ function applyLocalOps(frames, localStorageUpdateSource = { origin: "local", via: "mutation" }) {
10272
11568
  const [pframes, ops] = partition(
10273
11569
  frames,
10274
11570
  (f) => f.type === "presence"
@@ -10280,7 +11576,8 @@ function createRoom(options, config) {
10280
11576
  pframes,
10281
11577
  opsWithOpIds,
10282
11578
  /* isLocal */
10283
- true
11579
+ true,
11580
+ localStorageUpdateSource
10284
11581
  );
10285
11582
  return { opsToEmit: opsWithOpIds, reverse, updates };
10286
11583
  }
@@ -10292,7 +11589,7 @@ function createRoom(options, config) {
10292
11589
  false
10293
11590
  );
10294
11591
  }
10295
- function applyOps(pframes, ops, isLocal) {
11592
+ function applyOps(pframes, ops, isLocal, localStorageUpdateSource = { origin: "local", via: "mutation" }) {
10296
11593
  const output = {
10297
11594
  reverse: new Deque(),
10298
11595
  storageUpdates: /* @__PURE__ */ new Map(),
@@ -10330,6 +11627,7 @@ function createRoom(options, config) {
10330
11627
  }
10331
11628
  const applyOpResult = applyOp(op, source);
10332
11629
  if (applyOpResult.modified) {
11630
+ applyOpResult.modified[kStorageUpdateSource] = source === 1 /* THEIRS */ ? { origin: "remote" } : localStorageUpdateSource;
10333
11631
  const nodeId = applyOpResult.modified.node._id;
10334
11632
  if (!(nodeId && createdNodeIds.has(nodeId))) {
10335
11633
  output.storageUpdates.set(
@@ -10341,7 +11639,7 @@ function createRoom(options, config) {
10341
11639
  );
10342
11640
  output.reverse.pushLeft(applyOpResult.reverse);
10343
11641
  }
10344
- if (op.type === OpCode.CREATE_LIST || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_OBJECT) {
11642
+ if (op.type === OpCode.CREATE_LIST || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_TEXT) {
10345
11643
  createdNodeIds.add(op.id);
10346
11644
  }
10347
11645
  }
@@ -10361,6 +11659,7 @@ function createRoom(options, config) {
10361
11659
  switch (op.type) {
10362
11660
  case OpCode.DELETE_OBJECT_KEY:
10363
11661
  case OpCode.UPDATE_OBJECT:
11662
+ case OpCode.UPDATE_TEXT:
10364
11663
  case OpCode.DELETE_CRDT: {
10365
11664
  const node = context.pool.nodes.get(op.id);
10366
11665
  if (node === void 0) {
@@ -10385,6 +11684,7 @@ function createRoom(options, config) {
10385
11684
  case OpCode.CREATE_OBJECT:
10386
11685
  case OpCode.CREATE_LIST:
10387
11686
  case OpCode.CREATE_MAP:
11687
+ case OpCode.CREATE_TEXT:
10388
11688
  case OpCode.CREATE_REGISTER: {
10389
11689
  if (op.parentId === void 0) {
10390
11690
  return { modified: false };
@@ -10395,6 +11695,10 @@ function createRoom(options, config) {
10395
11695
  }
10396
11696
  return parentNode._attachChild(op, source);
10397
11697
  }
11698
+ // Unknown op codes can be received when older and newer clients are
11699
+ // both present in a same room. Older clients simply ignore them.
11700
+ default:
11701
+ return { modified: false };
10398
11702
  }
10399
11703
  }
10400
11704
  function updatePresence(patch, options2) {
@@ -10634,16 +11938,37 @@ function createRoom(options, config) {
10634
11938
  }
10635
11939
  break;
10636
11940
  }
10637
- // Receiving a RejectedOps message in the client means that the server is no
10638
- // longer in sync with the client. Trying to synchronize the client again by
10639
- // rolling back particular Ops may be hard/impossible. It's fine to not try and
10640
- // accept the out-of-sync reality and throw an error.
11941
+ // Receiving a RejectedOps message means the server refused some of
11942
+ // our ops, so our optimistic local state is out of sync with the
11943
+ // server. For LiveText ops this is a normal (if rare) situation
11944
+ // e.g. a client that was offline long enough to fall outside the
11945
+ // server's retained history window — and we can recover: drop the
11946
+ // rejected pending state and re-fetch the authoritative storage
11947
+ // snapshot. For other ops (e.g. permission rejections), rolling back
11948
+ // particular Ops is hard/impossible, so we keep the old behavior of
11949
+ // accepting the out-of-sync reality and surfacing an error.
10641
11950
  case ServerMsgCode.REJECT_STORAGE_OP: {
10642
11951
  errorWithTitle(
10643
11952
  "Storage mutation rejection error",
10644
11953
  message.reason
10645
11954
  );
10646
- if (process.env.NODE_ENV !== "production") {
11955
+ let needsStorageResync = false;
11956
+ for (const opId of message.opIds) {
11957
+ const rejectedOp = context.unacknowledgedOps.get(opId);
11958
+ context.unacknowledgedOps.delete(opId);
11959
+ context.buffer.storageOperations = context.buffer.storageOperations.filter((op) => op.opId !== opId);
11960
+ if (rejectedOp !== void 0 && rejectedOp.type === OpCode.UPDATE_TEXT) {
11961
+ const node = context.pool.nodes.get(rejectedOp.id);
11962
+ if (node !== void 0 && isLiveText(node)) {
11963
+ node._rejectPendingOp(opId);
11964
+ needsStorageResync = true;
11965
+ }
11966
+ }
11967
+ }
11968
+ if (needsStorageResync) {
11969
+ refreshStorage();
11970
+ flushNowOrSoon();
11971
+ } else if (process.env.NODE_ENV !== "production") {
10647
11972
  throw new Error(
10648
11973
  `Storage mutations rejected by server: ${message.reason}`
10649
11974
  );
@@ -11185,14 +12510,19 @@ function createRoom(options, config) {
11185
12510
  if (context.activeBatch) {
11186
12511
  throw new Error("undo is not allowed during a batch");
11187
12512
  }
11188
- const frames = context.undoStack.pop();
11189
- if (frames === void 0) {
12513
+ const item = context.undoStack.pop();
12514
+ if (item === void 0) {
11190
12515
  return;
11191
12516
  }
11192
12517
  context.pausedHistory = null;
11193
- const result = applyLocalOps(frames);
12518
+ const result = applyLocalOps(item.frames, {
12519
+ origin: "local",
12520
+ via: "history",
12521
+ action: "undo"
12522
+ });
12523
+ context.redoStack.push({ id: item.id, frames: result.reverse });
12524
+ notifyPrivateHistory({ action: "undo", id: item.id });
11194
12525
  notify(result.updates);
11195
- context.redoStack.push(result.reverse);
11196
12526
  onHistoryChange();
11197
12527
  for (const op of result.opsToEmit) {
11198
12528
  context.buffer.storageOperations.push(op);
@@ -11203,14 +12533,19 @@ function createRoom(options, config) {
11203
12533
  if (context.activeBatch) {
11204
12534
  throw new Error("redo is not allowed during a batch");
11205
12535
  }
11206
- const frames = context.redoStack.pop();
11207
- if (frames === void 0) {
12536
+ const item = context.redoStack.pop();
12537
+ if (item === void 0) {
11208
12538
  return;
11209
12539
  }
11210
12540
  context.pausedHistory = null;
11211
- const result = applyLocalOps(frames);
12541
+ const result = applyLocalOps(item.frames, {
12542
+ origin: "local",
12543
+ via: "history",
12544
+ action: "redo"
12545
+ });
12546
+ context.undoStack.push({ id: item.id, frames: result.reverse });
12547
+ notifyPrivateHistory({ action: "redo", id: item.id });
11212
12548
  notify(result.updates);
11213
- context.undoStack.push(result.reverse);
11214
12549
  onHistoryChange();
11215
12550
  for (const op of result.opsToEmit) {
11216
12551
  context.buffer.storageOperations.push(op);
@@ -11220,6 +12555,8 @@ function createRoom(options, config) {
11220
12555
  function clear() {
11221
12556
  context.undoStack.length = 0;
11222
12557
  context.redoStack.length = 0;
12558
+ notifyPrivateHistory({ action: "clear" });
12559
+ onHistoryChange();
11223
12560
  }
11224
12561
  function batch2(callback) {
11225
12562
  if (context.activeBatch) {
@@ -11248,7 +12585,7 @@ function createRoom(options, config) {
11248
12585
  commitPausedHistoryToUndoStack();
11249
12586
  }
11250
12587
  if (currentBatch.ops.length > 0) {
11251
- context.redoStack.length = 0;
12588
+ clearRedoStack();
11252
12589
  }
11253
12590
  if (currentBatch.ops.length > 0) {
11254
12591
  dispatchOps(currentBatch.ops);
@@ -11277,7 +12614,6 @@ function createRoom(options, config) {
11277
12614
  }
11278
12615
  commitPausedHistoryToUndoStack();
11279
12616
  }
11280
- let historyDisabled = 0;
11281
12617
  function disableHistory(fn) {
11282
12618
  const origUndo = context.undoStack;
11283
12619
  const origRedo = context.redoStack;
@@ -11517,13 +12853,28 @@ function createRoom(options, config) {
11517
12853
  },
11518
12854
  // prettier-ignore
11519
12855
  get undoStack() {
11520
- return deepClone(context.undoStack);
12856
+ return structuredClone(
12857
+ context.undoStack.map((item) => ({
12858
+ id: item.id,
12859
+ frames: item.frames
12860
+ }))
12861
+ );
12862
+ },
12863
+ // prettier-ignore
12864
+ get redoStack() {
12865
+ return structuredClone(
12866
+ context.redoStack.map((item) => ({
12867
+ id: item.id,
12868
+ frames: item.frames
12869
+ }))
12870
+ );
11521
12871
  },
11522
12872
  // prettier-ignore
11523
12873
  get nodeCount() {
11524
12874
  return context.pool.nodes.size;
11525
12875
  },
11526
12876
  // prettier-ignore
12877
+ history: eventHub.privateHistory.observable,
11527
12878
  getYjsProvider() {
11528
12879
  return context.yjsProvider;
11529
12880
  },
@@ -11546,9 +12897,16 @@ function createRoom(options, config) {
11546
12897
  // List versions of the document since the specified date
11547
12898
  listHistoryVersionsSince,
11548
12899
  // get a specific version
11549
- getYjsHistoryVersion,
12900
+ fetchStorageHistoryVersion,
12901
+ fetchYjsHistoryVersion,
12902
+ // reconstruct a storage version's nodes into a read-only LiveObject tree
12903
+ liveObjectFromNodeStream,
12904
+ // restore live storage to match a version's nodes
12905
+ reconcileStorageWithNodes,
11550
12906
  // create a version
11551
12907
  createVersionHistorySnapshot,
12908
+ // delete a version
12909
+ deleteHistoryVersion,
11552
12910
  // execute a contextual prompt
11553
12911
  executeContextualPrompt,
11554
12912
  // Support for the Liveblocks browser extension
@@ -12544,6 +13902,12 @@ function toPlainLson(lson) {
12544
13902
  liveblocksType: "LiveList",
12545
13903
  data: [...lson].map((item) => toPlainLson(item))
12546
13904
  };
13905
+ } else if (lson instanceof LiveText) {
13906
+ return {
13907
+ liveblocksType: "LiveText",
13908
+ data: lson.toJSON(),
13909
+ version: lson.version
13910
+ };
12547
13911
  } else {
12548
13912
  return lson;
12549
13913
  }
@@ -12725,6 +14089,7 @@ export {
12725
14089
  LiveList,
12726
14090
  LiveMap,
12727
14091
  LiveObject,
14092
+ LiveText,
12728
14093
  LiveblocksError,
12729
14094
  MENTION_CHARACTER,
12730
14095
  MutableSignal,
@@ -12736,6 +14101,7 @@ export {
12736
14101
  SortedList,
12737
14102
  TextEditorType,
12738
14103
  WebsocketCloseCodes,
14104
+ applyLiveTextOperations,
12739
14105
  asPos,
12740
14106
  assert,
12741
14107
  assertNever,
@@ -12793,8 +14159,10 @@ export {
12793
14159
  isRegisterStorageNode,
12794
14160
  isRootStorageNode,
12795
14161
  isStartsWithOperator,
14162
+ isTextStorageNode,
12796
14163
  isUrl,
12797
14164
  kInternal,
14165
+ kStorageUpdateSource,
12798
14166
  keys,
12799
14167
  makeAbortController,
12800
14168
  makeEventSource,
@@ -12821,6 +14189,7 @@ export {
12821
14189
  stringifyCommentBody,
12822
14190
  throwUsageError,
12823
14191
  toPlainLson,
14192
+ transformTextOperations,
12824
14193
  tryParseJson,
12825
14194
  url,
12826
14195
  urljoin,