@liveblocks/core 3.19.2 → 3.19.4-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.2";
9
+ var PKG_VERSION = "3.19.4-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)) {
@@ -2715,6 +2729,7 @@ var HttpClient = class {
2715
2729
  };
2716
2730
 
2717
2731
  // src/lib/fsm.ts
2732
+ var IGNORE = /* @__PURE__ */ Symbol("fsm.ignore");
2718
2733
  function distance(state1, state2) {
2719
2734
  if (state1 === state2) {
2720
2735
  return [0, 0];
@@ -2887,7 +2902,7 @@ var FSM = class {
2887
2902
  this.#eventHub = {
2888
2903
  didReceiveEvent: makeEventSource(),
2889
2904
  willTransition: makeEventSource(),
2890
- didIgnoreEvent: makeEventSource(),
2905
+ didIgnoreUnexpectedEvent: makeEventSource(),
2891
2906
  willExitState: makeEventSource(),
2892
2907
  didEnterState: makeEventSource(),
2893
2908
  didExitState: makeEventSource()
@@ -2895,7 +2910,7 @@ var FSM = class {
2895
2910
  this.events = {
2896
2911
  didReceiveEvent: this.#eventHub.didReceiveEvent.observable,
2897
2912
  willTransition: this.#eventHub.willTransition.observable,
2898
- didIgnoreEvent: this.#eventHub.didIgnoreEvent.observable,
2913
+ didIgnoreUnexpectedEvent: this.#eventHub.didIgnoreUnexpectedEvent.observable,
2899
2914
  willExitState: this.#eventHub.willExitState.observable,
2900
2915
  didEnterState: this.#eventHub.didEnterState.observable,
2901
2916
  didExitState: this.#eventHub.didExitState.observable
@@ -3018,9 +3033,14 @@ var FSM = class {
3018
3033
  * `context` params to conditionally decide which next state to transition
3019
3034
  * to.
3020
3035
  *
3021
- * If you set it to `null`, then the transition will be explicitly forbidden
3022
- * and throw an error. If you don't define a target for a transition, then
3023
- * such events will get ignored.
3036
+ * If you don't define a target for a transition, the event is treated
3037
+ * as unhandled in this state: `didIgnoreUnexpectedEvent` fires and the
3038
+ * state does not change.
3039
+ *
3040
+ * To declare an event as an intentional silent no-op in this state, use
3041
+ * the {@link IGNORE} sentinel — either statically (`{ EVENT: IGNORE }`)
3042
+ * or as a return value from a target function. IGNORE'd events do not
3043
+ * fire `didIgnoreUnexpectedEvent`.
3024
3044
  */
3025
3045
  addTransitions(nameOrPattern, mapping) {
3026
3046
  if (this.#runningState !== 0 /* NOT_STARTED_YET */) {
@@ -3040,7 +3060,12 @@ var FSM = class {
3040
3060
  }
3041
3061
  const target = target_;
3042
3062
  this.#knownEventTypes.add(type);
3043
- if (target !== void 0) {
3063
+ if (target === void 0) {
3064
+ continue;
3065
+ }
3066
+ if (target === IGNORE) {
3067
+ map.set(type, IGNORE);
3068
+ } else {
3044
3069
  const targetFn = typeof target === "function" ? target : () => target;
3045
3070
  map.set(type, targetFn);
3046
3071
  }
@@ -3139,12 +3164,14 @@ var FSM = class {
3139
3164
  if (this.#runningState === 2 /* STOPPED */) {
3140
3165
  return;
3141
3166
  }
3142
- const targetFn = this.#getTargetFn(event.type);
3143
- if (targetFn !== void 0) {
3144
- return this.#transition(event, targetFn);
3145
- } else {
3146
- this.#eventHub.didIgnoreEvent.notify(event);
3167
+ const entry = this.#getTargetFn(event.type);
3168
+ if (entry === IGNORE) {
3169
+ return;
3147
3170
  }
3171
+ if (entry !== void 0) {
3172
+ return this.#transition(event, entry);
3173
+ }
3174
+ this.#eventHub.didIgnoreUnexpectedEvent.notify(event);
3148
3175
  }
3149
3176
  #transition(event, target) {
3150
3177
  this.#eventHub.didReceiveEvent.notify(event);
@@ -3153,8 +3180,7 @@ var FSM = class {
3153
3180
  const nextTarget = targetFn(event, this.#currentContext.current);
3154
3181
  let nextState;
3155
3182
  let effects = void 0;
3156
- if (nextTarget === null) {
3157
- this.#eventHub.didIgnoreEvent.notify(event);
3183
+ if (nextTarget === IGNORE) {
3158
3184
  return;
3159
3185
  }
3160
3186
  if (typeof nextTarget === "string") {
@@ -3373,8 +3399,8 @@ function enableTracing(machine) {
3373
3399
  machine.events.didExitState.subscribe(
3374
3400
  ({ state, durationMs }) => log2(`Exited ${state} after ${durationMs.toFixed(0)}ms`)
3375
3401
  ),
3376
- machine.events.didIgnoreEvent.subscribe(
3377
- (e) => log2("Ignored event", e.type, e, "(current state won't handle it)")
3402
+ machine.events.didIgnoreUnexpectedEvent.subscribe(
3403
+ (e) => log2("Ignored unexpected event", e.type, e, "(no transition declared)")
3378
3404
  )
3379
3405
  ];
3380
3406
  return () => {
@@ -3486,7 +3512,12 @@ function createConnectionStateMachine(delegates, options) {
3486
3512
  );
3487
3513
  const onSocketError = (event) => machine.send({ type: "EXPLICIT_SOCKET_ERROR", event });
3488
3514
  const onSocketClose = (event) => machine.send({ type: "EXPLICIT_SOCKET_CLOSE", event });
3489
- const onSocketMessage = (event) => event.data === "pong" ? machine.send({ type: "PONG" }) : onMessage.notify(event);
3515
+ const onSocketMessage = (event) => {
3516
+ machine.send({ type: "ALIVE" });
3517
+ if (event.data !== "pong") {
3518
+ onMessage.notify(event);
3519
+ }
3520
+ };
3490
3521
  function teardownSocket(socket) {
3491
3522
  if (socket) {
3492
3523
  socket.removeEventListener("error", onSocketError);
@@ -3656,7 +3687,13 @@ function createConnectionStateMachine(delegates, options) {
3656
3687
  effect: [increaseBackoffDelay, logPrematureErrorOrCloseEvent(err)]
3657
3688
  };
3658
3689
  }
3659
- );
3690
+ ).addTransitions("@connecting.busy", {
3691
+ // The socket message listener is attached during @connecting.busy (see
3692
+ // onEnterAsync above), so server frames (most notably the actor-id
3693
+ // handshake) can fire onSocketMessage and emit a ALIVE before we
3694
+ // reach @ok.*. That's fine. Heartbeat only matters in @ok.*.
3695
+ ALIVE: IGNORE
3696
+ });
3660
3697
  const sendHeartbeat = {
3661
3698
  target: "@ok.awaiting-pong",
3662
3699
  effect: (ctx) => {
@@ -3671,7 +3708,8 @@ function createConnectionStateMachine(delegates, options) {
3671
3708
  machine.addTimedTransition("@ok.connected", HEARTBEAT_INTERVAL, maybeHeartbeat).addTransitions("@ok.connected", {
3672
3709
  NAVIGATOR_OFFLINE: maybeHeartbeat,
3673
3710
  // Don't take the browser's word for it when it says it's offline. Do a ping/pong to make sure.
3674
- WINDOW_GOT_FOCUS: sendHeartbeat
3711
+ WINDOW_GOT_FOCUS: sendHeartbeat,
3712
+ ALIVE: IGNORE
3675
3713
  });
3676
3714
  machine.addTransitions("@idle.zombie", {
3677
3715
  WINDOW_GOT_FOCUS: "@connecting.backoff"
@@ -3692,7 +3730,7 @@ function createConnectionStateMachine(delegates, options) {
3692
3730
  clearTimeout(timerID);
3693
3731
  onMessage.pause();
3694
3732
  };
3695
- }).addTransitions("@ok.awaiting-pong", { PONG: "@ok.connected" }).addTimedTransition("@ok.awaiting-pong", PONG_TIMEOUT, {
3733
+ }).addTransitions("@ok.awaiting-pong", { ALIVE: "@ok.connected" }).addTimedTransition("@ok.awaiting-pong", PONG_TIMEOUT, {
3696
3734
  target: "@connecting.busy",
3697
3735
  // Log implicit connection loss and drop the current open socket
3698
3736
  effect: log(
@@ -3705,7 +3743,7 @@ function createConnectionStateMachine(delegates, options) {
3705
3743
  // not. When still OPEN, don't transition.
3706
3744
  EXPLICIT_SOCKET_ERROR: (_, context) => {
3707
3745
  if (context.socket?.readyState === 1) {
3708
- return null;
3746
+ return IGNORE;
3709
3747
  }
3710
3748
  return {
3711
3749
  target: "@connecting.backoff",
@@ -5478,6 +5516,9 @@ var OpCode = Object.freeze({
5478
5516
  function isIgnoredOp(op) {
5479
5517
  return op.type === OpCode.DELETE_CRDT && op.id === "ACK";
5480
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
+ }
5481
5522
 
5482
5523
  // src/protocol/StorageNode.ts
5483
5524
  var CrdtType = Object.freeze({
@@ -5735,12 +5776,95 @@ function asPos(str) {
5735
5776
  return isPos(str) ? str : convertToPos(str);
5736
5777
  }
5737
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
+
5738
5861
  // src/crdts/AbstractCrdt.ts
5739
5862
  function createManagedPool(roomId, options) {
5740
5863
  const {
5741
5864
  getCurrentConnectionId,
5742
5865
  onDispatch,
5743
- isStorageWritable = () => true
5866
+ isStorageWritable = () => true,
5867
+ unacknowledgedOps = new UnacknowledgedOps()
5744
5868
  } = options;
5745
5869
  let clock = 0;
5746
5870
  let opClock = 0;
@@ -5762,7 +5886,8 @@ function createManagedPool(roomId, options) {
5762
5886
  "Cannot write to storage with a read only user, please ensure the user has write permissions"
5763
5887
  );
5764
5888
  }
5765
- }
5889
+ },
5890
+ unacknowledgedOps
5766
5891
  };
5767
5892
  }
5768
5893
  function crdtAsLiveNode(value) {
@@ -6044,11 +6169,9 @@ function childNodeLt(a, b) {
6044
6169
  var LiveList = class _LiveList extends AbstractCrdt {
6045
6170
  #items;
6046
6171
  #implicitlyDeletedItems;
6047
- #unacknowledgedSets;
6048
6172
  constructor(items) {
6049
6173
  super();
6050
6174
  this.#implicitlyDeletedItems = /* @__PURE__ */ new WeakSet();
6051
- this.#unacknowledgedSets = /* @__PURE__ */ new Map();
6052
6175
  const nodes = [];
6053
6176
  let lastPos;
6054
6177
  for (const item of items) {
@@ -6078,12 +6201,13 @@ var LiveList = class _LiveList extends AbstractCrdt {
6078
6201
  }
6079
6202
  /**
6080
6203
  * @internal
6081
- * This function assumes that the resulting ops will be sent to the server if they have an 'opId'
6082
- * so we mutate _unacknowledgedSets to avoid potential flickering
6083
- * 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).
6084
6208
  *
6085
- * This is quite unintuitive and should disappear as soon as
6086
- * 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.
6087
6211
  */
6088
6212
  _toOps(parentId, parentKey) {
6089
6213
  if (this._id === void 0) {
@@ -6099,9 +6223,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6099
6223
  ops.push(op);
6100
6224
  for (const item of this.#items) {
6101
6225
  const parentKey2 = item._getParentKeyOrThrow();
6102
- const childOps = HACK_addIntentAndDeletedIdToOperation(
6226
+ const childOps = addIntentToRootOp(
6103
6227
  item._toOps(this._id, parentKey2),
6104
- void 0
6228
+ "set"
6105
6229
  );
6106
6230
  for (const childOp of childOps) {
6107
6231
  ops.push(childOp);
@@ -6143,6 +6267,28 @@ var LiveList = class _LiveList extends AbstractCrdt {
6143
6267
  (item) => item._getParentKeyOrThrow() === position
6144
6268
  );
6145
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
+ }
6146
6292
  /** @internal */
6147
6293
  _attach(id, pool) {
6148
6294
  super._attach(id, pool);
@@ -6223,13 +6369,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6223
6369
  if (deletedDelta) {
6224
6370
  delta.push(deletedDelta);
6225
6371
  }
6226
- const unacknowledgedOpId = this.#unacknowledgedSets.get(op.parentKey);
6227
- if (unacknowledgedOpId !== void 0) {
6228
- if (unacknowledgedOpId !== op.opId) {
6229
- return delta.length === 0 ? { modified: false } : { modified: makeUpdate(this, delta), reverse: [] };
6230
- } else {
6231
- this.#unacknowledgedSets.delete(op.parentKey);
6232
- }
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: [] };
6233
6375
  }
6234
6376
  const indexOfItemWithSamePosition = this._indexOfPosition(op.parentKey);
6235
6377
  const existingItem = this.#items.find((item) => item._id === op.id);
@@ -6310,7 +6452,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6310
6452
  }
6311
6453
  return result.modified.updates[0];
6312
6454
  }
6313
- #applyRemoteInsert(op) {
6455
+ #applyRemoteInsert(op, fromSnapshot) {
6314
6456
  if (this._pool === void 0) {
6315
6457
  throw new Error("Can't attach child if managed pool is not present");
6316
6458
  }
@@ -6320,11 +6462,82 @@ var LiveList = class _LiveList extends AbstractCrdt {
6320
6462
  this.#shiftItemPosition(existingItemIndex, key);
6321
6463
  }
6322
6464
  const { newItem, newIndex } = this.#createAttachItemAndSort(op, key);
6465
+ const bumpDeltas = fromSnapshot ? [] : this.#bumpUnackedPushesAbove(key);
6323
6466
  return {
6324
- modified: makeUpdate(this, [insertDelta(newIndex, newItem)]),
6467
+ modified: makeUpdate(this, [
6468
+ insertDelta(newIndex, newItem),
6469
+ ...bumpDeltas
6470
+ ]),
6325
6471
  reverse: []
6326
6472
  };
6327
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
+ }
6328
6541
  #applyInsertAck(op) {
6329
6542
  const existingItem = this.#items.find((item) => item._id === op.id);
6330
6543
  const key = asPos(op.parentKey);
@@ -6405,7 +6618,6 @@ var LiveList = class _LiveList extends AbstractCrdt {
6405
6618
  if (this._pool?.getNode(id) !== void 0) {
6406
6619
  return { modified: false };
6407
6620
  }
6408
- this.#unacknowledgedSets.set(key, nn(op.opId));
6409
6621
  const indexOfItemWithSameKey = this._indexOfPosition(key);
6410
6622
  child._attach(id, nn(this._pool));
6411
6623
  child._setParentLink(this, key);
@@ -6415,8 +6627,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6415
6627
  existingItem._detach();
6416
6628
  this.#items.remove(existingItem);
6417
6629
  this.#items.add(child);
6418
- const reverse = HACK_addIntentAndDeletedIdToOperation(
6630
+ const reverse = addIntentToRootOp(
6419
6631
  existingItem._toOps(nn(this._id), key),
6632
+ "set",
6420
6633
  op.id
6421
6634
  );
6422
6635
  const delta = [setDelta(indexOfItemWithSameKey, child)];
@@ -6441,7 +6654,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6441
6654
  }
6442
6655
  }
6443
6656
  /** @internal */
6444
- _attachChild(op, source) {
6657
+ _attachChild(op, source, fromSnapshot = false) {
6445
6658
  if (this._pool === void 0) {
6446
6659
  throw new Error("Can't attach child if managed pool is not present");
6447
6660
  }
@@ -6456,7 +6669,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6456
6669
  }
6457
6670
  } else {
6458
6671
  if (source === 1 /* THEIRS */) {
6459
- result = this.#applyRemoteInsert(op);
6672
+ result = this.#applyRemoteInsert(op, fromSnapshot);
6460
6673
  } else if (source === 2 /* OURS */) {
6461
6674
  result = this.#applyInsertAck(op);
6462
6675
  } else {
@@ -6659,8 +6872,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6659
6872
  * @param element The element to add to the end of the LiveList.
6660
6873
  */
6661
6874
  push(element) {
6662
- this._pool?.assertStorageIsWritable();
6663
- return this.insert(element, this.length);
6875
+ return this.#injectAt(element, this.length, "push");
6664
6876
  }
6665
6877
  /**
6666
6878
  * Inserts one element at a specified index.
@@ -6668,6 +6880,15 @@ var LiveList = class _LiveList extends AbstractCrdt {
6668
6880
  * @param index The index at which you want to insert the element.
6669
6881
  */
6670
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) {
6671
6892
  this._pool?.assertStorageIsWritable();
6672
6893
  if (index < 0 || index > this.#items.length) {
6673
6894
  throw new Error(
@@ -6683,8 +6904,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6683
6904
  if (this._pool && this._id) {
6684
6905
  const id = this._pool.generateId();
6685
6906
  value._attach(id, this._pool);
6907
+ const ops = value._toOpsWithOpId(this._id, position, this._pool);
6686
6908
  this._pool.dispatch(
6687
- value._toOpsWithOpId(this._id, position, this._pool),
6909
+ intent === "push" ? addIntentToRootOp(ops, "push") : ops,
6688
6910
  [{ type: OpCode.DELETE_CRDT, id }],
6689
6911
  /* @__PURE__ */ new Map([
6690
6912
  [this._id, makeUpdate(this, [insertDelta(index, value)])]
@@ -6842,13 +7064,14 @@ var LiveList = class _LiveList extends AbstractCrdt {
6842
7064
  value._attach(id, this._pool);
6843
7065
  const storageUpdates = /* @__PURE__ */ new Map();
6844
7066
  storageUpdates.set(this._id, makeUpdate(this, [setDelta(index, value)]));
6845
- const ops = HACK_addIntentAndDeletedIdToOperation(
7067
+ const ops = addIntentToRootOp(
6846
7068
  value._toOpsWithOpId(this._id, position, this._pool),
7069
+ "set",
6847
7070
  existingId
6848
7071
  );
6849
- this.#unacknowledgedSets.set(position, nn(ops[0].opId));
6850
- const reverseOps = HACK_addIntentAndDeletedIdToOperation(
7072
+ const reverseOps = addIntentToRootOp(
6851
7073
  existingItem._toOps(this._id, position),
7074
+ "set",
6852
7075
  id
6853
7076
  );
6854
7077
  this._pool.dispatch(ops, reverseOps, storageUpdates);
@@ -7049,15 +7272,11 @@ function moveDelta(previousIndex, index, item) {
7049
7272
  previousIndex
7050
7273
  };
7051
7274
  }
7052
- function HACK_addIntentAndDeletedIdToOperation(ops, deletedId) {
7275
+ function addIntentToRootOp(ops, intent, deletedId) {
7053
7276
  return ops.map((op, index) => {
7054
7277
  if (index === 0) {
7055
7278
  const firstOp = op;
7056
- return {
7057
- ...firstOp,
7058
- intent: "set",
7059
- deletedId
7060
- };
7279
+ return { ...firstOp, intent, deletedId };
7061
7280
  } else {
7062
7281
  return op;
7063
7282
  }
@@ -8297,6 +8516,31 @@ function lsonToLiveNode(value) {
8297
8516
  return new LiveRegister(value);
8298
8517
  }
8299
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
+ }
8300
8544
  function getTreesDiffOperations(currentItems, newItems) {
8301
8545
  const ops = [];
8302
8546
  currentItems.forEach((_, id) => {
@@ -9306,6 +9550,7 @@ function createRoom(options, config) {
9306
9550
  delegates,
9307
9551
  config.enableDebugLogging
9308
9552
  );
9553
+ const unacknowledgedOps = new UnacknowledgedOps();
9309
9554
  const context = {
9310
9555
  buffer: {
9311
9556
  flushTimerID: void 0,
@@ -9333,14 +9578,15 @@ function createRoom(options, config) {
9333
9578
  pool: createManagedPool(roomId, {
9334
9579
  getCurrentConnectionId,
9335
9580
  onDispatch,
9336
- isStorageWritable
9581
+ isStorageWritable,
9582
+ unacknowledgedOps
9337
9583
  }),
9338
9584
  root: void 0,
9339
9585
  undoStack: [],
9340
9586
  redoStack: [],
9341
9587
  pausedHistory: null,
9342
9588
  activeBatch: null,
9343
- unacknowledgedOps: /* @__PURE__ */ new Map()
9589
+ unacknowledgedOps
9344
9590
  };
9345
9591
  const nodeMapBuffer = makeNodeMapBuffer();
9346
9592
  const stopwatch = config.enableDebugLogging ? makeStopWatch() : void 0;
@@ -9548,7 +9794,11 @@ function createRoom(options, config) {
9548
9794
  currentItems.set(id, crdt._serialize());
9549
9795
  }
9550
9796
  const ops = getTreesDiffOperations(currentItems, nodes);
9551
- const result = applyRemoteOps(ops);
9797
+ const result = applyRemoteOps(
9798
+ ops,
9799
+ /* fromSnapshot */
9800
+ true
9801
+ );
9552
9802
  notify(result.updates);
9553
9803
  } else {
9554
9804
  context.root = LiveObject._fromItems(
@@ -9630,15 +9880,16 @@ function createRoom(options, config) {
9630
9880
  );
9631
9881
  return { opsToEmit: opsWithOpIds, reverse, updates };
9632
9882
  }
9633
- function applyRemoteOps(ops) {
9883
+ function applyRemoteOps(ops, fromSnapshot = false) {
9634
9884
  return applyOps(
9635
9885
  [],
9636
9886
  ops,
9637
9887
  /* isLocal */
9638
- false
9888
+ false,
9889
+ fromSnapshot
9639
9890
  );
9640
9891
  }
9641
- function applyOps(pframes, ops, isLocal) {
9892
+ function applyOps(pframes, ops, isLocal, fromSnapshot = false) {
9642
9893
  const output = {
9643
9894
  reverse: new Deque(),
9644
9895
  storageUpdates: /* @__PURE__ */ new Map(),
@@ -9674,7 +9925,7 @@ function createRoom(options, config) {
9674
9925
  } else {
9675
9926
  source = 1 /* THEIRS */;
9676
9927
  }
9677
- const applyOpResult = applyOp(op, source);
9928
+ const applyOpResult = applyOp(op, source, fromSnapshot);
9678
9929
  if (applyOpResult.modified) {
9679
9930
  const nodeId = applyOpResult.modified.node._id;
9680
9931
  if (!(nodeId && createdNodeIds.has(nodeId))) {
@@ -9700,7 +9951,7 @@ function createRoom(options, config) {
9700
9951
  }
9701
9952
  };
9702
9953
  }
9703
- function applyOp(op, source) {
9954
+ function applyOp(op, source, fromSnapshot = false) {
9704
9955
  if (isIgnoredOp(op)) {
9705
9956
  return { modified: false };
9706
9957
  }
@@ -9739,7 +9990,7 @@ function createRoom(options, config) {
9739
9990
  if (parentNode === void 0) {
9740
9991
  return { modified: false };
9741
9992
  }
9742
- return parentNode._attachChild(op, source);
9993
+ return parentNode._attachChild(op, source, fromSnapshot);
9743
9994
  }
9744
9995
  }
9745
9996
  }
@@ -9879,12 +10130,11 @@ function createRoom(options, config) {
9879
10130
  }
9880
10131
  }
9881
10132
  function applyAndSendOfflineOps(unackedOps) {
9882
- if (unackedOps.size === 0) {
10133
+ if (unackedOps.length === 0) {
9883
10134
  return;
9884
10135
  }
9885
10136
  const messages = [];
9886
- const inOps = Array.from(unackedOps.values());
9887
- const result = applyLocalOps(inOps);
10137
+ const result = applyLocalOps(unackedOps);
9888
10138
  messages.push({
9889
10139
  type: ClientMsgCode.UPDATE_STORAGE,
9890
10140
  ops: result.opsToEmit
@@ -10108,7 +10358,7 @@ function createRoom(options, config) {
10108
10358
  const storageOps = context.buffer.storageOperations;
10109
10359
  if (storageOps.length > 0) {
10110
10360
  for (const op of storageOps) {
10111
- context.unacknowledgedOps.set(op.opId, op);
10361
+ context.unacknowledgedOps.add(op);
10112
10362
  }
10113
10363
  notifyStorageStatus();
10114
10364
  }
@@ -10335,9 +10585,9 @@ function createRoom(options, config) {
10335
10585
  }
10336
10586
  }
10337
10587
  function processInitialStorage(nodes) {
10338
- const unacknowledgedOps = new Map(context.unacknowledgedOps);
10588
+ const unacknowledgedOps2 = [...context.unacknowledgedOps.values()];
10339
10589
  createOrUpdateRootFromMessage(nodes);
10340
- applyAndSendOfflineOps(unacknowledgedOps);
10590
+ applyAndSendOfflineOps(unacknowledgedOps2);
10341
10591
  _resolveStoragePromise?.();
10342
10592
  notifyStorageStatus();
10343
10593
  eventHub.storageDidLoad.notify();
@@ -10926,6 +11176,11 @@ function createRoom(options, config) {
10926
11176
  connect: () => managedSocket.connect(),
10927
11177
  reconnect: () => managedSocket.reconnect(),
10928
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
+ },
10929
11184
  destroy: () => {
10930
11185
  pendingFeedsRequests.forEach(
10931
11186
  (request) => request.reject(new Error("Room destroyed"))
@@ -11433,6 +11688,7 @@ function createClient(options) {
11433
11688
  {
11434
11689
  enterRoom,
11435
11690
  getRoom,
11691
+ _dump: () => Array.from(roomsById.values(), ({ room }) => room._dump()).join("\n\n"),
11436
11692
  logout,
11437
11693
  // Public inbox notifications API
11438
11694
  getInboxNotifications: httpClient.getInboxNotifications,