@liveblocks/core 3.24.2 → 3.24.3-livetextreplay
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.cjs +79 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +79 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
9
|
+
var PKG_VERSION = "3.24.3-livetextreplay";
|
|
10
10
|
var PKG_FORMAT = "esm";
|
|
11
11
|
|
|
12
12
|
// src/dupe-detection.ts
|
|
@@ -9850,6 +9850,9 @@ 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
|
+
#bufferedRemoteOps = [];
|
|
9853
9856
|
/**
|
|
9854
9857
|
* Creates a new LiveText document.
|
|
9855
9858
|
*
|
|
@@ -10152,8 +10155,6 @@ var LiveText = class _LiveText extends AbstractCrdt {
|
|
|
10152
10155
|
#applyLocal(op, source) {
|
|
10153
10156
|
const mutableOp = op;
|
|
10154
10157
|
if (op.opId !== void 0 && op.opId === this.#inFlightOpId) {
|
|
10155
|
-
this.#inFlightOps = [...this.#inFlightOps, ...this.#queuedOps];
|
|
10156
|
-
this.#queuedOps = [];
|
|
10157
10158
|
mutableOp.baseVersion = this.#version;
|
|
10158
10159
|
mutableOp.ops = [...this.#inFlightOps];
|
|
10159
10160
|
return { modified: false };
|
|
@@ -10192,6 +10193,9 @@ var LiveText = class _LiveText extends AbstractCrdt {
|
|
|
10192
10193
|
}
|
|
10193
10194
|
/** Server acknowledgement of our in-flight op. */
|
|
10194
10195
|
#applyAck(op, source) {
|
|
10196
|
+
if (this.#reconnecting) {
|
|
10197
|
+
return this.#reconcileAck(op, source);
|
|
10198
|
+
}
|
|
10195
10199
|
const ackedVersion = op.version ?? Math.max(this.#version, op.baseVersion + 1);
|
|
10196
10200
|
const predicted = this.#inFlightOps;
|
|
10197
10201
|
const opId = this.#inFlightOpId;
|
|
@@ -10214,7 +10218,9 @@ var LiveText = class _LiveText extends AbstractCrdt {
|
|
|
10214
10218
|
node: this,
|
|
10215
10219
|
version: ackedVersion,
|
|
10216
10220
|
updates: rebuilt.changes,
|
|
10217
|
-
|
|
10221
|
+
// A correction was not applied by the originating editor. It
|
|
10222
|
+
// must not be filtered out as an echo of a local edit.
|
|
10223
|
+
source: REMOTE
|
|
10218
10224
|
}
|
|
10219
10225
|
};
|
|
10220
10226
|
}
|
|
@@ -10224,9 +10230,67 @@ var LiveText = class _LiveText extends AbstractCrdt {
|
|
|
10224
10230
|
this.#flushQueued();
|
|
10225
10231
|
return result;
|
|
10226
10232
|
}
|
|
10233
|
+
/** Recover the missing server timeline, preserving the normal OT invariants. */
|
|
10234
|
+
#reconcileAck(op, source) {
|
|
10235
|
+
const history = nn(
|
|
10236
|
+
op.history,
|
|
10237
|
+
"LiveText replay acknowledgement requires history"
|
|
10238
|
+
);
|
|
10239
|
+
this.#reconnecting = false;
|
|
10240
|
+
const buffered = this.#bufferedRemoteOps;
|
|
10241
|
+
this.#bufferedRemoteOps = [];
|
|
10242
|
+
const changes = [];
|
|
10243
|
+
const collect = (result) => {
|
|
10244
|
+
if (result.modified && result.modified.type === "LiveText") {
|
|
10245
|
+
changes.push(...result.modified.updates);
|
|
10246
|
+
}
|
|
10247
|
+
};
|
|
10248
|
+
for (const entry of history) {
|
|
10249
|
+
if (entry.version <= this.#version) continue;
|
|
10250
|
+
if (entry.version === op.version) {
|
|
10251
|
+
collect(this.#applyAck(op, source));
|
|
10252
|
+
} else {
|
|
10253
|
+
collect(
|
|
10254
|
+
this.#applyRemote(
|
|
10255
|
+
{
|
|
10256
|
+
type: OpCode.UPDATE_TEXT,
|
|
10257
|
+
id: op.id,
|
|
10258
|
+
baseVersion: entry.version - 1,
|
|
10259
|
+
version: entry.version,
|
|
10260
|
+
ops: entry.ops
|
|
10261
|
+
},
|
|
10262
|
+
REMOTE
|
|
10263
|
+
)
|
|
10264
|
+
);
|
|
10265
|
+
}
|
|
10266
|
+
}
|
|
10267
|
+
if (this.#inFlightOpId === op.opId) {
|
|
10268
|
+
collect(this.#applyAck(op, source));
|
|
10269
|
+
}
|
|
10270
|
+
for (const remote of buffered) {
|
|
10271
|
+
collect(this.#applyRemote(remote, REMOTE));
|
|
10272
|
+
}
|
|
10273
|
+
return changes.length === 0 ? { modified: false } : {
|
|
10274
|
+
reverse: [],
|
|
10275
|
+
modified: {
|
|
10276
|
+
type: "LiveText",
|
|
10277
|
+
node: this,
|
|
10278
|
+
version: this.#version,
|
|
10279
|
+
updates: changes,
|
|
10280
|
+
source: REMOTE
|
|
10281
|
+
}
|
|
10282
|
+
};
|
|
10283
|
+
}
|
|
10227
10284
|
/** An accepted op from another client (or a server-fabricated fix op). */
|
|
10228
10285
|
#applyRemote(op, source) {
|
|
10229
10286
|
const version = op.version ?? this.#version + 1;
|
|
10287
|
+
if (op.version !== void 0 && op.version <= this.#version) {
|
|
10288
|
+
return { modified: false };
|
|
10289
|
+
}
|
|
10290
|
+
if (this.#reconnecting) {
|
|
10291
|
+
this.#bufferedRemoteOps.push(op);
|
|
10292
|
+
return { modified: false };
|
|
10293
|
+
}
|
|
10230
10294
|
this.#confirmed = applyTextOperationsToSegments(this.#confirmed, op.ops);
|
|
10231
10295
|
const [overInFlight, inFlight] = transformTextOperationsX(
|
|
10232
10296
|
op.ops,
|
|
@@ -10332,14 +10396,17 @@ var LiveText = class _LiveText extends AbstractCrdt {
|
|
|
10332
10396
|
return { appliedOps, changes };
|
|
10333
10397
|
}
|
|
10334
10398
|
/**
|
|
10335
|
-
* Reconcile
|
|
10336
|
-
*
|
|
10337
|
-
* snapshot
|
|
10338
|
-
* will be re-sent by the offline-ops replay.
|
|
10399
|
+
* Reconcile against an authoritative snapshot. With pending edits, retain
|
|
10400
|
+
* the current document and confirmed version until replay history arrives:
|
|
10401
|
+
* a snapshot alone cannot re-express the queue over unseen operations.
|
|
10339
10402
|
*
|
|
10340
10403
|
* @internal
|
|
10341
10404
|
*/
|
|
10342
10405
|
_resyncText(data, version, source) {
|
|
10406
|
+
if (this.#inFlightOpId !== void 0) {
|
|
10407
|
+
this.#reconnecting = true;
|
|
10408
|
+
return void 0;
|
|
10409
|
+
}
|
|
10343
10410
|
this.#confirmed = dataToSegments(data);
|
|
10344
10411
|
this.#version = version;
|
|
10345
10412
|
this.#acceptedOps = [];
|
|
@@ -10369,6 +10436,8 @@ var LiveText = class _LiveText extends AbstractCrdt {
|
|
|
10369
10436
|
this.#inFlightOpId = void 0;
|
|
10370
10437
|
this.#inFlightOps = [];
|
|
10371
10438
|
this.#queuedOps = [];
|
|
10439
|
+
this.#bufferedRemoteOps = [];
|
|
10440
|
+
this.#reconnecting = false;
|
|
10372
10441
|
}
|
|
10373
10442
|
#recordAccepted(version, ops, opId) {
|
|
10374
10443
|
if (this.#acceptedOps.some((entry) => entry.version === version)) {
|
|
@@ -12537,7 +12606,8 @@ function createRoom(options, config) {
|
|
|
12537
12606
|
const result = applyLocalOps(unackedOps);
|
|
12538
12607
|
messages.push({
|
|
12539
12608
|
type: ClientMsgCode.UPDATE_STORAGE,
|
|
12540
|
-
ops: result.opsToEmit
|
|
12609
|
+
ops: result.opsToEmit,
|
|
12610
|
+
includeTextHistory: true
|
|
12541
12611
|
});
|
|
12542
12612
|
notify(result.updates);
|
|
12543
12613
|
sendMessages(messages);
|