@liveblocks/core 3.20.0-perm1 → 3.20.0-perm3

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-perm3";
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)) {
@@ -5297,29 +5311,16 @@ function hasPermissionCapabilityAccess(capabilities, resource, requiredAccess) {
5297
5311
 
5298
5312
  // src/permissions.ts
5299
5313
  var VALID_PERMISSIONS = new Set(Object.values(Permission));
5300
- var DEFAULT_PERMISSIONS = [
5301
- Permission.RoomRead,
5302
- Permission.RoomWrite
5303
- ];
5304
5314
  var ROOM_PERMISSION_OBJECT_KEYS = /* @__PURE__ */ new Set([
5305
5315
  "default",
5306
5316
  ...ROOM_PERMISSION_RESOURCES
5307
5317
  ]);
5308
- var RESOURCE_SPECIFIC_PERMISSIONS_BY_RESOURCE = {
5309
- presence: Object.values(RESOURCE_PERMISSIONS.presence).flat(),
5310
- storage: Object.values(RESOURCE_PERMISSIONS.storage).flat(),
5311
- comments: Object.values(RESOURCE_PERMISSIONS.comments).flat(),
5312
- feeds: Object.values(RESOURCE_PERMISSIONS.feeds).flat()
5313
- };
5314
- var RESOURCE_SPECIFIC_PERMISSIONS = ROOM_PERMISSION_RESOURCES.flatMap(
5315
- (resource) => RESOURCE_SPECIFIC_PERMISSIONS_BY_RESOURCE[resource]
5316
- );
5317
- function permissionForAccessLevel(resource, access) {
5318
+ function permissionForAccessLevel(resource, access, field = resource) {
5318
5319
  const levels = RESOURCE_PERMISSIONS[resource];
5319
5320
  const permissions = levels[access];
5320
5321
  if (permissions === void 0 || permissions.length === 0) {
5321
5322
  throw new Error(
5322
- `Invalid permission level for ${resource}: ${String(access)}`
5323
+ `Invalid permission level for ${field}: ${JSON.stringify(access) ?? String(access)}`
5323
5324
  );
5324
5325
  }
5325
5326
  return permissions[0];
@@ -5379,7 +5380,11 @@ function normalizeRoomPermissionObject(objectInput) {
5379
5380
  const permissions = [];
5380
5381
  if (objectInput.default !== void 0) {
5381
5382
  permissions.push(
5382
- permissionForAccessLevel(DEFAULT_PERMISSION_RESOURCE, objectInput.default)
5383
+ permissionForAccessLevel(
5384
+ DEFAULT_PERMISSION_RESOURCE,
5385
+ objectInput.default,
5386
+ "default"
5387
+ )
5383
5388
  );
5384
5389
  }
5385
5390
  for (const resource of ROOM_PERMISSION_RESOURCES) {
@@ -5415,17 +5420,37 @@ function normalizeRoomAccessesUpdateInput(input) {
5415
5420
  ])
5416
5421
  );
5417
5422
  }
5418
- function getRoomPermissionConflicts(permission) {
5419
- if (DEFAULT_PERMISSIONS.includes(permission)) {
5420
- return [...DEFAULT_PERMISSIONS, ...RESOURCE_SPECIFIC_PERMISSIONS];
5423
+ function mergePermissionCapabilities(sources) {
5424
+ return {
5425
+ creation: strongestCapabilityAccess(sources, "creation"),
5426
+ presence: strongestCapabilityAccess(sources, "presence"),
5427
+ storage: strongestCapabilityAccess(sources, "storage"),
5428
+ comments: strongestCapabilityAccess(sources, "comments"),
5429
+ feeds: strongestCapabilityAccess(sources, "feeds"),
5430
+ personal: "write"
5431
+ };
5432
+ }
5433
+ function permissionCapabilitiesToScopes(capabilities) {
5434
+ const scopes = [];
5435
+ const baseAccess = capabilities.creation;
5436
+ if (baseAccess !== "none") {
5437
+ scopes.push(
5438
+ permissionForAccessLevel(DEFAULT_PERMISSION_RESOURCE, baseAccess)
5439
+ );
5421
5440
  }
5422
- for (const resource of ROOM_PERMISSION_RESOURCES) {
5423
- const permissions = RESOURCE_SPECIFIC_PERMISSIONS_BY_RESOURCE[resource];
5424
- if (permissions.includes(permission)) {
5425
- return permissions;
5441
+ for (const capability of ROOM_PERMISSION_RESOURCES) {
5442
+ const access = capabilities[capability];
5443
+ if (access !== baseAccess) {
5444
+ scopes.push(permissionForAccessLevel(capability, access));
5426
5445
  }
5427
5446
  }
5428
- return [];
5447
+ return scopes;
5448
+ }
5449
+ function strongestCapabilityAccess(sources, resource) {
5450
+ return sources.reduce(
5451
+ (strongest, source) => strongestAccess(strongest, source[resource]),
5452
+ "none"
5453
+ );
5429
5454
  }
5430
5455
  function strongestAccess(left, right) {
5431
5456
  return ACCESS_RANKS[right] > ACCESS_RANKS[left] ? right : left;
@@ -5723,6 +5748,9 @@ var OpCode = Object.freeze({
5723
5748
  function isIgnoredOp(op) {
5724
5749
  return op.type === OpCode.DELETE_CRDT && op.id === "ACK";
5725
5750
  }
5751
+ function isCreateOp(op) {
5752
+ return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST;
5753
+ }
5726
5754
 
5727
5755
  // src/protocol/StorageNode.ts
5728
5756
  var CrdtType = Object.freeze({
@@ -5980,12 +6008,112 @@ function asPos(str) {
5980
6008
  return isPos(str) ? str : convertToPos(str);
5981
6009
  }
5982
6010
 
6011
+ // src/crdts/UnacknowledgedOps.ts
6012
+ var UnacknowledgedOps = class {
6013
+ // opId -> op
6014
+ #byOpId = /* @__PURE__ */ new Map();
6015
+ // position -> (opId -> Create op)
6016
+ #createOpsByPosition = /* @__PURE__ */ new Map();
6017
+ // parentId -> (opId -> Create op)
6018
+ #createOpsByParent = /* @__PURE__ */ new Map();
6019
+ // opIds of pending ops that were in flight when a connection died, so the
6020
+ // server may already have processed them. See isPossiblyStored().
6021
+ #possiblyStoredOpIds = /* @__PURE__ */ new Set();
6022
+ #posKey(parentId, parentKey) {
6023
+ return `${parentId}
6024
+ ${parentKey}`;
6025
+ }
6026
+ get size() {
6027
+ return this.#byOpId.size;
6028
+ }
6029
+ /**
6030
+ * Mark the given Op as still unacknowledged.
6031
+ */
6032
+ add(op) {
6033
+ this.#byOpId.set(op.opId, op);
6034
+ if (isCreateOp(op)) {
6035
+ const posKey = this.#posKey(op.parentId, op.parentKey);
6036
+ let atPosition = this.#createOpsByPosition.get(posKey);
6037
+ if (atPosition === void 0) {
6038
+ atPosition = /* @__PURE__ */ new Map();
6039
+ this.#createOpsByPosition.set(posKey, atPosition);
6040
+ }
6041
+ atPosition.set(op.opId, op);
6042
+ let inParent = this.#createOpsByParent.get(op.parentId);
6043
+ if (inParent === void 0) {
6044
+ inParent = /* @__PURE__ */ new Map();
6045
+ this.#createOpsByParent.set(op.parentId, inParent);
6046
+ }
6047
+ inParent.set(op.opId, op);
6048
+ }
6049
+ }
6050
+ /**
6051
+ * Drop the op with the given opId from the set, because the server has
6052
+ * acknowledged it (confirmed our own op, or signalled it was seen but
6053
+ * ignored).
6054
+ */
6055
+ delete(opId) {
6056
+ const op = this.#byOpId.get(opId);
6057
+ if (op === void 0) {
6058
+ return;
6059
+ }
6060
+ this.#byOpId.delete(opId);
6061
+ this.#possiblyStoredOpIds.delete(opId);
6062
+ if (isCreateOp(op)) {
6063
+ const posKey = this.#posKey(op.parentId, op.parentKey);
6064
+ const atPosition = this.#createOpsByPosition.get(posKey);
6065
+ atPosition?.delete(opId);
6066
+ if (atPosition !== void 0 && atPosition.size === 0) {
6067
+ this.#createOpsByPosition.delete(posKey);
6068
+ }
6069
+ const inParent = this.#createOpsByParent.get(op.parentId);
6070
+ inParent?.delete(opId);
6071
+ if (inParent !== void 0 && inParent.size === 0) {
6072
+ this.#createOpsByParent.delete(op.parentId);
6073
+ }
6074
+ }
6075
+ }
6076
+ /**
6077
+ * The still-unacknowledged Create ops with the given `parentId` and
6078
+ * `parentKey` (targeting one exact position), in dispatch order. O(1) lookup.
6079
+ * Empty if none.
6080
+ */
6081
+ getByParentIdAndKey(parentId, parentKey) {
6082
+ return this.#createOpsByPosition.get(this.#posKey(parentId, parentKey))?.values() ?? [];
6083
+ }
6084
+ /**
6085
+ * The still-unacknowledged Create ops with the given `parentId` (across all
6086
+ * positions), in dispatch order. O(1) lookup. Empty if none.
6087
+ */
6088
+ getByParentId(parentId) {
6089
+ return this.#createOpsByParent.get(parentId)?.values() ?? [];
6090
+ }
6091
+ /** All still-unacknowledged ops, in dispatch order. */
6092
+ values() {
6093
+ return this.#byOpId.values();
6094
+ }
6095
+ isPossiblyStored(opId) {
6096
+ return this.#possiblyStoredOpIds.has(opId);
6097
+ }
6098
+ /**
6099
+ * Mark every currently pending op as possibly stored on the server. Called
6100
+ * when the connection dies: all of these ops were in flight, and their
6101
+ * (possibly lost) acks would have been the only way to know their fate.
6102
+ */
6103
+ markAllAsPossiblyStored() {
6104
+ for (const opId of this.#byOpId.keys()) {
6105
+ this.#possiblyStoredOpIds.add(opId);
6106
+ }
6107
+ }
6108
+ };
6109
+
5983
6110
  // src/crdts/AbstractCrdt.ts
5984
6111
  function createManagedPool(roomId, options) {
5985
6112
  const {
5986
6113
  getCurrentConnectionId,
5987
6114
  onDispatch,
5988
- isStorageWritable = () => true
6115
+ isStorageWritable = () => true,
6116
+ unacknowledgedOps = new UnacknowledgedOps()
5989
6117
  } = options;
5990
6118
  let clock = 0;
5991
6119
  let opClock = 0;
@@ -6007,7 +6135,8 @@ function createManagedPool(roomId, options) {
6007
6135
  "Cannot write to storage with a read only user, please ensure the user has write permissions"
6008
6136
  );
6009
6137
  }
6010
- }
6138
+ },
6139
+ unacknowledgedOps
6011
6140
  };
6012
6141
  }
6013
6142
  function crdtAsLiveNode(value) {
@@ -6289,11 +6418,9 @@ function childNodeLt(a, b) {
6289
6418
  var LiveList = class _LiveList extends AbstractCrdt {
6290
6419
  #items;
6291
6420
  #implicitlyDeletedItems;
6292
- #unacknowledgedSets;
6293
6421
  constructor(items) {
6294
6422
  super();
6295
6423
  this.#implicitlyDeletedItems = /* @__PURE__ */ new WeakSet();
6296
- this.#unacknowledgedSets = /* @__PURE__ */ new Map();
6297
6424
  const nodes = [];
6298
6425
  let lastPos;
6299
6426
  for (const item of items) {
@@ -6323,12 +6450,13 @@ var LiveList = class _LiveList extends AbstractCrdt {
6323
6450
  }
6324
6451
  /**
6325
6452
  * @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
6453
+ * Serializes this list (and its children) into Create ops. Each child's
6454
+ * create is tagged with the "set" intent (in the loop below) so that a list
6455
+ * created and immediately mutated doesn't transiently re-show its initial
6456
+ * items (flicker, https://github.com/liveblocks/liveblocks/pull/1177).
6329
6457
  *
6330
- * This is quite unintuitive and should disappear as soon as
6331
- * we introduce an explicit LiveList.Set operation
6458
+ * This is quite unintuitive and should disappear as soon as we introduce an
6459
+ * explicit LiveList.Set operation.
6332
6460
  */
6333
6461
  _toOps(parentId, parentKey) {
6334
6462
  if (this._id === void 0) {
@@ -6344,9 +6472,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6344
6472
  ops.push(op);
6345
6473
  for (const item of this.#items) {
6346
6474
  const parentKey2 = item._getParentKeyOrThrow();
6347
- const childOps = HACK_addIntentAndDeletedIdToOperation(
6475
+ const childOps = addIntentToRootOp(
6348
6476
  item._toOps(this._id, parentKey2),
6349
- void 0
6477
+ "set"
6350
6478
  );
6351
6479
  for (const childOp of childOps) {
6352
6480
  ops.push(childOp);
@@ -6388,6 +6516,28 @@ var LiveList = class _LiveList extends AbstractCrdt {
6388
6516
  (item) => item._getParentKeyOrThrow() === position
6389
6517
  );
6390
6518
  }
6519
+ /**
6520
+ * The opId of this list's still-unacknowledged "set" op at the given position,
6521
+ * or undefined if none. Derived from the room's unacknowledgedOps (the single
6522
+ * source of truth) rather than tracked in a per-instance map. The pool's
6523
+ * position index already scopes to this list's (parentId, position); the last
6524
+ * match wins, matching the original last-write-wins map semantics.
6525
+ */
6526
+ #unacknowledgedSetOpIdAt(position) {
6527
+ if (this._pool === void 0 || this._id === void 0) {
6528
+ return void 0;
6529
+ }
6530
+ let opId;
6531
+ for (const op of this._pool.unacknowledgedOps.getByParentIdAndKey(
6532
+ this._id,
6533
+ position
6534
+ )) {
6535
+ if (op.intent === "set") {
6536
+ opId = op.opId;
6537
+ }
6538
+ }
6539
+ return opId;
6540
+ }
6391
6541
  /** @internal */
6392
6542
  _attach(id, pool) {
6393
6543
  super._attach(id, pool);
@@ -6468,13 +6618,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6468
6618
  if (deletedDelta) {
6469
6619
  delta.push(deletedDelta);
6470
6620
  }
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
- }
6621
+ const unacknowledgedOpId = this.#unacknowledgedSetOpIdAt(op.parentKey);
6622
+ if (unacknowledgedOpId !== void 0 && unacknowledgedOpId !== op.opId) {
6623
+ return delta.length === 0 ? { modified: false } : { modified: makeUpdate(this, delta), reverse: [] };
6478
6624
  }
6479
6625
  const indexOfItemWithSamePosition = this._indexOfPosition(op.parentKey);
6480
6626
  const existingItem = this.#items.find((item) => item._id === op.id);
@@ -6565,11 +6711,92 @@ var LiveList = class _LiveList extends AbstractCrdt {
6565
6711
  this.#shiftItemPosition(existingItemIndex, key);
6566
6712
  }
6567
6713
  const { newItem, newIndex } = this.#createAttachItemAndSort(op, key);
6714
+ const bumpDeltas = this.#bumpUnackedPushesAbove(key);
6568
6715
  return {
6569
- modified: makeUpdate(this, [insertDelta(newIndex, newItem)]),
6716
+ modified: makeUpdate(this, [
6717
+ insertDelta(newIndex, newItem),
6718
+ ...bumpDeltas
6719
+ ]),
6570
6720
  reverse: []
6571
6721
  };
6572
6722
  }
6723
+ /**
6724
+ * This list's own still-unacknowledged pushed items (their `intent: "push"`
6725
+ * Create op is still pending in the room's unacknowledgedOps). Derived from
6726
+ * the single source of truth, so an item drops out the instant its op is
6727
+ * acked, with no per-instance membership to leak. Yielded in push order.
6728
+ *
6729
+ * Excludes ops that may already be stored on the server (they were in
6730
+ * flight when a connection died, so their fate is unknown): the bump
6731
+ * prediction assumes the server has not processed the op yet, which is only
6732
+ * guaranteed for ops sent on the current connection. For these excluded
6733
+ * ops, the server's (re-)ack states the authoritative position; predicting
6734
+ * locally could produce a wrong position that no ack would correct.
6735
+ *
6736
+ * Restricted to items currently in `#items`: a pushed node whose op is still
6737
+ * pending may have been pulled out of the list (e.g. implicitly deleted by a
6738
+ * remote set, or removed by an undo) while still living in the pool, and such
6739
+ * a node must not be repositioned.
6740
+ */
6741
+ *#unackedPushNodes() {
6742
+ if (this._pool === void 0 || this._id === void 0) {
6743
+ return;
6744
+ }
6745
+ for (const op of this._pool.unacknowledgedOps.getByParentId(this._id)) {
6746
+ if (op.intent !== "push") {
6747
+ continue;
6748
+ }
6749
+ if (this._pool.unacknowledgedOps.isPossiblyStored(op.opId)) {
6750
+ continue;
6751
+ }
6752
+ const node = this._pool.getNode(op.id);
6753
+ if (node !== void 0 && this.#items.includes(node)) {
6754
+ yield node;
6755
+ }
6756
+ }
6757
+ }
6758
+ /**
6759
+ * Optimistic no-flip for pushed items. When a remote op lands at or below my
6760
+ * still-unacked pushed items, those items must end up *after* it: FIFO plus
6761
+ * the room's serial processing guarantee the remote was processed first, so
6762
+ * my unacked pushes belong behind it. Re-chain the whole unacked-push block,
6763
+ * in push order, to sit after the highest confirmed sibling, so it keeps
6764
+ * rendering as a contiguous tail instead of getting interleaved. Local-only;
6765
+ * the real acks overwrite these keys with the (identical) server keys.
6766
+ */
6767
+ #bumpUnackedPushesAbove(remoteKey) {
6768
+ const pending = new Set(this.#unackedPushNodes());
6769
+ if (pending.size === 0) {
6770
+ return [];
6771
+ }
6772
+ let minPending;
6773
+ for (const node of pending) {
6774
+ const pos = node._parentPos;
6775
+ if (minPending === void 0 || pos < minPending) {
6776
+ minPending = pos;
6777
+ }
6778
+ }
6779
+ if (remoteKey < nn(minPending)) {
6780
+ return [];
6781
+ }
6782
+ let base;
6783
+ for (const item of this.#items) {
6784
+ if (!pending.has(item)) {
6785
+ base = item._parentPos;
6786
+ }
6787
+ }
6788
+ const deltas = [];
6789
+ for (const node of pending) {
6790
+ const previousIndex = this.#items.findIndex((item) => item === node);
6791
+ base = makePosition(base);
6792
+ this.#updateItemPosition(node, base);
6793
+ const index = this.#items.findIndex((item) => item === node);
6794
+ if (index !== previousIndex) {
6795
+ deltas.push(moveDelta(previousIndex, index, node));
6796
+ }
6797
+ }
6798
+ return deltas;
6799
+ }
6573
6800
  #applyInsertAck(op) {
6574
6801
  const existingItem = this.#items.find((item) => item._id === op.id);
6575
6802
  const key = asPos(op.parentKey);
@@ -6650,7 +6877,6 @@ var LiveList = class _LiveList extends AbstractCrdt {
6650
6877
  if (this._pool?.getNode(id) !== void 0) {
6651
6878
  return { modified: false };
6652
6879
  }
6653
- this.#unacknowledgedSets.set(key, nn(op.opId));
6654
6880
  const indexOfItemWithSameKey = this._indexOfPosition(key);
6655
6881
  child._attach(id, nn(this._pool));
6656
6882
  child._setParentLink(this, key);
@@ -6660,8 +6886,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6660
6886
  existingItem._detach();
6661
6887
  this.#items.remove(existingItem);
6662
6888
  this.#items.add(child);
6663
- const reverse = HACK_addIntentAndDeletedIdToOperation(
6889
+ const reverse = addIntentToRootOp(
6664
6890
  existingItem._toOps(nn(this._id), key),
6891
+ "set",
6665
6892
  op.id
6666
6893
  );
6667
6894
  const delta = [setDelta(indexOfItemWithSameKey, child)];
@@ -6904,8 +7131,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6904
7131
  * @param element The element to add to the end of the LiveList.
6905
7132
  */
6906
7133
  push(element) {
6907
- this._pool?.assertStorageIsWritable();
6908
- return this.insert(element, this.length);
7134
+ return this.#injectAt(element, this.length, "push");
6909
7135
  }
6910
7136
  /**
6911
7137
  * Inserts one element at a specified index.
@@ -6913,6 +7139,15 @@ var LiveList = class _LiveList extends AbstractCrdt {
6913
7139
  * @param index The index at which you want to insert the element.
6914
7140
  */
6915
7141
  insert(element, index) {
7142
+ return this.#injectAt(element, index, "insert");
7143
+ }
7144
+ /**
7145
+ * Shared implementation of `insert` and `push`. A `"push"` intent leaves the
7146
+ * client-computed position untouched (so optimistic rendering is unchanged),
7147
+ * but tags the Op so the server appends it to the true end of the list
7148
+ * instead of resolving its position against the client's stale view.
7149
+ */
7150
+ #injectAt(element, index, intent) {
6916
7151
  this._pool?.assertStorageIsWritable();
6917
7152
  if (index < 0 || index > this.#items.length) {
6918
7153
  throw new Error(
@@ -6928,8 +7163,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6928
7163
  if (this._pool && this._id) {
6929
7164
  const id = this._pool.generateId();
6930
7165
  value._attach(id, this._pool);
7166
+ const ops = value._toOpsWithOpId(this._id, position, this._pool);
6931
7167
  this._pool.dispatch(
6932
- value._toOpsWithOpId(this._id, position, this._pool),
7168
+ intent === "push" ? addIntentToRootOp(ops, "push") : ops,
6933
7169
  [{ type: OpCode.DELETE_CRDT, id }],
6934
7170
  /* @__PURE__ */ new Map([
6935
7171
  [this._id, makeUpdate(this, [insertDelta(index, value)])]
@@ -7087,13 +7323,14 @@ var LiveList = class _LiveList extends AbstractCrdt {
7087
7323
  value._attach(id, this._pool);
7088
7324
  const storageUpdates = /* @__PURE__ */ new Map();
7089
7325
  storageUpdates.set(this._id, makeUpdate(this, [setDelta(index, value)]));
7090
- const ops = HACK_addIntentAndDeletedIdToOperation(
7326
+ const ops = addIntentToRootOp(
7091
7327
  value._toOpsWithOpId(this._id, position, this._pool),
7328
+ "set",
7092
7329
  existingId
7093
7330
  );
7094
- this.#unacknowledgedSets.set(position, nn(ops[0].opId));
7095
- const reverseOps = HACK_addIntentAndDeletedIdToOperation(
7331
+ const reverseOps = addIntentToRootOp(
7096
7332
  existingItem._toOps(this._id, position),
7333
+ "set",
7097
7334
  id
7098
7335
  );
7099
7336
  this._pool.dispatch(ops, reverseOps, storageUpdates);
@@ -7294,15 +7531,11 @@ function moveDelta(previousIndex, index, item) {
7294
7531
  previousIndex
7295
7532
  };
7296
7533
  }
7297
- function HACK_addIntentAndDeletedIdToOperation(ops, deletedId) {
7534
+ function addIntentToRootOp(ops, intent, deletedId) {
7298
7535
  return ops.map((op, index) => {
7299
7536
  if (index === 0) {
7300
7537
  const firstOp = op;
7301
- return {
7302
- ...firstOp,
7303
- intent: "set",
7304
- deletedId
7305
- };
7538
+ return { ...firstOp, intent, deletedId };
7306
7539
  } else {
7307
7540
  return op;
7308
7541
  }
@@ -7954,6 +8187,7 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
7954
8187
  const id = nn(this._id);
7955
8188
  const parentKey = nn(child._parentKey);
7956
8189
  const reverse = child._toOps(id, parentKey);
8190
+ const deletedItem = liveNodeToLson(child);
7957
8191
  for (const [key, value] of this.#synced) {
7958
8192
  if (value === child) {
7959
8193
  this.#synced.delete(key);
@@ -7965,7 +8199,7 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
7965
8199
  node: this,
7966
8200
  type: "LiveObject",
7967
8201
  updates: {
7968
- [parentKey]: { type: "delete" }
8202
+ [parentKey]: { type: "delete", deletedItem }
7969
8203
  }
7970
8204
  };
7971
8205
  return { modified: storageUpdate, reverse };
@@ -8542,6 +8776,60 @@ function lsonToLiveNode(value) {
8542
8776
  return new LiveRegister(value);
8543
8777
  }
8544
8778
  }
8779
+ function dumpPool(pool) {
8780
+ const rows = Array.from(pool.nodes.values(), (node) => {
8781
+ const parent = node.parent;
8782
+ const parentId = parent.type === "HasParent" ? parent.node._id ?? "?" : parent.type === "Orphaned" ? "<orphaned>" : "-";
8783
+ let value;
8784
+ if (node instanceof LiveRegister) {
8785
+ value = stringifyOrLog(node.data);
8786
+ } else if (node instanceof LiveList) {
8787
+ value = "<LiveList>";
8788
+ } else if (node instanceof LiveMap) {
8789
+ value = "<LiveMap>";
8790
+ } else {
8791
+ value = "<LiveObject>";
8792
+ }
8793
+ return { id: nn(node._id), parentId, key: node._parentKey ?? "", value };
8794
+ });
8795
+ rows.sort((a, b) => {
8796
+ if (a.parentId !== b.parentId) return a.parentId < b.parentId ? -1 : 1;
8797
+ if (a.key !== b.key) return a.key < b.key ? -1 : 1;
8798
+ return 0;
8799
+ });
8800
+ return rows.map(
8801
+ (r) => ` ${r.id} parent=${r.parentId} key=${r.key || "\u2014"} ${r.value}`
8802
+ ).join("\n");
8803
+ }
8804
+ function isJsonEq(a, b) {
8805
+ if (a === b) {
8806
+ return true;
8807
+ }
8808
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
8809
+ return false;
8810
+ }
8811
+ if (Array.isArray(a) || Array.isArray(b)) {
8812
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
8813
+ return false;
8814
+ }
8815
+ for (let i = 0; i < a.length; i++) {
8816
+ if (!isJsonEq(a[i], b[i])) {
8817
+ return false;
8818
+ }
8819
+ }
8820
+ return true;
8821
+ }
8822
+ const aKeys = Object.keys(a);
8823
+ if (aKeys.length !== Object.keys(b).length) {
8824
+ return false;
8825
+ }
8826
+ for (const key of aKeys) {
8827
+ if (!isJsonEq(a[key], b[key])) {
8828
+ return false;
8829
+ }
8830
+ }
8831
+ return true;
8832
+ }
8545
8833
  function getTreesDiffOperations(currentItems, newItems) {
8546
8834
  const ops = [];
8547
8835
  currentItems.forEach((_, id) => {
@@ -8553,12 +8841,28 @@ function getTreesDiffOperations(currentItems, newItems) {
8553
8841
  const currentCrdt = currentItems.get(id);
8554
8842
  if (currentCrdt) {
8555
8843
  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
- });
8844
+ if (currentCrdt.type !== CrdtType.OBJECT) {
8845
+ ops.push({ type: OpCode.UPDATE_OBJECT, id, data: crdt.data });
8846
+ } else {
8847
+ const changed = /* @__PURE__ */ new Map();
8848
+ for (const key of Object.keys(crdt.data)) {
8849
+ const value = crdt.data[key];
8850
+ if (value !== void 0 && !isJsonEq(value, currentCrdt.data[key])) {
8851
+ changed.set(key, value);
8852
+ }
8853
+ }
8854
+ if (changed.size > 0) {
8855
+ ops.push({
8856
+ type: OpCode.UPDATE_OBJECT,
8857
+ id,
8858
+ data: Object.fromEntries(changed)
8859
+ });
8860
+ }
8861
+ for (const key of Object.keys(currentCrdt.data)) {
8862
+ if (!(key in crdt.data)) {
8863
+ ops.push({ type: OpCode.DELETE_OBJECT_KEY, id, key });
8864
+ }
8865
+ }
8562
8866
  }
8563
8867
  }
8564
8868
  if (crdt.parentKey !== currentCrdt.parentKey) {
@@ -9551,6 +9855,7 @@ function createRoom(options, config) {
9551
9855
  delegates,
9552
9856
  config.enableDebugLogging
9553
9857
  );
9858
+ const unacknowledgedOps = new UnacknowledgedOps();
9554
9859
  const context = {
9555
9860
  buffer: {
9556
9861
  flushTimerID: void 0,
@@ -9578,14 +9883,15 @@ function createRoom(options, config) {
9578
9883
  pool: createManagedPool(roomId, {
9579
9884
  getCurrentConnectionId,
9580
9885
  onDispatch,
9581
- isStorageWritable
9886
+ isStorageWritable,
9887
+ unacknowledgedOps
9582
9888
  }),
9583
9889
  root: void 0,
9584
9890
  undoStack: [],
9585
9891
  redoStack: [],
9586
9892
  pausedHistory: null,
9587
9893
  activeBatch: null,
9588
- unacknowledgedOps: /* @__PURE__ */ new Map()
9894
+ unacknowledgedOps
9589
9895
  };
9590
9896
  const nodeMapBuffer = makeNodeMapBuffer();
9591
9897
  const stopwatch = config.enableDebugLogging ? makeStopWatch() : void 0;
@@ -9652,6 +9958,7 @@ function createRoom(options, config) {
9652
9958
  }
9653
9959
  function onDidDisconnect() {
9654
9960
  clearTimeout(context.buffer.flushTimerID);
9961
+ context.unacknowledgedOps.markAllAsPossiblyStored();
9655
9962
  }
9656
9963
  managedSocket.events.onMessage.subscribe(handleServerMessage);
9657
9964
  managedSocket.events.statusDidChange.subscribe(onStatusDidChange);
@@ -10132,12 +10439,11 @@ function createRoom(options, config) {
10132
10439
  }
10133
10440
  }
10134
10441
  function applyAndSendOfflineOps(unackedOps) {
10135
- if (unackedOps.size === 0) {
10442
+ if (unackedOps.length === 0) {
10136
10443
  return;
10137
10444
  }
10138
10445
  const messages = [];
10139
- const inOps = Array.from(unackedOps.values());
10140
- const result = applyLocalOps(inOps);
10446
+ const result = applyLocalOps(unackedOps);
10141
10447
  messages.push({
10142
10448
  type: ClientMsgCode.UPDATE_STORAGE,
10143
10449
  ops: result.opsToEmit
@@ -10361,7 +10667,7 @@ function createRoom(options, config) {
10361
10667
  const storageOps = context.buffer.storageOperations;
10362
10668
  if (storageOps.length > 0) {
10363
10669
  for (const op of storageOps) {
10364
- context.unacknowledgedOps.set(op.opId, op);
10670
+ context.unacknowledgedOps.add(op);
10365
10671
  }
10366
10672
  notifyStorageStatus();
10367
10673
  }
@@ -10588,9 +10894,9 @@ function createRoom(options, config) {
10588
10894
  }
10589
10895
  }
10590
10896
  function processInitialStorage(nodes) {
10591
- const unacknowledgedOps = new Map(context.unacknowledgedOps);
10897
+ const unacknowledgedOps2 = [...context.unacknowledgedOps.values()];
10592
10898
  createOrUpdateRootFromMessage(nodes);
10593
- applyAndSendOfflineOps(unacknowledgedOps);
10899
+ applyAndSendOfflineOps(unacknowledgedOps2);
10594
10900
  _resolveStoragePromise?.();
10595
10901
  notifyStorageStatus();
10596
10902
  eventHub.storageDidLoad.notify();
@@ -11179,6 +11485,11 @@ function createRoom(options, config) {
11179
11485
  connect: () => managedSocket.connect(),
11180
11486
  reconnect: () => managedSocket.reconnect(),
11181
11487
  disconnect: () => managedSocket.disconnect(),
11488
+ _dump: () => {
11489
+ const n = context.pool.nodes.size;
11490
+ return `Room "${roomId}" (${n} node${n === 1 ? "" : "s"}):
11491
+ ${dumpPool(context.pool)}`;
11492
+ },
11182
11493
  destroy: () => {
11183
11494
  pendingFeedsRequests.forEach(
11184
11495
  (request) => request.reject(new Error("Room destroyed"))
@@ -11692,6 +12003,7 @@ function createClient(options) {
11692
12003
  {
11693
12004
  enterRoom,
11694
12005
  getRoom,
12006
+ _dump: () => Array.from(roomsById.values(), ({ room }) => room._dump()).join("\n\n"),
11695
12007
  logout,
11696
12008
  // Public inbox notifications API
11697
12009
  getInboxNotifications: httpClient.getInboxNotifications,
@@ -12362,6 +12674,7 @@ export {
12362
12674
  createManagedPool,
12363
12675
  createNotificationSettings,
12364
12676
  createThreadId,
12677
+ deepLiveify,
12365
12678
  defineAiTool,
12366
12679
  deprecate,
12367
12680
  deprecateIf,
@@ -12372,7 +12685,6 @@ export {
12372
12685
  freeze,
12373
12686
  generateUrl,
12374
12687
  getMentionsFromCommentBody,
12375
- getRoomPermissionConflicts,
12376
12688
  getSubscriptionKey,
12377
12689
  hasPermissionCapability,
12378
12690
  hasPermissionCapabilityAccess,
@@ -12403,6 +12715,7 @@ export {
12403
12715
  makePosition,
12404
12716
  mapValues,
12405
12717
  memoizeOnSuccess,
12718
+ mergePermissionCapabilities,
12406
12719
  nanoid,
12407
12720
  nn,
12408
12721
  nodeStreamToCompactNodes,
@@ -12412,6 +12725,7 @@ export {
12412
12725
  objectToQuery,
12413
12726
  patchNotificationSettings,
12414
12727
  permissionCapabilitiesFromScopes,
12728
+ permissionCapabilitiesToScopes,
12415
12729
  raise,
12416
12730
  resolveMentionsInCommentBody,
12417
12731
  sanitizeUrl,