@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.cjs +1662 -293
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +382 -37
- package/dist/index.d.ts +382 -37
- package/dist/index.js +1581 -212
- 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.
|
|
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/${
|
|
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/${
|
|
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
|
|
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
|
-
|
|
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(
|
|
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/
|
|
8762
|
-
function
|
|
8763
|
-
|
|
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
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
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
|
|
8780
|
-
|
|
8781
|
-
|
|
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
|
-
|
|
8784
|
-
|
|
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
|
-
|
|
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
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
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
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
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
|
|
8815
|
-
|
|
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
|
|
8818
|
-
|
|
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
|
|
8821
|
-
|
|
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
|
|
8824
|
-
|
|
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
|
|
8827
|
-
|
|
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
|
|
8830
|
-
|
|
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
|
|
8833
|
-
return
|
|
9049
|
+
function oppositeOrder(order) {
|
|
9050
|
+
return order === "before" ? "after" : "before";
|
|
8834
9051
|
}
|
|
8835
|
-
function
|
|
8836
|
-
if (
|
|
8837
|
-
return
|
|
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
|
|
8845
|
-
if (
|
|
8846
|
-
|
|
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
|
|
9067
|
+
return [{ ...op }];
|
|
8849
9068
|
}
|
|
8850
9069
|
}
|
|
8851
|
-
function
|
|
8852
|
-
const
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
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
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
}
|
|
8876
|
-
|
|
8877
|
-
|
|
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
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
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
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
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
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
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
|
|
9231
|
+
return next;
|
|
8904
9232
|
}
|
|
8905
|
-
function
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10234
|
+
merged = mergeObjectStorageUpdates(first, second);
|
|
9026
10235
|
} else if (first.type === "LiveMap" && second.type === "LiveMap") {
|
|
9027
|
-
|
|
10236
|
+
merged = mergeMapStorageUpdates(first, second);
|
|
9028
10237
|
} else if (first.type === "LiveList" && second.type === "LiveList") {
|
|
9029
|
-
|
|
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
|
|
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(
|
|
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
|
|
10140
|
-
return httpClient.
|
|
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
|
|
10200
|
-
for (const [id, crdt] of
|
|
10201
|
-
|
|
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
|
-
|
|
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
|
|
10638
|
-
//
|
|
10639
|
-
//
|
|
10640
|
-
//
|
|
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
|
-
|
|
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
|
|
11189
|
-
if (
|
|
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
|
|
11207
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|