@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.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.0";
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
- // TODO: 9 and 10 are used by LiveText, wait until it's merged.
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
- // TODO: 4 is used by LiveText, wait until it's merged.
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 diffNodeMap(prev, next) {
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
- return mergeObjectStorageUpdates(first, second);
10706
+ merged = mergeObjectStorageUpdates(first, second);
9511
10707
  } else if (first.type === "LiveMap" && second.type === "LiveMap") {
9512
- return mergeMapStorageUpdates(first, second);
10708
+ merged = mergeMapStorageUpdates(first, second);
9513
10709
  } else if (first.type === "LiveList" && second.type === "LiveList") {
9514
- return mergeListStorageUpdates(first, second);
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 second;
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
- context.undoStack.push(frames);
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 opsWithOpIds = ops.map(
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 in the client means that the server is no
11158
- // longer in sync with the client. Trying to synchronize the client again by
11159
- // rolling back particular Ops may be hard/impossible. It's fine to not try and
11160
- // accept the out-of-sync reality and throw an error.
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
- if (process.env.NODE_ENV !== "production") {
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 frames = context.undoStack.pop();
11709
- if (frames === void 0) {
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 frames = context.redoStack.pop();
11727
- if (frames === void 0) {
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
- context.redoStack.length = 0;
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 deepClone(context.undoStack);
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,