@liveblocks/core 3.23.0 → 3.23.1-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 +1473 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +475 -141
- package/dist/index.d.ts +475 -141
- package/dist/index.js +1401 -47
- 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.23.
|
|
9
|
+
var PKG_VERSION = "3.23.1-exp1";
|
|
10
10
|
var PKG_FORMAT = "esm";
|
|
11
11
|
|
|
12
12
|
// src/dupe-detection.ts
|
|
@@ -988,14 +988,15 @@ var OpCode = Object.freeze({
|
|
|
988
988
|
DELETE_OBJECT_KEY: 6,
|
|
989
989
|
CREATE_MAP: 7,
|
|
990
990
|
CREATE_REGISTER: 8,
|
|
991
|
-
|
|
991
|
+
CREATE_TEXT: 9,
|
|
992
|
+
UPDATE_TEXT: 10,
|
|
992
993
|
CREATE_FILE: 11
|
|
993
994
|
});
|
|
994
995
|
function isIgnoredOp(op) {
|
|
995
996
|
return op.type === OpCode.DELETE_CRDT && op.id === "ACK";
|
|
996
997
|
}
|
|
997
998
|
function isCreateOp(op) {
|
|
998
|
-
return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_FILE || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST;
|
|
999
|
+
return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_FILE || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST || op.type === OpCode.CREATE_TEXT;
|
|
999
1000
|
}
|
|
1000
1001
|
|
|
1001
1002
|
// src/protocol/StorageNode.ts
|
|
@@ -1004,7 +1005,7 @@ var CrdtType = Object.freeze({
|
|
|
1004
1005
|
LIST: 1,
|
|
1005
1006
|
MAP: 2,
|
|
1006
1007
|
REGISTER: 3,
|
|
1007
|
-
|
|
1008
|
+
TEXT: 4,
|
|
1008
1009
|
FILE: 5
|
|
1009
1010
|
});
|
|
1010
1011
|
function isRootStorageNode(node) {
|
|
@@ -1022,6 +1023,9 @@ function isMapStorageNode(node) {
|
|
|
1022
1023
|
function isRegisterStorageNode(node) {
|
|
1023
1024
|
return node[1].type === CrdtType.REGISTER;
|
|
1024
1025
|
}
|
|
1026
|
+
function isTextStorageNode(node) {
|
|
1027
|
+
return node[1].type === CrdtType.TEXT;
|
|
1028
|
+
}
|
|
1025
1029
|
function isFileStorageNode(node) {
|
|
1026
1030
|
return node[1].type === CrdtType.FILE;
|
|
1027
1031
|
}
|
|
@@ -1047,6 +1051,9 @@ function* compactNodesToNodeStream(compactNodes) {
|
|
|
1047
1051
|
case CrdtType.REGISTER:
|
|
1048
1052
|
yield [cnode[0], { type: CrdtType.REGISTER, parentId: cnode[2], parentKey: cnode[3], data: cnode[4] }];
|
|
1049
1053
|
break;
|
|
1054
|
+
case CrdtType.TEXT:
|
|
1055
|
+
yield [cnode[0], { type: CrdtType.TEXT, parentId: cnode[2], parentKey: cnode[3], data: cnode[4], version: cnode[5] }];
|
|
1056
|
+
break;
|
|
1050
1057
|
case CrdtType.FILE:
|
|
1051
1058
|
yield [cnode[0], { type: CrdtType.FILE, parentId: cnode[2], parentKey: cnode[3], data: cnode[4] }];
|
|
1052
1059
|
break;
|
|
@@ -1078,6 +1085,17 @@ function* nodeStreamToCompactNodes(nodes) {
|
|
|
1078
1085
|
const id = node[0];
|
|
1079
1086
|
const crdt = node[1];
|
|
1080
1087
|
yield [id, CrdtType.REGISTER, crdt.parentId, crdt.parentKey, crdt.data];
|
|
1088
|
+
} else if (isTextStorageNode(node)) {
|
|
1089
|
+
const id = node[0];
|
|
1090
|
+
const crdt = node[1];
|
|
1091
|
+
yield [
|
|
1092
|
+
id,
|
|
1093
|
+
CrdtType.TEXT,
|
|
1094
|
+
crdt.parentId,
|
|
1095
|
+
crdt.parentKey,
|
|
1096
|
+
crdt.data,
|
|
1097
|
+
crdt.version
|
|
1098
|
+
];
|
|
1081
1099
|
} else if (isFileStorageNode(node)) {
|
|
1082
1100
|
const id = node[0];
|
|
1083
1101
|
const crdt = node[1];
|
|
@@ -1087,6 +1105,10 @@ function* nodeStreamToCompactNodes(nodes) {
|
|
|
1087
1105
|
}
|
|
1088
1106
|
}
|
|
1089
1107
|
|
|
1108
|
+
// src/internal.ts
|
|
1109
|
+
var kInternal = /* @__PURE__ */ Symbol();
|
|
1110
|
+
var kStorageUpdateSource = /* @__PURE__ */ Symbol();
|
|
1111
|
+
|
|
1090
1112
|
// src/lib/position.ts
|
|
1091
1113
|
var MIN_CODE = 32;
|
|
1092
1114
|
var MAX_CODE = 126;
|
|
@@ -1284,6 +1306,10 @@ ${parentKey}`;
|
|
|
1284
1306
|
get size() {
|
|
1285
1307
|
return this.#byOpId.size;
|
|
1286
1308
|
}
|
|
1309
|
+
/** The still-unacknowledged op with the given opId, if any. */
|
|
1310
|
+
get(opId) {
|
|
1311
|
+
return this.#byOpId.get(opId);
|
|
1312
|
+
}
|
|
1287
1313
|
/**
|
|
1288
1314
|
* Mark the given Op as still unacknowledged.
|
|
1289
1315
|
*/
|
|
@@ -1383,8 +1409,8 @@ function createManagedPool(options) {
|
|
|
1383
1409
|
deleteNode: (id) => void nodes.delete(id),
|
|
1384
1410
|
generateId: () => `${getCurrentConnectionId()}:${clock++}`,
|
|
1385
1411
|
generateOpId: () => `${getCurrentConnectionId()}:${opClock++}`,
|
|
1386
|
-
dispatch(ops, reverse, storageUpdates) {
|
|
1387
|
-
onDispatch?.(ops, reverse, storageUpdates);
|
|
1412
|
+
dispatch(ops, reverse, storageUpdates, options2) {
|
|
1413
|
+
onDispatch?.(ops, reverse, storageUpdates, options2);
|
|
1388
1414
|
},
|
|
1389
1415
|
assertStorageIsWritable: () => {
|
|
1390
1416
|
if (!isStorageWritable()) {
|
|
@@ -1407,10 +1433,17 @@ function Orphaned(oldKey, oldPos = asPos(oldKey)) {
|
|
|
1407
1433
|
return Object.freeze({ type: "Orphaned", oldKey, oldPos });
|
|
1408
1434
|
}
|
|
1409
1435
|
var AbstractCrdt = class {
|
|
1410
|
-
// ^^^^^^^^^^^^ TODO: Make this an interface
|
|
1411
1436
|
#pool;
|
|
1412
1437
|
#id;
|
|
1413
1438
|
#parent = NoParent;
|
|
1439
|
+
constructor() {
|
|
1440
|
+
Object.defineProperty(this, kInternal, {
|
|
1441
|
+
value: {
|
|
1442
|
+
getId: () => this.#id
|
|
1443
|
+
},
|
|
1444
|
+
enumerable: false
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1414
1447
|
/** @internal */
|
|
1415
1448
|
_getParentKeyOrThrow() {
|
|
1416
1449
|
switch (this.parent.type) {
|
|
@@ -4877,9 +4910,6 @@ var ManagedSocket = class {
|
|
|
4877
4910
|
}
|
|
4878
4911
|
};
|
|
4879
4912
|
|
|
4880
|
-
// src/internal.ts
|
|
4881
|
-
var kInternal = /* @__PURE__ */ Symbol();
|
|
4882
|
-
|
|
4883
4913
|
// src/lib/IncrementalJsonParser.ts
|
|
4884
4914
|
var EMPTY_OBJECT = Object.freeze({});
|
|
4885
4915
|
var NULL_KEYWORD_CHARS = Array.from(new Set("null"));
|
|
@@ -9179,6 +9209,1116 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
|
|
|
9179
9209
|
}
|
|
9180
9210
|
};
|
|
9181
9211
|
|
|
9212
|
+
// src/crdts/liveTextOps.ts
|
|
9213
|
+
function attributesEqual(left, right) {
|
|
9214
|
+
if (left === right) {
|
|
9215
|
+
return true;
|
|
9216
|
+
}
|
|
9217
|
+
if (left === void 0 || right === void 0) {
|
|
9218
|
+
return false;
|
|
9219
|
+
}
|
|
9220
|
+
const leftKeys = Object.keys(left);
|
|
9221
|
+
const rightKeys = Object.keys(right);
|
|
9222
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
9223
|
+
return false;
|
|
9224
|
+
}
|
|
9225
|
+
for (const key of leftKeys) {
|
|
9226
|
+
if (left[key] !== right[key]) {
|
|
9227
|
+
return false;
|
|
9228
|
+
}
|
|
9229
|
+
}
|
|
9230
|
+
return true;
|
|
9231
|
+
}
|
|
9232
|
+
function cloneAttributes(attributes) {
|
|
9233
|
+
return attributes === void 0 ? void 0 : freeze({ ...attributes });
|
|
9234
|
+
}
|
|
9235
|
+
function normalizeSegments(segments) {
|
|
9236
|
+
const normalized = [];
|
|
9237
|
+
for (const segment of segments) {
|
|
9238
|
+
if (segment.text.length === 0) {
|
|
9239
|
+
continue;
|
|
9240
|
+
}
|
|
9241
|
+
const last = normalized.at(-1);
|
|
9242
|
+
const attributes = cloneAttributes(segment.attributes);
|
|
9243
|
+
if (last !== void 0 && attributesEqual(last.attributes, attributes)) {
|
|
9244
|
+
last.text += segment.text;
|
|
9245
|
+
} else {
|
|
9246
|
+
normalized.push({ text: segment.text, attributes });
|
|
9247
|
+
}
|
|
9248
|
+
}
|
|
9249
|
+
return normalized;
|
|
9250
|
+
}
|
|
9251
|
+
function dataToSegments(data) {
|
|
9252
|
+
return normalizeSegments(
|
|
9253
|
+
data.map(([text, attributes]) => ({
|
|
9254
|
+
text,
|
|
9255
|
+
attributes
|
|
9256
|
+
}))
|
|
9257
|
+
);
|
|
9258
|
+
}
|
|
9259
|
+
function segmentsToData(segments) {
|
|
9260
|
+
return segments.map(
|
|
9261
|
+
(segment) => segment.attributes === void 0 ? [segment.text] : [segment.text, { ...segment.attributes }]
|
|
9262
|
+
);
|
|
9263
|
+
}
|
|
9264
|
+
function textLength(segments) {
|
|
9265
|
+
return segments.reduce((sum, segment) => sum + segment.text.length, 0);
|
|
9266
|
+
}
|
|
9267
|
+
function splitSegmentsAt(segments, index) {
|
|
9268
|
+
const result = [];
|
|
9269
|
+
let offset = 0;
|
|
9270
|
+
for (const segment of segments) {
|
|
9271
|
+
const end = offset + segment.text.length;
|
|
9272
|
+
if (index > offset && index < end) {
|
|
9273
|
+
const before2 = segment.text.slice(0, index - offset);
|
|
9274
|
+
const after2 = segment.text.slice(index - offset);
|
|
9275
|
+
result.push({ text: before2, attributes: segment.attributes });
|
|
9276
|
+
result.push({ text: after2, attributes: segment.attributes });
|
|
9277
|
+
} else {
|
|
9278
|
+
result.push({ text: segment.text, attributes: segment.attributes });
|
|
9279
|
+
}
|
|
9280
|
+
offset = end;
|
|
9281
|
+
}
|
|
9282
|
+
return result;
|
|
9283
|
+
}
|
|
9284
|
+
function clipRange(index, length, contentLength) {
|
|
9285
|
+
const clippedIndex = Math.max(0, Math.min(index, contentLength));
|
|
9286
|
+
const clippedEnd = Math.max(
|
|
9287
|
+
clippedIndex,
|
|
9288
|
+
Math.min(index + length, contentLength)
|
|
9289
|
+
);
|
|
9290
|
+
return { index: clippedIndex, length: clippedEnd - clippedIndex };
|
|
9291
|
+
}
|
|
9292
|
+
function applyInsert(segments, index, text, attributes) {
|
|
9293
|
+
if (text.length === 0) {
|
|
9294
|
+
return normalizeSegments(segments);
|
|
9295
|
+
}
|
|
9296
|
+
const split = splitSegmentsAt(segments, index);
|
|
9297
|
+
const result = [];
|
|
9298
|
+
let offset = 0;
|
|
9299
|
+
let inserted = false;
|
|
9300
|
+
for (const segment of split) {
|
|
9301
|
+
if (!inserted && offset === index) {
|
|
9302
|
+
result.push({ text, attributes });
|
|
9303
|
+
inserted = true;
|
|
9304
|
+
}
|
|
9305
|
+
result.push(segment);
|
|
9306
|
+
offset += segment.text.length;
|
|
9307
|
+
}
|
|
9308
|
+
if (!inserted) {
|
|
9309
|
+
result.push({ text, attributes });
|
|
9310
|
+
}
|
|
9311
|
+
return normalizeSegments(result);
|
|
9312
|
+
}
|
|
9313
|
+
function extractDeletedSegments(segments, index, length) {
|
|
9314
|
+
const split = splitSegmentsAt(
|
|
9315
|
+
splitSegmentsAt(segments, index),
|
|
9316
|
+
index + length
|
|
9317
|
+
);
|
|
9318
|
+
const deleted = [];
|
|
9319
|
+
let offset = 0;
|
|
9320
|
+
for (const segment of split) {
|
|
9321
|
+
const end = offset + segment.text.length;
|
|
9322
|
+
if (offset >= index && end <= index + length) {
|
|
9323
|
+
deleted.push({
|
|
9324
|
+
text: segment.text,
|
|
9325
|
+
attributes: segment.attributes
|
|
9326
|
+
});
|
|
9327
|
+
}
|
|
9328
|
+
offset = end;
|
|
9329
|
+
}
|
|
9330
|
+
return normalizeSegments(deleted);
|
|
9331
|
+
}
|
|
9332
|
+
function applyDelete(segments, index, length) {
|
|
9333
|
+
const deletedSegments = extractDeletedSegments(segments, index, length);
|
|
9334
|
+
const split = splitSegmentsAt(
|
|
9335
|
+
splitSegmentsAt(segments, index),
|
|
9336
|
+
index + length
|
|
9337
|
+
);
|
|
9338
|
+
const result = [];
|
|
9339
|
+
let offset = 0;
|
|
9340
|
+
let deletedText = "";
|
|
9341
|
+
for (const segment of split) {
|
|
9342
|
+
const end = offset + segment.text.length;
|
|
9343
|
+
if (offset >= index && end <= index + length) {
|
|
9344
|
+
deletedText += segment.text;
|
|
9345
|
+
} else {
|
|
9346
|
+
result.push(segment);
|
|
9347
|
+
}
|
|
9348
|
+
offset = end;
|
|
9349
|
+
}
|
|
9350
|
+
return {
|
|
9351
|
+
segments: normalizeSegments(result),
|
|
9352
|
+
deletedText,
|
|
9353
|
+
deletedSegments
|
|
9354
|
+
};
|
|
9355
|
+
}
|
|
9356
|
+
function applyFormat(segments, index, length, attributes) {
|
|
9357
|
+
const split = splitSegmentsAt(
|
|
9358
|
+
splitSegmentsAt(segments, index),
|
|
9359
|
+
index + length
|
|
9360
|
+
);
|
|
9361
|
+
const result = [];
|
|
9362
|
+
let offset = 0;
|
|
9363
|
+
for (const segment of split) {
|
|
9364
|
+
const end = offset + segment.text.length;
|
|
9365
|
+
if (offset >= index && end <= index + length) {
|
|
9366
|
+
const nextAttributes = {
|
|
9367
|
+
...segment.attributes ?? {}
|
|
9368
|
+
};
|
|
9369
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
9370
|
+
if (value === null) {
|
|
9371
|
+
delete nextAttributes[key];
|
|
9372
|
+
} else {
|
|
9373
|
+
nextAttributes[key] = value;
|
|
9374
|
+
}
|
|
9375
|
+
}
|
|
9376
|
+
result.push({
|
|
9377
|
+
text: segment.text,
|
|
9378
|
+
attributes: Object.keys(nextAttributes).length === 0 ? void 0 : freeze(nextAttributes)
|
|
9379
|
+
});
|
|
9380
|
+
} else {
|
|
9381
|
+
result.push(segment);
|
|
9382
|
+
}
|
|
9383
|
+
offset = end;
|
|
9384
|
+
}
|
|
9385
|
+
return normalizeSegments(result);
|
|
9386
|
+
}
|
|
9387
|
+
function formatReverseOperations(segments, index, length, patch) {
|
|
9388
|
+
const split = splitSegmentsAt(
|
|
9389
|
+
splitSegmentsAt(segments, index),
|
|
9390
|
+
index + length
|
|
9391
|
+
);
|
|
9392
|
+
const result = [];
|
|
9393
|
+
let offset = 0;
|
|
9394
|
+
for (const segment of split) {
|
|
9395
|
+
const end = offset + segment.text.length;
|
|
9396
|
+
if (offset >= index && end <= index + length) {
|
|
9397
|
+
const attributes = {};
|
|
9398
|
+
const current = segment.attributes ?? {};
|
|
9399
|
+
for (const key of Object.keys(patch)) {
|
|
9400
|
+
const value = Object.hasOwn(current, key) ? current[key] : void 0;
|
|
9401
|
+
attributes[key] = value ?? null;
|
|
9402
|
+
}
|
|
9403
|
+
result.push({
|
|
9404
|
+
type: "format",
|
|
9405
|
+
index: offset,
|
|
9406
|
+
length: segment.text.length,
|
|
9407
|
+
attributes
|
|
9408
|
+
});
|
|
9409
|
+
}
|
|
9410
|
+
offset = end;
|
|
9411
|
+
}
|
|
9412
|
+
return result;
|
|
9413
|
+
}
|
|
9414
|
+
function mapIndexThroughOperation(index, op) {
|
|
9415
|
+
if (op.type === "insert") {
|
|
9416
|
+
return op.index <= index ? index + op.text.length : index;
|
|
9417
|
+
} else if (op.type === "delete") {
|
|
9418
|
+
if (op.index >= index) {
|
|
9419
|
+
return index;
|
|
9420
|
+
}
|
|
9421
|
+
return Math.max(op.index, index - op.length);
|
|
9422
|
+
} else {
|
|
9423
|
+
return index;
|
|
9424
|
+
}
|
|
9425
|
+
}
|
|
9426
|
+
function mapTextIndexThroughOperations(index, ops) {
|
|
9427
|
+
let mapped = index;
|
|
9428
|
+
for (const op of ops) {
|
|
9429
|
+
mapped = mapIndexThroughOperation(mapped, op);
|
|
9430
|
+
}
|
|
9431
|
+
return mapped;
|
|
9432
|
+
}
|
|
9433
|
+
function inverseMapIndexThroughOperation(index, op) {
|
|
9434
|
+
if (op.type === "insert") {
|
|
9435
|
+
if (index <= op.index) {
|
|
9436
|
+
return index;
|
|
9437
|
+
}
|
|
9438
|
+
return Math.max(op.index, index - op.text.length);
|
|
9439
|
+
} else if (op.type === "delete") {
|
|
9440
|
+
return op.index <= index ? index + op.length : index;
|
|
9441
|
+
} else {
|
|
9442
|
+
return index;
|
|
9443
|
+
}
|
|
9444
|
+
}
|
|
9445
|
+
function inverseMapTextIndexThroughOperations(index, ops) {
|
|
9446
|
+
let mapped = index;
|
|
9447
|
+
for (let i = ops.length - 1; i >= 0; i--) {
|
|
9448
|
+
mapped = inverseMapIndexThroughOperation(mapped, ops[i]);
|
|
9449
|
+
}
|
|
9450
|
+
return mapped;
|
|
9451
|
+
}
|
|
9452
|
+
function oppositeOrder(order) {
|
|
9453
|
+
return order === "before" ? "after" : "before";
|
|
9454
|
+
}
|
|
9455
|
+
function mapIndexOverDelete(index, deleteIndex, deleteLength) {
|
|
9456
|
+
if (deleteIndex >= index) {
|
|
9457
|
+
return index;
|
|
9458
|
+
}
|
|
9459
|
+
return Math.max(deleteIndex, index - deleteLength);
|
|
9460
|
+
}
|
|
9461
|
+
function transformInsert(op, over, order) {
|
|
9462
|
+
if (over.type === "insert") {
|
|
9463
|
+
const shifts = over.index < op.index || over.index === op.index && order === "after";
|
|
9464
|
+
return [shifts ? { ...op, index: op.index + over.text.length } : { ...op }];
|
|
9465
|
+
} else if (over.type === "delete") {
|
|
9466
|
+
return [
|
|
9467
|
+
{ ...op, index: mapIndexOverDelete(op.index, over.index, over.length) }
|
|
9468
|
+
];
|
|
9469
|
+
} else {
|
|
9470
|
+
return [{ ...op }];
|
|
9471
|
+
}
|
|
9472
|
+
}
|
|
9473
|
+
function transformDelete(op, over) {
|
|
9474
|
+
const start = op.index;
|
|
9475
|
+
const end = op.index + op.length;
|
|
9476
|
+
if (over.type === "insert") {
|
|
9477
|
+
const at = over.index;
|
|
9478
|
+
const len = over.text.length;
|
|
9479
|
+
if (at <= start) {
|
|
9480
|
+
return [{ ...op, index: start + len }];
|
|
9481
|
+
}
|
|
9482
|
+
if (at >= end) {
|
|
9483
|
+
return [{ ...op }];
|
|
9484
|
+
}
|
|
9485
|
+
return [
|
|
9486
|
+
{ type: "delete", index: start, length: at - start },
|
|
9487
|
+
{ type: "delete", index: start + len, length: end - at }
|
|
9488
|
+
];
|
|
9489
|
+
} else if (over.type === "delete") {
|
|
9490
|
+
const newStart = mapIndexOverDelete(start, over.index, over.length);
|
|
9491
|
+
const newEnd = mapIndexOverDelete(end, over.index, over.length);
|
|
9492
|
+
return newEnd - newStart > 0 ? [{ type: "delete", index: newStart, length: newEnd - newStart }] : [];
|
|
9493
|
+
} else {
|
|
9494
|
+
return [{ ...op }];
|
|
9495
|
+
}
|
|
9496
|
+
}
|
|
9497
|
+
function transformFormat(op, over, order) {
|
|
9498
|
+
const start = op.index;
|
|
9499
|
+
const end = op.index + op.length;
|
|
9500
|
+
if (over.type === "insert") {
|
|
9501
|
+
const at = over.index;
|
|
9502
|
+
const len = over.text.length;
|
|
9503
|
+
if (at <= start) {
|
|
9504
|
+
return [{ ...op, index: start + len }];
|
|
9505
|
+
}
|
|
9506
|
+
if (at >= end) {
|
|
9507
|
+
return [{ ...op }];
|
|
9508
|
+
}
|
|
9509
|
+
return [
|
|
9510
|
+
{
|
|
9511
|
+
type: "format",
|
|
9512
|
+
index: start,
|
|
9513
|
+
length: at - start,
|
|
9514
|
+
attributes: op.attributes
|
|
9515
|
+
},
|
|
9516
|
+
{
|
|
9517
|
+
type: "format",
|
|
9518
|
+
index: at + len,
|
|
9519
|
+
length: end - at,
|
|
9520
|
+
attributes: op.attributes
|
|
9521
|
+
}
|
|
9522
|
+
];
|
|
9523
|
+
} else if (over.type === "delete") {
|
|
9524
|
+
const newStart = mapIndexOverDelete(start, over.index, over.length);
|
|
9525
|
+
const newEnd = mapIndexOverDelete(end, over.index, over.length);
|
|
9526
|
+
return newEnd - newStart > 0 ? [
|
|
9527
|
+
{
|
|
9528
|
+
type: "format",
|
|
9529
|
+
index: newStart,
|
|
9530
|
+
length: newEnd - newStart,
|
|
9531
|
+
attributes: op.attributes
|
|
9532
|
+
}
|
|
9533
|
+
] : [];
|
|
9534
|
+
} else {
|
|
9535
|
+
if (order === "after") {
|
|
9536
|
+
return [{ ...op }];
|
|
9537
|
+
}
|
|
9538
|
+
const overlapStart = Math.max(start, over.index);
|
|
9539
|
+
const overlapEnd = Math.min(end, over.index + over.length);
|
|
9540
|
+
if (overlapStart >= overlapEnd) {
|
|
9541
|
+
return [{ ...op }];
|
|
9542
|
+
}
|
|
9543
|
+
const hasConflict = Object.keys(op.attributes).some(
|
|
9544
|
+
(key) => Object.hasOwn(over.attributes, key)
|
|
9545
|
+
);
|
|
9546
|
+
if (!hasConflict) {
|
|
9547
|
+
return [{ ...op }];
|
|
9548
|
+
}
|
|
9549
|
+
const reduced = {};
|
|
9550
|
+
for (const [key, value] of Object.entries(op.attributes)) {
|
|
9551
|
+
if (!Object.hasOwn(over.attributes, key)) {
|
|
9552
|
+
reduced[key] = value;
|
|
9553
|
+
}
|
|
9554
|
+
}
|
|
9555
|
+
const pieces = [];
|
|
9556
|
+
if (start < overlapStart) {
|
|
9557
|
+
pieces.push({
|
|
9558
|
+
type: "format",
|
|
9559
|
+
index: start,
|
|
9560
|
+
length: overlapStart - start,
|
|
9561
|
+
attributes: op.attributes
|
|
9562
|
+
});
|
|
9563
|
+
}
|
|
9564
|
+
if (Object.keys(reduced).length > 0) {
|
|
9565
|
+
pieces.push({
|
|
9566
|
+
type: "format",
|
|
9567
|
+
index: overlapStart,
|
|
9568
|
+
length: overlapEnd - overlapStart,
|
|
9569
|
+
attributes: reduced
|
|
9570
|
+
});
|
|
9571
|
+
}
|
|
9572
|
+
if (overlapEnd < end) {
|
|
9573
|
+
pieces.push({
|
|
9574
|
+
type: "format",
|
|
9575
|
+
index: overlapEnd,
|
|
9576
|
+
length: end - overlapEnd,
|
|
9577
|
+
attributes: op.attributes
|
|
9578
|
+
});
|
|
9579
|
+
}
|
|
9580
|
+
return pieces;
|
|
9581
|
+
}
|
|
9582
|
+
}
|
|
9583
|
+
function transformSingle(op, over, order) {
|
|
9584
|
+
switch (op.type) {
|
|
9585
|
+
case "insert":
|
|
9586
|
+
return transformInsert(op, over, order);
|
|
9587
|
+
case "delete":
|
|
9588
|
+
return transformDelete(op, over);
|
|
9589
|
+
case "format":
|
|
9590
|
+
return transformFormat(op, over, order);
|
|
9591
|
+
}
|
|
9592
|
+
}
|
|
9593
|
+
function transformTextOperationsX(a, b, order) {
|
|
9594
|
+
if (a.length === 0 || b.length === 0) {
|
|
9595
|
+
return [[...a], [...b]];
|
|
9596
|
+
}
|
|
9597
|
+
if (a.length === 1 && b.length === 1) {
|
|
9598
|
+
return [
|
|
9599
|
+
transformSingle(a[0], b[0], order),
|
|
9600
|
+
transformSingle(b[0], a[0], oppositeOrder(order))
|
|
9601
|
+
];
|
|
9602
|
+
}
|
|
9603
|
+
if (a.length > 1) {
|
|
9604
|
+
const [headA1, b1] = transformTextOperationsX([a[0]], b, order);
|
|
9605
|
+
const [restA1, b2] = transformTextOperationsX(a.slice(1), b1, order);
|
|
9606
|
+
return [[...headA1, ...restA1], b2];
|
|
9607
|
+
}
|
|
9608
|
+
const [a1, headB1] = transformTextOperationsX(a, [b[0]], order);
|
|
9609
|
+
const [a2, restB1] = transformTextOperationsX(a1, b.slice(1), order);
|
|
9610
|
+
return [a2, [...headB1, ...restB1]];
|
|
9611
|
+
}
|
|
9612
|
+
function transformTextOperations(ops, over, order) {
|
|
9613
|
+
return transformTextOperationsX(ops, over, order)[0];
|
|
9614
|
+
}
|
|
9615
|
+
function textOperationsEqual(a, b) {
|
|
9616
|
+
return a === b || stableStringify(a) === stableStringify(b);
|
|
9617
|
+
}
|
|
9618
|
+
function applyTextOperationsToSegments(segments, ops) {
|
|
9619
|
+
let next = [...segments];
|
|
9620
|
+
for (const op of ops) {
|
|
9621
|
+
if (op.type === "insert") {
|
|
9622
|
+
const index = Math.max(0, Math.min(op.index, textLength(next)));
|
|
9623
|
+
next = applyInsert(next, index, op.text, op.attributes);
|
|
9624
|
+
} else if (op.type === "delete") {
|
|
9625
|
+
const index = Math.max(0, Math.min(op.index, textLength(next)));
|
|
9626
|
+
const clipped = clipRange(index, op.length, textLength(next));
|
|
9627
|
+
next = applyDelete(next, clipped.index, clipped.length).segments;
|
|
9628
|
+
} else {
|
|
9629
|
+
const index = Math.max(0, Math.min(op.index, textLength(next)));
|
|
9630
|
+
const clipped = clipRange(index, op.length, textLength(next));
|
|
9631
|
+
next = applyFormat(next, clipped.index, clipped.length, op.attributes);
|
|
9632
|
+
}
|
|
9633
|
+
}
|
|
9634
|
+
return next;
|
|
9635
|
+
}
|
|
9636
|
+
function applyLiveTextOperations(data, ops) {
|
|
9637
|
+
return segmentsToData(
|
|
9638
|
+
applyTextOperationsToSegments(dataToSegments(data), ops)
|
|
9639
|
+
);
|
|
9640
|
+
}
|
|
9641
|
+
function invertTextOperations(segments, ops) {
|
|
9642
|
+
let shadow = [...segments];
|
|
9643
|
+
const reverse = [];
|
|
9644
|
+
for (const op of ops) {
|
|
9645
|
+
if (op.type === "insert") {
|
|
9646
|
+
shadow = applyInsert(shadow, op.index, op.text, op.attributes);
|
|
9647
|
+
reverse.unshift({
|
|
9648
|
+
type: "delete",
|
|
9649
|
+
index: op.index,
|
|
9650
|
+
length: op.text.length
|
|
9651
|
+
});
|
|
9652
|
+
} else if (op.type === "delete") {
|
|
9653
|
+
const deletedSegments = extractDeletedSegments(
|
|
9654
|
+
shadow,
|
|
9655
|
+
op.index,
|
|
9656
|
+
op.length
|
|
9657
|
+
);
|
|
9658
|
+
shadow = applyDelete(shadow, op.index, op.length).segments;
|
|
9659
|
+
const inserts = [];
|
|
9660
|
+
let insertIndex = op.index;
|
|
9661
|
+
for (const segment of deletedSegments) {
|
|
9662
|
+
inserts.push({
|
|
9663
|
+
type: "insert",
|
|
9664
|
+
index: insertIndex,
|
|
9665
|
+
text: segment.text,
|
|
9666
|
+
attributes: segment.attributes
|
|
9667
|
+
});
|
|
9668
|
+
insertIndex += segment.text.length;
|
|
9669
|
+
}
|
|
9670
|
+
for (let index = inserts.length - 1; index >= 0; index--) {
|
|
9671
|
+
reverse.unshift(inserts[index]);
|
|
9672
|
+
}
|
|
9673
|
+
} else {
|
|
9674
|
+
const inverse = formatReverseOperations(
|
|
9675
|
+
shadow,
|
|
9676
|
+
op.index,
|
|
9677
|
+
op.length,
|
|
9678
|
+
op.attributes
|
|
9679
|
+
);
|
|
9680
|
+
shadow = applyFormat(shadow, op.index, op.length, op.attributes);
|
|
9681
|
+
reverse.unshift(...inverse.reverse());
|
|
9682
|
+
}
|
|
9683
|
+
}
|
|
9684
|
+
return reverse;
|
|
9685
|
+
}
|
|
9686
|
+
|
|
9687
|
+
// src/crdts/LiveText.ts
|
|
9688
|
+
var ACCEPTED_OPS_HISTORY_LIMIT = 1e3;
|
|
9689
|
+
var LiveText = class _LiveText extends AbstractCrdt {
|
|
9690
|
+
/** The local document: #confirmed ⊕ #inFlightOps ⊕ #queuedOps. */
|
|
9691
|
+
#segments;
|
|
9692
|
+
/** The server-confirmed document (only authoritative ops applied). */
|
|
9693
|
+
#confirmed;
|
|
9694
|
+
#version;
|
|
9695
|
+
/** The op currently awaiting server acknowledgement (at most one). */
|
|
9696
|
+
#inFlightOpId;
|
|
9697
|
+
/** Its ops, continuously re-expressed against current server state. */
|
|
9698
|
+
#inFlightOps = [];
|
|
9699
|
+
/** Local edits made while an op is in flight; sent after the ack. */
|
|
9700
|
+
#queuedOps = [];
|
|
9701
|
+
#acceptedOps = [];
|
|
9702
|
+
/**
|
|
9703
|
+
* Creates a new LiveText document.
|
|
9704
|
+
*
|
|
9705
|
+
* @param textOrData Initial plain text, or an array of `[text]` /
|
|
9706
|
+
* `[text, attributes]` segments. Defaults to an empty document.
|
|
9707
|
+
*
|
|
9708
|
+
* @example
|
|
9709
|
+
* new LiveText();
|
|
9710
|
+
* new LiveText("Hello world");
|
|
9711
|
+
* new LiveText([["Hello ", { bold: true }], ["world"]]);
|
|
9712
|
+
*/
|
|
9713
|
+
constructor(textOrData = "", version = 0) {
|
|
9714
|
+
super();
|
|
9715
|
+
this.#segments = typeof textOrData === "string" ? textOrData.length === 0 ? [] : [{ text: textOrData }] : dataToSegments(textOrData);
|
|
9716
|
+
this.#confirmed = [...this.#segments];
|
|
9717
|
+
this.#version = version;
|
|
9718
|
+
Object.assign(this[kInternal], {
|
|
9719
|
+
encodeIndex: (localIndex) => this.#encodeIndex(localIndex),
|
|
9720
|
+
decodeIndex: (index, fromVersion) => this.#decodeIndex(index, fromVersion)
|
|
9721
|
+
});
|
|
9722
|
+
}
|
|
9723
|
+
get version() {
|
|
9724
|
+
return this.#version;
|
|
9725
|
+
}
|
|
9726
|
+
get length() {
|
|
9727
|
+
return textLength(this.#segments);
|
|
9728
|
+
}
|
|
9729
|
+
/** @internal */
|
|
9730
|
+
static _deserialize([id, item], _parentToChildren, pool) {
|
|
9731
|
+
const text = new _LiveText(item.data, item.version);
|
|
9732
|
+
text._attach(id, pool);
|
|
9733
|
+
return text;
|
|
9734
|
+
}
|
|
9735
|
+
/** @internal */
|
|
9736
|
+
_toOps(parentId, parentKey) {
|
|
9737
|
+
if (this._id === void 0) {
|
|
9738
|
+
throw new Error("Cannot serialize LiveText if it is not attached");
|
|
9739
|
+
}
|
|
9740
|
+
return [
|
|
9741
|
+
{
|
|
9742
|
+
type: OpCode.CREATE_TEXT,
|
|
9743
|
+
id: this._id,
|
|
9744
|
+
parentId,
|
|
9745
|
+
parentKey,
|
|
9746
|
+
data: this.toJSON(),
|
|
9747
|
+
version: this.#version
|
|
9748
|
+
}
|
|
9749
|
+
];
|
|
9750
|
+
}
|
|
9751
|
+
/** @internal */
|
|
9752
|
+
_serialize() {
|
|
9753
|
+
if (this.parent.type !== "HasParent") {
|
|
9754
|
+
throw new Error("Cannot serialize LiveText if parent is missing");
|
|
9755
|
+
}
|
|
9756
|
+
return {
|
|
9757
|
+
type: CrdtType.TEXT,
|
|
9758
|
+
parentId: nn(this.parent.node._id, "Parent node expected to have ID"),
|
|
9759
|
+
parentKey: this.parent.key,
|
|
9760
|
+
data: this.toJSON(),
|
|
9761
|
+
version: this.#version
|
|
9762
|
+
};
|
|
9763
|
+
}
|
|
9764
|
+
/** @internal */
|
|
9765
|
+
_attachChild(_op) {
|
|
9766
|
+
throw new Error("LiveText cannot contain child nodes");
|
|
9767
|
+
}
|
|
9768
|
+
/** @internal */
|
|
9769
|
+
_detachChild(_crdt) {
|
|
9770
|
+
throw new Error("LiveText cannot contain child nodes");
|
|
9771
|
+
}
|
|
9772
|
+
/** @internal */
|
|
9773
|
+
_apply(op, isLocal) {
|
|
9774
|
+
if (op.type !== OpCode.UPDATE_TEXT) {
|
|
9775
|
+
return super._apply(op, isLocal);
|
|
9776
|
+
}
|
|
9777
|
+
if (isLocal) {
|
|
9778
|
+
return this.#applyLocal(op);
|
|
9779
|
+
}
|
|
9780
|
+
if (op.opId !== void 0 && op.opId === this.#inFlightOpId) {
|
|
9781
|
+
return this.#applyAck(op);
|
|
9782
|
+
}
|
|
9783
|
+
if (op.opId !== void 0 && this.#acceptedOps.some((entry) => entry.opId === op.opId)) {
|
|
9784
|
+
this.#version = Math.max(this.#version, op.version ?? op.baseVersion + 1);
|
|
9785
|
+
return { modified: false };
|
|
9786
|
+
}
|
|
9787
|
+
return this.#applyRemote(op);
|
|
9788
|
+
}
|
|
9789
|
+
/**
|
|
9790
|
+
* Inserts text at the given index.
|
|
9791
|
+
*
|
|
9792
|
+
* @param index Character index at which to insert. Values outside the
|
|
9793
|
+
* document range are clipped.
|
|
9794
|
+
* @param text Text to insert.
|
|
9795
|
+
* @param attributes Optional inline attributes for the inserted text.
|
|
9796
|
+
*
|
|
9797
|
+
* @example
|
|
9798
|
+
* const text = new LiveText("Hello");
|
|
9799
|
+
* text.insert(5, " world");
|
|
9800
|
+
* text.insert(0, "Say: ", { italic: true });
|
|
9801
|
+
*/
|
|
9802
|
+
insert(index, text, attributes) {
|
|
9803
|
+
const clippedIndex = Math.max(0, Math.min(index, this.length));
|
|
9804
|
+
this.#dispatch([{ type: "insert", index: clippedIndex, text, attributes }]);
|
|
9805
|
+
}
|
|
9806
|
+
/**
|
|
9807
|
+
* Deletes `length` characters starting at `index`.
|
|
9808
|
+
*
|
|
9809
|
+
* @example
|
|
9810
|
+
* const text = new LiveText("Hello world");
|
|
9811
|
+
* text.delete(5, 6); // "Hello"
|
|
9812
|
+
*/
|
|
9813
|
+
delete(index, length) {
|
|
9814
|
+
const clipped = clipRange(index, length, this.length);
|
|
9815
|
+
if (clipped.length === 0) {
|
|
9816
|
+
return;
|
|
9817
|
+
}
|
|
9818
|
+
this.#dispatch([
|
|
9819
|
+
{ type: "delete", index: clipped.index, length: clipped.length }
|
|
9820
|
+
]);
|
|
9821
|
+
}
|
|
9822
|
+
/**
|
|
9823
|
+
* Replaces a range of text with new text.
|
|
9824
|
+
*
|
|
9825
|
+
* @example
|
|
9826
|
+
* const text = new LiveText("Hello world");
|
|
9827
|
+
* text.replace(0, 5, "Hi"); // "Hi world"
|
|
9828
|
+
*/
|
|
9829
|
+
replace(index, length, text, attributes) {
|
|
9830
|
+
const clipped = clipRange(index, length, this.length);
|
|
9831
|
+
const ops = [];
|
|
9832
|
+
if (clipped.length > 0) {
|
|
9833
|
+
ops.push({
|
|
9834
|
+
type: "delete",
|
|
9835
|
+
index: clipped.index,
|
|
9836
|
+
length: clipped.length
|
|
9837
|
+
});
|
|
9838
|
+
}
|
|
9839
|
+
if (text.length > 0) {
|
|
9840
|
+
ops.push({ type: "insert", index: clipped.index, text, attributes });
|
|
9841
|
+
}
|
|
9842
|
+
this.#dispatch(ops);
|
|
9843
|
+
}
|
|
9844
|
+
/**
|
|
9845
|
+
* Encode a local-document index (an offset into this LiveText's current
|
|
9846
|
+
* #segments, which CodeMirror or any consumer mirrors as its document)
|
|
9847
|
+
* into server-confirmed coordinates suitable for broadcasting to peers via
|
|
9848
|
+
* presence or any other side channel.
|
|
9849
|
+
*
|
|
9850
|
+
* The returned index is in this LiveText's current #confirmed coordinates
|
|
9851
|
+
* — that is, with this client's local pending ops inverse-mapped out.
|
|
9852
|
+
* Pair it with the current {@link LiveText.version} when sending so the
|
|
9853
|
+
* receiver can call {@link PrivateLiveTextApi.decodeIndex} to land the
|
|
9854
|
+
* position in their own local document coordinates regardless of their
|
|
9855
|
+
* private pending ops.
|
|
9856
|
+
*
|
|
9857
|
+
* Index ambiguity at boundaries is resolved by an inverse-of-forward
|
|
9858
|
+
* convention: a position at or before a local insertion is reported as
|
|
9859
|
+
* the position right before the insertion in #confirmed; a position past
|
|
9860
|
+
* the insertion shifts left by the insertion's length. Positions inside
|
|
9861
|
+
* an own-pending insertion collapse to the insertion point.
|
|
9862
|
+
*/
|
|
9863
|
+
#encodeIndex(localIndex) {
|
|
9864
|
+
let mapped = Math.max(0, Math.min(localIndex, this.length));
|
|
9865
|
+
mapped = inverseMapTextIndexThroughOperations(mapped, this.#queuedOps);
|
|
9866
|
+
mapped = inverseMapTextIndexThroughOperations(mapped, this.#inFlightOps);
|
|
9867
|
+
return mapped;
|
|
9868
|
+
}
|
|
9869
|
+
/**
|
|
9870
|
+
* Decode an `(index, fromVersion)` pair produced by
|
|
9871
|
+
* {@link PrivateLiveTextApi.encodeIndex} — typically on a peer — into an
|
|
9872
|
+
* offset in this LiveText's current local document (an index suitable for
|
|
9873
|
+
* placing a CodeMirror marker, an annotation anchor, or anything else that
|
|
9874
|
+
* lives over #segments).
|
|
9875
|
+
*
|
|
9876
|
+
* Composes the accepted ops applied since `fromVersion` (drawn from
|
|
9877
|
+
* #acceptedOps in locally-applied form) with this client's own local
|
|
9878
|
+
* pending ops, in that order. The result is in current #segments
|
|
9879
|
+
* coordinates.
|
|
9880
|
+
*
|
|
9881
|
+
* Returns `null` when the position cannot be decoded against the current
|
|
9882
|
+
* state:
|
|
9883
|
+
* - `fromVersion` is greater than this LiveText's current version: the
|
|
9884
|
+
* peer is ahead of us. The caller should park the message and retry
|
|
9885
|
+
* after more accepted ops arrive.
|
|
9886
|
+
* - `fromVersion` falls outside the retained accepted-ops history. This
|
|
9887
|
+
* only happens after very long-lived disconnections; the caller can
|
|
9888
|
+
* fall back to using the raw index and letting subsequent local
|
|
9889
|
+
* transactions map it (with bounded drift).
|
|
9890
|
+
*/
|
|
9891
|
+
#decodeIndex(index, fromVersion) {
|
|
9892
|
+
if (fromVersion > this.#version) {
|
|
9893
|
+
return null;
|
|
9894
|
+
}
|
|
9895
|
+
if (fromVersion < this.#version) {
|
|
9896
|
+
const oldest = this.#acceptedOps[0]?.version;
|
|
9897
|
+
if (oldest === void 0 || oldest > fromVersion + 1) {
|
|
9898
|
+
return null;
|
|
9899
|
+
}
|
|
9900
|
+
}
|
|
9901
|
+
let mapped = index;
|
|
9902
|
+
for (const entry of this.#acceptedOps) {
|
|
9903
|
+
if (entry.version <= fromVersion) continue;
|
|
9904
|
+
if (entry.version > this.#version) break;
|
|
9905
|
+
if (entry.ops.length === 0) continue;
|
|
9906
|
+
mapped = mapTextIndexThroughOperations(mapped, entry.ops);
|
|
9907
|
+
}
|
|
9908
|
+
mapped = mapTextIndexThroughOperations(mapped, this.#inFlightOps);
|
|
9909
|
+
mapped = mapTextIndexThroughOperations(mapped, this.#queuedOps);
|
|
9910
|
+
return Math.max(0, Math.min(mapped, this.length));
|
|
9911
|
+
}
|
|
9912
|
+
/**
|
|
9913
|
+
* Applies or removes inline attributes on a range of text.
|
|
9914
|
+
*
|
|
9915
|
+
* Set an attribute to `null` to remove it from the range.
|
|
9916
|
+
*
|
|
9917
|
+
* @example
|
|
9918
|
+
* const text = new LiveText("Hello world");
|
|
9919
|
+
* text.format(0, 5, { bold: true });
|
|
9920
|
+
* text.format(0, 5, { bold: null });
|
|
9921
|
+
*/
|
|
9922
|
+
format(index, length, attributes) {
|
|
9923
|
+
const clipped = clipRange(index, length, this.length);
|
|
9924
|
+
if (clipped.length === 0) {
|
|
9925
|
+
return;
|
|
9926
|
+
}
|
|
9927
|
+
this.#dispatch([
|
|
9928
|
+
{
|
|
9929
|
+
type: "format",
|
|
9930
|
+
index: clipped.index,
|
|
9931
|
+
length: clipped.length,
|
|
9932
|
+
attributes
|
|
9933
|
+
}
|
|
9934
|
+
]);
|
|
9935
|
+
}
|
|
9936
|
+
/** Local edits made through the public API. */
|
|
9937
|
+
#dispatch(ops) {
|
|
9938
|
+
if (ops.length === 0) {
|
|
9939
|
+
return;
|
|
9940
|
+
}
|
|
9941
|
+
this._pool?.assertStorageIsWritable();
|
|
9942
|
+
const attached = this._pool !== void 0 && this._id !== void 0;
|
|
9943
|
+
const reverse = attached ? this.#invertOperations(ops) : [];
|
|
9944
|
+
const changes = this.#applyOperationsLocally(ops);
|
|
9945
|
+
if (!attached) {
|
|
9946
|
+
return;
|
|
9947
|
+
}
|
|
9948
|
+
const pool = nn(this._pool);
|
|
9949
|
+
const id = nn(this._id);
|
|
9950
|
+
const updates = /* @__PURE__ */ new Map([
|
|
9951
|
+
[
|
|
9952
|
+
id,
|
|
9953
|
+
{
|
|
9954
|
+
type: "LiveText",
|
|
9955
|
+
node: this,
|
|
9956
|
+
version: this.#version,
|
|
9957
|
+
updates: changes
|
|
9958
|
+
}
|
|
9959
|
+
]
|
|
9960
|
+
]);
|
|
9961
|
+
if (this.#inFlightOpId === void 0) {
|
|
9962
|
+
const opId = pool.generateOpId();
|
|
9963
|
+
this.#inFlightOpId = opId;
|
|
9964
|
+
this.#inFlightOps = [...ops];
|
|
9965
|
+
pool.dispatch(
|
|
9966
|
+
[
|
|
9967
|
+
{
|
|
9968
|
+
type: OpCode.UPDATE_TEXT,
|
|
9969
|
+
id,
|
|
9970
|
+
opId,
|
|
9971
|
+
baseVersion: this.#version,
|
|
9972
|
+
ops: [...ops]
|
|
9973
|
+
}
|
|
9974
|
+
],
|
|
9975
|
+
reverse,
|
|
9976
|
+
updates
|
|
9977
|
+
);
|
|
9978
|
+
} else {
|
|
9979
|
+
this.#queuedOps.push(...ops);
|
|
9980
|
+
pool.dispatch([], reverse, updates, { clearRedoStack: true });
|
|
9981
|
+
}
|
|
9982
|
+
}
|
|
9983
|
+
/**
|
|
9984
|
+
* A local replay of an existing wire op: an undo/redo frame, or an
|
|
9985
|
+
* unacknowledged op re-sent after a reconnect.
|
|
9986
|
+
*/
|
|
9987
|
+
#applyLocal(op) {
|
|
9988
|
+
const mutableOp = op;
|
|
9989
|
+
if (op.opId !== void 0 && op.opId === this.#inFlightOpId) {
|
|
9990
|
+
this.#inFlightOps = [...this.#inFlightOps, ...this.#queuedOps];
|
|
9991
|
+
this.#queuedOps = [];
|
|
9992
|
+
mutableOp.baseVersion = this.#version;
|
|
9993
|
+
mutableOp.ops = [...this.#inFlightOps];
|
|
9994
|
+
return { modified: false };
|
|
9995
|
+
}
|
|
9996
|
+
let ops = op.ops;
|
|
9997
|
+
for (const entry of this.#acceptedOps) {
|
|
9998
|
+
if (entry.version > op.baseVersion && entry.ops.length > 0) {
|
|
9999
|
+
ops = transformTextOperations(ops, entry.ops, "after");
|
|
10000
|
+
}
|
|
10001
|
+
}
|
|
10002
|
+
const reverse = this.#invertOperations(ops);
|
|
10003
|
+
const changes = this.#applyOperationsLocally(ops);
|
|
10004
|
+
if (this.#inFlightOpId === void 0 && ops.length > 0) {
|
|
10005
|
+
this.#inFlightOpId = nn(op.opId, "Local ops must have an opId");
|
|
10006
|
+
this.#inFlightOps = [...ops];
|
|
10007
|
+
mutableOp.baseVersion = this.#version;
|
|
10008
|
+
mutableOp.ops = [...ops];
|
|
10009
|
+
} else {
|
|
10010
|
+
this.#queuedOps.push(...ops);
|
|
10011
|
+
mutableOp.baseVersion = this.#version;
|
|
10012
|
+
mutableOp.ops = [];
|
|
10013
|
+
}
|
|
10014
|
+
if (changes.length === 0) {
|
|
10015
|
+
return { modified: false };
|
|
10016
|
+
}
|
|
10017
|
+
return {
|
|
10018
|
+
reverse,
|
|
10019
|
+
modified: {
|
|
10020
|
+
type: "LiveText",
|
|
10021
|
+
node: this,
|
|
10022
|
+
version: this.#version,
|
|
10023
|
+
updates: changes
|
|
10024
|
+
}
|
|
10025
|
+
};
|
|
10026
|
+
}
|
|
10027
|
+
/** Server acknowledgement of our in-flight op. */
|
|
10028
|
+
#applyAck(op) {
|
|
10029
|
+
const ackedVersion = op.version ?? Math.max(this.#version, op.baseVersion + 1);
|
|
10030
|
+
const predicted = this.#inFlightOps;
|
|
10031
|
+
const opId = this.#inFlightOpId;
|
|
10032
|
+
this.#confirmed = applyTextOperationsToSegments(this.#confirmed, op.ops);
|
|
10033
|
+
this.#inFlightOpId = void 0;
|
|
10034
|
+
this.#inFlightOps = [];
|
|
10035
|
+
let appliedOps = [];
|
|
10036
|
+
let result = { modified: false };
|
|
10037
|
+
if (!textOperationsEqual(op.ops, predicted)) {
|
|
10038
|
+
error2(
|
|
10039
|
+
"LiveText: acknowledgement did not match the local prediction; resynchronizing"
|
|
10040
|
+
);
|
|
10041
|
+
const rebuilt = this.#rebuildLocalFromConfirmed();
|
|
10042
|
+
appliedOps = rebuilt.appliedOps;
|
|
10043
|
+
if (rebuilt.changes.length > 0) {
|
|
10044
|
+
result = {
|
|
10045
|
+
reverse: [],
|
|
10046
|
+
modified: {
|
|
10047
|
+
type: "LiveText",
|
|
10048
|
+
node: this,
|
|
10049
|
+
version: ackedVersion,
|
|
10050
|
+
updates: rebuilt.changes
|
|
10051
|
+
}
|
|
10052
|
+
};
|
|
10053
|
+
}
|
|
10054
|
+
}
|
|
10055
|
+
this.#version = Math.max(this.#version, ackedVersion);
|
|
10056
|
+
this.#recordAccepted(ackedVersion, appliedOps, opId);
|
|
10057
|
+
this.#flushQueued();
|
|
10058
|
+
return result;
|
|
10059
|
+
}
|
|
10060
|
+
/** An accepted op from another client (or a server-fabricated fix op). */
|
|
10061
|
+
#applyRemote(op) {
|
|
10062
|
+
const version = op.version ?? this.#version + 1;
|
|
10063
|
+
this.#confirmed = applyTextOperationsToSegments(this.#confirmed, op.ops);
|
|
10064
|
+
const [overInFlight, inFlight] = transformTextOperationsX(
|
|
10065
|
+
op.ops,
|
|
10066
|
+
this.#inFlightOps,
|
|
10067
|
+
"before"
|
|
10068
|
+
);
|
|
10069
|
+
const [applied, queued] = transformTextOperationsX(
|
|
10070
|
+
overInFlight,
|
|
10071
|
+
this.#queuedOps,
|
|
10072
|
+
"before"
|
|
10073
|
+
);
|
|
10074
|
+
this.#inFlightOps = inFlight;
|
|
10075
|
+
this.#queuedOps = queued;
|
|
10076
|
+
this.#recordAccepted(version, applied, op.opId);
|
|
10077
|
+
if (applied.length === 0) {
|
|
10078
|
+
this.#version = Math.max(this.#version, version);
|
|
10079
|
+
return { modified: false };
|
|
10080
|
+
}
|
|
10081
|
+
const reverse = this.#invertOperations(applied);
|
|
10082
|
+
const changes = this.#applyOperationsLocally(applied);
|
|
10083
|
+
this.#version = Math.max(this.#version, version);
|
|
10084
|
+
return {
|
|
10085
|
+
reverse,
|
|
10086
|
+
modified: {
|
|
10087
|
+
type: "LiveText",
|
|
10088
|
+
node: this,
|
|
10089
|
+
version: this.#version,
|
|
10090
|
+
updates: changes
|
|
10091
|
+
}
|
|
10092
|
+
};
|
|
10093
|
+
}
|
|
10094
|
+
/** Send the queued ops as the next in-flight op (after an ack). */
|
|
10095
|
+
#flushQueued() {
|
|
10096
|
+
if (this.#queuedOps.length === 0 || this._pool === void 0 || this._id === void 0) {
|
|
10097
|
+
return;
|
|
10098
|
+
}
|
|
10099
|
+
const opId = this._pool.generateOpId();
|
|
10100
|
+
this.#inFlightOpId = opId;
|
|
10101
|
+
this.#inFlightOps = this.#queuedOps;
|
|
10102
|
+
this.#queuedOps = [];
|
|
10103
|
+
this._pool.dispatch(
|
|
10104
|
+
[
|
|
10105
|
+
{
|
|
10106
|
+
type: OpCode.UPDATE_TEXT,
|
|
10107
|
+
id: this._id,
|
|
10108
|
+
opId,
|
|
10109
|
+
baseVersion: this.#version,
|
|
10110
|
+
ops: [...this.#inFlightOps]
|
|
10111
|
+
}
|
|
10112
|
+
],
|
|
10113
|
+
[],
|
|
10114
|
+
/* @__PURE__ */ new Map(),
|
|
10115
|
+
// The local content was already applied (and made undoable) when the
|
|
10116
|
+
// edits happened; this is purely an outbound flush.
|
|
10117
|
+
{ clearRedoStack: false }
|
|
10118
|
+
);
|
|
10119
|
+
}
|
|
10120
|
+
/**
|
|
10121
|
+
* Rebuild the local document as confirmed ⊕ queued ops, returning the
|
|
10122
|
+
* coarse delta that was applied. Only used by defensive recovery paths.
|
|
10123
|
+
*/
|
|
10124
|
+
#rebuildLocalFromConfirmed() {
|
|
10125
|
+
const before2 = this.#segments;
|
|
10126
|
+
const after2 = applyTextOperationsToSegments(this.#confirmed, [
|
|
10127
|
+
...this.#inFlightOps,
|
|
10128
|
+
...this.#queuedOps
|
|
10129
|
+
]);
|
|
10130
|
+
if (stableStringify(segmentsToData(before2)) === stableStringify(segmentsToData(after2))) {
|
|
10131
|
+
this.#segments = after2;
|
|
10132
|
+
return { appliedOps: [], changes: [] };
|
|
10133
|
+
}
|
|
10134
|
+
const beforeText = before2.map((segment) => segment.text).join("");
|
|
10135
|
+
this.#segments = after2;
|
|
10136
|
+
this.invalidate();
|
|
10137
|
+
const appliedOps = [];
|
|
10138
|
+
const changes = [];
|
|
10139
|
+
if (beforeText.length > 0) {
|
|
10140
|
+
appliedOps.push({ type: "delete", index: 0, length: beforeText.length });
|
|
10141
|
+
changes.push({
|
|
10142
|
+
type: "delete",
|
|
10143
|
+
index: 0,
|
|
10144
|
+
length: beforeText.length,
|
|
10145
|
+
deletedText: beforeText
|
|
10146
|
+
});
|
|
10147
|
+
}
|
|
10148
|
+
let index = 0;
|
|
10149
|
+
for (const segment of after2) {
|
|
10150
|
+
appliedOps.push({
|
|
10151
|
+
type: "insert",
|
|
10152
|
+
index,
|
|
10153
|
+
text: segment.text,
|
|
10154
|
+
attributes: segment.attributes
|
|
10155
|
+
});
|
|
10156
|
+
changes.push({
|
|
10157
|
+
type: "insert",
|
|
10158
|
+
index,
|
|
10159
|
+
text: segment.text,
|
|
10160
|
+
attributes: segment.attributes
|
|
10161
|
+
});
|
|
10162
|
+
index += segment.text.length;
|
|
10163
|
+
}
|
|
10164
|
+
return { appliedOps, changes };
|
|
10165
|
+
}
|
|
10166
|
+
/**
|
|
10167
|
+
* Reconcile this node against an authoritative storage snapshot (e.g.
|
|
10168
|
+
* after a reconnect). The confirmed state and version are replaced by the
|
|
10169
|
+
* snapshot's; pending (in-flight + queued) ops are preserved on top and
|
|
10170
|
+
* will be re-sent by the offline-ops replay.
|
|
10171
|
+
*
|
|
10172
|
+
* @internal
|
|
10173
|
+
*/
|
|
10174
|
+
_resyncText(data, version) {
|
|
10175
|
+
this.#confirmed = dataToSegments(data);
|
|
10176
|
+
this.#version = version;
|
|
10177
|
+
this.#acceptedOps = [];
|
|
10178
|
+
const rebuilt = this.#rebuildLocalFromConfirmed();
|
|
10179
|
+
if (rebuilt.changes.length === 0) {
|
|
10180
|
+
return void 0;
|
|
10181
|
+
}
|
|
10182
|
+
return {
|
|
10183
|
+
type: "LiveText",
|
|
10184
|
+
node: this,
|
|
10185
|
+
version: this.#version,
|
|
10186
|
+
updates: rebuilt.changes
|
|
10187
|
+
};
|
|
10188
|
+
}
|
|
10189
|
+
/**
|
|
10190
|
+
* Called when the server rejected one of our ops. Drops all pending state
|
|
10191
|
+
* for this node (edits queued behind a rejected op cannot be trusted
|
|
10192
|
+
* either); the room follows up with a storage resync.
|
|
10193
|
+
*
|
|
10194
|
+
* @internal
|
|
10195
|
+
*/
|
|
10196
|
+
_rejectPendingOp(opId) {
|
|
10197
|
+
if (opId !== this.#inFlightOpId) {
|
|
10198
|
+
return;
|
|
10199
|
+
}
|
|
10200
|
+
this.#inFlightOpId = void 0;
|
|
10201
|
+
this.#inFlightOps = [];
|
|
10202
|
+
this.#queuedOps = [];
|
|
10203
|
+
}
|
|
10204
|
+
#recordAccepted(version, ops, opId) {
|
|
10205
|
+
if (this.#acceptedOps.some((entry) => entry.version === version)) {
|
|
10206
|
+
return;
|
|
10207
|
+
}
|
|
10208
|
+
this.#acceptedOps.push({ version, opId, ops: [...ops] });
|
|
10209
|
+
this.#acceptedOps.sort((left, right) => left.version - right.version);
|
|
10210
|
+
if (this.#acceptedOps.length > ACCEPTED_OPS_HISTORY_LIMIT) {
|
|
10211
|
+
this.#acceptedOps.splice(
|
|
10212
|
+
0,
|
|
10213
|
+
this.#acceptedOps.length - ACCEPTED_OPS_HISTORY_LIMIT
|
|
10214
|
+
);
|
|
10215
|
+
}
|
|
10216
|
+
}
|
|
10217
|
+
#applyOperationsLocally(ops) {
|
|
10218
|
+
const changes = [];
|
|
10219
|
+
for (const op of ops) {
|
|
10220
|
+
if (op.type === "insert") {
|
|
10221
|
+
this.#segments = applyInsert(
|
|
10222
|
+
this.#segments,
|
|
10223
|
+
op.index,
|
|
10224
|
+
op.text,
|
|
10225
|
+
op.attributes
|
|
10226
|
+
);
|
|
10227
|
+
changes.push({
|
|
10228
|
+
type: "insert",
|
|
10229
|
+
index: op.index,
|
|
10230
|
+
text: op.text,
|
|
10231
|
+
attributes: op.attributes
|
|
10232
|
+
});
|
|
10233
|
+
} else if (op.type === "delete") {
|
|
10234
|
+
const result = applyDelete(this.#segments, op.index, op.length);
|
|
10235
|
+
this.#segments = result.segments;
|
|
10236
|
+
changes.push({
|
|
10237
|
+
type: "delete",
|
|
10238
|
+
index: op.index,
|
|
10239
|
+
length: op.length,
|
|
10240
|
+
deletedText: result.deletedText
|
|
10241
|
+
});
|
|
10242
|
+
} else {
|
|
10243
|
+
this.#segments = applyFormat(
|
|
10244
|
+
this.#segments,
|
|
10245
|
+
op.index,
|
|
10246
|
+
op.length,
|
|
10247
|
+
op.attributes
|
|
10248
|
+
);
|
|
10249
|
+
changes.push({
|
|
10250
|
+
type: "format",
|
|
10251
|
+
index: op.index,
|
|
10252
|
+
length: op.length,
|
|
10253
|
+
attributes: op.attributes
|
|
10254
|
+
});
|
|
10255
|
+
}
|
|
10256
|
+
}
|
|
10257
|
+
this.invalidate();
|
|
10258
|
+
return changes;
|
|
10259
|
+
}
|
|
10260
|
+
#invertOperations(ops) {
|
|
10261
|
+
return [
|
|
10262
|
+
{
|
|
10263
|
+
type: OpCode.UPDATE_TEXT,
|
|
10264
|
+
id: nn(this._id),
|
|
10265
|
+
baseVersion: this.#version,
|
|
10266
|
+
ops: invertTextOperations(this.#segments, ops)
|
|
10267
|
+
}
|
|
10268
|
+
];
|
|
10269
|
+
}
|
|
10270
|
+
/** Returns the plain text content without attributes. Equivalent to joining the text from each segment in {@link LiveText.toJSON}. */
|
|
10271
|
+
toString() {
|
|
10272
|
+
return this.#segments.map((segment) => segment.text).join("");
|
|
10273
|
+
}
|
|
10274
|
+
/**
|
|
10275
|
+
* Returns a JSON-compatible snapshot of the document as a {@link LiveTextData}
|
|
10276
|
+
* array.
|
|
10277
|
+
*
|
|
10278
|
+
* @example
|
|
10279
|
+
* new LiveText([["Hello ", { bold: true }], ["world"]]).toJSON();
|
|
10280
|
+
* // [["Hello ", { bold: true }], ["world"]]
|
|
10281
|
+
*/
|
|
10282
|
+
toJSON() {
|
|
10283
|
+
return super.toJSON();
|
|
10284
|
+
}
|
|
10285
|
+
/** @internal */
|
|
10286
|
+
_toJSON() {
|
|
10287
|
+
return segmentsToData(this.#segments);
|
|
10288
|
+
}
|
|
10289
|
+
/** @internal */
|
|
10290
|
+
toTreeNode(key) {
|
|
10291
|
+
return super.toTreeNode(key);
|
|
10292
|
+
}
|
|
10293
|
+
/** @internal */
|
|
10294
|
+
_toTreeNode(key) {
|
|
10295
|
+
const nodeId = this._id ?? nanoid();
|
|
10296
|
+
const payload = this.toJSON().map(
|
|
10297
|
+
(segment, index) => ({
|
|
10298
|
+
type: "Json",
|
|
10299
|
+
id: `${nodeId}:${index}`,
|
|
10300
|
+
key: String(index),
|
|
10301
|
+
payload: segment
|
|
10302
|
+
})
|
|
10303
|
+
);
|
|
10304
|
+
payload.push({
|
|
10305
|
+
type: "Json",
|
|
10306
|
+
id: `${nodeId}:version`,
|
|
10307
|
+
key: "version",
|
|
10308
|
+
payload: this.version
|
|
10309
|
+
});
|
|
10310
|
+
return {
|
|
10311
|
+
type: "LiveText",
|
|
10312
|
+
id: nodeId,
|
|
10313
|
+
key,
|
|
10314
|
+
payload
|
|
10315
|
+
};
|
|
10316
|
+
}
|
|
10317
|
+
clone() {
|
|
10318
|
+
return new _LiveText(this.toJSON(), this.#version);
|
|
10319
|
+
}
|
|
10320
|
+
};
|
|
10321
|
+
|
|
9182
10322
|
// src/crdts/liveblocks-helpers.ts
|
|
9183
10323
|
function creationOpToLiveNode(op) {
|
|
9184
10324
|
return lsonToLiveNode(creationOpToLson(op));
|
|
@@ -9195,6 +10335,8 @@ function creationOpToLson(op) {
|
|
|
9195
10335
|
return new LiveMap();
|
|
9196
10336
|
case OpCode.CREATE_LIST:
|
|
9197
10337
|
return new LiveList([]);
|
|
10338
|
+
case OpCode.CREATE_TEXT:
|
|
10339
|
+
return new LiveText(op.data, op.version);
|
|
9198
10340
|
default:
|
|
9199
10341
|
return assertNever(op, "Unknown creation Op");
|
|
9200
10342
|
}
|
|
@@ -9227,6 +10369,8 @@ function deserialize(node, parentToChildren, pool) {
|
|
|
9227
10369
|
return LiveMap._deserialize(node, parentToChildren, pool);
|
|
9228
10370
|
} else if (isRegisterStorageNode(node)) {
|
|
9229
10371
|
return LiveRegister._deserialize(node, parentToChildren, pool);
|
|
10372
|
+
} else if (isTextStorageNode(node)) {
|
|
10373
|
+
return LiveText._deserialize(node, parentToChildren, pool);
|
|
9230
10374
|
} else if (isFileStorageNode(node)) {
|
|
9231
10375
|
return LiveFile._deserialize(node, parentToChildren, pool);
|
|
9232
10376
|
} else {
|
|
@@ -9242,6 +10386,8 @@ function deserializeToLson(node, parentToChildren, pool) {
|
|
|
9242
10386
|
return LiveMap._deserialize(node, parentToChildren, pool);
|
|
9243
10387
|
} else if (isRegisterStorageNode(node)) {
|
|
9244
10388
|
return node[1].data;
|
|
10389
|
+
} else if (isTextStorageNode(node)) {
|
|
10390
|
+
return LiveText._deserialize(node, parentToChildren, pool);
|
|
9245
10391
|
} else if (isFileStorageNode(node)) {
|
|
9246
10392
|
return LiveFile._deserialize(node, parentToChildren, pool);
|
|
9247
10393
|
} else {
|
|
@@ -9249,7 +10395,7 @@ function deserializeToLson(node, parentToChildren, pool) {
|
|
|
9249
10395
|
}
|
|
9250
10396
|
}
|
|
9251
10397
|
function isLiveStructure(value) {
|
|
9252
|
-
return isLiveList(value) || isLiveMap(value) || isLiveObject(value) || isLiveFile(value);
|
|
10398
|
+
return isLiveList(value) || isLiveMap(value) || isLiveObject(value) || isLiveText(value) || isLiveFile(value);
|
|
9253
10399
|
}
|
|
9254
10400
|
function isLiveNode(value) {
|
|
9255
10401
|
return isLiveStructure(value) || isLiveRegister(value);
|
|
@@ -9263,6 +10409,9 @@ function isLiveMap(value) {
|
|
|
9263
10409
|
function isLiveObject(value) {
|
|
9264
10410
|
return value instanceof LiveObject;
|
|
9265
10411
|
}
|
|
10412
|
+
function isLiveText(value) {
|
|
10413
|
+
return value instanceof LiveText;
|
|
10414
|
+
}
|
|
9266
10415
|
function isLiveFile(value) {
|
|
9267
10416
|
return value instanceof LiveFile;
|
|
9268
10417
|
}
|
|
@@ -9275,14 +10424,14 @@ function cloneLson(value) {
|
|
|
9275
10424
|
function liveNodeToLson(obj) {
|
|
9276
10425
|
if (obj instanceof LiveRegister) {
|
|
9277
10426
|
return obj.data;
|
|
9278
|
-
} else if (obj instanceof LiveList || obj instanceof LiveMap || obj instanceof LiveObject || obj instanceof LiveFile) {
|
|
10427
|
+
} else if (obj instanceof LiveList || obj instanceof LiveMap || obj instanceof LiveObject || obj instanceof LiveText || obj instanceof LiveFile) {
|
|
9279
10428
|
return obj;
|
|
9280
10429
|
} else {
|
|
9281
10430
|
return assertNever(obj, "Unknown AbstractCrdt");
|
|
9282
10431
|
}
|
|
9283
10432
|
}
|
|
9284
10433
|
function lsonToLiveNode(value) {
|
|
9285
|
-
if (value instanceof LiveObject || value instanceof LiveMap || value instanceof LiveList || value instanceof LiveFile) {
|
|
10434
|
+
if (value instanceof LiveObject || value instanceof LiveMap || value instanceof LiveList || value instanceof LiveText || value instanceof LiveFile) {
|
|
9286
10435
|
return value;
|
|
9287
10436
|
} else {
|
|
9288
10437
|
return new LiveRegister(value);
|
|
@@ -9344,7 +10493,26 @@ function isJsonEq(a, b) {
|
|
|
9344
10493
|
}
|
|
9345
10494
|
return true;
|
|
9346
10495
|
}
|
|
9347
|
-
function
|
|
10496
|
+
function liveTextDataToReplaceOps(before2, after2) {
|
|
10497
|
+
const ops = [];
|
|
10498
|
+
const beforeLength = before2.reduce(
|
|
10499
|
+
(length, [text]) => length + text.length,
|
|
10500
|
+
0
|
|
10501
|
+
);
|
|
10502
|
+
if (beforeLength > 0) {
|
|
10503
|
+
ops.push({ type: "delete", index: 0, length: beforeLength });
|
|
10504
|
+
}
|
|
10505
|
+
let index = 0;
|
|
10506
|
+
for (const [text, attributes] of after2) {
|
|
10507
|
+
if (text.length === 0) {
|
|
10508
|
+
continue;
|
|
10509
|
+
}
|
|
10510
|
+
ops.push({ type: "insert", index, text, attributes });
|
|
10511
|
+
index += text.length;
|
|
10512
|
+
}
|
|
10513
|
+
return ops;
|
|
10514
|
+
}
|
|
10515
|
+
function diffNodeMap(prev, next, options) {
|
|
9348
10516
|
const ops = [];
|
|
9349
10517
|
const idsToRecreate = /* @__PURE__ */ new Set();
|
|
9350
10518
|
next.forEach((nextCrdt, id) => {
|
|
@@ -9436,6 +10604,16 @@ function diffNodeMap(prev, next) {
|
|
|
9436
10604
|
parentKey: crdt.parentKey
|
|
9437
10605
|
});
|
|
9438
10606
|
break;
|
|
10607
|
+
case CrdtType.TEXT:
|
|
10608
|
+
ops.push({
|
|
10609
|
+
type: OpCode.CREATE_TEXT,
|
|
10610
|
+
id,
|
|
10611
|
+
parentId: crdt.parentId,
|
|
10612
|
+
parentKey: crdt.parentKey,
|
|
10613
|
+
data: crdt.data,
|
|
10614
|
+
version: crdt.version
|
|
10615
|
+
});
|
|
10616
|
+
break;
|
|
9439
10617
|
}
|
|
9440
10618
|
}
|
|
9441
10619
|
next.forEach((crdt, id) => {
|
|
@@ -9462,6 +10640,17 @@ function diffNodeMap(prev, next) {
|
|
|
9462
10640
|
}
|
|
9463
10641
|
}
|
|
9464
10642
|
}
|
|
10643
|
+
if (options?.includeLiveTextUpdates === true && crdt.type === CrdtType.TEXT && currentCrdt.type === CrdtType.TEXT && !isJsonEq(crdt.data, currentCrdt.data)) {
|
|
10644
|
+
ops.push({
|
|
10645
|
+
type: OpCode.UPDATE_TEXT,
|
|
10646
|
+
id,
|
|
10647
|
+
// A restore is a new edit in the current timeline. The version from
|
|
10648
|
+
// the historic snapshot describes its old timeline and must not move
|
|
10649
|
+
// this node's current version backwards.
|
|
10650
|
+
baseVersion: currentCrdt.version,
|
|
10651
|
+
ops: liveTextDataToReplaceOps(currentCrdt.data, crdt.data)
|
|
10652
|
+
});
|
|
10653
|
+
}
|
|
9465
10654
|
if (crdt.parentKey !== currentCrdt.parentKey) {
|
|
9466
10655
|
ops.push({
|
|
9467
10656
|
type: OpCode.SET_PARENT_KEY,
|
|
@@ -9502,19 +10691,43 @@ function mergeListStorageUpdates(first, second) {
|
|
|
9502
10691
|
updates: updates.concat(second.updates)
|
|
9503
10692
|
};
|
|
9504
10693
|
}
|
|
10694
|
+
function mergeTextStorageUpdates(first, second) {
|
|
10695
|
+
return {
|
|
10696
|
+
...second,
|
|
10697
|
+
updates: first.updates.concat(second.updates)
|
|
10698
|
+
};
|
|
10699
|
+
}
|
|
9505
10700
|
function mergeStorageUpdates(first, second) {
|
|
9506
10701
|
if (first === void 0) {
|
|
9507
10702
|
return second;
|
|
9508
10703
|
}
|
|
10704
|
+
let merged;
|
|
9509
10705
|
if (first.type === "LiveObject" && second.type === "LiveObject") {
|
|
9510
|
-
|
|
10706
|
+
merged = mergeObjectStorageUpdates(first, second);
|
|
9511
10707
|
} else if (first.type === "LiveMap" && second.type === "LiveMap") {
|
|
9512
|
-
|
|
10708
|
+
merged = mergeMapStorageUpdates(first, second);
|
|
9513
10709
|
} else if (first.type === "LiveList" && second.type === "LiveList") {
|
|
9514
|
-
|
|
10710
|
+
merged = mergeListStorageUpdates(first, second);
|
|
10711
|
+
} else if (first.type === "LiveText" && second.type === "LiveText") {
|
|
10712
|
+
merged = mergeTextStorageUpdates(first, second);
|
|
9515
10713
|
} else {
|
|
10714
|
+
merged = second;
|
|
10715
|
+
}
|
|
10716
|
+
const sa = first[kStorageUpdateSource];
|
|
10717
|
+
const sb = second[kStorageUpdateSource];
|
|
10718
|
+
if (sa !== void 0 || sb !== void 0) {
|
|
10719
|
+
if (sa?.origin === "remote" || sb?.origin === "remote") {
|
|
10720
|
+
merged[kStorageUpdateSource] = { origin: "remote" };
|
|
10721
|
+
} else if (sa?.via === "history" || sb?.via === "history") {
|
|
10722
|
+
const historySource = sb?.via === "history" ? sb : sa?.via === "history" ? sa : void 0;
|
|
10723
|
+
if (historySource?.via === "history") {
|
|
10724
|
+
merged[kStorageUpdateSource] = historySource;
|
|
10725
|
+
}
|
|
10726
|
+
} else {
|
|
10727
|
+
merged[kStorageUpdateSource] = { origin: "local", via: "mutation" };
|
|
10728
|
+
}
|
|
9516
10729
|
}
|
|
9517
|
-
return
|
|
10730
|
+
return merged;
|
|
9518
10731
|
}
|
|
9519
10732
|
|
|
9520
10733
|
// src/devtools/bridge.ts
|
|
@@ -10471,6 +11684,8 @@ function createRoom(options, config) {
|
|
|
10471
11684
|
activeBatch: null,
|
|
10472
11685
|
unacknowledgedOps
|
|
10473
11686
|
};
|
|
11687
|
+
let nextHistoryItemId = 0;
|
|
11688
|
+
let historyDisabled = 0;
|
|
10474
11689
|
const nodeMapBuffer = makeNodeMapBuffer();
|
|
10475
11690
|
const stopwatch = config.enableDebugLogging ? makeStopWatch() : void 0;
|
|
10476
11691
|
let lastTokenKey;
|
|
@@ -10555,7 +11770,10 @@ function createRoom(options, config) {
|
|
|
10555
11770
|
}
|
|
10556
11771
|
}
|
|
10557
11772
|
});
|
|
10558
|
-
function onDispatch(ops, reverse, storageUpdates) {
|
|
11773
|
+
function onDispatch(ops, reverse, storageUpdates, options2) {
|
|
11774
|
+
for (const value of storageUpdates.values()) {
|
|
11775
|
+
value[kStorageUpdateSource] = { origin: "local", via: "mutation" };
|
|
11776
|
+
}
|
|
10559
11777
|
if (context.activeBatch) {
|
|
10560
11778
|
for (const op of ops) {
|
|
10561
11779
|
context.activeBatch.ops.push(op);
|
|
@@ -10570,12 +11788,17 @@ function createRoom(options, config) {
|
|
|
10570
11788
|
);
|
|
10571
11789
|
}
|
|
10572
11790
|
context.activeBatch.reverseOps.pushLeft(reverse);
|
|
11791
|
+
if (options2?.clearRedoStack) {
|
|
11792
|
+
context.activeBatch.clearRedoStack = true;
|
|
11793
|
+
}
|
|
10573
11794
|
} else {
|
|
10574
11795
|
if (reverse.length > 0) {
|
|
10575
11796
|
addToUndoStack(reverse);
|
|
10576
11797
|
}
|
|
11798
|
+
if (options2?.clearRedoStack ?? ops.length > 0) {
|
|
11799
|
+
clearRedoStack();
|
|
11800
|
+
}
|
|
10577
11801
|
if (ops.length > 0) {
|
|
10578
|
-
context.redoStack.length = 0;
|
|
10579
11802
|
dispatchOps(ops);
|
|
10580
11803
|
}
|
|
10581
11804
|
notify({ storageUpdates });
|
|
@@ -10595,6 +11818,7 @@ function createRoom(options, config) {
|
|
|
10595
11818
|
others: makeEventSource(),
|
|
10596
11819
|
storageBatch: makeEventSource(),
|
|
10597
11820
|
history: makeEventSource(),
|
|
11821
|
+
privateHistory: makeEventSource(),
|
|
10598
11822
|
storageDidLoad: makeEventSource(),
|
|
10599
11823
|
storageStatus: makeEventSource(),
|
|
10600
11824
|
ydoc: makeEventSource(),
|
|
@@ -10682,12 +11906,12 @@ function createRoom(options, config) {
|
|
|
10682
11906
|
self,
|
|
10683
11907
|
(me) => me !== null ? userToTreeNode("Me", me) : null
|
|
10684
11908
|
);
|
|
10685
|
-
function diffCurrentStorageAgainst(target) {
|
|
11909
|
+
function diffCurrentStorageAgainst(target, options2) {
|
|
10686
11910
|
const current = /* @__PURE__ */ new Map();
|
|
10687
11911
|
for (const [id, crdt] of context.pool.nodes) {
|
|
10688
11912
|
current.set(id, crdt._serialize());
|
|
10689
11913
|
}
|
|
10690
|
-
return diffNodeMap(current, target);
|
|
11914
|
+
return diffNodeMap(current, target, options2);
|
|
10691
11915
|
}
|
|
10692
11916
|
function createOrUpdateRootFromMessage(nodes) {
|
|
10693
11917
|
if (nodes.size === 0) {
|
|
@@ -10695,6 +11919,23 @@ function createRoom(options, config) {
|
|
|
10695
11919
|
}
|
|
10696
11920
|
if (context.root !== void 0) {
|
|
10697
11921
|
const result = applyRemoteOps(diffCurrentStorageAgainst(nodes));
|
|
11922
|
+
for (const [id, crdt] of nodes) {
|
|
11923
|
+
if (crdt.type === CrdtType.TEXT) {
|
|
11924
|
+
const node = context.pool.nodes.get(id);
|
|
11925
|
+
if (node !== void 0 && isLiveText(node)) {
|
|
11926
|
+
const update = node._resyncText(crdt.data, crdt.version);
|
|
11927
|
+
if (update !== void 0) {
|
|
11928
|
+
result.updates.storageUpdates.set(
|
|
11929
|
+
id,
|
|
11930
|
+
mergeStorageUpdates(
|
|
11931
|
+
result.updates.storageUpdates.get(id),
|
|
11932
|
+
update
|
|
11933
|
+
)
|
|
11934
|
+
);
|
|
11935
|
+
}
|
|
11936
|
+
}
|
|
11937
|
+
}
|
|
11938
|
+
}
|
|
10698
11939
|
notify(result.updates);
|
|
10699
11940
|
} else {
|
|
10700
11941
|
context.root = LiveObject._fromItems(
|
|
@@ -10719,12 +11960,23 @@ function createRoom(options, config) {
|
|
|
10719
11960
|
}
|
|
10720
11961
|
});
|
|
10721
11962
|
}
|
|
11963
|
+
function notifyPrivateHistory(event) {
|
|
11964
|
+
if (historyDisabled > 0) return;
|
|
11965
|
+
eventHub.privateHistory.notify(event);
|
|
11966
|
+
}
|
|
11967
|
+
function clearRedoStack() {
|
|
11968
|
+
if (context.redoStack.length === 0) return;
|
|
11969
|
+
const ids = context.redoStack.map((item) => item.id);
|
|
11970
|
+
context.redoStack.length = 0;
|
|
11971
|
+
notifyPrivateHistory({ action: "discard", ids });
|
|
11972
|
+
}
|
|
10722
11973
|
function reconcileStorageWithNodes(nodes) {
|
|
10723
11974
|
if (context.root === void 0) {
|
|
10724
11975
|
throw new Error("Cannot reconcile storage before it is loaded");
|
|
10725
11976
|
}
|
|
10726
11977
|
const ops = diffCurrentStorageAgainst(
|
|
10727
|
-
new Map(nodes)
|
|
11978
|
+
new Map(nodes),
|
|
11979
|
+
{ includeLiveTextUpdates: true }
|
|
10728
11980
|
);
|
|
10729
11981
|
if (ops.length === 0) {
|
|
10730
11982
|
return;
|
|
@@ -10743,9 +11995,14 @@ function createRoom(options, config) {
|
|
|
10743
11995
|
}
|
|
10744
11996
|
function _addToRealUndoStack(frames) {
|
|
10745
11997
|
if (context.undoStack.length >= 50) {
|
|
10746
|
-
context.undoStack.shift();
|
|
11998
|
+
const evicted = context.undoStack.shift();
|
|
11999
|
+
if (evicted !== void 0) {
|
|
12000
|
+
notifyPrivateHistory({ action: "discard", ids: [evicted.id] });
|
|
12001
|
+
}
|
|
10747
12002
|
}
|
|
10748
|
-
|
|
12003
|
+
const id = nextHistoryItemId++;
|
|
12004
|
+
context.undoStack.push({ id, frames });
|
|
12005
|
+
notifyPrivateHistory({ action: "push", id });
|
|
10749
12006
|
onHistoryChange();
|
|
10750
12007
|
}
|
|
10751
12008
|
function addToUndoStack(frames) {
|
|
@@ -10783,19 +12040,55 @@ function createRoom(options, config) {
|
|
|
10783
12040
|
"Internal. Tried to get connection id but connection was never open"
|
|
10784
12041
|
);
|
|
10785
12042
|
}
|
|
10786
|
-
function applyLocalOps(frames) {
|
|
12043
|
+
function applyLocalOps(frames, localStorageUpdateSource = { origin: "local", via: "mutation" }) {
|
|
10787
12044
|
const [pframes, ops] = partition(
|
|
10788
12045
|
frames,
|
|
10789
12046
|
(f) => f.type === "presence"
|
|
10790
12047
|
);
|
|
10791
|
-
const
|
|
12048
|
+
const restoredTextIds = /* @__PURE__ */ new Map();
|
|
12049
|
+
for (const op of ops) {
|
|
12050
|
+
if (op.type === OpCode.CREATE_TEXT && op.opId === void 0 && context.pool.nodes.get(op.id) === void 0 && !restoredTextIds.has(op.id)) {
|
|
12051
|
+
restoredTextIds.set(op.id, context.pool.generateId());
|
|
12052
|
+
}
|
|
12053
|
+
}
|
|
12054
|
+
const remappedOps = restoredTextIds.size === 0 ? ops : ops.map((op) => {
|
|
12055
|
+
if (op.opId !== void 0) {
|
|
12056
|
+
return op;
|
|
12057
|
+
}
|
|
12058
|
+
const id = restoredTextIds.get(op.id);
|
|
12059
|
+
if (isCreateOp(op)) {
|
|
12060
|
+
const parentId = restoredTextIds.get(op.parentId);
|
|
12061
|
+
const deletedId = op.deletedId === void 0 ? void 0 : restoredTextIds.get(op.deletedId);
|
|
12062
|
+
if (id === void 0 && parentId === void 0 && deletedId === void 0) {
|
|
12063
|
+
return op;
|
|
12064
|
+
}
|
|
12065
|
+
if (op.type === OpCode.CREATE_TEXT && id !== void 0) {
|
|
12066
|
+
return {
|
|
12067
|
+
...op,
|
|
12068
|
+
id,
|
|
12069
|
+
version: 0,
|
|
12070
|
+
...parentId === void 0 ? {} : { parentId },
|
|
12071
|
+
...deletedId === void 0 ? {} : { deletedId }
|
|
12072
|
+
};
|
|
12073
|
+
}
|
|
12074
|
+
return {
|
|
12075
|
+
...op,
|
|
12076
|
+
...id === void 0 ? {} : { id },
|
|
12077
|
+
...parentId === void 0 ? {} : { parentId },
|
|
12078
|
+
...deletedId === void 0 ? {} : { deletedId }
|
|
12079
|
+
};
|
|
12080
|
+
}
|
|
12081
|
+
return id === void 0 ? op : { ...op, id };
|
|
12082
|
+
});
|
|
12083
|
+
const opsWithOpIds = remappedOps.map(
|
|
10792
12084
|
(op) => op.opId === void 0 ? { ...op, opId: context.pool.generateOpId() } : op
|
|
10793
12085
|
);
|
|
10794
12086
|
const { reverse, updates } = applyOps(
|
|
10795
12087
|
pframes,
|
|
10796
12088
|
opsWithOpIds,
|
|
10797
12089
|
/* isLocal */
|
|
10798
|
-
true
|
|
12090
|
+
true,
|
|
12091
|
+
localStorageUpdateSource
|
|
10799
12092
|
);
|
|
10800
12093
|
return { opsToEmit: opsWithOpIds, reverse, updates };
|
|
10801
12094
|
}
|
|
@@ -10807,7 +12100,7 @@ function createRoom(options, config) {
|
|
|
10807
12100
|
false
|
|
10808
12101
|
);
|
|
10809
12102
|
}
|
|
10810
|
-
function applyOps(pframes, ops, isLocal) {
|
|
12103
|
+
function applyOps(pframes, ops, isLocal, localStorageUpdateSource = { origin: "local", via: "mutation" }) {
|
|
10811
12104
|
const output = {
|
|
10812
12105
|
reverse: new Deque(),
|
|
10813
12106
|
storageUpdates: /* @__PURE__ */ new Map(),
|
|
@@ -10845,6 +12138,7 @@ function createRoom(options, config) {
|
|
|
10845
12138
|
}
|
|
10846
12139
|
const applyOpResult = applyOp(op, source);
|
|
10847
12140
|
if (applyOpResult.modified) {
|
|
12141
|
+
applyOpResult.modified[kStorageUpdateSource] = source === 1 /* THEIRS */ ? { origin: "remote" } : localStorageUpdateSource;
|
|
10848
12142
|
const nodeId = applyOpResult.modified.node._id;
|
|
10849
12143
|
if (!(nodeId && createdNodeIds.has(nodeId))) {
|
|
10850
12144
|
output.storageUpdates.set(
|
|
@@ -10856,7 +12150,7 @@ function createRoom(options, config) {
|
|
|
10856
12150
|
);
|
|
10857
12151
|
output.reverse.pushLeft(applyOpResult.reverse);
|
|
10858
12152
|
}
|
|
10859
|
-
if (op.type === OpCode.CREATE_LIST || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_FILE) {
|
|
12153
|
+
if (op.type === OpCode.CREATE_LIST || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_TEXT || op.type === OpCode.CREATE_FILE) {
|
|
10860
12154
|
createdNodeIds.add(op.id);
|
|
10861
12155
|
}
|
|
10862
12156
|
}
|
|
@@ -10876,6 +12170,7 @@ function createRoom(options, config) {
|
|
|
10876
12170
|
switch (op.type) {
|
|
10877
12171
|
case OpCode.DELETE_OBJECT_KEY:
|
|
10878
12172
|
case OpCode.UPDATE_OBJECT:
|
|
12173
|
+
case OpCode.UPDATE_TEXT:
|
|
10879
12174
|
case OpCode.DELETE_CRDT: {
|
|
10880
12175
|
const node = context.pool.nodes.get(op.id);
|
|
10881
12176
|
if (node === void 0) {
|
|
@@ -10900,6 +12195,7 @@ function createRoom(options, config) {
|
|
|
10900
12195
|
case OpCode.CREATE_OBJECT:
|
|
10901
12196
|
case OpCode.CREATE_LIST:
|
|
10902
12197
|
case OpCode.CREATE_MAP:
|
|
12198
|
+
case OpCode.CREATE_TEXT:
|
|
10903
12199
|
case OpCode.CREATE_FILE:
|
|
10904
12200
|
case OpCode.CREATE_REGISTER: {
|
|
10905
12201
|
if (op.parentId === void 0) {
|
|
@@ -11154,16 +12450,37 @@ function createRoom(options, config) {
|
|
|
11154
12450
|
}
|
|
11155
12451
|
break;
|
|
11156
12452
|
}
|
|
11157
|
-
// Receiving a RejectedOps message
|
|
11158
|
-
//
|
|
11159
|
-
//
|
|
11160
|
-
//
|
|
12453
|
+
// Receiving a RejectedOps message means the server refused some of
|
|
12454
|
+
// our ops, so our optimistic local state is out of sync with the
|
|
12455
|
+
// server. For LiveText ops this is a normal (if rare) situation —
|
|
12456
|
+
// e.g. a client that was offline long enough to fall outside the
|
|
12457
|
+
// server's retained history window — and we can recover: drop the
|
|
12458
|
+
// rejected pending state and re-fetch the authoritative storage
|
|
12459
|
+
// snapshot. For other ops (e.g. permission rejections), rolling back
|
|
12460
|
+
// particular Ops is hard/impossible, so we keep the old behavior of
|
|
12461
|
+
// accepting the out-of-sync reality and surfacing an error.
|
|
11161
12462
|
case ServerMsgCode.REJECT_STORAGE_OP: {
|
|
11162
12463
|
errorWithTitle(
|
|
11163
12464
|
"Storage mutation rejection error",
|
|
11164
12465
|
message.reason
|
|
11165
12466
|
);
|
|
11166
|
-
|
|
12467
|
+
let needsStorageResync = false;
|
|
12468
|
+
for (const opId of message.opIds) {
|
|
12469
|
+
const rejectedOp = context.unacknowledgedOps.get(opId);
|
|
12470
|
+
context.unacknowledgedOps.delete(opId);
|
|
12471
|
+
context.buffer.storageOperations = context.buffer.storageOperations.filter((op) => op.opId !== opId);
|
|
12472
|
+
if (rejectedOp !== void 0 && rejectedOp.type === OpCode.UPDATE_TEXT) {
|
|
12473
|
+
const node = context.pool.nodes.get(rejectedOp.id);
|
|
12474
|
+
if (node !== void 0 && isLiveText(node)) {
|
|
12475
|
+
node._rejectPendingOp(opId);
|
|
12476
|
+
needsStorageResync = true;
|
|
12477
|
+
}
|
|
12478
|
+
}
|
|
12479
|
+
}
|
|
12480
|
+
if (needsStorageResync) {
|
|
12481
|
+
refreshStorage();
|
|
12482
|
+
flushNowOrSoon();
|
|
12483
|
+
} else if (process.env.NODE_ENV !== "production") {
|
|
11167
12484
|
throw new Error(
|
|
11168
12485
|
`Storage mutations rejected by server: ${message.reason}`
|
|
11169
12486
|
);
|
|
@@ -11705,14 +13022,19 @@ function createRoom(options, config) {
|
|
|
11705
13022
|
if (context.activeBatch) {
|
|
11706
13023
|
throw new Error("undo is not allowed during a batch");
|
|
11707
13024
|
}
|
|
11708
|
-
const
|
|
11709
|
-
if (
|
|
13025
|
+
const item = context.undoStack.pop();
|
|
13026
|
+
if (item === void 0) {
|
|
11710
13027
|
return;
|
|
11711
13028
|
}
|
|
11712
13029
|
context.pausedHistory = null;
|
|
11713
|
-
const result = applyLocalOps(frames
|
|
13030
|
+
const result = applyLocalOps(item.frames, {
|
|
13031
|
+
origin: "local",
|
|
13032
|
+
via: "history",
|
|
13033
|
+
action: "undo"
|
|
13034
|
+
});
|
|
13035
|
+
context.redoStack.push({ id: item.id, frames: result.reverse });
|
|
13036
|
+
notifyPrivateHistory({ action: "undo", id: item.id });
|
|
11714
13037
|
notify(result.updates);
|
|
11715
|
-
context.redoStack.push(result.reverse);
|
|
11716
13038
|
onHistoryChange();
|
|
11717
13039
|
for (const op of result.opsToEmit) {
|
|
11718
13040
|
context.buffer.storageOperations.push(op);
|
|
@@ -11723,14 +13045,19 @@ function createRoom(options, config) {
|
|
|
11723
13045
|
if (context.activeBatch) {
|
|
11724
13046
|
throw new Error("redo is not allowed during a batch");
|
|
11725
13047
|
}
|
|
11726
|
-
const
|
|
11727
|
-
if (
|
|
13048
|
+
const item = context.redoStack.pop();
|
|
13049
|
+
if (item === void 0) {
|
|
11728
13050
|
return;
|
|
11729
13051
|
}
|
|
11730
13052
|
context.pausedHistory = null;
|
|
11731
|
-
const result = applyLocalOps(frames
|
|
13053
|
+
const result = applyLocalOps(item.frames, {
|
|
13054
|
+
origin: "local",
|
|
13055
|
+
via: "history",
|
|
13056
|
+
action: "redo"
|
|
13057
|
+
});
|
|
13058
|
+
context.undoStack.push({ id: item.id, frames: result.reverse });
|
|
13059
|
+
notifyPrivateHistory({ action: "redo", id: item.id });
|
|
11732
13060
|
notify(result.updates);
|
|
11733
|
-
context.undoStack.push(result.reverse);
|
|
11734
13061
|
onHistoryChange();
|
|
11735
13062
|
for (const op of result.opsToEmit) {
|
|
11736
13063
|
context.buffer.storageOperations.push(op);
|
|
@@ -11740,6 +13067,8 @@ function createRoom(options, config) {
|
|
|
11740
13067
|
function clear() {
|
|
11741
13068
|
context.undoStack.length = 0;
|
|
11742
13069
|
context.redoStack.length = 0;
|
|
13070
|
+
notifyPrivateHistory({ action: "clear" });
|
|
13071
|
+
onHistoryChange();
|
|
11743
13072
|
}
|
|
11744
13073
|
function batch2(callback) {
|
|
11745
13074
|
if (context.activeBatch) {
|
|
@@ -11767,8 +13096,8 @@ function createRoom(options, config) {
|
|
|
11767
13096
|
if (currentBatch.scheduleHistoryResume) {
|
|
11768
13097
|
commitPausedHistoryToUndoStack();
|
|
11769
13098
|
}
|
|
11770
|
-
if (currentBatch.ops.length > 0) {
|
|
11771
|
-
|
|
13099
|
+
if (currentBatch.ops.length > 0 || currentBatch.clearRedoStack) {
|
|
13100
|
+
clearRedoStack();
|
|
11772
13101
|
}
|
|
11773
13102
|
if (currentBatch.ops.length > 0) {
|
|
11774
13103
|
dispatchOps(currentBatch.ops);
|
|
@@ -11797,7 +13126,6 @@ function createRoom(options, config) {
|
|
|
11797
13126
|
}
|
|
11798
13127
|
commitPausedHistoryToUndoStack();
|
|
11799
13128
|
}
|
|
11800
|
-
let historyDisabled = 0;
|
|
11801
13129
|
function disableHistory(fn) {
|
|
11802
13130
|
const origUndo = context.undoStack;
|
|
11803
13131
|
const origRedo = context.redoStack;
|
|
@@ -12047,13 +13375,28 @@ function createRoom(options, config) {
|
|
|
12047
13375
|
},
|
|
12048
13376
|
// prettier-ignore
|
|
12049
13377
|
get undoStack() {
|
|
12050
|
-
return
|
|
13378
|
+
return structuredClone(
|
|
13379
|
+
context.undoStack.map((item) => ({
|
|
13380
|
+
id: item.id,
|
|
13381
|
+
frames: item.frames
|
|
13382
|
+
}))
|
|
13383
|
+
);
|
|
13384
|
+
},
|
|
13385
|
+
// prettier-ignore
|
|
13386
|
+
get redoStack() {
|
|
13387
|
+
return structuredClone(
|
|
13388
|
+
context.redoStack.map((item) => ({
|
|
13389
|
+
id: item.id,
|
|
13390
|
+
frames: item.frames
|
|
13391
|
+
}))
|
|
13392
|
+
);
|
|
12051
13393
|
},
|
|
12052
13394
|
// prettier-ignore
|
|
12053
13395
|
get nodeCount() {
|
|
12054
13396
|
return context.pool.nodes.size;
|
|
12055
13397
|
},
|
|
12056
13398
|
// prettier-ignore
|
|
13399
|
+
history: eventHub.privateHistory.observable,
|
|
12057
13400
|
getYjsProvider() {
|
|
12058
13401
|
return context.yjsProvider;
|
|
12059
13402
|
},
|
|
@@ -13084,6 +14427,12 @@ function toPlainLson(lson) {
|
|
|
13084
14427
|
liveblocksType: "LiveList",
|
|
13085
14428
|
data: [...lson].map((item) => toPlainLson(item))
|
|
13086
14429
|
};
|
|
14430
|
+
} else if (lson instanceof LiveText) {
|
|
14431
|
+
return {
|
|
14432
|
+
liveblocksType: "LiveText",
|
|
14433
|
+
data: lson.toJSON(),
|
|
14434
|
+
version: lson.version
|
|
14435
|
+
};
|
|
13087
14436
|
} else if (lson instanceof LiveFile) {
|
|
13088
14437
|
return {
|
|
13089
14438
|
liveblocksType: "LiveFile",
|
|
@@ -13271,6 +14620,7 @@ export {
|
|
|
13271
14620
|
LiveList,
|
|
13272
14621
|
LiveMap,
|
|
13273
14622
|
LiveObject,
|
|
14623
|
+
LiveText,
|
|
13274
14624
|
LiveblocksError,
|
|
13275
14625
|
MENTION_CHARACTER,
|
|
13276
14626
|
MutableSignal,
|
|
@@ -13282,6 +14632,7 @@ export {
|
|
|
13282
14632
|
SortedList,
|
|
13283
14633
|
TextEditorType,
|
|
13284
14634
|
WebsocketCloseCodes,
|
|
14635
|
+
applyLiveTextOperations,
|
|
13285
14636
|
asPos,
|
|
13286
14637
|
assert,
|
|
13287
14638
|
assertNever,
|
|
@@ -13342,8 +14693,10 @@ export {
|
|
|
13342
14693
|
isRegisterStorageNode,
|
|
13343
14694
|
isRootStorageNode,
|
|
13344
14695
|
isStartsWithOperator,
|
|
14696
|
+
isTextStorageNode,
|
|
13345
14697
|
isUrl,
|
|
13346
14698
|
kInternal,
|
|
14699
|
+
kStorageUpdateSource,
|
|
13347
14700
|
keys,
|
|
13348
14701
|
makeAbortController,
|
|
13349
14702
|
makeEventSource,
|
|
@@ -13370,6 +14723,7 @@ export {
|
|
|
13370
14723
|
stringifyCommentBody,
|
|
13371
14724
|
throwUsageError,
|
|
13372
14725
|
toPlainLson,
|
|
14726
|
+
transformTextOperations,
|
|
13373
14727
|
tryParseJson,
|
|
13374
14728
|
url,
|
|
13375
14729
|
urljoin,
|