@liveblocks/core 3.20.0-perm1 → 3.20.0-perm2

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.20.0-perm1";
9
+ var PKG_VERSION = "3.20.0-perm2";
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)) {
@@ -5723,6 +5737,9 @@ var OpCode = Object.freeze({
5723
5737
  function isIgnoredOp(op) {
5724
5738
  return op.type === OpCode.DELETE_CRDT && op.id === "ACK";
5725
5739
  }
5740
+ function isCreateOp(op) {
5741
+ return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST;
5742
+ }
5726
5743
 
5727
5744
  // src/protocol/StorageNode.ts
5728
5745
  var CrdtType = Object.freeze({
@@ -5980,12 +5997,112 @@ function asPos(str) {
5980
5997
  return isPos(str) ? str : convertToPos(str);
5981
5998
  }
5982
5999
 
6000
+ // src/crdts/UnacknowledgedOps.ts
6001
+ var UnacknowledgedOps = class {
6002
+ // opId -> op
6003
+ #byOpId = /* @__PURE__ */ new Map();
6004
+ // position -> (opId -> Create op)
6005
+ #createOpsByPosition = /* @__PURE__ */ new Map();
6006
+ // parentId -> (opId -> Create op)
6007
+ #createOpsByParent = /* @__PURE__ */ new Map();
6008
+ // opIds of pending ops that were in flight when a connection died, so the
6009
+ // server may already have processed them. See isPossiblyStored().
6010
+ #possiblyStoredOpIds = /* @__PURE__ */ new Set();
6011
+ #posKey(parentId, parentKey) {
6012
+ return `${parentId}
6013
+ ${parentKey}`;
6014
+ }
6015
+ get size() {
6016
+ return this.#byOpId.size;
6017
+ }
6018
+ /**
6019
+ * Mark the given Op as still unacknowledged.
6020
+ */
6021
+ add(op) {
6022
+ this.#byOpId.set(op.opId, op);
6023
+ if (isCreateOp(op)) {
6024
+ const posKey = this.#posKey(op.parentId, op.parentKey);
6025
+ let atPosition = this.#createOpsByPosition.get(posKey);
6026
+ if (atPosition === void 0) {
6027
+ atPosition = /* @__PURE__ */ new Map();
6028
+ this.#createOpsByPosition.set(posKey, atPosition);
6029
+ }
6030
+ atPosition.set(op.opId, op);
6031
+ let inParent = this.#createOpsByParent.get(op.parentId);
6032
+ if (inParent === void 0) {
6033
+ inParent = /* @__PURE__ */ new Map();
6034
+ this.#createOpsByParent.set(op.parentId, inParent);
6035
+ }
6036
+ inParent.set(op.opId, op);
6037
+ }
6038
+ }
6039
+ /**
6040
+ * Drop the op with the given opId from the set, because the server has
6041
+ * acknowledged it (confirmed our own op, or signalled it was seen but
6042
+ * ignored).
6043
+ */
6044
+ delete(opId) {
6045
+ const op = this.#byOpId.get(opId);
6046
+ if (op === void 0) {
6047
+ return;
6048
+ }
6049
+ this.#byOpId.delete(opId);
6050
+ this.#possiblyStoredOpIds.delete(opId);
6051
+ if (isCreateOp(op)) {
6052
+ const posKey = this.#posKey(op.parentId, op.parentKey);
6053
+ const atPosition = this.#createOpsByPosition.get(posKey);
6054
+ atPosition?.delete(opId);
6055
+ if (atPosition !== void 0 && atPosition.size === 0) {
6056
+ this.#createOpsByPosition.delete(posKey);
6057
+ }
6058
+ const inParent = this.#createOpsByParent.get(op.parentId);
6059
+ inParent?.delete(opId);
6060
+ if (inParent !== void 0 && inParent.size === 0) {
6061
+ this.#createOpsByParent.delete(op.parentId);
6062
+ }
6063
+ }
6064
+ }
6065
+ /**
6066
+ * The still-unacknowledged Create ops with the given `parentId` and
6067
+ * `parentKey` (targeting one exact position), in dispatch order. O(1) lookup.
6068
+ * Empty if none.
6069
+ */
6070
+ getByParentIdAndKey(parentId, parentKey) {
6071
+ return this.#createOpsByPosition.get(this.#posKey(parentId, parentKey))?.values() ?? [];
6072
+ }
6073
+ /**
6074
+ * The still-unacknowledged Create ops with the given `parentId` (across all
6075
+ * positions), in dispatch order. O(1) lookup. Empty if none.
6076
+ */
6077
+ getByParentId(parentId) {
6078
+ return this.#createOpsByParent.get(parentId)?.values() ?? [];
6079
+ }
6080
+ /** All still-unacknowledged ops, in dispatch order. */
6081
+ values() {
6082
+ return this.#byOpId.values();
6083
+ }
6084
+ isPossiblyStored(opId) {
6085
+ return this.#possiblyStoredOpIds.has(opId);
6086
+ }
6087
+ /**
6088
+ * Mark every currently pending op as possibly stored on the server. Called
6089
+ * when the connection dies: all of these ops were in flight, and their
6090
+ * (possibly lost) acks would have been the only way to know their fate.
6091
+ */
6092
+ markAllAsPossiblyStored() {
6093
+ for (const opId of this.#byOpId.keys()) {
6094
+ this.#possiblyStoredOpIds.add(opId);
6095
+ }
6096
+ }
6097
+ };
6098
+
5983
6099
  // src/crdts/AbstractCrdt.ts
5984
6100
  function createManagedPool(roomId, options) {
5985
6101
  const {
5986
6102
  getCurrentConnectionId,
5987
6103
  onDispatch,
5988
- isStorageWritable = () => true
6104
+ isStorageWritable = () => true,
6105
+ unacknowledgedOps = new UnacknowledgedOps()
5989
6106
  } = options;
5990
6107
  let clock = 0;
5991
6108
  let opClock = 0;
@@ -6007,7 +6124,8 @@ function createManagedPool(roomId, options) {
6007
6124
  "Cannot write to storage with a read only user, please ensure the user has write permissions"
6008
6125
  );
6009
6126
  }
6010
- }
6127
+ },
6128
+ unacknowledgedOps
6011
6129
  };
6012
6130
  }
6013
6131
  function crdtAsLiveNode(value) {
@@ -6289,11 +6407,9 @@ function childNodeLt(a, b) {
6289
6407
  var LiveList = class _LiveList extends AbstractCrdt {
6290
6408
  #items;
6291
6409
  #implicitlyDeletedItems;
6292
- #unacknowledgedSets;
6293
6410
  constructor(items) {
6294
6411
  super();
6295
6412
  this.#implicitlyDeletedItems = /* @__PURE__ */ new WeakSet();
6296
- this.#unacknowledgedSets = /* @__PURE__ */ new Map();
6297
6413
  const nodes = [];
6298
6414
  let lastPos;
6299
6415
  for (const item of items) {
@@ -6323,12 +6439,13 @@ var LiveList = class _LiveList extends AbstractCrdt {
6323
6439
  }
6324
6440
  /**
6325
6441
  * @internal
6326
- * This function assumes that the resulting ops will be sent to the server if they have an 'opId'
6327
- * so we mutate _unacknowledgedSets to avoid potential flickering
6328
- * https://github.com/liveblocks/liveblocks/pull/1177
6442
+ * Serializes this list (and its children) into Create ops. Each child's
6443
+ * create is tagged with the "set" intent (in the loop below) so that a list
6444
+ * created and immediately mutated doesn't transiently re-show its initial
6445
+ * items (flicker, https://github.com/liveblocks/liveblocks/pull/1177).
6329
6446
  *
6330
- * This is quite unintuitive and should disappear as soon as
6331
- * we introduce an explicit LiveList.Set operation
6447
+ * This is quite unintuitive and should disappear as soon as we introduce an
6448
+ * explicit LiveList.Set operation.
6332
6449
  */
6333
6450
  _toOps(parentId, parentKey) {
6334
6451
  if (this._id === void 0) {
@@ -6344,9 +6461,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6344
6461
  ops.push(op);
6345
6462
  for (const item of this.#items) {
6346
6463
  const parentKey2 = item._getParentKeyOrThrow();
6347
- const childOps = HACK_addIntentAndDeletedIdToOperation(
6464
+ const childOps = addIntentToRootOp(
6348
6465
  item._toOps(this._id, parentKey2),
6349
- void 0
6466
+ "set"
6350
6467
  );
6351
6468
  for (const childOp of childOps) {
6352
6469
  ops.push(childOp);
@@ -6388,6 +6505,28 @@ var LiveList = class _LiveList extends AbstractCrdt {
6388
6505
  (item) => item._getParentKeyOrThrow() === position
6389
6506
  );
6390
6507
  }
6508
+ /**
6509
+ * The opId of this list's still-unacknowledged "set" op at the given position,
6510
+ * or undefined if none. Derived from the room's unacknowledgedOps (the single
6511
+ * source of truth) rather than tracked in a per-instance map. The pool's
6512
+ * position index already scopes to this list's (parentId, position); the last
6513
+ * match wins, matching the original last-write-wins map semantics.
6514
+ */
6515
+ #unacknowledgedSetOpIdAt(position) {
6516
+ if (this._pool === void 0 || this._id === void 0) {
6517
+ return void 0;
6518
+ }
6519
+ let opId;
6520
+ for (const op of this._pool.unacknowledgedOps.getByParentIdAndKey(
6521
+ this._id,
6522
+ position
6523
+ )) {
6524
+ if (op.intent === "set") {
6525
+ opId = op.opId;
6526
+ }
6527
+ }
6528
+ return opId;
6529
+ }
6391
6530
  /** @internal */
6392
6531
  _attach(id, pool) {
6393
6532
  super._attach(id, pool);
@@ -6468,13 +6607,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6468
6607
  if (deletedDelta) {
6469
6608
  delta.push(deletedDelta);
6470
6609
  }
6471
- const unacknowledgedOpId = this.#unacknowledgedSets.get(op.parentKey);
6472
- if (unacknowledgedOpId !== void 0) {
6473
- if (unacknowledgedOpId !== op.opId) {
6474
- return delta.length === 0 ? { modified: false } : { modified: makeUpdate(this, delta), reverse: [] };
6475
- } else {
6476
- this.#unacknowledgedSets.delete(op.parentKey);
6477
- }
6610
+ const unacknowledgedOpId = this.#unacknowledgedSetOpIdAt(op.parentKey);
6611
+ if (unacknowledgedOpId !== void 0 && unacknowledgedOpId !== op.opId) {
6612
+ return delta.length === 0 ? { modified: false } : { modified: makeUpdate(this, delta), reverse: [] };
6478
6613
  }
6479
6614
  const indexOfItemWithSamePosition = this._indexOfPosition(op.parentKey);
6480
6615
  const existingItem = this.#items.find((item) => item._id === op.id);
@@ -6565,11 +6700,92 @@ var LiveList = class _LiveList extends AbstractCrdt {
6565
6700
  this.#shiftItemPosition(existingItemIndex, key);
6566
6701
  }
6567
6702
  const { newItem, newIndex } = this.#createAttachItemAndSort(op, key);
6703
+ const bumpDeltas = this.#bumpUnackedPushesAbove(key);
6568
6704
  return {
6569
- modified: makeUpdate(this, [insertDelta(newIndex, newItem)]),
6705
+ modified: makeUpdate(this, [
6706
+ insertDelta(newIndex, newItem),
6707
+ ...bumpDeltas
6708
+ ]),
6570
6709
  reverse: []
6571
6710
  };
6572
6711
  }
6712
+ /**
6713
+ * This list's own still-unacknowledged pushed items (their `intent: "push"`
6714
+ * Create op is still pending in the room's unacknowledgedOps). Derived from
6715
+ * the single source of truth, so an item drops out the instant its op is
6716
+ * acked, with no per-instance membership to leak. Yielded in push order.
6717
+ *
6718
+ * Excludes ops that may already be stored on the server (they were in
6719
+ * flight when a connection died, so their fate is unknown): the bump
6720
+ * prediction assumes the server has not processed the op yet, which is only
6721
+ * guaranteed for ops sent on the current connection. For these excluded
6722
+ * ops, the server's (re-)ack states the authoritative position; predicting
6723
+ * locally could produce a wrong position that no ack would correct.
6724
+ *
6725
+ * Restricted to items currently in `#items`: a pushed node whose op is still
6726
+ * pending may have been pulled out of the list (e.g. implicitly deleted by a
6727
+ * remote set, or removed by an undo) while still living in the pool, and such
6728
+ * a node must not be repositioned.
6729
+ */
6730
+ *#unackedPushNodes() {
6731
+ if (this._pool === void 0 || this._id === void 0) {
6732
+ return;
6733
+ }
6734
+ for (const op of this._pool.unacknowledgedOps.getByParentId(this._id)) {
6735
+ if (op.intent !== "push") {
6736
+ continue;
6737
+ }
6738
+ if (this._pool.unacknowledgedOps.isPossiblyStored(op.opId)) {
6739
+ continue;
6740
+ }
6741
+ const node = this._pool.getNode(op.id);
6742
+ if (node !== void 0 && this.#items.includes(node)) {
6743
+ yield node;
6744
+ }
6745
+ }
6746
+ }
6747
+ /**
6748
+ * Optimistic no-flip for pushed items. When a remote op lands at or below my
6749
+ * still-unacked pushed items, those items must end up *after* it: FIFO plus
6750
+ * the room's serial processing guarantee the remote was processed first, so
6751
+ * my unacked pushes belong behind it. Re-chain the whole unacked-push block,
6752
+ * in push order, to sit after the highest confirmed sibling, so it keeps
6753
+ * rendering as a contiguous tail instead of getting interleaved. Local-only;
6754
+ * the real acks overwrite these keys with the (identical) server keys.
6755
+ */
6756
+ #bumpUnackedPushesAbove(remoteKey) {
6757
+ const pending = new Set(this.#unackedPushNodes());
6758
+ if (pending.size === 0) {
6759
+ return [];
6760
+ }
6761
+ let minPending;
6762
+ for (const node of pending) {
6763
+ const pos = node._parentPos;
6764
+ if (minPending === void 0 || pos < minPending) {
6765
+ minPending = pos;
6766
+ }
6767
+ }
6768
+ if (remoteKey < nn(minPending)) {
6769
+ return [];
6770
+ }
6771
+ let base;
6772
+ for (const item of this.#items) {
6773
+ if (!pending.has(item)) {
6774
+ base = item._parentPos;
6775
+ }
6776
+ }
6777
+ const deltas = [];
6778
+ for (const node of pending) {
6779
+ const previousIndex = this.#items.findIndex((item) => item === node);
6780
+ base = makePosition(base);
6781
+ this.#updateItemPosition(node, base);
6782
+ const index = this.#items.findIndex((item) => item === node);
6783
+ if (index !== previousIndex) {
6784
+ deltas.push(moveDelta(previousIndex, index, node));
6785
+ }
6786
+ }
6787
+ return deltas;
6788
+ }
6573
6789
  #applyInsertAck(op) {
6574
6790
  const existingItem = this.#items.find((item) => item._id === op.id);
6575
6791
  const key = asPos(op.parentKey);
@@ -6650,7 +6866,6 @@ var LiveList = class _LiveList extends AbstractCrdt {
6650
6866
  if (this._pool?.getNode(id) !== void 0) {
6651
6867
  return { modified: false };
6652
6868
  }
6653
- this.#unacknowledgedSets.set(key, nn(op.opId));
6654
6869
  const indexOfItemWithSameKey = this._indexOfPosition(key);
6655
6870
  child._attach(id, nn(this._pool));
6656
6871
  child._setParentLink(this, key);
@@ -6660,8 +6875,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6660
6875
  existingItem._detach();
6661
6876
  this.#items.remove(existingItem);
6662
6877
  this.#items.add(child);
6663
- const reverse = HACK_addIntentAndDeletedIdToOperation(
6878
+ const reverse = addIntentToRootOp(
6664
6879
  existingItem._toOps(nn(this._id), key),
6880
+ "set",
6665
6881
  op.id
6666
6882
  );
6667
6883
  const delta = [setDelta(indexOfItemWithSameKey, child)];
@@ -6904,8 +7120,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6904
7120
  * @param element The element to add to the end of the LiveList.
6905
7121
  */
6906
7122
  push(element) {
6907
- this._pool?.assertStorageIsWritable();
6908
- return this.insert(element, this.length);
7123
+ return this.#injectAt(element, this.length, "push");
6909
7124
  }
6910
7125
  /**
6911
7126
  * Inserts one element at a specified index.
@@ -6913,6 +7128,15 @@ var LiveList = class _LiveList extends AbstractCrdt {
6913
7128
  * @param index The index at which you want to insert the element.
6914
7129
  */
6915
7130
  insert(element, index) {
7131
+ return this.#injectAt(element, index, "insert");
7132
+ }
7133
+ /**
7134
+ * Shared implementation of `insert` and `push`. A `"push"` intent leaves the
7135
+ * client-computed position untouched (so optimistic rendering is unchanged),
7136
+ * but tags the Op so the server appends it to the true end of the list
7137
+ * instead of resolving its position against the client's stale view.
7138
+ */
7139
+ #injectAt(element, index, intent) {
6916
7140
  this._pool?.assertStorageIsWritable();
6917
7141
  if (index < 0 || index > this.#items.length) {
6918
7142
  throw new Error(
@@ -6928,8 +7152,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6928
7152
  if (this._pool && this._id) {
6929
7153
  const id = this._pool.generateId();
6930
7154
  value._attach(id, this._pool);
7155
+ const ops = value._toOpsWithOpId(this._id, position, this._pool);
6931
7156
  this._pool.dispatch(
6932
- value._toOpsWithOpId(this._id, position, this._pool),
7157
+ intent === "push" ? addIntentToRootOp(ops, "push") : ops,
6933
7158
  [{ type: OpCode.DELETE_CRDT, id }],
6934
7159
  /* @__PURE__ */ new Map([
6935
7160
  [this._id, makeUpdate(this, [insertDelta(index, value)])]
@@ -7087,13 +7312,14 @@ var LiveList = class _LiveList extends AbstractCrdt {
7087
7312
  value._attach(id, this._pool);
7088
7313
  const storageUpdates = /* @__PURE__ */ new Map();
7089
7314
  storageUpdates.set(this._id, makeUpdate(this, [setDelta(index, value)]));
7090
- const ops = HACK_addIntentAndDeletedIdToOperation(
7315
+ const ops = addIntentToRootOp(
7091
7316
  value._toOpsWithOpId(this._id, position, this._pool),
7317
+ "set",
7092
7318
  existingId
7093
7319
  );
7094
- this.#unacknowledgedSets.set(position, nn(ops[0].opId));
7095
- const reverseOps = HACK_addIntentAndDeletedIdToOperation(
7320
+ const reverseOps = addIntentToRootOp(
7096
7321
  existingItem._toOps(this._id, position),
7322
+ "set",
7097
7323
  id
7098
7324
  );
7099
7325
  this._pool.dispatch(ops, reverseOps, storageUpdates);
@@ -7294,15 +7520,11 @@ function moveDelta(previousIndex, index, item) {
7294
7520
  previousIndex
7295
7521
  };
7296
7522
  }
7297
- function HACK_addIntentAndDeletedIdToOperation(ops, deletedId) {
7523
+ function addIntentToRootOp(ops, intent, deletedId) {
7298
7524
  return ops.map((op, index) => {
7299
7525
  if (index === 0) {
7300
7526
  const firstOp = op;
7301
- return {
7302
- ...firstOp,
7303
- intent: "set",
7304
- deletedId
7305
- };
7527
+ return { ...firstOp, intent, deletedId };
7306
7528
  } else {
7307
7529
  return op;
7308
7530
  }
@@ -7954,6 +8176,7 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
7954
8176
  const id = nn(this._id);
7955
8177
  const parentKey = nn(child._parentKey);
7956
8178
  const reverse = child._toOps(id, parentKey);
8179
+ const deletedItem = liveNodeToLson(child);
7957
8180
  for (const [key, value] of this.#synced) {
7958
8181
  if (value === child) {
7959
8182
  this.#synced.delete(key);
@@ -7965,7 +8188,7 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
7965
8188
  node: this,
7966
8189
  type: "LiveObject",
7967
8190
  updates: {
7968
- [parentKey]: { type: "delete" }
8191
+ [parentKey]: { type: "delete", deletedItem }
7969
8192
  }
7970
8193
  };
7971
8194
  return { modified: storageUpdate, reverse };
@@ -8542,6 +8765,60 @@ function lsonToLiveNode(value) {
8542
8765
  return new LiveRegister(value);
8543
8766
  }
8544
8767
  }
8768
+ function dumpPool(pool) {
8769
+ const rows = Array.from(pool.nodes.values(), (node) => {
8770
+ const parent = node.parent;
8771
+ const parentId = parent.type === "HasParent" ? parent.node._id ?? "?" : parent.type === "Orphaned" ? "<orphaned>" : "-";
8772
+ let value;
8773
+ if (node instanceof LiveRegister) {
8774
+ value = stringifyOrLog(node.data);
8775
+ } else if (node instanceof LiveList) {
8776
+ value = "<LiveList>";
8777
+ } else if (node instanceof LiveMap) {
8778
+ value = "<LiveMap>";
8779
+ } else {
8780
+ value = "<LiveObject>";
8781
+ }
8782
+ return { id: nn(node._id), parentId, key: node._parentKey ?? "", value };
8783
+ });
8784
+ rows.sort((a, b) => {
8785
+ if (a.parentId !== b.parentId) return a.parentId < b.parentId ? -1 : 1;
8786
+ if (a.key !== b.key) return a.key < b.key ? -1 : 1;
8787
+ return 0;
8788
+ });
8789
+ return rows.map(
8790
+ (r) => ` ${r.id} parent=${r.parentId} key=${r.key || "\u2014"} ${r.value}`
8791
+ ).join("\n");
8792
+ }
8793
+ function isJsonEq(a, b) {
8794
+ if (a === b) {
8795
+ return true;
8796
+ }
8797
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
8798
+ return false;
8799
+ }
8800
+ if (Array.isArray(a) || Array.isArray(b)) {
8801
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
8802
+ return false;
8803
+ }
8804
+ for (let i = 0; i < a.length; i++) {
8805
+ if (!isJsonEq(a[i], b[i])) {
8806
+ return false;
8807
+ }
8808
+ }
8809
+ return true;
8810
+ }
8811
+ const aKeys = Object.keys(a);
8812
+ if (aKeys.length !== Object.keys(b).length) {
8813
+ return false;
8814
+ }
8815
+ for (const key of aKeys) {
8816
+ if (!isJsonEq(a[key], b[key])) {
8817
+ return false;
8818
+ }
8819
+ }
8820
+ return true;
8821
+ }
8545
8822
  function getTreesDiffOperations(currentItems, newItems) {
8546
8823
  const ops = [];
8547
8824
  currentItems.forEach((_, id) => {
@@ -8553,12 +8830,28 @@ function getTreesDiffOperations(currentItems, newItems) {
8553
8830
  const currentCrdt = currentItems.get(id);
8554
8831
  if (currentCrdt) {
8555
8832
  if (crdt.type === CrdtType.OBJECT) {
8556
- if (currentCrdt.type !== CrdtType.OBJECT || stringifyOrLog(crdt.data) !== stringifyOrLog(currentCrdt.data)) {
8557
- ops.push({
8558
- type: OpCode.UPDATE_OBJECT,
8559
- id,
8560
- data: crdt.data
8561
- });
8833
+ if (currentCrdt.type !== CrdtType.OBJECT) {
8834
+ ops.push({ type: OpCode.UPDATE_OBJECT, id, data: crdt.data });
8835
+ } else {
8836
+ const changed = /* @__PURE__ */ new Map();
8837
+ for (const key of Object.keys(crdt.data)) {
8838
+ const value = crdt.data[key];
8839
+ if (value !== void 0 && !isJsonEq(value, currentCrdt.data[key])) {
8840
+ changed.set(key, value);
8841
+ }
8842
+ }
8843
+ if (changed.size > 0) {
8844
+ ops.push({
8845
+ type: OpCode.UPDATE_OBJECT,
8846
+ id,
8847
+ data: Object.fromEntries(changed)
8848
+ });
8849
+ }
8850
+ for (const key of Object.keys(currentCrdt.data)) {
8851
+ if (!(key in crdt.data)) {
8852
+ ops.push({ type: OpCode.DELETE_OBJECT_KEY, id, key });
8853
+ }
8854
+ }
8562
8855
  }
8563
8856
  }
8564
8857
  if (crdt.parentKey !== currentCrdt.parentKey) {
@@ -9551,6 +9844,7 @@ function createRoom(options, config) {
9551
9844
  delegates,
9552
9845
  config.enableDebugLogging
9553
9846
  );
9847
+ const unacknowledgedOps = new UnacknowledgedOps();
9554
9848
  const context = {
9555
9849
  buffer: {
9556
9850
  flushTimerID: void 0,
@@ -9578,14 +9872,15 @@ function createRoom(options, config) {
9578
9872
  pool: createManagedPool(roomId, {
9579
9873
  getCurrentConnectionId,
9580
9874
  onDispatch,
9581
- isStorageWritable
9875
+ isStorageWritable,
9876
+ unacknowledgedOps
9582
9877
  }),
9583
9878
  root: void 0,
9584
9879
  undoStack: [],
9585
9880
  redoStack: [],
9586
9881
  pausedHistory: null,
9587
9882
  activeBatch: null,
9588
- unacknowledgedOps: /* @__PURE__ */ new Map()
9883
+ unacknowledgedOps
9589
9884
  };
9590
9885
  const nodeMapBuffer = makeNodeMapBuffer();
9591
9886
  const stopwatch = config.enableDebugLogging ? makeStopWatch() : void 0;
@@ -9652,6 +9947,7 @@ function createRoom(options, config) {
9652
9947
  }
9653
9948
  function onDidDisconnect() {
9654
9949
  clearTimeout(context.buffer.flushTimerID);
9950
+ context.unacknowledgedOps.markAllAsPossiblyStored();
9655
9951
  }
9656
9952
  managedSocket.events.onMessage.subscribe(handleServerMessage);
9657
9953
  managedSocket.events.statusDidChange.subscribe(onStatusDidChange);
@@ -10132,12 +10428,11 @@ function createRoom(options, config) {
10132
10428
  }
10133
10429
  }
10134
10430
  function applyAndSendOfflineOps(unackedOps) {
10135
- if (unackedOps.size === 0) {
10431
+ if (unackedOps.length === 0) {
10136
10432
  return;
10137
10433
  }
10138
10434
  const messages = [];
10139
- const inOps = Array.from(unackedOps.values());
10140
- const result = applyLocalOps(inOps);
10435
+ const result = applyLocalOps(unackedOps);
10141
10436
  messages.push({
10142
10437
  type: ClientMsgCode.UPDATE_STORAGE,
10143
10438
  ops: result.opsToEmit
@@ -10361,7 +10656,7 @@ function createRoom(options, config) {
10361
10656
  const storageOps = context.buffer.storageOperations;
10362
10657
  if (storageOps.length > 0) {
10363
10658
  for (const op of storageOps) {
10364
- context.unacknowledgedOps.set(op.opId, op);
10659
+ context.unacknowledgedOps.add(op);
10365
10660
  }
10366
10661
  notifyStorageStatus();
10367
10662
  }
@@ -10588,9 +10883,9 @@ function createRoom(options, config) {
10588
10883
  }
10589
10884
  }
10590
10885
  function processInitialStorage(nodes) {
10591
- const unacknowledgedOps = new Map(context.unacknowledgedOps);
10886
+ const unacknowledgedOps2 = [...context.unacknowledgedOps.values()];
10592
10887
  createOrUpdateRootFromMessage(nodes);
10593
- applyAndSendOfflineOps(unacknowledgedOps);
10888
+ applyAndSendOfflineOps(unacknowledgedOps2);
10594
10889
  _resolveStoragePromise?.();
10595
10890
  notifyStorageStatus();
10596
10891
  eventHub.storageDidLoad.notify();
@@ -11179,6 +11474,11 @@ function createRoom(options, config) {
11179
11474
  connect: () => managedSocket.connect(),
11180
11475
  reconnect: () => managedSocket.reconnect(),
11181
11476
  disconnect: () => managedSocket.disconnect(),
11477
+ _dump: () => {
11478
+ const n = context.pool.nodes.size;
11479
+ return `Room "${roomId}" (${n} node${n === 1 ? "" : "s"}):
11480
+ ${dumpPool(context.pool)}`;
11481
+ },
11182
11482
  destroy: () => {
11183
11483
  pendingFeedsRequests.forEach(
11184
11484
  (request) => request.reject(new Error("Room destroyed"))
@@ -11692,6 +11992,7 @@ function createClient(options) {
11692
11992
  {
11693
11993
  enterRoom,
11694
11994
  getRoom,
11995
+ _dump: () => Array.from(roomsById.values(), ({ room }) => room._dump()).join("\n\n"),
11695
11996
  logout,
11696
11997
  // Public inbox notifications API
11697
11998
  getInboxNotifications: httpClient.getInboxNotifications,
@@ -12362,6 +12663,7 @@ export {
12362
12663
  createManagedPool,
12363
12664
  createNotificationSettings,
12364
12665
  createThreadId,
12666
+ deepLiveify,
12365
12667
  defineAiTool,
12366
12668
  deprecate,
12367
12669
  deprecateIf,