@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.cjs +486 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -10
- package/dist/index.d.ts +63 -10
- package/dist/index.js +388 -74
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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-
|
|
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
|
-
|
|
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 ${
|
|
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(
|
|
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
|
|
5419
|
-
|
|
5420
|
-
|
|
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
|
|
5423
|
-
const
|
|
5424
|
-
if (
|
|
5425
|
-
|
|
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
|
-
*
|
|
6327
|
-
*
|
|
6328
|
-
*
|
|
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
|
-
*
|
|
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 =
|
|
6475
|
+
const childOps = addIntentToRootOp(
|
|
6348
6476
|
item._toOps(this._id, parentKey2),
|
|
6349
|
-
|
|
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.#
|
|
6472
|
-
if (unacknowledgedOpId !== void 0) {
|
|
6473
|
-
|
|
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, [
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
7326
|
+
const ops = addIntentToRootOp(
|
|
7091
7327
|
value._toOpsWithOpId(this._id, position, this._pool),
|
|
7328
|
+
"set",
|
|
7092
7329
|
existingId
|
|
7093
7330
|
);
|
|
7094
|
-
|
|
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
|
|
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
|
|
8557
|
-
ops.push({
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
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
|
|
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.
|
|
10442
|
+
if (unackedOps.length === 0) {
|
|
10136
10443
|
return;
|
|
10137
10444
|
}
|
|
10138
10445
|
const messages = [];
|
|
10139
|
-
const
|
|
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.
|
|
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
|
|
10897
|
+
const unacknowledgedOps2 = [...context.unacknowledgedOps.values()];
|
|
10592
10898
|
createOrUpdateRootFromMessage(nodes);
|
|
10593
|
-
applyAndSendOfflineOps(
|
|
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,
|