@liveblocks/core 3.19.3 → 3.19.5-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.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.19.3";
9
+ var PKG_VERSION = "3.19.5-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,9 +6223,9 @@ 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 = HACK_addIntentAndDeletedIdToOperation(
6226
+ const childOps = addIntentToRootOp(
6127
6227
  item._toOps(this._id, parentKey2),
6128
- void 0
6228
+ "set"
6129
6229
  );
6130
6230
  for (const childOp of childOps) {
6131
6231
  ops.push(childOp);
@@ -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,8 +6627,9 @@ 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 = HACK_addIntentAndDeletedIdToOperation(
6630
+ const reverse = addIntentToRootOp(
6443
6631
  existingItem._toOps(nn(this._id), key),
6632
+ "set",
6444
6633
  op.id
6445
6634
  );
6446
6635
  const delta = [setDelta(indexOfItemWithSameKey, child)];
@@ -6465,7 +6654,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6465
6654
  }
6466
6655
  }
6467
6656
  /** @internal */
6468
- _attachChild(op, source) {
6657
+ _attachChild(op, source, fromSnapshot = false) {
6469
6658
  if (this._pool === void 0) {
6470
6659
  throw new Error("Can't attach child if managed pool is not present");
6471
6660
  }
@@ -6480,7 +6669,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6480
6669
  }
6481
6670
  } else {
6482
6671
  if (source === 1 /* THEIRS */) {
6483
- result = this.#applyRemoteInsert(op);
6672
+ result = this.#applyRemoteInsert(op, fromSnapshot);
6484
6673
  } else if (source === 2 /* OURS */) {
6485
6674
  result = this.#applyInsertAck(op);
6486
6675
  } else {
@@ -6683,8 +6872,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6683
6872
  * @param element The element to add to the end of the LiveList.
6684
6873
  */
6685
6874
  push(element) {
6686
- this._pool?.assertStorageIsWritable();
6687
- return this.insert(element, this.length);
6875
+ return this.#injectAt(element, this.length, "push");
6688
6876
  }
6689
6877
  /**
6690
6878
  * Inserts one element at a specified index.
@@ -6692,6 +6880,15 @@ var LiveList = class _LiveList extends AbstractCrdt {
6692
6880
  * @param index The index at which you want to insert the element.
6693
6881
  */
6694
6882
  insert(element, index) {
6883
+ return this.#injectAt(element, index, "insert");
6884
+ }
6885
+ /**
6886
+ * Shared implementation of `insert` and `push`. A `"push"` intent leaves the
6887
+ * client-computed position untouched (so optimistic rendering is unchanged),
6888
+ * but tags the Op so the server appends it to the true end of the list
6889
+ * instead of resolving its position against the client's stale view.
6890
+ */
6891
+ #injectAt(element, index, intent) {
6695
6892
  this._pool?.assertStorageIsWritable();
6696
6893
  if (index < 0 || index > this.#items.length) {
6697
6894
  throw new Error(
@@ -6707,8 +6904,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6707
6904
  if (this._pool && this._id) {
6708
6905
  const id = this._pool.generateId();
6709
6906
  value._attach(id, this._pool);
6907
+ const ops = value._toOpsWithOpId(this._id, position, this._pool);
6710
6908
  this._pool.dispatch(
6711
- value._toOpsWithOpId(this._id, position, this._pool),
6909
+ intent === "push" ? addIntentToRootOp(ops, "push") : ops,
6712
6910
  [{ type: OpCode.DELETE_CRDT, id }],
6713
6911
  /* @__PURE__ */ new Map([
6714
6912
  [this._id, makeUpdate(this, [insertDelta(index, value)])]
@@ -6866,13 +7064,14 @@ var LiveList = class _LiveList extends AbstractCrdt {
6866
7064
  value._attach(id, this._pool);
6867
7065
  const storageUpdates = /* @__PURE__ */ new Map();
6868
7066
  storageUpdates.set(this._id, makeUpdate(this, [setDelta(index, value)]));
6869
- const ops = HACK_addIntentAndDeletedIdToOperation(
7067
+ const ops = addIntentToRootOp(
6870
7068
  value._toOpsWithOpId(this._id, position, this._pool),
7069
+ "set",
6871
7070
  existingId
6872
7071
  );
6873
- this.#unacknowledgedSets.set(position, nn(ops[0].opId));
6874
- const reverseOps = HACK_addIntentAndDeletedIdToOperation(
7072
+ const reverseOps = addIntentToRootOp(
6875
7073
  existingItem._toOps(this._id, position),
7074
+ "set",
6876
7075
  id
6877
7076
  );
6878
7077
  this._pool.dispatch(ops, reverseOps, storageUpdates);
@@ -7073,15 +7272,11 @@ function moveDelta(previousIndex, index, item) {
7073
7272
  previousIndex
7074
7273
  };
7075
7274
  }
7076
- function HACK_addIntentAndDeletedIdToOperation(ops, deletedId) {
7275
+ function addIntentToRootOp(ops, intent, deletedId) {
7077
7276
  return ops.map((op, index) => {
7078
7277
  if (index === 0) {
7079
7278
  const firstOp = op;
7080
- return {
7081
- ...firstOp,
7082
- intent: "set",
7083
- deletedId
7084
- };
7279
+ return { ...firstOp, intent, deletedId };
7085
7280
  } else {
7086
7281
  return op;
7087
7282
  }
@@ -8321,6 +8516,31 @@ function lsonToLiveNode(value) {
8321
8516
  return new LiveRegister(value);
8322
8517
  }
8323
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
+ }
8324
8544
  function getTreesDiffOperations(currentItems, newItems) {
8325
8545
  const ops = [];
8326
8546
  currentItems.forEach((_, id) => {
@@ -9330,6 +9550,7 @@ function createRoom(options, config) {
9330
9550
  delegates,
9331
9551
  config.enableDebugLogging
9332
9552
  );
9553
+ const unacknowledgedOps = new UnacknowledgedOps();
9333
9554
  const context = {
9334
9555
  buffer: {
9335
9556
  flushTimerID: void 0,
@@ -9357,14 +9578,15 @@ function createRoom(options, config) {
9357
9578
  pool: createManagedPool(roomId, {
9358
9579
  getCurrentConnectionId,
9359
9580
  onDispatch,
9360
- isStorageWritable
9581
+ isStorageWritable,
9582
+ unacknowledgedOps
9361
9583
  }),
9362
9584
  root: void 0,
9363
9585
  undoStack: [],
9364
9586
  redoStack: [],
9365
9587
  pausedHistory: null,
9366
9588
  activeBatch: null,
9367
- unacknowledgedOps: /* @__PURE__ */ new Map()
9589
+ unacknowledgedOps
9368
9590
  };
9369
9591
  const nodeMapBuffer = makeNodeMapBuffer();
9370
9592
  const stopwatch = config.enableDebugLogging ? makeStopWatch() : void 0;
@@ -9572,7 +9794,11 @@ function createRoom(options, config) {
9572
9794
  currentItems.set(id, crdt._serialize());
9573
9795
  }
9574
9796
  const ops = getTreesDiffOperations(currentItems, nodes);
9575
- const result = applyRemoteOps(ops);
9797
+ const result = applyRemoteOps(
9798
+ ops,
9799
+ /* fromSnapshot */
9800
+ true
9801
+ );
9576
9802
  notify(result.updates);
9577
9803
  } else {
9578
9804
  context.root = LiveObject._fromItems(
@@ -9654,15 +9880,16 @@ function createRoom(options, config) {
9654
9880
  );
9655
9881
  return { opsToEmit: opsWithOpIds, reverse, updates };
9656
9882
  }
9657
- function applyRemoteOps(ops) {
9883
+ function applyRemoteOps(ops, fromSnapshot = false) {
9658
9884
  return applyOps(
9659
9885
  [],
9660
9886
  ops,
9661
9887
  /* isLocal */
9662
- false
9888
+ false,
9889
+ fromSnapshot
9663
9890
  );
9664
9891
  }
9665
- function applyOps(pframes, ops, isLocal) {
9892
+ function applyOps(pframes, ops, isLocal, fromSnapshot = false) {
9666
9893
  const output = {
9667
9894
  reverse: new Deque(),
9668
9895
  storageUpdates: /* @__PURE__ */ new Map(),
@@ -9698,7 +9925,7 @@ function createRoom(options, config) {
9698
9925
  } else {
9699
9926
  source = 1 /* THEIRS */;
9700
9927
  }
9701
- const applyOpResult = applyOp(op, source);
9928
+ const applyOpResult = applyOp(op, source, fromSnapshot);
9702
9929
  if (applyOpResult.modified) {
9703
9930
  const nodeId = applyOpResult.modified.node._id;
9704
9931
  if (!(nodeId && createdNodeIds.has(nodeId))) {
@@ -9724,7 +9951,7 @@ function createRoom(options, config) {
9724
9951
  }
9725
9952
  };
9726
9953
  }
9727
- function applyOp(op, source) {
9954
+ function applyOp(op, source, fromSnapshot = false) {
9728
9955
  if (isIgnoredOp(op)) {
9729
9956
  return { modified: false };
9730
9957
  }
@@ -9763,7 +9990,7 @@ function createRoom(options, config) {
9763
9990
  if (parentNode === void 0) {
9764
9991
  return { modified: false };
9765
9992
  }
9766
- return parentNode._attachChild(op, source);
9993
+ return parentNode._attachChild(op, source, fromSnapshot);
9767
9994
  }
9768
9995
  }
9769
9996
  }
@@ -9903,12 +10130,11 @@ function createRoom(options, config) {
9903
10130
  }
9904
10131
  }
9905
10132
  function applyAndSendOfflineOps(unackedOps) {
9906
- if (unackedOps.size === 0) {
10133
+ if (unackedOps.length === 0) {
9907
10134
  return;
9908
10135
  }
9909
10136
  const messages = [];
9910
- const inOps = Array.from(unackedOps.values());
9911
- const result = applyLocalOps(inOps);
10137
+ const result = applyLocalOps(unackedOps);
9912
10138
  messages.push({
9913
10139
  type: ClientMsgCode.UPDATE_STORAGE,
9914
10140
  ops: result.opsToEmit
@@ -10132,7 +10358,7 @@ function createRoom(options, config) {
10132
10358
  const storageOps = context.buffer.storageOperations;
10133
10359
  if (storageOps.length > 0) {
10134
10360
  for (const op of storageOps) {
10135
- context.unacknowledgedOps.set(op.opId, op);
10361
+ context.unacknowledgedOps.add(op);
10136
10362
  }
10137
10363
  notifyStorageStatus();
10138
10364
  }
@@ -10359,9 +10585,9 @@ function createRoom(options, config) {
10359
10585
  }
10360
10586
  }
10361
10587
  function processInitialStorage(nodes) {
10362
- const unacknowledgedOps = new Map(context.unacknowledgedOps);
10588
+ const unacknowledgedOps2 = [...context.unacknowledgedOps.values()];
10363
10589
  createOrUpdateRootFromMessage(nodes);
10364
- applyAndSendOfflineOps(unacknowledgedOps);
10590
+ applyAndSendOfflineOps(unacknowledgedOps2);
10365
10591
  _resolveStoragePromise?.();
10366
10592
  notifyStorageStatus();
10367
10593
  eventHub.storageDidLoad.notify();
@@ -10950,6 +11176,11 @@ function createRoom(options, config) {
10950
11176
  connect: () => managedSocket.connect(),
10951
11177
  reconnect: () => managedSocket.reconnect(),
10952
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
+ },
10953
11184
  destroy: () => {
10954
11185
  pendingFeedsRequests.forEach(
10955
11186
  (request) => request.reject(new Error("Room destroyed"))
@@ -11457,6 +11688,7 @@ function createClient(options) {
11457
11688
  {
11458
11689
  enterRoom,
11459
11690
  getRoom,
11691
+ _dump: () => Array.from(roomsById.values(), ({ room }) => room._dump()).join("\n\n"),
11460
11692
  logout,
11461
11693
  // Public inbox notifications API
11462
11694
  getInboxNotifications: httpClient.getInboxNotifications,