@liveblocks/core 3.24.2 → 3.24.3

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.d.cts CHANGED
@@ -431,6 +431,11 @@ type UpdateTextOp = {
431
431
  readonly baseVersion: number;
432
432
  readonly version?: number;
433
433
  readonly ops: TextOperation[];
434
+ /** Server-only: accepted ops after the requested base version when reconciling. */
435
+ readonly history?: {
436
+ readonly version: number;
437
+ readonly ops: TextOperation[];
438
+ }[];
434
439
  };
435
440
  type DeleteCrdtOp = {
436
441
  readonly opId?: string;
@@ -970,6 +975,13 @@ type PrivateLiveTextApi = PrivateLiveNodeApi & {
970
975
  * the pending ops are re-expressed over the remote op in turn ("after"
971
976
  * order), keeping them in server coordinates at all times.
972
977
  *
978
+ * Reconnect replay: keep the pre-disconnect confirmed state and re-send only
979
+ * the in-flight op at that version. The server returns the intervening
980
+ * accepted operations with its acknowledgement, including the original op
981
+ * if it was already stored. Replaying that history through the normal OT
982
+ * paths rebases the pending edits exactly, without applying them twice to a
983
+ * snapshot. Queued edits stay queued until their predecessor is acknowledged.
984
+ *
973
985
  * @example
974
986
  * const text = new LiveText("Hello");
975
987
  * text.insert(5, " world");
@@ -3189,6 +3201,8 @@ type UpdatePresenceClientMsg<P extends JsonObject> = {
3189
3201
  type UpdateStorageClientMsg = {
3190
3202
  readonly type: ClientMsgCode.UPDATE_STORAGE;
3191
3203
  readonly ops: ClientWireOp[];
3204
+ /** Include authoritative history in text acknowledgements after a storage resync. */
3205
+ readonly includeTextHistory?: true;
3192
3206
  };
3193
3207
  type FetchStorageClientMsg = {
3194
3208
  readonly type: ClientMsgCode.FETCH_STORAGE;
package/dist/index.d.ts CHANGED
@@ -431,6 +431,11 @@ type UpdateTextOp = {
431
431
  readonly baseVersion: number;
432
432
  readonly version?: number;
433
433
  readonly ops: TextOperation[];
434
+ /** Server-only: accepted ops after the requested base version when reconciling. */
435
+ readonly history?: {
436
+ readonly version: number;
437
+ readonly ops: TextOperation[];
438
+ }[];
434
439
  };
435
440
  type DeleteCrdtOp = {
436
441
  readonly opId?: string;
@@ -970,6 +975,13 @@ type PrivateLiveTextApi = PrivateLiveNodeApi & {
970
975
  * the pending ops are re-expressed over the remote op in turn ("after"
971
976
  * order), keeping them in server coordinates at all times.
972
977
  *
978
+ * Reconnect replay: keep the pre-disconnect confirmed state and re-send only
979
+ * the in-flight op at that version. The server returns the intervening
980
+ * accepted operations with its acknowledgement, including the original op
981
+ * if it was already stored. Replaying that history through the normal OT
982
+ * paths rebases the pending edits exactly, without applying them twice to a
983
+ * snapshot. Queued edits stay queued until their predecessor is acknowledged.
984
+ *
973
985
  * @example
974
986
  * const text = new LiveText("Hello");
975
987
  * text.insert(5, " world");
@@ -3189,6 +3201,8 @@ type UpdatePresenceClientMsg<P extends JsonObject> = {
3189
3201
  type UpdateStorageClientMsg = {
3190
3202
  readonly type: ClientMsgCode.UPDATE_STORAGE;
3191
3203
  readonly ops: ClientWireOp[];
3204
+ /** Include authoritative history in text acknowledgements after a storage resync. */
3205
+ readonly includeTextHistory?: true;
3192
3206
  };
3193
3207
  type FetchStorageClientMsg = {
3194
3208
  readonly type: ClientMsgCode.FETCH_STORAGE;
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.24.2";
9
+ var PKG_VERSION = "3.24.3";
10
10
  var PKG_FORMAT = "esm";
11
11
 
12
12
  // src/dupe-detection.ts
@@ -9850,6 +9850,10 @@ var LiveText = class _LiveText extends AbstractCrdt {
9850
9850
  /** Local edits made while an op is in flight; sent after the ack. */
9851
9851
  #queuedOps = [];
9852
9852
  #acceptedOps = [];
9853
+ /** Wait for replay history before applying post-snapshot remote ops. */
9854
+ #reconnecting = false;
9855
+ #deferredHistorylessAck = false;
9856
+ #bufferedRemoteOps = [];
9853
9857
  /**
9854
9858
  * Creates a new LiveText document.
9855
9859
  *
@@ -9921,6 +9925,14 @@ var LiveText = class _LiveText extends AbstractCrdt {
9921
9925
  throw new Error("LiveText cannot contain child nodes");
9922
9926
  }
9923
9927
  /** @internal */
9928
+ _deferAckWithoutHistory(op) {
9929
+ if (this.#reconnecting && !this.#deferredHistorylessAck && op.opId !== void 0 && op.opId === this.#inFlightOpId && op.history === void 0) {
9930
+ this.#deferredHistorylessAck = true;
9931
+ return true;
9932
+ }
9933
+ return false;
9934
+ }
9935
+ /** @internal */
9924
9936
  _apply(op, source) {
9925
9937
  if (op.type !== OpCode.UPDATE_TEXT) {
9926
9938
  return super._apply(op, source);
@@ -10152,8 +10164,6 @@ var LiveText = class _LiveText extends AbstractCrdt {
10152
10164
  #applyLocal(op, source) {
10153
10165
  const mutableOp = op;
10154
10166
  if (op.opId !== void 0 && op.opId === this.#inFlightOpId) {
10155
- this.#inFlightOps = [...this.#inFlightOps, ...this.#queuedOps];
10156
- this.#queuedOps = [];
10157
10167
  mutableOp.baseVersion = this.#version;
10158
10168
  mutableOp.ops = [...this.#inFlightOps];
10159
10169
  return { modified: false };
@@ -10192,6 +10202,9 @@ var LiveText = class _LiveText extends AbstractCrdt {
10192
10202
  }
10193
10203
  /** Server acknowledgement of our in-flight op. */
10194
10204
  #applyAck(op, source) {
10205
+ if (this.#reconnecting) {
10206
+ return this.#reconcileAck(op, source);
10207
+ }
10195
10208
  const ackedVersion = op.version ?? Math.max(this.#version, op.baseVersion + 1);
10196
10209
  const predicted = this.#inFlightOps;
10197
10210
  const opId = this.#inFlightOpId;
@@ -10214,7 +10227,9 @@ var LiveText = class _LiveText extends AbstractCrdt {
10214
10227
  node: this,
10215
10228
  version: ackedVersion,
10216
10229
  updates: rebuilt.changes,
10217
- source
10230
+ // A correction was not applied by the originating editor. It
10231
+ // must not be filtered out as an echo of a local edit.
10232
+ source: REMOTE
10218
10233
  }
10219
10234
  };
10220
10235
  }
@@ -10224,9 +10239,69 @@ var LiveText = class _LiveText extends AbstractCrdt {
10224
10239
  this.#flushQueued();
10225
10240
  return result;
10226
10241
  }
10242
+ /** Recover the missing server timeline, preserving the normal OT invariants. */
10243
+ #reconcileAck(op, source) {
10244
+ const history = op.history;
10245
+ if (history === void 0) {
10246
+ this._rejectPendingOp(nn(op.opId));
10247
+ return { modified: false, needsStorageResync: true };
10248
+ }
10249
+ this.#reconnecting = false;
10250
+ this.#deferredHistorylessAck = false;
10251
+ const buffered = this.#bufferedRemoteOps;
10252
+ this.#bufferedRemoteOps = [];
10253
+ const changes = [];
10254
+ const collect = (result) => {
10255
+ if (result.modified && result.modified.type === "LiveText") {
10256
+ changes.push(...result.modified.updates);
10257
+ }
10258
+ };
10259
+ for (const entry of history) {
10260
+ if (entry.version <= this.#version) continue;
10261
+ if (entry.version === op.version) {
10262
+ collect(this.#applyAck(op, source));
10263
+ } else {
10264
+ collect(
10265
+ this.#applyRemote(
10266
+ {
10267
+ type: OpCode.UPDATE_TEXT,
10268
+ id: op.id,
10269
+ baseVersion: entry.version - 1,
10270
+ version: entry.version,
10271
+ ops: entry.ops
10272
+ },
10273
+ REMOTE
10274
+ )
10275
+ );
10276
+ }
10277
+ }
10278
+ if (this.#inFlightOpId === op.opId) {
10279
+ collect(this.#applyAck(op, source));
10280
+ }
10281
+ for (const remote of buffered) {
10282
+ collect(this.#applyRemote(remote, REMOTE));
10283
+ }
10284
+ return changes.length === 0 ? { modified: false } : {
10285
+ reverse: [],
10286
+ modified: {
10287
+ type: "LiveText",
10288
+ node: this,
10289
+ version: this.#version,
10290
+ updates: changes,
10291
+ source: REMOTE
10292
+ }
10293
+ };
10294
+ }
10227
10295
  /** An accepted op from another client (or a server-fabricated fix op). */
10228
10296
  #applyRemote(op, source) {
10229
10297
  const version = op.version ?? this.#version + 1;
10298
+ if (op.version !== void 0 && op.version <= this.#version) {
10299
+ return { modified: false };
10300
+ }
10301
+ if (this.#reconnecting) {
10302
+ this.#bufferedRemoteOps.push(op);
10303
+ return { modified: false };
10304
+ }
10230
10305
  this.#confirmed = applyTextOperationsToSegments(this.#confirmed, op.ops);
10231
10306
  const [overInFlight, inFlight] = transformTextOperationsX(
10232
10307
  op.ops,
@@ -10332,14 +10407,18 @@ var LiveText = class _LiveText extends AbstractCrdt {
10332
10407
  return { appliedOps, changes };
10333
10408
  }
10334
10409
  /**
10335
- * Reconcile this node against an authoritative storage snapshot (e.g.
10336
- * after a reconnect). The confirmed state and version are replaced by the
10337
- * snapshot's; pending (in-flight + queued) ops are preserved on top and
10338
- * will be re-sent by the offline-ops replay.
10410
+ * Reconcile against an authoritative snapshot. With pending edits, retain
10411
+ * the current document and confirmed version until replay history arrives:
10412
+ * a snapshot alone cannot re-express the queue over unseen operations.
10339
10413
  *
10340
10414
  * @internal
10341
10415
  */
10342
10416
  _resyncText(data, version, source) {
10417
+ if (this.#inFlightOpId !== void 0) {
10418
+ this.#reconnecting = true;
10419
+ this.#deferredHistorylessAck = false;
10420
+ return void 0;
10421
+ }
10343
10422
  this.#confirmed = dataToSegments(data);
10344
10423
  this.#version = version;
10345
10424
  this.#acceptedOps = [];
@@ -10369,6 +10448,9 @@ var LiveText = class _LiveText extends AbstractCrdt {
10369
10448
  this.#inFlightOpId = void 0;
10370
10449
  this.#inFlightOps = [];
10371
10450
  this.#queuedOps = [];
10451
+ this.#bufferedRemoteOps = [];
10452
+ this.#reconnecting = false;
10453
+ this.#deferredHistorylessAck = false;
10372
10454
  }
10373
10455
  #recordAccepted(version, ops, opId) {
10374
10456
  if (this.#acceptedOps.some((entry) => entry.version === version)) {
@@ -12303,6 +12385,12 @@ function createRoom(options, config) {
12303
12385
  }
12304
12386
  const createdNodeIds = /* @__PURE__ */ new Set();
12305
12387
  for (const op of ops) {
12388
+ if (!isLocal && op.type === OpCode.UPDATE_TEXT && op.opId !== void 0) {
12389
+ const node = context.pool.nodes.get(op.id);
12390
+ if (node !== void 0 && isLiveText(node) && node._deferAckWithoutHistory(op)) {
12391
+ continue;
12392
+ }
12393
+ }
12306
12394
  let source;
12307
12395
  if (isLocal) {
12308
12396
  source = { ...localSource, optimistic: true };
@@ -12317,6 +12405,10 @@ function createRoom(options, config) {
12317
12405
  source = REMOTE;
12318
12406
  }
12319
12407
  const applyOpResult = applyOp(op, source);
12408
+ if ("needsStorageResync" in applyOpResult) {
12409
+ refreshStorage();
12410
+ flushNowOrSoon();
12411
+ }
12320
12412
  if (applyOpResult.modified) {
12321
12413
  const nodeId = applyOpResult.modified.node._id;
12322
12414
  if (!(nodeId && createdNodeIds.has(nodeId))) {
@@ -12537,7 +12629,8 @@ function createRoom(options, config) {
12537
12629
  const result = applyLocalOps(unackedOps);
12538
12630
  messages.push({
12539
12631
  type: ClientMsgCode.UPDATE_STORAGE,
12540
- ops: result.opsToEmit
12632
+ ops: result.opsToEmit,
12633
+ includeTextHistory: true
12541
12634
  });
12542
12635
  notify(result.updates);
12543
12636
  sendMessages(messages);