@liveblocks/core 3.20.0-exp7 → 3.20.0-exp8

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.20.0-exp7";
9
+ var PKG_VERSION = "3.20.0-exp8";
10
10
  var PKG_FORMAT = "esm";
11
11
 
12
12
  // src/dupe-detection.ts
@@ -5519,7 +5519,7 @@ function isIgnoredOp(op) {
5519
5519
  return op.type === OpCode.DELETE_CRDT && op.id === "ACK";
5520
5520
  }
5521
5521
  function isCreateOp(op) {
5522
- return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST;
5522
+ 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;
5523
5523
  }
5524
5524
 
5525
5525
  // src/protocol/StorageNode.ts
@@ -5814,6 +5814,10 @@ ${parentKey}`;
5814
5814
  get size() {
5815
5815
  return this.#byOpId.size;
5816
5816
  }
5817
+ /** The still-unacknowledged op with the given opId, if any. */
5818
+ get(opId) {
5819
+ return this.#byOpId.get(opId);
5820
+ }
5817
5821
  /**
5818
5822
  * Mark the given Op as still unacknowledged.
5819
5823
  */
@@ -5914,8 +5918,8 @@ function createManagedPool(roomId, options) {
5914
5918
  deleteNode: (id) => void nodes.delete(id),
5915
5919
  generateId: () => `${getCurrentConnectionId()}:${clock++}`,
5916
5920
  generateOpId: () => `${getCurrentConnectionId()}:${opClock++}`,
5917
- dispatch(ops, reverse, storageUpdates) {
5918
- onDispatch?.(ops, reverse, storageUpdates);
5921
+ dispatch(ops, reverse, storageUpdates, options2) {
5922
+ onDispatch?.(ops, reverse, storageUpdates, options2);
5919
5923
  },
5920
5924
  assertStorageIsWritable: () => {
5921
5925
  if (!isStorageWritable()) {
@@ -8674,43 +8678,171 @@ function formatReverseOperations(segments, index, length, patch) {
8674
8678
  }
8675
8679
  return result;
8676
8680
  }
8677
- function mapIndexThroughOperation(index, op) {
8678
- if (op.type === "insert") {
8679
- return op.index <= index ? index + op.text.length : index;
8680
- } else if (op.type === "delete") {
8681
- if (op.index >= index) {
8682
- return index;
8683
- }
8684
- return Math.max(op.index, index - op.length);
8685
- } else {
8681
+ function oppositeOrder(order) {
8682
+ return order === "before" ? "after" : "before";
8683
+ }
8684
+ function mapIndexOverDelete(index, deleteIndex, deleteLength) {
8685
+ if (deleteIndex >= index) {
8686
8686
  return index;
8687
8687
  }
8688
+ return Math.max(deleteIndex, index - deleteLength);
8688
8689
  }
8689
- function mapTextIndexThroughOperations(index, ops) {
8690
- let mapped = index;
8691
- for (const op of ops) {
8692
- mapped = mapIndexThroughOperation(mapped, op);
8690
+ function transformInsert(op, over, order) {
8691
+ if (over.type === "insert") {
8692
+ const shifts = over.index < op.index || over.index === op.index && order === "after";
8693
+ return [shifts ? { ...op, index: op.index + over.text.length } : { ...op }];
8694
+ } else if (over.type === "delete") {
8695
+ return [
8696
+ { ...op, index: mapIndexOverDelete(op.index, over.index, over.length) }
8697
+ ];
8698
+ } else {
8699
+ return [{ ...op }];
8693
8700
  }
8694
- return mapped;
8695
8701
  }
8696
- function rebaseTextOperations(ops, acceptedOps) {
8697
- return ops.map((op) => {
8698
- if (op.type === "insert") {
8699
- return {
8700
- ...op,
8701
- index: mapTextIndexThroughOperations(op.index, acceptedOps)
8702
- };
8703
- } else if (op.type === "delete" || op.type === "format") {
8704
- const start = mapTextIndexThroughOperations(op.index, acceptedOps);
8705
- const end = mapTextIndexThroughOperations(
8706
- op.index + op.length,
8707
- acceptedOps
8708
- );
8709
- return { ...op, index: start, length: Math.max(0, end - start) };
8710
- } else {
8711
- return op;
8702
+ function transformDelete(op, over) {
8703
+ const start = op.index;
8704
+ const end = op.index + op.length;
8705
+ if (over.type === "insert") {
8706
+ const at = over.index;
8707
+ const len = over.text.length;
8708
+ if (at <= start) {
8709
+ return [{ ...op, index: start + len }];
8712
8710
  }
8713
- });
8711
+ if (at >= end) {
8712
+ return [{ ...op }];
8713
+ }
8714
+ return [
8715
+ { type: "delete", index: start, length: at - start },
8716
+ { type: "delete", index: start + len, length: end - at }
8717
+ ];
8718
+ } else if (over.type === "delete") {
8719
+ const newStart = mapIndexOverDelete(start, over.index, over.length);
8720
+ const newEnd = mapIndexOverDelete(end, over.index, over.length);
8721
+ return newEnd - newStart > 0 ? [{ type: "delete", index: newStart, length: newEnd - newStart }] : [];
8722
+ } else {
8723
+ return [{ ...op }];
8724
+ }
8725
+ }
8726
+ function transformFormat(op, over, order) {
8727
+ const start = op.index;
8728
+ const end = op.index + op.length;
8729
+ if (over.type === "insert") {
8730
+ const at = over.index;
8731
+ const len = over.text.length;
8732
+ if (at <= start) {
8733
+ return [{ ...op, index: start + len }];
8734
+ }
8735
+ if (at >= end) {
8736
+ return [{ ...op }];
8737
+ }
8738
+ return [
8739
+ {
8740
+ type: "format",
8741
+ index: start,
8742
+ length: at - start,
8743
+ attributes: op.attributes
8744
+ },
8745
+ {
8746
+ type: "format",
8747
+ index: at + len,
8748
+ length: end - at,
8749
+ attributes: op.attributes
8750
+ }
8751
+ ];
8752
+ } else if (over.type === "delete") {
8753
+ const newStart = mapIndexOverDelete(start, over.index, over.length);
8754
+ const newEnd = mapIndexOverDelete(end, over.index, over.length);
8755
+ return newEnd - newStart > 0 ? [
8756
+ {
8757
+ type: "format",
8758
+ index: newStart,
8759
+ length: newEnd - newStart,
8760
+ attributes: op.attributes
8761
+ }
8762
+ ] : [];
8763
+ } else {
8764
+ if (order === "after") {
8765
+ return [{ ...op }];
8766
+ }
8767
+ const overlapStart = Math.max(start, over.index);
8768
+ const overlapEnd = Math.min(end, over.index + over.length);
8769
+ if (overlapStart >= overlapEnd) {
8770
+ return [{ ...op }];
8771
+ }
8772
+ const hasConflict = Object.keys(op.attributes).some(
8773
+ (key) => key in over.attributes
8774
+ );
8775
+ if (!hasConflict) {
8776
+ return [{ ...op }];
8777
+ }
8778
+ const reduced = {};
8779
+ for (const [key, value] of Object.entries(op.attributes)) {
8780
+ if (!(key in over.attributes)) {
8781
+ reduced[key] = value;
8782
+ }
8783
+ }
8784
+ const pieces = [];
8785
+ if (start < overlapStart) {
8786
+ pieces.push({
8787
+ type: "format",
8788
+ index: start,
8789
+ length: overlapStart - start,
8790
+ attributes: op.attributes
8791
+ });
8792
+ }
8793
+ if (Object.keys(reduced).length > 0) {
8794
+ pieces.push({
8795
+ type: "format",
8796
+ index: overlapStart,
8797
+ length: overlapEnd - overlapStart,
8798
+ attributes: reduced
8799
+ });
8800
+ }
8801
+ if (overlapEnd < end) {
8802
+ pieces.push({
8803
+ type: "format",
8804
+ index: overlapEnd,
8805
+ length: end - overlapEnd,
8806
+ attributes: op.attributes
8807
+ });
8808
+ }
8809
+ return pieces;
8810
+ }
8811
+ }
8812
+ function transformSingle(op, over, order) {
8813
+ switch (op.type) {
8814
+ case "insert":
8815
+ return transformInsert(op, over, order);
8816
+ case "delete":
8817
+ return transformDelete(op, over);
8818
+ case "format":
8819
+ return transformFormat(op, over, order);
8820
+ }
8821
+ }
8822
+ function transformTextOperationsX(a, b, order) {
8823
+ if (a.length === 0 || b.length === 0) {
8824
+ return [[...a], [...b]];
8825
+ }
8826
+ if (a.length === 1 && b.length === 1) {
8827
+ return [
8828
+ transformSingle(a[0], b[0], order),
8829
+ transformSingle(b[0], a[0], oppositeOrder(order))
8830
+ ];
8831
+ }
8832
+ if (a.length > 1) {
8833
+ const [headA1, b1] = transformTextOperationsX([a[0]], b, order);
8834
+ const [restA1, b2] = transformTextOperationsX(a.slice(1), b1, order);
8835
+ return [[...headA1, ...restA1], b2];
8836
+ }
8837
+ const [a1, headB1] = transformTextOperationsX(a, [b[0]], order);
8838
+ const [a2, restB1] = transformTextOperationsX(a1, b.slice(1), order);
8839
+ return [a2, [...headB1, ...restB1]];
8840
+ }
8841
+ function transformTextOperations(ops, over, order) {
8842
+ return transformTextOperationsX(ops, over, order)[0];
8843
+ }
8844
+ function textOperationsEqual(a, b) {
8845
+ return a === b || stableStringify(a) === stableStringify(b);
8714
8846
  }
8715
8847
  function applyTextOperationsToSegments(segments, ops) {
8716
8848
  let next = [...segments];
@@ -8782,21 +8914,31 @@ function invertTextOperations(segments, ops) {
8782
8914
  }
8783
8915
 
8784
8916
  // src/crdts/LiveText.ts
8917
+ var ACCEPTED_OPS_HISTORY_LIMIT = 1e3;
8785
8918
  var LiveText = class _LiveText extends AbstractCrdt {
8919
+ /** The local document: #confirmed ⊕ #inFlightOps ⊕ #queuedOps. */
8786
8920
  #segments;
8921
+ /** The server-confirmed document (only authoritative ops applied). */
8922
+ #confirmed;
8787
8923
  #version;
8788
- #pendingOps;
8924
+ /** The op currently awaiting server acknowledgement (at most one). */
8925
+ #inFlightOpId;
8926
+ /** Its ops, continuously re-expressed against current server state. */
8927
+ #inFlightOps = [];
8928
+ /** Local edits made while an op is in flight; sent after the ack. */
8929
+ #queuedOps = [];
8930
+ #acceptedOps = [];
8789
8931
  constructor(textOrData = "", version = 0) {
8790
8932
  super();
8791
8933
  this.#segments = typeof textOrData === "string" ? textOrData.length === 0 ? [] : [{ text: textOrData }] : dataToSegments(textOrData);
8934
+ this.#confirmed = [...this.#segments];
8792
8935
  this.#version = version;
8793
- this.#pendingOps = /* @__PURE__ */ new Map();
8794
8936
  }
8795
8937
  get version() {
8796
8938
  return this.#version;
8797
8939
  }
8798
8940
  get length() {
8799
- return this.toString().length;
8941
+ return textLength(this.#segments);
8800
8942
  }
8801
8943
  /** @internal */
8802
8944
  static _deserialize([id, item], _parentToChildren, pool) {
@@ -8847,30 +8989,16 @@ var LiveText = class _LiveText extends AbstractCrdt {
8847
8989
  return super._apply(op, isLocal);
8848
8990
  }
8849
8991
  if (isLocal) {
8850
- this.#pendingOps.set(nn(op.opId), op.ops);
8851
- return this.#applyOperations(op.ops, op.version ?? this.#version);
8852
- }
8853
- if (op.opId !== void 0) {
8854
- const pending2 = this.#pendingOps.get(op.opId);
8855
- this.#pendingOps.delete(op.opId);
8856
- const otherPending = Array.from(this.#pendingOps.values()).flat();
8857
- if (pending2 !== void 0 && otherPending.length > 0) {
8858
- this.#segments = applyTextOperationsToSegments(
8859
- this.#segments,
8860
- invertTextOperations(this.#segments, pending2)
8861
- );
8862
- const ops2 = rebaseTextOperations(op.ops, otherPending);
8863
- return this.#applyOperations(
8864
- ops2,
8865
- op.version ?? Math.max(this.#version, op.baseVersion + 1)
8866
- );
8867
- }
8868
- this.#version = op.version ?? Math.max(this.#version, op.baseVersion + 1);
8992
+ return this.#applyLocal(op);
8993
+ }
8994
+ if (op.opId !== void 0 && op.opId === this.#inFlightOpId) {
8995
+ return this.#applyAck(op);
8996
+ }
8997
+ if (op.opId !== void 0 && this.#acceptedOps.some((entry) => entry.opId === op.opId)) {
8998
+ this.#version = Math.max(this.#version, op.version ?? op.baseVersion + 1);
8869
8999
  return { modified: false };
8870
9000
  }
8871
- const pending = Array.from(this.#pendingOps.values()).flat();
8872
- const ops = pending.length > 0 ? rebaseTextOperations(op.ops, pending) : op.ops;
8873
- return this.#applyOperations(ops, op.version ?? this.#version + 1);
9001
+ return this.#applyRemote(op);
8874
9002
  }
8875
9003
  insert(index, text, attributes) {
8876
9004
  const clippedIndex = Math.max(0, Math.min(index, this.length));
@@ -8914,45 +9042,153 @@ var LiveText = class _LiveText extends AbstractCrdt {
8914
9042
  }
8915
9043
  ]);
8916
9044
  }
9045
+ /** Local edits made through the public API. */
8917
9046
  #dispatch(ops) {
8918
9047
  if (ops.length === 0) {
8919
9048
  return;
8920
9049
  }
8921
9050
  this._pool?.assertStorageIsWritable();
8922
- const baseVersion = this.#version;
8923
- const reverse = this._pool !== void 0 && this._id !== void 0 ? this.#invertOperations(ops) : [];
9051
+ const attached = this._pool !== void 0 && this._id !== void 0;
9052
+ const reverse = attached ? this.#invertOperations(ops) : [];
8924
9053
  const changes = this.#applyOperationsLocally(ops);
8925
- if (this._pool !== void 0 && this._id !== void 0) {
8926
- const opId = this._pool.generateOpId();
8927
- this.#pendingOps.set(opId, ops);
8928
- this._pool.dispatch(
9054
+ if (!attached) {
9055
+ return;
9056
+ }
9057
+ const pool = nn(this._pool);
9058
+ const id = nn(this._id);
9059
+ const updates = /* @__PURE__ */ new Map([
9060
+ [
9061
+ id,
9062
+ {
9063
+ type: "LiveText",
9064
+ node: this,
9065
+ version: this.#version,
9066
+ updates: changes
9067
+ }
9068
+ ]
9069
+ ]);
9070
+ if (this.#inFlightOpId === void 0) {
9071
+ const opId = pool.generateOpId();
9072
+ this.#inFlightOpId = opId;
9073
+ this.#inFlightOps = [...ops];
9074
+ pool.dispatch(
8929
9075
  [
8930
9076
  {
8931
9077
  type: OpCode.UPDATE_TEXT,
8932
- id: this._id,
9078
+ id,
8933
9079
  opId,
8934
- baseVersion,
9080
+ baseVersion: this.#version,
8935
9081
  ops: [...ops]
8936
9082
  }
8937
9083
  ],
8938
9084
  reverse,
8939
- /* @__PURE__ */ new Map([
8940
- [
8941
- this._id,
8942
- {
8943
- type: "LiveText",
8944
- node: this,
8945
- version: this.#version,
8946
- updates: changes
8947
- }
8948
- ]
8949
- ])
9085
+ updates
8950
9086
  );
9087
+ } else {
9088
+ this.#queuedOps.push(...ops);
9089
+ pool.dispatch([], reverse, updates, { clearRedoStack: true });
8951
9090
  }
8952
9091
  }
8953
- #applyOperations(ops, version) {
9092
+ /**
9093
+ * A local replay of an existing wire op: an undo/redo frame, or an
9094
+ * unacknowledged op re-sent after a reconnect.
9095
+ */
9096
+ #applyLocal(op) {
9097
+ const mutableOp = op;
9098
+ if (op.opId !== void 0 && op.opId === this.#inFlightOpId) {
9099
+ this.#inFlightOps = [...this.#inFlightOps, ...this.#queuedOps];
9100
+ this.#queuedOps = [];
9101
+ mutableOp.baseVersion = this.#version;
9102
+ mutableOp.ops = [...this.#inFlightOps];
9103
+ return { modified: false };
9104
+ }
9105
+ let ops = op.ops;
9106
+ for (const entry of this.#acceptedOps) {
9107
+ if (entry.version > op.baseVersion && entry.ops.length > 0) {
9108
+ ops = transformTextOperations(ops, entry.ops, "after");
9109
+ }
9110
+ }
8954
9111
  const reverse = this.#invertOperations(ops);
8955
9112
  const changes = this.#applyOperationsLocally(ops);
9113
+ if (this.#inFlightOpId === void 0 && ops.length > 0) {
9114
+ this.#inFlightOpId = nn(op.opId, "Local ops must have an opId");
9115
+ this.#inFlightOps = [...ops];
9116
+ mutableOp.baseVersion = this.#version;
9117
+ mutableOp.ops = [...ops];
9118
+ } else {
9119
+ this.#queuedOps.push(...ops);
9120
+ mutableOp.baseVersion = this.#version;
9121
+ mutableOp.ops = [];
9122
+ }
9123
+ if (changes.length === 0) {
9124
+ return { modified: false };
9125
+ }
9126
+ return {
9127
+ reverse,
9128
+ modified: {
9129
+ type: "LiveText",
9130
+ node: this,
9131
+ version: this.#version,
9132
+ updates: changes
9133
+ }
9134
+ };
9135
+ }
9136
+ /** Server acknowledgement of our in-flight op. */
9137
+ #applyAck(op) {
9138
+ const ackedVersion = op.version ?? Math.max(this.#version, op.baseVersion + 1);
9139
+ const predicted = this.#inFlightOps;
9140
+ const opId = this.#inFlightOpId;
9141
+ this.#confirmed = applyTextOperationsToSegments(this.#confirmed, op.ops);
9142
+ this.#inFlightOpId = void 0;
9143
+ this.#inFlightOps = [];
9144
+ let appliedOps = [];
9145
+ let result = { modified: false };
9146
+ if (!textOperationsEqual(op.ops, predicted)) {
9147
+ error2(
9148
+ "LiveText: acknowledgement did not match the local prediction; resynchronizing"
9149
+ );
9150
+ const rebuilt = this.#rebuildLocalFromConfirmed();
9151
+ appliedOps = rebuilt.appliedOps;
9152
+ if (rebuilt.changes.length > 0) {
9153
+ result = {
9154
+ reverse: [],
9155
+ modified: {
9156
+ type: "LiveText",
9157
+ node: this,
9158
+ version: ackedVersion,
9159
+ updates: rebuilt.changes
9160
+ }
9161
+ };
9162
+ }
9163
+ }
9164
+ this.#version = Math.max(this.#version, ackedVersion);
9165
+ this.#recordAccepted(ackedVersion, appliedOps, opId);
9166
+ this.#flushQueued();
9167
+ return result;
9168
+ }
9169
+ /** An accepted op from another client (or a server-fabricated fix op). */
9170
+ #applyRemote(op) {
9171
+ const version = op.version ?? this.#version + 1;
9172
+ this.#confirmed = applyTextOperationsToSegments(this.#confirmed, op.ops);
9173
+ const [overInFlight, inFlight] = transformTextOperationsX(
9174
+ op.ops,
9175
+ this.#inFlightOps,
9176
+ "before"
9177
+ );
9178
+ const [applied, queued] = transformTextOperationsX(
9179
+ overInFlight,
9180
+ this.#queuedOps,
9181
+ "before"
9182
+ );
9183
+ this.#inFlightOps = inFlight;
9184
+ this.#queuedOps = queued;
9185
+ this.#recordAccepted(version, applied, op.opId);
9186
+ if (applied.length === 0) {
9187
+ this.#version = Math.max(this.#version, version);
9188
+ return { modified: false };
9189
+ }
9190
+ const reverse = this.#invertOperations(applied);
9191
+ const changes = this.#applyOperationsLocally(applied);
8956
9192
  this.#version = Math.max(this.#version, version);
8957
9193
  return {
8958
9194
  reverse,
@@ -8964,6 +9200,129 @@ var LiveText = class _LiveText extends AbstractCrdt {
8964
9200
  }
8965
9201
  };
8966
9202
  }
9203
+ /** Send the queued ops as the next in-flight op (after an ack). */
9204
+ #flushQueued() {
9205
+ if (this.#queuedOps.length === 0 || this._pool === void 0 || this._id === void 0) {
9206
+ return;
9207
+ }
9208
+ const opId = this._pool.generateOpId();
9209
+ this.#inFlightOpId = opId;
9210
+ this.#inFlightOps = this.#queuedOps;
9211
+ this.#queuedOps = [];
9212
+ this._pool.dispatch(
9213
+ [
9214
+ {
9215
+ type: OpCode.UPDATE_TEXT,
9216
+ id: this._id,
9217
+ opId,
9218
+ baseVersion: this.#version,
9219
+ ops: [...this.#inFlightOps]
9220
+ }
9221
+ ],
9222
+ [],
9223
+ /* @__PURE__ */ new Map(),
9224
+ // The local content was already applied (and made undoable) when the
9225
+ // edits happened; this is purely an outbound flush.
9226
+ { clearRedoStack: false }
9227
+ );
9228
+ }
9229
+ /**
9230
+ * Rebuild the local document as confirmed ⊕ queued ops, returning the
9231
+ * coarse delta that was applied. Only used by defensive recovery paths.
9232
+ */
9233
+ #rebuildLocalFromConfirmed() {
9234
+ const before2 = this.#segments;
9235
+ const after2 = applyTextOperationsToSegments(this.#confirmed, [
9236
+ ...this.#inFlightOps,
9237
+ ...this.#queuedOps
9238
+ ]);
9239
+ if (stableStringify(segmentsToData(before2)) === stableStringify(segmentsToData(after2))) {
9240
+ this.#segments = after2;
9241
+ return { appliedOps: [], changes: [] };
9242
+ }
9243
+ const beforeText = before2.map((segment) => segment.text).join("");
9244
+ this.#segments = after2;
9245
+ this.invalidate();
9246
+ const appliedOps = [];
9247
+ const changes = [];
9248
+ if (beforeText.length > 0) {
9249
+ appliedOps.push({ type: "delete", index: 0, length: beforeText.length });
9250
+ changes.push({
9251
+ type: "delete",
9252
+ index: 0,
9253
+ length: beforeText.length,
9254
+ deletedText: beforeText
9255
+ });
9256
+ }
9257
+ let index = 0;
9258
+ for (const segment of after2) {
9259
+ appliedOps.push({
9260
+ type: "insert",
9261
+ index,
9262
+ text: segment.text,
9263
+ attributes: segment.attributes
9264
+ });
9265
+ changes.push({
9266
+ type: "insert",
9267
+ index,
9268
+ text: segment.text,
9269
+ attributes: segment.attributes
9270
+ });
9271
+ index += segment.text.length;
9272
+ }
9273
+ return { appliedOps, changes };
9274
+ }
9275
+ /**
9276
+ * Reconcile this node against an authoritative storage snapshot (e.g.
9277
+ * after a reconnect). The confirmed state and version are replaced by the
9278
+ * snapshot's; pending (in-flight + queued) ops are preserved on top and
9279
+ * will be re-sent by the offline-ops replay.
9280
+ *
9281
+ * @internal
9282
+ */
9283
+ _resyncText(data, version) {
9284
+ this.#confirmed = dataToSegments(data);
9285
+ this.#version = version;
9286
+ this.#acceptedOps = [];
9287
+ const rebuilt = this.#rebuildLocalFromConfirmed();
9288
+ if (rebuilt.changes.length === 0) {
9289
+ return void 0;
9290
+ }
9291
+ return {
9292
+ type: "LiveText",
9293
+ node: this,
9294
+ version: this.#version,
9295
+ updates: rebuilt.changes
9296
+ };
9297
+ }
9298
+ /**
9299
+ * Called when the server rejected one of our ops. Drops all pending state
9300
+ * for this node (edits queued behind a rejected op cannot be trusted
9301
+ * either); the room follows up with a storage resync.
9302
+ *
9303
+ * @internal
9304
+ */
9305
+ _rejectPendingOp(opId) {
9306
+ if (opId !== this.#inFlightOpId) {
9307
+ return;
9308
+ }
9309
+ this.#inFlightOpId = void 0;
9310
+ this.#inFlightOps = [];
9311
+ this.#queuedOps = [];
9312
+ }
9313
+ #recordAccepted(version, ops, opId) {
9314
+ if (this.#acceptedOps.some((entry) => entry.version === version)) {
9315
+ return;
9316
+ }
9317
+ this.#acceptedOps.push({ version, opId, ops: [...ops] });
9318
+ this.#acceptedOps.sort((left, right) => left.version - right.version);
9319
+ if (this.#acceptedOps.length > ACCEPTED_OPS_HISTORY_LIMIT) {
9320
+ this.#acceptedOps.splice(
9321
+ 0,
9322
+ this.#acceptedOps.length - ACCEPTED_OPS_HISTORY_LIMIT
9323
+ );
9324
+ }
9325
+ }
8967
9326
  #applyOperationsLocally(ops) {
8968
9327
  const changes = [];
8969
9328
  for (const op of ops) {
@@ -9236,40 +9595,6 @@ function getTreesDiffOperations(currentItems, newItems) {
9236
9595
  }
9237
9596
  }
9238
9597
  }
9239
- if (crdt.type === CrdtType.TEXT) {
9240
- if (currentCrdt.type !== CrdtType.TEXT || stringifyOrLog(crdt.data) !== stringifyOrLog(currentCrdt.data) || crdt.version !== currentCrdt.version) {
9241
- ops.push({
9242
- type: OpCode.UPDATE_TEXT,
9243
- id,
9244
- baseVersion: currentCrdt.type === CrdtType.TEXT ? currentCrdt.version : 0,
9245
- version: crdt.version,
9246
- ops: [
9247
- {
9248
- type: "delete",
9249
- index: 0,
9250
- length: currentCrdt.type === CrdtType.TEXT ? currentCrdt.data.reduce(
9251
- (sum, segment) => sum + segment[0].length,
9252
- 0
9253
- ) : 0
9254
- },
9255
- ...crdt.data.map((segment, index, items) => {
9256
- const [text, attributes] = segment;
9257
- const insertIndex = items.slice(0, index).reduce((sum, item) => sum + item[0].length, 0);
9258
- return attributes === void 0 ? {
9259
- type: "insert",
9260
- index: insertIndex,
9261
- text
9262
- } : {
9263
- type: "insert",
9264
- index: insertIndex,
9265
- text,
9266
- attributes
9267
- };
9268
- })
9269
- ]
9270
- });
9271
- }
9272
- }
9273
9598
  if (crdt.parentKey !== currentCrdt.parentKey) {
9274
9599
  ops.push({
9275
9600
  type: OpCode.SET_PARENT_KEY,
@@ -10400,7 +10725,7 @@ function createRoom(options, config) {
10400
10725
  }
10401
10726
  }
10402
10727
  });
10403
- function onDispatch(ops, reverse, storageUpdates) {
10728
+ function onDispatch(ops, reverse, storageUpdates, options2) {
10404
10729
  if (context.activeBatch) {
10405
10730
  for (const op of ops) {
10406
10731
  context.activeBatch.ops.push(op);
@@ -10419,8 +10744,10 @@ function createRoom(options, config) {
10419
10744
  if (reverse.length > 0) {
10420
10745
  addToUndoStack(reverse);
10421
10746
  }
10422
- if (ops.length > 0) {
10747
+ if (options2?.clearRedoStack ?? ops.length > 0) {
10423
10748
  context.redoStack.length = 0;
10749
+ }
10750
+ if (ops.length > 0) {
10424
10751
  dispatchOps(ops);
10425
10752
  }
10426
10753
  notify({ storageUpdates });
@@ -10524,6 +10851,23 @@ function createRoom(options, config) {
10524
10851
  }
10525
10852
  const ops = getTreesDiffOperations(currentItems, nodes);
10526
10853
  const result = applyRemoteOps(ops);
10854
+ for (const [id, crdt] of nodes) {
10855
+ if (crdt.type === CrdtType.TEXT) {
10856
+ const node = context.pool.nodes.get(id);
10857
+ if (node !== void 0 && isLiveText(node)) {
10858
+ const update = node._resyncText(crdt.data, crdt.version);
10859
+ if (update !== void 0) {
10860
+ result.updates.storageUpdates.set(
10861
+ id,
10862
+ mergeStorageUpdates(
10863
+ result.updates.storageUpdates.get(id),
10864
+ update
10865
+ )
10866
+ );
10867
+ }
10868
+ }
10869
+ }
10870
+ }
10527
10871
  notify(result.updates);
10528
10872
  } else {
10529
10873
  context.root = LiveObject._fromItems(
@@ -10955,16 +11299,36 @@ function createRoom(options, config) {
10955
11299
  }
10956
11300
  break;
10957
11301
  }
10958
- // Receiving a RejectedOps message in the client means that the server is no
10959
- // longer in sync with the client. Trying to synchronize the client again by
10960
- // rolling back particular Ops may be hard/impossible. It's fine to not try and
10961
- // accept the out-of-sync reality and throw an error.
11302
+ // Receiving a RejectedOps message means the server refused some of
11303
+ // our ops, so our optimistic local state is out of sync with the
11304
+ // server. For LiveText ops this is a normal (if rare) situation
11305
+ // e.g. a client that was offline long enough to fall outside the
11306
+ // server's retained history window — and we can recover: drop the
11307
+ // rejected pending state and re-fetch the authoritative storage
11308
+ // snapshot. For other ops (e.g. permission rejections), rolling back
11309
+ // particular Ops is hard/impossible, so we keep the old behavior of
11310
+ // accepting the out-of-sync reality and surfacing an error.
10962
11311
  case ServerMsgCode.REJECT_STORAGE_OP: {
10963
11312
  errorWithTitle(
10964
11313
  "Storage mutation rejection error",
10965
11314
  message.reason
10966
11315
  );
10967
- if (process.env.NODE_ENV !== "production") {
11316
+ let needsStorageResync = false;
11317
+ for (const opId of message.opIds) {
11318
+ const rejectedOp = context.unacknowledgedOps.get(opId);
11319
+ context.unacknowledgedOps.delete(opId);
11320
+ context.buffer.storageOperations = context.buffer.storageOperations.filter((op) => op.opId !== opId);
11321
+ if (rejectedOp !== void 0 && rejectedOp.type === OpCode.UPDATE_TEXT) {
11322
+ const node = context.pool.nodes.get(rejectedOp.id);
11323
+ if (node !== void 0 && isLiveText(node)) {
11324
+ node._rejectPendingOp(opId);
11325
+ needsStorageResync = true;
11326
+ }
11327
+ }
11328
+ }
11329
+ if (needsStorageResync) {
11330
+ refreshStorage({ flush: true });
11331
+ } else if (process.env.NODE_ENV !== "production") {
10968
11332
  throw new Error(
10969
11333
  `Storage mutations rejected by server: ${message.reason}`
10970
11334
  );