@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.cjs +548 -184
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -2
- package/dist/index.d.ts +28 -2
- package/dist/index.js +485 -121
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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-
|
|
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
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
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
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
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
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
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
|
-
|
|
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
|
|
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.#
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
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
|
-
|
|
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
|
|
8923
|
-
const reverse =
|
|
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 (
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
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
|
|
9078
|
+
id,
|
|
8933
9079
|
opId,
|
|
8934
|
-
baseVersion,
|
|
9080
|
+
baseVersion: this.#version,
|
|
8935
9081
|
ops: [...ops]
|
|
8936
9082
|
}
|
|
8937
9083
|
],
|
|
8938
9084
|
reverse,
|
|
8939
|
-
|
|
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
|
-
|
|
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
|
|
10959
|
-
//
|
|
10960
|
-
//
|
|
10961
|
-
//
|
|
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
|
-
|
|
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
|
);
|