@liveblocks/core 3.20.0-pre1 → 3.20.0-rc1

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.d.cts CHANGED
@@ -519,6 +519,7 @@ type HasOpId = {
519
519
  * acknowledge the receipt.
520
520
  */
521
521
  type ClientWireOp = Op & HasOpId;
522
+ type ClientWireCreateOp = CreateOp & HasOpId;
522
523
  /**
523
524
  * ServerWireOp: Ops sent from server → client. Three variants:
524
525
  * 1. ClientWireOp — Full echo back of our own op, confirming it was applied
@@ -864,6 +865,21 @@ type LiveListUpdate = LiveListUpdates<Lson>;
864
865
  */
865
866
  type StorageUpdate = LiveMapUpdate | LiveObjectUpdate | LiveListUpdate;
866
867
 
868
+ /**
869
+ * Read-only query surface over {@link UnacknowledgedOps}, handed to CRDTs so
870
+ * they can look up their own still-pending Create ops without being able to
871
+ * mutate the set (only the room adds/acks).
872
+ */
873
+ interface ReadonlyUnacknowledgedOps {
874
+ /** Still-unacknowledged Create ops whose `parentId` is the given one. */
875
+ getByParentId(parentId: string): Iterable<ClientWireCreateOp>;
876
+ /**
877
+ * Still-unacknowledged Create ops whose `parentId` and `parentKey` are both
878
+ * the given ones (i.e. targeting one exact position).
879
+ */
880
+ getByParentIdAndKey(parentId: string, parentKey: string): Iterable<ClientWireCreateOp>;
881
+ }
882
+
867
883
  /**
868
884
  * The managed pool is a namespace registry (i.e. a context) that "owns" all
869
885
  * the individual live nodes, ensuring each one has a unique ID, and holding on
@@ -892,6 +908,11 @@ interface ManagedPool {
892
908
  * @returns {void}
893
909
  */
894
910
  assertStorageIsWritable: () => void;
911
+ /**
912
+ * Read-only view of the client's still-unacknowledged ops (sent or
913
+ * pending-send, not yet confirmed by the server).
914
+ */
915
+ readonly unacknowledgedOps: ReadonlyUnacknowledgedOps;
895
916
  }
896
917
  type CreateManagedPoolOptions = {
897
918
  /**
@@ -911,6 +932,13 @@ type CreateManagedPoolOptions = {
911
932
  * have an effect upstream.
912
933
  */
913
934
  isStorageWritable?: () => boolean;
935
+ /**
936
+ * Read-only view of the client's still-unacknowledged ops. Used by CRDTs
937
+ * (e.g. LiveList) to know which of their optimistic mutations the server
938
+ * hasn't confirmed yet. Defaults to an empty view (e.g. server-side pools
939
+ * that dispatch-and-flush have no optimistic state to track).
940
+ */
941
+ unacknowledgedOps?: ReadonlyUnacknowledgedOps;
914
942
  };
915
943
  /**
916
944
  * @private Private API, never use this API directly.
@@ -5457,6 +5485,13 @@ declare class SortedList<T> {
5457
5485
  reposition(value: T): number;
5458
5486
  at(index: number): T | undefined;
5459
5487
  get length(): number;
5488
+ /**
5489
+ * Whether the given value is present, by identity. O(log n) plus the length
5490
+ * of any run of items that share its sort key (normally 1). Bisects on the
5491
+ * value's own key, so it only finds values sitting at their sorted position,
5492
+ * which is true for any item currently in the list.
5493
+ */
5494
+ includes(value: T): boolean;
5460
5495
  filter(predicate: (value: T) => boolean): IterableIterator<T>;
5461
5496
  findAllRight(predicate: (value: T, index: number) => unknown): IterableIterator<T>;
5462
5497
  [Symbol.iterator](): IterableIterator<T>;
package/dist/index.d.ts CHANGED
@@ -519,6 +519,7 @@ type HasOpId = {
519
519
  * acknowledge the receipt.
520
520
  */
521
521
  type ClientWireOp = Op & HasOpId;
522
+ type ClientWireCreateOp = CreateOp & HasOpId;
522
523
  /**
523
524
  * ServerWireOp: Ops sent from server → client. Three variants:
524
525
  * 1. ClientWireOp — Full echo back of our own op, confirming it was applied
@@ -864,6 +865,21 @@ type LiveListUpdate = LiveListUpdates<Lson>;
864
865
  */
865
866
  type StorageUpdate = LiveMapUpdate | LiveObjectUpdate | LiveListUpdate;
866
867
 
868
+ /**
869
+ * Read-only query surface over {@link UnacknowledgedOps}, handed to CRDTs so
870
+ * they can look up their own still-pending Create ops without being able to
871
+ * mutate the set (only the room adds/acks).
872
+ */
873
+ interface ReadonlyUnacknowledgedOps {
874
+ /** Still-unacknowledged Create ops whose `parentId` is the given one. */
875
+ getByParentId(parentId: string): Iterable<ClientWireCreateOp>;
876
+ /**
877
+ * Still-unacknowledged Create ops whose `parentId` and `parentKey` are both
878
+ * the given ones (i.e. targeting one exact position).
879
+ */
880
+ getByParentIdAndKey(parentId: string, parentKey: string): Iterable<ClientWireCreateOp>;
881
+ }
882
+
867
883
  /**
868
884
  * The managed pool is a namespace registry (i.e. a context) that "owns" all
869
885
  * the individual live nodes, ensuring each one has a unique ID, and holding on
@@ -892,6 +908,11 @@ interface ManagedPool {
892
908
  * @returns {void}
893
909
  */
894
910
  assertStorageIsWritable: () => void;
911
+ /**
912
+ * Read-only view of the client's still-unacknowledged ops (sent or
913
+ * pending-send, not yet confirmed by the server).
914
+ */
915
+ readonly unacknowledgedOps: ReadonlyUnacknowledgedOps;
895
916
  }
896
917
  type CreateManagedPoolOptions = {
897
918
  /**
@@ -911,6 +932,13 @@ type CreateManagedPoolOptions = {
911
932
  * have an effect upstream.
912
933
  */
913
934
  isStorageWritable?: () => boolean;
935
+ /**
936
+ * Read-only view of the client's still-unacknowledged ops. Used by CRDTs
937
+ * (e.g. LiveList) to know which of their optimistic mutations the server
938
+ * hasn't confirmed yet. Defaults to an empty view (e.g. server-side pools
939
+ * that dispatch-and-flush have no optimistic state to track).
940
+ */
941
+ unacknowledgedOps?: ReadonlyUnacknowledgedOps;
914
942
  };
915
943
  /**
916
944
  * @private Private API, never use this API directly.
@@ -5457,6 +5485,13 @@ declare class SortedList<T> {
5457
5485
  reposition(value: T): number;
5458
5486
  at(index: number): T | undefined;
5459
5487
  get length(): number;
5488
+ /**
5489
+ * Whether the given value is present, by identity. O(log n) plus the length
5490
+ * of any run of items that share its sort key (normally 1). Bisects on the
5491
+ * value's own key, so it only finds values sitting at their sorted position,
5492
+ * which is true for any item currently in the list.
5493
+ */
5494
+ includes(value: T): boolean;
5460
5495
  filter(predicate: (value: T) => boolean): IterableIterator<T>;
5461
5496
  findAllRight(predicate: (value: T, index: number) => unknown): IterableIterator<T>;
5462
5497
  [Symbol.iterator](): IterableIterator<T>;
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
 
7
7
  // src/version.ts
8
8
  var PKG_NAME = "@liveblocks/core";
9
- var PKG_VERSION = "3.20.0-pre1";
9
+ var PKG_VERSION = "3.20.0-rc1";
10
10
  var PKG_FORMAT = "esm";
11
11
 
12
12
  // src/dupe-detection.ts
@@ -701,6 +701,20 @@ var SortedList = class _SortedList {
701
701
  get length() {
702
702
  return this.#data.length;
703
703
  }
704
+ /**
705
+ * Whether the given value is present, by identity. O(log n) plus the length
706
+ * of any run of items that share its sort key (normally 1). Bisects on the
707
+ * value's own key, so it only finds values sitting at their sorted position,
708
+ * which is true for any item currently in the list.
709
+ */
710
+ includes(value) {
711
+ for (let i = bisectRight(this.#data, value, this.#lt) - 1; i >= 0 && !this.#lt(this.#data[i], value); i--) {
712
+ if (this.#data[i] === value) {
713
+ return true;
714
+ }
715
+ }
716
+ return false;
717
+ }
704
718
  *filter(predicate) {
705
719
  for (const item of this.#data) {
706
720
  if (predicate(item)) {
@@ -5502,6 +5516,9 @@ var OpCode = Object.freeze({
5502
5516
  function isIgnoredOp(op) {
5503
5517
  return op.type === OpCode.DELETE_CRDT && op.id === "ACK";
5504
5518
  }
5519
+ function isCreateOp(op) {
5520
+ return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST;
5521
+ }
5505
5522
 
5506
5523
  // src/protocol/StorageNode.ts
5507
5524
  var CrdtType = Object.freeze({
@@ -5759,12 +5776,95 @@ function asPos(str) {
5759
5776
  return isPos(str) ? str : convertToPos(str);
5760
5777
  }
5761
5778
 
5779
+ // src/crdts/UnacknowledgedOps.ts
5780
+ var UnacknowledgedOps = class {
5781
+ // opId -> op
5782
+ #byOpId = /* @__PURE__ */ new Map();
5783
+ // position -> (opId -> Create op)
5784
+ #createOpsByPosition = /* @__PURE__ */ new Map();
5785
+ // parentId -> (opId -> Create op)
5786
+ #createOpsByParent = /* @__PURE__ */ new Map();
5787
+ #posKey(parentId, parentKey) {
5788
+ return `${parentId}
5789
+ ${parentKey}`;
5790
+ }
5791
+ get size() {
5792
+ return this.#byOpId.size;
5793
+ }
5794
+ /**
5795
+ * Mark the given Op as still unacknowledged.
5796
+ */
5797
+ add(op) {
5798
+ this.#byOpId.set(op.opId, op);
5799
+ if (isCreateOp(op)) {
5800
+ const posKey = this.#posKey(op.parentId, op.parentKey);
5801
+ let atPosition = this.#createOpsByPosition.get(posKey);
5802
+ if (atPosition === void 0) {
5803
+ atPosition = /* @__PURE__ */ new Map();
5804
+ this.#createOpsByPosition.set(posKey, atPosition);
5805
+ }
5806
+ atPosition.set(op.opId, op);
5807
+ let inParent = this.#createOpsByParent.get(op.parentId);
5808
+ if (inParent === void 0) {
5809
+ inParent = /* @__PURE__ */ new Map();
5810
+ this.#createOpsByParent.set(op.parentId, inParent);
5811
+ }
5812
+ inParent.set(op.opId, op);
5813
+ }
5814
+ }
5815
+ /**
5816
+ * Drop the op with the given opId from the set, because the server has
5817
+ * acknowledged it (confirmed our own op, or signalled it was seen but
5818
+ * ignored).
5819
+ */
5820
+ delete(opId) {
5821
+ const op = this.#byOpId.get(opId);
5822
+ if (op === void 0) {
5823
+ return;
5824
+ }
5825
+ this.#byOpId.delete(opId);
5826
+ if (isCreateOp(op)) {
5827
+ const posKey = this.#posKey(op.parentId, op.parentKey);
5828
+ const atPosition = this.#createOpsByPosition.get(posKey);
5829
+ atPosition?.delete(opId);
5830
+ if (atPosition !== void 0 && atPosition.size === 0) {
5831
+ this.#createOpsByPosition.delete(posKey);
5832
+ }
5833
+ const inParent = this.#createOpsByParent.get(op.parentId);
5834
+ inParent?.delete(opId);
5835
+ if (inParent !== void 0 && inParent.size === 0) {
5836
+ this.#createOpsByParent.delete(op.parentId);
5837
+ }
5838
+ }
5839
+ }
5840
+ /**
5841
+ * The still-unacknowledged Create ops with the given `parentId` and
5842
+ * `parentKey` (targeting one exact position), in dispatch order. O(1) lookup.
5843
+ * Empty if none.
5844
+ */
5845
+ getByParentIdAndKey(parentId, parentKey) {
5846
+ return this.#createOpsByPosition.get(this.#posKey(parentId, parentKey))?.values() ?? [];
5847
+ }
5848
+ /**
5849
+ * The still-unacknowledged Create ops with the given `parentId` (across all
5850
+ * positions), in dispatch order. O(1) lookup. Empty if none.
5851
+ */
5852
+ getByParentId(parentId) {
5853
+ return this.#createOpsByParent.get(parentId)?.values() ?? [];
5854
+ }
5855
+ /** All still-unacknowledged ops, in dispatch order. */
5856
+ values() {
5857
+ return this.#byOpId.values();
5858
+ }
5859
+ };
5860
+
5762
5861
  // src/crdts/AbstractCrdt.ts
5763
5862
  function createManagedPool(roomId, options) {
5764
5863
  const {
5765
5864
  getCurrentConnectionId,
5766
5865
  onDispatch,
5767
- isStorageWritable = () => true
5866
+ isStorageWritable = () => true,
5867
+ unacknowledgedOps = new UnacknowledgedOps()
5768
5868
  } = options;
5769
5869
  let clock = 0;
5770
5870
  let opClock = 0;
@@ -5786,7 +5886,8 @@ function createManagedPool(roomId, options) {
5786
5886
  "Cannot write to storage with a read only user, please ensure the user has write permissions"
5787
5887
  );
5788
5888
  }
5789
- }
5889
+ },
5890
+ unacknowledgedOps
5790
5891
  };
5791
5892
  }
5792
5893
  function crdtAsLiveNode(value) {
@@ -6068,11 +6169,9 @@ function childNodeLt(a, b) {
6068
6169
  var LiveList = class _LiveList extends AbstractCrdt {
6069
6170
  #items;
6070
6171
  #implicitlyDeletedItems;
6071
- #unacknowledgedSets;
6072
6172
  constructor(items) {
6073
6173
  super();
6074
6174
  this.#implicitlyDeletedItems = /* @__PURE__ */ new WeakSet();
6075
- this.#unacknowledgedSets = /* @__PURE__ */ new Map();
6076
6175
  const nodes = [];
6077
6176
  let lastPos;
6078
6177
  for (const item of items) {
@@ -6102,12 +6201,13 @@ var LiveList = class _LiveList extends AbstractCrdt {
6102
6201
  }
6103
6202
  /**
6104
6203
  * @internal
6105
- * This function assumes that the resulting ops will be sent to the server if they have an 'opId'
6106
- * so we mutate _unacknowledgedSets to avoid potential flickering
6107
- * https://github.com/liveblocks/liveblocks/pull/1177
6204
+ * Serializes this list (and its children) into Create ops. Each child's
6205
+ * create is tagged with the "set" intent (in the loop below) so that a list
6206
+ * created and immediately mutated doesn't transiently re-show its initial
6207
+ * items (flicker, https://github.com/liveblocks/liveblocks/pull/1177).
6108
6208
  *
6109
- * This is quite unintuitive and should disappear as soon as
6110
- * we introduce an explicit LiveList.Set operation
6209
+ * This is quite unintuitive and should disappear as soon as we introduce an
6210
+ * explicit LiveList.Set operation.
6111
6211
  */
6112
6212
  _toOps(parentId, parentKey) {
6113
6213
  if (this._id === void 0) {
@@ -6123,7 +6223,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6123
6223
  ops.push(op);
6124
6224
  for (const item of this.#items) {
6125
6225
  const parentKey2 = item._getParentKeyOrThrow();
6126
- const childOps = addIntentToOpTree(
6226
+ const childOps = addIntentToRootOp(
6127
6227
  item._toOps(this._id, parentKey2),
6128
6228
  "set"
6129
6229
  );
@@ -6167,6 +6267,28 @@ var LiveList = class _LiveList extends AbstractCrdt {
6167
6267
  (item) => item._getParentKeyOrThrow() === position
6168
6268
  );
6169
6269
  }
6270
+ /**
6271
+ * The opId of this list's still-unacknowledged "set" op at the given position,
6272
+ * or undefined if none. Derived from the room's unacknowledgedOps (the single
6273
+ * source of truth) rather than tracked in a per-instance map. The pool's
6274
+ * position index already scopes to this list's (parentId, position); the last
6275
+ * match wins, matching the original last-write-wins map semantics.
6276
+ */
6277
+ #unacknowledgedSetOpIdAt(position) {
6278
+ if (this._pool === void 0 || this._id === void 0) {
6279
+ return void 0;
6280
+ }
6281
+ let opId;
6282
+ for (const op of this._pool.unacknowledgedOps.getByParentIdAndKey(
6283
+ this._id,
6284
+ position
6285
+ )) {
6286
+ if (op.intent === "set") {
6287
+ opId = op.opId;
6288
+ }
6289
+ }
6290
+ return opId;
6291
+ }
6170
6292
  /** @internal */
6171
6293
  _attach(id, pool) {
6172
6294
  super._attach(id, pool);
@@ -6247,13 +6369,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6247
6369
  if (deletedDelta) {
6248
6370
  delta.push(deletedDelta);
6249
6371
  }
6250
- const unacknowledgedOpId = this.#unacknowledgedSets.get(op.parentKey);
6251
- if (unacknowledgedOpId !== void 0) {
6252
- if (unacknowledgedOpId !== op.opId) {
6253
- return delta.length === 0 ? { modified: false } : { modified: makeUpdate(this, delta), reverse: [] };
6254
- } else {
6255
- this.#unacknowledgedSets.delete(op.parentKey);
6256
- }
6372
+ const unacknowledgedOpId = this.#unacknowledgedSetOpIdAt(op.parentKey);
6373
+ if (unacknowledgedOpId !== void 0 && unacknowledgedOpId !== op.opId) {
6374
+ return delta.length === 0 ? { modified: false } : { modified: makeUpdate(this, delta), reverse: [] };
6257
6375
  }
6258
6376
  const indexOfItemWithSamePosition = this._indexOfPosition(op.parentKey);
6259
6377
  const existingItem = this.#items.find((item) => item._id === op.id);
@@ -6334,7 +6452,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6334
6452
  }
6335
6453
  return result.modified.updates[0];
6336
6454
  }
6337
- #applyRemoteInsert(op) {
6455
+ #applyRemoteInsert(op, fromSnapshot) {
6338
6456
  if (this._pool === void 0) {
6339
6457
  throw new Error("Can't attach child if managed pool is not present");
6340
6458
  }
@@ -6344,11 +6462,82 @@ var LiveList = class _LiveList extends AbstractCrdt {
6344
6462
  this.#shiftItemPosition(existingItemIndex, key);
6345
6463
  }
6346
6464
  const { newItem, newIndex } = this.#createAttachItemAndSort(op, key);
6465
+ const bumpDeltas = fromSnapshot ? [] : this.#bumpUnackedPushesAbove(key);
6347
6466
  return {
6348
- modified: makeUpdate(this, [insertDelta(newIndex, newItem)]),
6467
+ modified: makeUpdate(this, [
6468
+ insertDelta(newIndex, newItem),
6469
+ ...bumpDeltas
6470
+ ]),
6349
6471
  reverse: []
6350
6472
  };
6351
6473
  }
6474
+ /**
6475
+ * This list's own still-unacknowledged pushed items (their `intent: "push"`
6476
+ * Create op is still pending in the room's unacknowledgedOps). Derived from
6477
+ * the single source of truth, so an item drops out the instant its op is
6478
+ * acked, with no per-instance membership to leak. Yielded in push order.
6479
+ *
6480
+ * Restricted to items currently in `#items`: a pushed node whose op is still
6481
+ * pending may have been pulled out of the list (e.g. implicitly deleted by a
6482
+ * remote set, or removed by an undo) while still living in the pool, and such
6483
+ * a node must not be repositioned.
6484
+ */
6485
+ *#unackedPushNodes() {
6486
+ if (this._pool === void 0 || this._id === void 0) {
6487
+ return;
6488
+ }
6489
+ for (const op of this._pool.unacknowledgedOps.getByParentId(this._id)) {
6490
+ if (op.intent !== "push") {
6491
+ continue;
6492
+ }
6493
+ const node = this._pool.getNode(op.id);
6494
+ if (node !== void 0 && this.#items.includes(node)) {
6495
+ yield node;
6496
+ }
6497
+ }
6498
+ }
6499
+ /**
6500
+ * Optimistic no-flip for pushed items. When a remote op lands at or below my
6501
+ * still-unacked pushed items, those items must end up *after* it: FIFO plus
6502
+ * the room's serial processing guarantee the remote was processed first, so
6503
+ * my unacked pushes belong behind it. Re-chain the whole unacked-push block,
6504
+ * in push order, to sit after the highest confirmed sibling, so it keeps
6505
+ * rendering as a contiguous tail instead of getting interleaved. Local-only;
6506
+ * the real acks overwrite these keys with the (identical) server keys.
6507
+ */
6508
+ #bumpUnackedPushesAbove(remoteKey) {
6509
+ const pending = new Set(this.#unackedPushNodes());
6510
+ if (pending.size === 0) {
6511
+ return [];
6512
+ }
6513
+ let minPending;
6514
+ for (const node of pending) {
6515
+ const pos = node._parentPos;
6516
+ if (minPending === void 0 || pos < minPending) {
6517
+ minPending = pos;
6518
+ }
6519
+ }
6520
+ if (remoteKey < nn(minPending)) {
6521
+ return [];
6522
+ }
6523
+ let base;
6524
+ for (const item of this.#items) {
6525
+ if (!pending.has(item)) {
6526
+ base = item._parentPos;
6527
+ }
6528
+ }
6529
+ const deltas = [];
6530
+ for (const node of pending) {
6531
+ const previousIndex = this.#items.findIndex((item) => item === node);
6532
+ base = makePosition(base);
6533
+ this.#updateItemPosition(node, base);
6534
+ const index = this.#items.findIndex((item) => item === node);
6535
+ if (index !== previousIndex) {
6536
+ deltas.push(moveDelta(previousIndex, index, node));
6537
+ }
6538
+ }
6539
+ return deltas;
6540
+ }
6352
6541
  #applyInsertAck(op) {
6353
6542
  const existingItem = this.#items.find((item) => item._id === op.id);
6354
6543
  const key = asPos(op.parentKey);
@@ -6429,7 +6618,6 @@ var LiveList = class _LiveList extends AbstractCrdt {
6429
6618
  if (this._pool?.getNode(id) !== void 0) {
6430
6619
  return { modified: false };
6431
6620
  }
6432
- this.#unacknowledgedSets.set(key, nn(op.opId));
6433
6621
  const indexOfItemWithSameKey = this._indexOfPosition(key);
6434
6622
  child._attach(id, nn(this._pool));
6435
6623
  child._setParentLink(this, key);
@@ -6439,7 +6627,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6439
6627
  existingItem._detach();
6440
6628
  this.#items.remove(existingItem);
6441
6629
  this.#items.add(child);
6442
- const reverse = addIntentToOpTree(
6630
+ const reverse = addIntentToRootOp(
6443
6631
  existingItem._toOps(nn(this._id), key),
6444
6632
  "set",
6445
6633
  op.id
@@ -6466,7 +6654,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6466
6654
  }
6467
6655
  }
6468
6656
  /** @internal */
6469
- _attachChild(op, source) {
6657
+ _attachChild(op, source, fromSnapshot = false) {
6470
6658
  if (this._pool === void 0) {
6471
6659
  throw new Error("Can't attach child if managed pool is not present");
6472
6660
  }
@@ -6481,7 +6669,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6481
6669
  }
6482
6670
  } else {
6483
6671
  if (source === 1 /* THEIRS */) {
6484
- result = this.#applyRemoteInsert(op);
6672
+ result = this.#applyRemoteInsert(op, fromSnapshot);
6485
6673
  } else if (source === 2 /* OURS */) {
6486
6674
  result = this.#applyInsertAck(op);
6487
6675
  } else {
@@ -6718,7 +6906,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6718
6906
  value._attach(id, this._pool);
6719
6907
  const ops = value._toOpsWithOpId(this._id, position, this._pool);
6720
6908
  this._pool.dispatch(
6721
- intent === "push" ? addIntentToOpTree(ops, "push") : ops,
6909
+ intent === "push" ? addIntentToRootOp(ops, "push") : ops,
6722
6910
  [{ type: OpCode.DELETE_CRDT, id }],
6723
6911
  /* @__PURE__ */ new Map([
6724
6912
  [this._id, makeUpdate(this, [insertDelta(index, value)])]
@@ -6876,13 +7064,12 @@ var LiveList = class _LiveList extends AbstractCrdt {
6876
7064
  value._attach(id, this._pool);
6877
7065
  const storageUpdates = /* @__PURE__ */ new Map();
6878
7066
  storageUpdates.set(this._id, makeUpdate(this, [setDelta(index, value)]));
6879
- const ops = addIntentToOpTree(
7067
+ const ops = addIntentToRootOp(
6880
7068
  value._toOpsWithOpId(this._id, position, this._pool),
6881
7069
  "set",
6882
7070
  existingId
6883
7071
  );
6884
- this.#unacknowledgedSets.set(position, nn(ops[0].opId));
6885
- const reverseOps = addIntentToOpTree(
7072
+ const reverseOps = addIntentToRootOp(
6886
7073
  existingItem._toOps(this._id, position),
6887
7074
  "set",
6888
7075
  id
@@ -7085,7 +7272,7 @@ function moveDelta(previousIndex, index, item) {
7085
7272
  previousIndex
7086
7273
  };
7087
7274
  }
7088
- function addIntentToOpTree(ops, intent, deletedId) {
7275
+ function addIntentToRootOp(ops, intent, deletedId) {
7089
7276
  return ops.map((op, index) => {
7090
7277
  if (index === 0) {
7091
7278
  const firstOp = op;
@@ -8329,6 +8516,31 @@ function lsonToLiveNode(value) {
8329
8516
  return new LiveRegister(value);
8330
8517
  }
8331
8518
  }
8519
+ function dumpPool(pool) {
8520
+ const rows = Array.from(pool.nodes.values(), (node) => {
8521
+ const parent = node.parent;
8522
+ const parentId = parent.type === "HasParent" ? parent.node._id ?? "?" : parent.type === "Orphaned" ? "<orphaned>" : "-";
8523
+ let value;
8524
+ if (node instanceof LiveRegister) {
8525
+ value = stringifyOrLog(node.data);
8526
+ } else if (node instanceof LiveList) {
8527
+ value = "<LiveList>";
8528
+ } else if (node instanceof LiveMap) {
8529
+ value = "<LiveMap>";
8530
+ } else {
8531
+ value = "<LiveObject>";
8532
+ }
8533
+ return { id: nn(node._id), parentId, key: node._parentKey ?? "", value };
8534
+ });
8535
+ rows.sort((a, b) => {
8536
+ if (a.parentId !== b.parentId) return a.parentId < b.parentId ? -1 : 1;
8537
+ if (a.key !== b.key) return a.key < b.key ? -1 : 1;
8538
+ return 0;
8539
+ });
8540
+ return rows.map(
8541
+ (r) => ` ${r.id} parent=${r.parentId} key=${r.key || "\u2014"} ${r.value}`
8542
+ ).join("\n");
8543
+ }
8332
8544
  function getTreesDiffOperations(currentItems, newItems) {
8333
8545
  const ops = [];
8334
8546
  currentItems.forEach((_, id) => {
@@ -9338,6 +9550,7 @@ function createRoom(options, config) {
9338
9550
  delegates,
9339
9551
  config.enableDebugLogging
9340
9552
  );
9553
+ const unacknowledgedOps = new UnacknowledgedOps();
9341
9554
  const context = {
9342
9555
  buffer: {
9343
9556
  flushTimerID: void 0,
@@ -9365,14 +9578,15 @@ function createRoom(options, config) {
9365
9578
  pool: createManagedPool(roomId, {
9366
9579
  getCurrentConnectionId,
9367
9580
  onDispatch,
9368
- isStorageWritable
9581
+ isStorageWritable,
9582
+ unacknowledgedOps
9369
9583
  }),
9370
9584
  root: void 0,
9371
9585
  undoStack: [],
9372
9586
  redoStack: [],
9373
9587
  pausedHistory: null,
9374
9588
  activeBatch: null,
9375
- unacknowledgedOps: /* @__PURE__ */ new Map()
9589
+ unacknowledgedOps
9376
9590
  };
9377
9591
  const nodeMapBuffer = makeNodeMapBuffer();
9378
9592
  const stopwatch = config.enableDebugLogging ? makeStopWatch() : void 0;
@@ -9580,7 +9794,11 @@ function createRoom(options, config) {
9580
9794
  currentItems.set(id, crdt._serialize());
9581
9795
  }
9582
9796
  const ops = getTreesDiffOperations(currentItems, nodes);
9583
- const result = applyRemoteOps(ops);
9797
+ const result = applyRemoteOps(
9798
+ ops,
9799
+ /* fromSnapshot */
9800
+ true
9801
+ );
9584
9802
  notify(result.updates);
9585
9803
  } else {
9586
9804
  context.root = LiveObject._fromItems(
@@ -9662,15 +9880,16 @@ function createRoom(options, config) {
9662
9880
  );
9663
9881
  return { opsToEmit: opsWithOpIds, reverse, updates };
9664
9882
  }
9665
- function applyRemoteOps(ops) {
9883
+ function applyRemoteOps(ops, fromSnapshot = false) {
9666
9884
  return applyOps(
9667
9885
  [],
9668
9886
  ops,
9669
9887
  /* isLocal */
9670
- false
9888
+ false,
9889
+ fromSnapshot
9671
9890
  );
9672
9891
  }
9673
- function applyOps(pframes, ops, isLocal) {
9892
+ function applyOps(pframes, ops, isLocal, fromSnapshot = false) {
9674
9893
  const output = {
9675
9894
  reverse: new Deque(),
9676
9895
  storageUpdates: /* @__PURE__ */ new Map(),
@@ -9706,7 +9925,7 @@ function createRoom(options, config) {
9706
9925
  } else {
9707
9926
  source = 1 /* THEIRS */;
9708
9927
  }
9709
- const applyOpResult = applyOp(op, source);
9928
+ const applyOpResult = applyOp(op, source, fromSnapshot);
9710
9929
  if (applyOpResult.modified) {
9711
9930
  const nodeId = applyOpResult.modified.node._id;
9712
9931
  if (!(nodeId && createdNodeIds.has(nodeId))) {
@@ -9732,7 +9951,7 @@ function createRoom(options, config) {
9732
9951
  }
9733
9952
  };
9734
9953
  }
9735
- function applyOp(op, source) {
9954
+ function applyOp(op, source, fromSnapshot = false) {
9736
9955
  if (isIgnoredOp(op)) {
9737
9956
  return { modified: false };
9738
9957
  }
@@ -9771,7 +9990,7 @@ function createRoom(options, config) {
9771
9990
  if (parentNode === void 0) {
9772
9991
  return { modified: false };
9773
9992
  }
9774
- return parentNode._attachChild(op, source);
9993
+ return parentNode._attachChild(op, source, fromSnapshot);
9775
9994
  }
9776
9995
  }
9777
9996
  }
@@ -9911,12 +10130,11 @@ function createRoom(options, config) {
9911
10130
  }
9912
10131
  }
9913
10132
  function applyAndSendOfflineOps(unackedOps) {
9914
- if (unackedOps.size === 0) {
10133
+ if (unackedOps.length === 0) {
9915
10134
  return;
9916
10135
  }
9917
10136
  const messages = [];
9918
- const inOps = Array.from(unackedOps.values());
9919
- const result = applyLocalOps(inOps);
10137
+ const result = applyLocalOps(unackedOps);
9920
10138
  messages.push({
9921
10139
  type: ClientMsgCode.UPDATE_STORAGE,
9922
10140
  ops: result.opsToEmit
@@ -10140,7 +10358,7 @@ function createRoom(options, config) {
10140
10358
  const storageOps = context.buffer.storageOperations;
10141
10359
  if (storageOps.length > 0) {
10142
10360
  for (const op of storageOps) {
10143
- context.unacknowledgedOps.set(op.opId, op);
10361
+ context.unacknowledgedOps.add(op);
10144
10362
  }
10145
10363
  notifyStorageStatus();
10146
10364
  }
@@ -10367,9 +10585,9 @@ function createRoom(options, config) {
10367
10585
  }
10368
10586
  }
10369
10587
  function processInitialStorage(nodes) {
10370
- const unacknowledgedOps = new Map(context.unacknowledgedOps);
10588
+ const unacknowledgedOps2 = [...context.unacknowledgedOps.values()];
10371
10589
  createOrUpdateRootFromMessage(nodes);
10372
- applyAndSendOfflineOps(unacknowledgedOps);
10590
+ applyAndSendOfflineOps(unacknowledgedOps2);
10373
10591
  _resolveStoragePromise?.();
10374
10592
  notifyStorageStatus();
10375
10593
  eventHub.storageDidLoad.notify();
@@ -10958,6 +11176,11 @@ function createRoom(options, config) {
10958
11176
  connect: () => managedSocket.connect(),
10959
11177
  reconnect: () => managedSocket.reconnect(),
10960
11178
  disconnect: () => managedSocket.disconnect(),
11179
+ _dump: () => {
11180
+ const n = context.pool.nodes.size;
11181
+ return `Room "${roomId}" (${n} node${n === 1 ? "" : "s"}):
11182
+ ${dumpPool(context.pool)}`;
11183
+ },
10961
11184
  destroy: () => {
10962
11185
  pendingFeedsRequests.forEach(
10963
11186
  (request) => request.reject(new Error("Room destroyed"))
@@ -11465,6 +11688,7 @@ function createClient(options) {
11465
11688
  {
11466
11689
  enterRoom,
11467
11690
  getRoom,
11691
+ _dump: () => Array.from(roomsById.values(), ({ room }) => room._dump()).join("\n\n"),
11468
11692
  logout,
11469
11693
  // Public inbox notifications API
11470
11694
  getInboxNotifications: httpClient.getInboxNotifications,