@liveblocks/core 3.20.0-pre1 → 3.20.0-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.cjs +366 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +268 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -519,6 +519,7 @@ type HasOpId = {
|
|
|
519
519
|
* acknowledge the receipt.
|
|
520
520
|
*/
|
|
521
521
|
type ClientWireOp = Op & HasOpId;
|
|
522
|
+
type ClientWireCreateOp = CreateOp & HasOpId;
|
|
522
523
|
/**
|
|
523
524
|
* ServerWireOp: Ops sent from server → client. Three variants:
|
|
524
525
|
* 1. ClientWireOp — Full echo back of our own op, confirming it was applied
|
|
@@ -864,6 +865,21 @@ type LiveListUpdate = LiveListUpdates<Lson>;
|
|
|
864
865
|
*/
|
|
865
866
|
type StorageUpdate = LiveMapUpdate | LiveObjectUpdate | LiveListUpdate;
|
|
866
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Read-only query surface over {@link UnacknowledgedOps}, handed to CRDTs so
|
|
870
|
+
* they can look up their own still-pending Create ops without being able to
|
|
871
|
+
* mutate the set (only the room adds/acks).
|
|
872
|
+
*/
|
|
873
|
+
interface ReadonlyUnacknowledgedOps {
|
|
874
|
+
/** Still-unacknowledged Create ops whose `parentId` is the given one. */
|
|
875
|
+
getByParentId(parentId: string): Iterable<ClientWireCreateOp>;
|
|
876
|
+
/**
|
|
877
|
+
* Still-unacknowledged Create ops whose `parentId` and `parentKey` are both
|
|
878
|
+
* the given ones (i.e. targeting one exact position).
|
|
879
|
+
*/
|
|
880
|
+
getByParentIdAndKey(parentId: string, parentKey: string): Iterable<ClientWireCreateOp>;
|
|
881
|
+
}
|
|
882
|
+
|
|
867
883
|
/**
|
|
868
884
|
* The managed pool is a namespace registry (i.e. a context) that "owns" all
|
|
869
885
|
* the individual live nodes, ensuring each one has a unique ID, and holding on
|
|
@@ -892,6 +908,11 @@ interface ManagedPool {
|
|
|
892
908
|
* @returns {void}
|
|
893
909
|
*/
|
|
894
910
|
assertStorageIsWritable: () => void;
|
|
911
|
+
/**
|
|
912
|
+
* Read-only view of the client's still-unacknowledged ops (sent or
|
|
913
|
+
* pending-send, not yet confirmed by the server).
|
|
914
|
+
*/
|
|
915
|
+
readonly unacknowledgedOps: ReadonlyUnacknowledgedOps;
|
|
895
916
|
}
|
|
896
917
|
type CreateManagedPoolOptions = {
|
|
897
918
|
/**
|
|
@@ -911,6 +932,13 @@ type CreateManagedPoolOptions = {
|
|
|
911
932
|
* have an effect upstream.
|
|
912
933
|
*/
|
|
913
934
|
isStorageWritable?: () => boolean;
|
|
935
|
+
/**
|
|
936
|
+
* Read-only view of the client's still-unacknowledged ops. Used by CRDTs
|
|
937
|
+
* (e.g. LiveList) to know which of their optimistic mutations the server
|
|
938
|
+
* hasn't confirmed yet. Defaults to an empty view (e.g. server-side pools
|
|
939
|
+
* that dispatch-and-flush have no optimistic state to track).
|
|
940
|
+
*/
|
|
941
|
+
unacknowledgedOps?: ReadonlyUnacknowledgedOps;
|
|
914
942
|
};
|
|
915
943
|
/**
|
|
916
944
|
* @private Private API, never use this API directly.
|
|
@@ -5457,6 +5485,13 @@ declare class SortedList<T> {
|
|
|
5457
5485
|
reposition(value: T): number;
|
|
5458
5486
|
at(index: number): T | undefined;
|
|
5459
5487
|
get length(): number;
|
|
5488
|
+
/**
|
|
5489
|
+
* Whether the given value is present, by identity. O(log n) plus the length
|
|
5490
|
+
* of any run of items that share its sort key (normally 1). Bisects on the
|
|
5491
|
+
* value's own key, so it only finds values sitting at their sorted position,
|
|
5492
|
+
* which is true for any item currently in the list.
|
|
5493
|
+
*/
|
|
5494
|
+
includes(value: T): boolean;
|
|
5460
5495
|
filter(predicate: (value: T) => boolean): IterableIterator<T>;
|
|
5461
5496
|
findAllRight(predicate: (value: T, index: number) => unknown): IterableIterator<T>;
|
|
5462
5497
|
[Symbol.iterator](): IterableIterator<T>;
|
package/dist/index.d.ts
CHANGED
|
@@ -519,6 +519,7 @@ type HasOpId = {
|
|
|
519
519
|
* acknowledge the receipt.
|
|
520
520
|
*/
|
|
521
521
|
type ClientWireOp = Op & HasOpId;
|
|
522
|
+
type ClientWireCreateOp = CreateOp & HasOpId;
|
|
522
523
|
/**
|
|
523
524
|
* ServerWireOp: Ops sent from server → client. Three variants:
|
|
524
525
|
* 1. ClientWireOp — Full echo back of our own op, confirming it was applied
|
|
@@ -864,6 +865,21 @@ type LiveListUpdate = LiveListUpdates<Lson>;
|
|
|
864
865
|
*/
|
|
865
866
|
type StorageUpdate = LiveMapUpdate | LiveObjectUpdate | LiveListUpdate;
|
|
866
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Read-only query surface over {@link UnacknowledgedOps}, handed to CRDTs so
|
|
870
|
+
* they can look up their own still-pending Create ops without being able to
|
|
871
|
+
* mutate the set (only the room adds/acks).
|
|
872
|
+
*/
|
|
873
|
+
interface ReadonlyUnacknowledgedOps {
|
|
874
|
+
/** Still-unacknowledged Create ops whose `parentId` is the given one. */
|
|
875
|
+
getByParentId(parentId: string): Iterable<ClientWireCreateOp>;
|
|
876
|
+
/**
|
|
877
|
+
* Still-unacknowledged Create ops whose `parentId` and `parentKey` are both
|
|
878
|
+
* the given ones (i.e. targeting one exact position).
|
|
879
|
+
*/
|
|
880
|
+
getByParentIdAndKey(parentId: string, parentKey: string): Iterable<ClientWireCreateOp>;
|
|
881
|
+
}
|
|
882
|
+
|
|
867
883
|
/**
|
|
868
884
|
* The managed pool is a namespace registry (i.e. a context) that "owns" all
|
|
869
885
|
* the individual live nodes, ensuring each one has a unique ID, and holding on
|
|
@@ -892,6 +908,11 @@ interface ManagedPool {
|
|
|
892
908
|
* @returns {void}
|
|
893
909
|
*/
|
|
894
910
|
assertStorageIsWritable: () => void;
|
|
911
|
+
/**
|
|
912
|
+
* Read-only view of the client's still-unacknowledged ops (sent or
|
|
913
|
+
* pending-send, not yet confirmed by the server).
|
|
914
|
+
*/
|
|
915
|
+
readonly unacknowledgedOps: ReadonlyUnacknowledgedOps;
|
|
895
916
|
}
|
|
896
917
|
type CreateManagedPoolOptions = {
|
|
897
918
|
/**
|
|
@@ -911,6 +932,13 @@ type CreateManagedPoolOptions = {
|
|
|
911
932
|
* have an effect upstream.
|
|
912
933
|
*/
|
|
913
934
|
isStorageWritable?: () => boolean;
|
|
935
|
+
/**
|
|
936
|
+
* Read-only view of the client's still-unacknowledged ops. Used by CRDTs
|
|
937
|
+
* (e.g. LiveList) to know which of their optimistic mutations the server
|
|
938
|
+
* hasn't confirmed yet. Defaults to an empty view (e.g. server-side pools
|
|
939
|
+
* that dispatch-and-flush have no optimistic state to track).
|
|
940
|
+
*/
|
|
941
|
+
unacknowledgedOps?: ReadonlyUnacknowledgedOps;
|
|
914
942
|
};
|
|
915
943
|
/**
|
|
916
944
|
* @private Private API, never use this API directly.
|
|
@@ -5457,6 +5485,13 @@ declare class SortedList<T> {
|
|
|
5457
5485
|
reposition(value: T): number;
|
|
5458
5486
|
at(index: number): T | undefined;
|
|
5459
5487
|
get length(): number;
|
|
5488
|
+
/**
|
|
5489
|
+
* Whether the given value is present, by identity. O(log n) plus the length
|
|
5490
|
+
* of any run of items that share its sort key (normally 1). Bisects on the
|
|
5491
|
+
* value's own key, so it only finds values sitting at their sorted position,
|
|
5492
|
+
* which is true for any item currently in the list.
|
|
5493
|
+
*/
|
|
5494
|
+
includes(value: T): boolean;
|
|
5460
5495
|
filter(predicate: (value: T) => boolean): IterableIterator<T>;
|
|
5461
5496
|
findAllRight(predicate: (value: T, index: number) => unknown): IterableIterator<T>;
|
|
5462
5497
|
[Symbol.iterator](): IterableIterator<T>;
|
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-rc1";
|
|
10
10
|
var PKG_FORMAT = "esm";
|
|
11
11
|
|
|
12
12
|
// src/dupe-detection.ts
|
|
@@ -701,6 +701,20 @@ var SortedList = class _SortedList {
|
|
|
701
701
|
get length() {
|
|
702
702
|
return this.#data.length;
|
|
703
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Whether the given value is present, by identity. O(log n) plus the length
|
|
706
|
+
* of any run of items that share its sort key (normally 1). Bisects on the
|
|
707
|
+
* value's own key, so it only finds values sitting at their sorted position,
|
|
708
|
+
* which is true for any item currently in the list.
|
|
709
|
+
*/
|
|
710
|
+
includes(value) {
|
|
711
|
+
for (let i = bisectRight(this.#data, value, this.#lt) - 1; i >= 0 && !this.#lt(this.#data[i], value); i--) {
|
|
712
|
+
if (this.#data[i] === value) {
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
704
718
|
*filter(predicate) {
|
|
705
719
|
for (const item of this.#data) {
|
|
706
720
|
if (predicate(item)) {
|
|
@@ -5502,6 +5516,9 @@ var OpCode = Object.freeze({
|
|
|
5502
5516
|
function isIgnoredOp(op) {
|
|
5503
5517
|
return op.type === OpCode.DELETE_CRDT && op.id === "ACK";
|
|
5504
5518
|
}
|
|
5519
|
+
function isCreateOp(op) {
|
|
5520
|
+
return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST;
|
|
5521
|
+
}
|
|
5505
5522
|
|
|
5506
5523
|
// src/protocol/StorageNode.ts
|
|
5507
5524
|
var CrdtType = Object.freeze({
|
|
@@ -5759,12 +5776,95 @@ function asPos(str) {
|
|
|
5759
5776
|
return isPos(str) ? str : convertToPos(str);
|
|
5760
5777
|
}
|
|
5761
5778
|
|
|
5779
|
+
// src/crdts/UnacknowledgedOps.ts
|
|
5780
|
+
var UnacknowledgedOps = class {
|
|
5781
|
+
// opId -> op
|
|
5782
|
+
#byOpId = /* @__PURE__ */ new Map();
|
|
5783
|
+
// position -> (opId -> Create op)
|
|
5784
|
+
#createOpsByPosition = /* @__PURE__ */ new Map();
|
|
5785
|
+
// parentId -> (opId -> Create op)
|
|
5786
|
+
#createOpsByParent = /* @__PURE__ */ new Map();
|
|
5787
|
+
#posKey(parentId, parentKey) {
|
|
5788
|
+
return `${parentId}
|
|
5789
|
+
${parentKey}`;
|
|
5790
|
+
}
|
|
5791
|
+
get size() {
|
|
5792
|
+
return this.#byOpId.size;
|
|
5793
|
+
}
|
|
5794
|
+
/**
|
|
5795
|
+
* Mark the given Op as still unacknowledged.
|
|
5796
|
+
*/
|
|
5797
|
+
add(op) {
|
|
5798
|
+
this.#byOpId.set(op.opId, op);
|
|
5799
|
+
if (isCreateOp(op)) {
|
|
5800
|
+
const posKey = this.#posKey(op.parentId, op.parentKey);
|
|
5801
|
+
let atPosition = this.#createOpsByPosition.get(posKey);
|
|
5802
|
+
if (atPosition === void 0) {
|
|
5803
|
+
atPosition = /* @__PURE__ */ new Map();
|
|
5804
|
+
this.#createOpsByPosition.set(posKey, atPosition);
|
|
5805
|
+
}
|
|
5806
|
+
atPosition.set(op.opId, op);
|
|
5807
|
+
let inParent = this.#createOpsByParent.get(op.parentId);
|
|
5808
|
+
if (inParent === void 0) {
|
|
5809
|
+
inParent = /* @__PURE__ */ new Map();
|
|
5810
|
+
this.#createOpsByParent.set(op.parentId, inParent);
|
|
5811
|
+
}
|
|
5812
|
+
inParent.set(op.opId, op);
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
/**
|
|
5816
|
+
* Drop the op with the given opId from the set, because the server has
|
|
5817
|
+
* acknowledged it (confirmed our own op, or signalled it was seen but
|
|
5818
|
+
* ignored).
|
|
5819
|
+
*/
|
|
5820
|
+
delete(opId) {
|
|
5821
|
+
const op = this.#byOpId.get(opId);
|
|
5822
|
+
if (op === void 0) {
|
|
5823
|
+
return;
|
|
5824
|
+
}
|
|
5825
|
+
this.#byOpId.delete(opId);
|
|
5826
|
+
if (isCreateOp(op)) {
|
|
5827
|
+
const posKey = this.#posKey(op.parentId, op.parentKey);
|
|
5828
|
+
const atPosition = this.#createOpsByPosition.get(posKey);
|
|
5829
|
+
atPosition?.delete(opId);
|
|
5830
|
+
if (atPosition !== void 0 && atPosition.size === 0) {
|
|
5831
|
+
this.#createOpsByPosition.delete(posKey);
|
|
5832
|
+
}
|
|
5833
|
+
const inParent = this.#createOpsByParent.get(op.parentId);
|
|
5834
|
+
inParent?.delete(opId);
|
|
5835
|
+
if (inParent !== void 0 && inParent.size === 0) {
|
|
5836
|
+
this.#createOpsByParent.delete(op.parentId);
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
/**
|
|
5841
|
+
* The still-unacknowledged Create ops with the given `parentId` and
|
|
5842
|
+
* `parentKey` (targeting one exact position), in dispatch order. O(1) lookup.
|
|
5843
|
+
* Empty if none.
|
|
5844
|
+
*/
|
|
5845
|
+
getByParentIdAndKey(parentId, parentKey) {
|
|
5846
|
+
return this.#createOpsByPosition.get(this.#posKey(parentId, parentKey))?.values() ?? [];
|
|
5847
|
+
}
|
|
5848
|
+
/**
|
|
5849
|
+
* The still-unacknowledged Create ops with the given `parentId` (across all
|
|
5850
|
+
* positions), in dispatch order. O(1) lookup. Empty if none.
|
|
5851
|
+
*/
|
|
5852
|
+
getByParentId(parentId) {
|
|
5853
|
+
return this.#createOpsByParent.get(parentId)?.values() ?? [];
|
|
5854
|
+
}
|
|
5855
|
+
/** All still-unacknowledged ops, in dispatch order. */
|
|
5856
|
+
values() {
|
|
5857
|
+
return this.#byOpId.values();
|
|
5858
|
+
}
|
|
5859
|
+
};
|
|
5860
|
+
|
|
5762
5861
|
// src/crdts/AbstractCrdt.ts
|
|
5763
5862
|
function createManagedPool(roomId, options) {
|
|
5764
5863
|
const {
|
|
5765
5864
|
getCurrentConnectionId,
|
|
5766
5865
|
onDispatch,
|
|
5767
|
-
isStorageWritable = () => true
|
|
5866
|
+
isStorageWritable = () => true,
|
|
5867
|
+
unacknowledgedOps = new UnacknowledgedOps()
|
|
5768
5868
|
} = options;
|
|
5769
5869
|
let clock = 0;
|
|
5770
5870
|
let opClock = 0;
|
|
@@ -5786,7 +5886,8 @@ function createManagedPool(roomId, options) {
|
|
|
5786
5886
|
"Cannot write to storage with a read only user, please ensure the user has write permissions"
|
|
5787
5887
|
);
|
|
5788
5888
|
}
|
|
5789
|
-
}
|
|
5889
|
+
},
|
|
5890
|
+
unacknowledgedOps
|
|
5790
5891
|
};
|
|
5791
5892
|
}
|
|
5792
5893
|
function crdtAsLiveNode(value) {
|
|
@@ -6068,11 +6169,9 @@ function childNodeLt(a, b) {
|
|
|
6068
6169
|
var LiveList = class _LiveList extends AbstractCrdt {
|
|
6069
6170
|
#items;
|
|
6070
6171
|
#implicitlyDeletedItems;
|
|
6071
|
-
#unacknowledgedSets;
|
|
6072
6172
|
constructor(items) {
|
|
6073
6173
|
super();
|
|
6074
6174
|
this.#implicitlyDeletedItems = /* @__PURE__ */ new WeakSet();
|
|
6075
|
-
this.#unacknowledgedSets = /* @__PURE__ */ new Map();
|
|
6076
6175
|
const nodes = [];
|
|
6077
6176
|
let lastPos;
|
|
6078
6177
|
for (const item of items) {
|
|
@@ -6102,12 +6201,13 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6102
6201
|
}
|
|
6103
6202
|
/**
|
|
6104
6203
|
* @internal
|
|
6105
|
-
*
|
|
6106
|
-
*
|
|
6107
|
-
*
|
|
6204
|
+
* Serializes this list (and its children) into Create ops. Each child's
|
|
6205
|
+
* create is tagged with the "set" intent (in the loop below) so that a list
|
|
6206
|
+
* created and immediately mutated doesn't transiently re-show its initial
|
|
6207
|
+
* items (flicker, https://github.com/liveblocks/liveblocks/pull/1177).
|
|
6108
6208
|
*
|
|
6109
|
-
* This is quite unintuitive and should disappear as soon as
|
|
6110
|
-
*
|
|
6209
|
+
* This is quite unintuitive and should disappear as soon as we introduce an
|
|
6210
|
+
* explicit LiveList.Set operation.
|
|
6111
6211
|
*/
|
|
6112
6212
|
_toOps(parentId, parentKey) {
|
|
6113
6213
|
if (this._id === void 0) {
|
|
@@ -6123,7 +6223,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6123
6223
|
ops.push(op);
|
|
6124
6224
|
for (const item of this.#items) {
|
|
6125
6225
|
const parentKey2 = item._getParentKeyOrThrow();
|
|
6126
|
-
const childOps =
|
|
6226
|
+
const childOps = addIntentToRootOp(
|
|
6127
6227
|
item._toOps(this._id, parentKey2),
|
|
6128
6228
|
"set"
|
|
6129
6229
|
);
|
|
@@ -6167,6 +6267,28 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6167
6267
|
(item) => item._getParentKeyOrThrow() === position
|
|
6168
6268
|
);
|
|
6169
6269
|
}
|
|
6270
|
+
/**
|
|
6271
|
+
* The opId of this list's still-unacknowledged "set" op at the given position,
|
|
6272
|
+
* or undefined if none. Derived from the room's unacknowledgedOps (the single
|
|
6273
|
+
* source of truth) rather than tracked in a per-instance map. The pool's
|
|
6274
|
+
* position index already scopes to this list's (parentId, position); the last
|
|
6275
|
+
* match wins, matching the original last-write-wins map semantics.
|
|
6276
|
+
*/
|
|
6277
|
+
#unacknowledgedSetOpIdAt(position) {
|
|
6278
|
+
if (this._pool === void 0 || this._id === void 0) {
|
|
6279
|
+
return void 0;
|
|
6280
|
+
}
|
|
6281
|
+
let opId;
|
|
6282
|
+
for (const op of this._pool.unacknowledgedOps.getByParentIdAndKey(
|
|
6283
|
+
this._id,
|
|
6284
|
+
position
|
|
6285
|
+
)) {
|
|
6286
|
+
if (op.intent === "set") {
|
|
6287
|
+
opId = op.opId;
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6290
|
+
return opId;
|
|
6291
|
+
}
|
|
6170
6292
|
/** @internal */
|
|
6171
6293
|
_attach(id, pool) {
|
|
6172
6294
|
super._attach(id, pool);
|
|
@@ -6247,13 +6369,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6247
6369
|
if (deletedDelta) {
|
|
6248
6370
|
delta.push(deletedDelta);
|
|
6249
6371
|
}
|
|
6250
|
-
const unacknowledgedOpId = this.#
|
|
6251
|
-
if (unacknowledgedOpId !== void 0) {
|
|
6252
|
-
|
|
6253
|
-
return delta.length === 0 ? { modified: false } : { modified: makeUpdate(this, delta), reverse: [] };
|
|
6254
|
-
} else {
|
|
6255
|
-
this.#unacknowledgedSets.delete(op.parentKey);
|
|
6256
|
-
}
|
|
6372
|
+
const unacknowledgedOpId = this.#unacknowledgedSetOpIdAt(op.parentKey);
|
|
6373
|
+
if (unacknowledgedOpId !== void 0 && unacknowledgedOpId !== op.opId) {
|
|
6374
|
+
return delta.length === 0 ? { modified: false } : { modified: makeUpdate(this, delta), reverse: [] };
|
|
6257
6375
|
}
|
|
6258
6376
|
const indexOfItemWithSamePosition = this._indexOfPosition(op.parentKey);
|
|
6259
6377
|
const existingItem = this.#items.find((item) => item._id === op.id);
|
|
@@ -6334,7 +6452,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6334
6452
|
}
|
|
6335
6453
|
return result.modified.updates[0];
|
|
6336
6454
|
}
|
|
6337
|
-
#applyRemoteInsert(op) {
|
|
6455
|
+
#applyRemoteInsert(op, fromSnapshot) {
|
|
6338
6456
|
if (this._pool === void 0) {
|
|
6339
6457
|
throw new Error("Can't attach child if managed pool is not present");
|
|
6340
6458
|
}
|
|
@@ -6344,11 +6462,82 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6344
6462
|
this.#shiftItemPosition(existingItemIndex, key);
|
|
6345
6463
|
}
|
|
6346
6464
|
const { newItem, newIndex } = this.#createAttachItemAndSort(op, key);
|
|
6465
|
+
const bumpDeltas = fromSnapshot ? [] : this.#bumpUnackedPushesAbove(key);
|
|
6347
6466
|
return {
|
|
6348
|
-
modified: makeUpdate(this, [
|
|
6467
|
+
modified: makeUpdate(this, [
|
|
6468
|
+
insertDelta(newIndex, newItem),
|
|
6469
|
+
...bumpDeltas
|
|
6470
|
+
]),
|
|
6349
6471
|
reverse: []
|
|
6350
6472
|
};
|
|
6351
6473
|
}
|
|
6474
|
+
/**
|
|
6475
|
+
* This list's own still-unacknowledged pushed items (their `intent: "push"`
|
|
6476
|
+
* Create op is still pending in the room's unacknowledgedOps). Derived from
|
|
6477
|
+
* the single source of truth, so an item drops out the instant its op is
|
|
6478
|
+
* acked, with no per-instance membership to leak. Yielded in push order.
|
|
6479
|
+
*
|
|
6480
|
+
* Restricted to items currently in `#items`: a pushed node whose op is still
|
|
6481
|
+
* pending may have been pulled out of the list (e.g. implicitly deleted by a
|
|
6482
|
+
* remote set, or removed by an undo) while still living in the pool, and such
|
|
6483
|
+
* a node must not be repositioned.
|
|
6484
|
+
*/
|
|
6485
|
+
*#unackedPushNodes() {
|
|
6486
|
+
if (this._pool === void 0 || this._id === void 0) {
|
|
6487
|
+
return;
|
|
6488
|
+
}
|
|
6489
|
+
for (const op of this._pool.unacknowledgedOps.getByParentId(this._id)) {
|
|
6490
|
+
if (op.intent !== "push") {
|
|
6491
|
+
continue;
|
|
6492
|
+
}
|
|
6493
|
+
const node = this._pool.getNode(op.id);
|
|
6494
|
+
if (node !== void 0 && this.#items.includes(node)) {
|
|
6495
|
+
yield node;
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
}
|
|
6499
|
+
/**
|
|
6500
|
+
* Optimistic no-flip for pushed items. When a remote op lands at or below my
|
|
6501
|
+
* still-unacked pushed items, those items must end up *after* it: FIFO plus
|
|
6502
|
+
* the room's serial processing guarantee the remote was processed first, so
|
|
6503
|
+
* my unacked pushes belong behind it. Re-chain the whole unacked-push block,
|
|
6504
|
+
* in push order, to sit after the highest confirmed sibling, so it keeps
|
|
6505
|
+
* rendering as a contiguous tail instead of getting interleaved. Local-only;
|
|
6506
|
+
* the real acks overwrite these keys with the (identical) server keys.
|
|
6507
|
+
*/
|
|
6508
|
+
#bumpUnackedPushesAbove(remoteKey) {
|
|
6509
|
+
const pending = new Set(this.#unackedPushNodes());
|
|
6510
|
+
if (pending.size === 0) {
|
|
6511
|
+
return [];
|
|
6512
|
+
}
|
|
6513
|
+
let minPending;
|
|
6514
|
+
for (const node of pending) {
|
|
6515
|
+
const pos = node._parentPos;
|
|
6516
|
+
if (minPending === void 0 || pos < minPending) {
|
|
6517
|
+
minPending = pos;
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6520
|
+
if (remoteKey < nn(minPending)) {
|
|
6521
|
+
return [];
|
|
6522
|
+
}
|
|
6523
|
+
let base;
|
|
6524
|
+
for (const item of this.#items) {
|
|
6525
|
+
if (!pending.has(item)) {
|
|
6526
|
+
base = item._parentPos;
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
const deltas = [];
|
|
6530
|
+
for (const node of pending) {
|
|
6531
|
+
const previousIndex = this.#items.findIndex((item) => item === node);
|
|
6532
|
+
base = makePosition(base);
|
|
6533
|
+
this.#updateItemPosition(node, base);
|
|
6534
|
+
const index = this.#items.findIndex((item) => item === node);
|
|
6535
|
+
if (index !== previousIndex) {
|
|
6536
|
+
deltas.push(moveDelta(previousIndex, index, node));
|
|
6537
|
+
}
|
|
6538
|
+
}
|
|
6539
|
+
return deltas;
|
|
6540
|
+
}
|
|
6352
6541
|
#applyInsertAck(op) {
|
|
6353
6542
|
const existingItem = this.#items.find((item) => item._id === op.id);
|
|
6354
6543
|
const key = asPos(op.parentKey);
|
|
@@ -6429,7 +6618,6 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6429
6618
|
if (this._pool?.getNode(id) !== void 0) {
|
|
6430
6619
|
return { modified: false };
|
|
6431
6620
|
}
|
|
6432
|
-
this.#unacknowledgedSets.set(key, nn(op.opId));
|
|
6433
6621
|
const indexOfItemWithSameKey = this._indexOfPosition(key);
|
|
6434
6622
|
child._attach(id, nn(this._pool));
|
|
6435
6623
|
child._setParentLink(this, key);
|
|
@@ -6439,7 +6627,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6439
6627
|
existingItem._detach();
|
|
6440
6628
|
this.#items.remove(existingItem);
|
|
6441
6629
|
this.#items.add(child);
|
|
6442
|
-
const reverse =
|
|
6630
|
+
const reverse = addIntentToRootOp(
|
|
6443
6631
|
existingItem._toOps(nn(this._id), key),
|
|
6444
6632
|
"set",
|
|
6445
6633
|
op.id
|
|
@@ -6466,7 +6654,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6466
6654
|
}
|
|
6467
6655
|
}
|
|
6468
6656
|
/** @internal */
|
|
6469
|
-
_attachChild(op, source) {
|
|
6657
|
+
_attachChild(op, source, fromSnapshot = false) {
|
|
6470
6658
|
if (this._pool === void 0) {
|
|
6471
6659
|
throw new Error("Can't attach child if managed pool is not present");
|
|
6472
6660
|
}
|
|
@@ -6481,7 +6669,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6481
6669
|
}
|
|
6482
6670
|
} else {
|
|
6483
6671
|
if (source === 1 /* THEIRS */) {
|
|
6484
|
-
result = this.#applyRemoteInsert(op);
|
|
6672
|
+
result = this.#applyRemoteInsert(op, fromSnapshot);
|
|
6485
6673
|
} else if (source === 2 /* OURS */) {
|
|
6486
6674
|
result = this.#applyInsertAck(op);
|
|
6487
6675
|
} else {
|
|
@@ -6718,7 +6906,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6718
6906
|
value._attach(id, this._pool);
|
|
6719
6907
|
const ops = value._toOpsWithOpId(this._id, position, this._pool);
|
|
6720
6908
|
this._pool.dispatch(
|
|
6721
|
-
intent === "push" ?
|
|
6909
|
+
intent === "push" ? addIntentToRootOp(ops, "push") : ops,
|
|
6722
6910
|
[{ type: OpCode.DELETE_CRDT, id }],
|
|
6723
6911
|
/* @__PURE__ */ new Map([
|
|
6724
6912
|
[this._id, makeUpdate(this, [insertDelta(index, value)])]
|
|
@@ -6876,13 +7064,12 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6876
7064
|
value._attach(id, this._pool);
|
|
6877
7065
|
const storageUpdates = /* @__PURE__ */ new Map();
|
|
6878
7066
|
storageUpdates.set(this._id, makeUpdate(this, [setDelta(index, value)]));
|
|
6879
|
-
const ops =
|
|
7067
|
+
const ops = addIntentToRootOp(
|
|
6880
7068
|
value._toOpsWithOpId(this._id, position, this._pool),
|
|
6881
7069
|
"set",
|
|
6882
7070
|
existingId
|
|
6883
7071
|
);
|
|
6884
|
-
|
|
6885
|
-
const reverseOps = addIntentToOpTree(
|
|
7072
|
+
const reverseOps = addIntentToRootOp(
|
|
6886
7073
|
existingItem._toOps(this._id, position),
|
|
6887
7074
|
"set",
|
|
6888
7075
|
id
|
|
@@ -7085,7 +7272,7 @@ function moveDelta(previousIndex, index, item) {
|
|
|
7085
7272
|
previousIndex
|
|
7086
7273
|
};
|
|
7087
7274
|
}
|
|
7088
|
-
function
|
|
7275
|
+
function addIntentToRootOp(ops, intent, deletedId) {
|
|
7089
7276
|
return ops.map((op, index) => {
|
|
7090
7277
|
if (index === 0) {
|
|
7091
7278
|
const firstOp = op;
|
|
@@ -8329,6 +8516,31 @@ function lsonToLiveNode(value) {
|
|
|
8329
8516
|
return new LiveRegister(value);
|
|
8330
8517
|
}
|
|
8331
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
|
+
}
|
|
8332
8544
|
function getTreesDiffOperations(currentItems, newItems) {
|
|
8333
8545
|
const ops = [];
|
|
8334
8546
|
currentItems.forEach((_, id) => {
|
|
@@ -9338,6 +9550,7 @@ function createRoom(options, config) {
|
|
|
9338
9550
|
delegates,
|
|
9339
9551
|
config.enableDebugLogging
|
|
9340
9552
|
);
|
|
9553
|
+
const unacknowledgedOps = new UnacknowledgedOps();
|
|
9341
9554
|
const context = {
|
|
9342
9555
|
buffer: {
|
|
9343
9556
|
flushTimerID: void 0,
|
|
@@ -9365,14 +9578,15 @@ function createRoom(options, config) {
|
|
|
9365
9578
|
pool: createManagedPool(roomId, {
|
|
9366
9579
|
getCurrentConnectionId,
|
|
9367
9580
|
onDispatch,
|
|
9368
|
-
isStorageWritable
|
|
9581
|
+
isStorageWritable,
|
|
9582
|
+
unacknowledgedOps
|
|
9369
9583
|
}),
|
|
9370
9584
|
root: void 0,
|
|
9371
9585
|
undoStack: [],
|
|
9372
9586
|
redoStack: [],
|
|
9373
9587
|
pausedHistory: null,
|
|
9374
9588
|
activeBatch: null,
|
|
9375
|
-
unacknowledgedOps
|
|
9589
|
+
unacknowledgedOps
|
|
9376
9590
|
};
|
|
9377
9591
|
const nodeMapBuffer = makeNodeMapBuffer();
|
|
9378
9592
|
const stopwatch = config.enableDebugLogging ? makeStopWatch() : void 0;
|
|
@@ -9580,7 +9794,11 @@ function createRoom(options, config) {
|
|
|
9580
9794
|
currentItems.set(id, crdt._serialize());
|
|
9581
9795
|
}
|
|
9582
9796
|
const ops = getTreesDiffOperations(currentItems, nodes);
|
|
9583
|
-
const result = applyRemoteOps(
|
|
9797
|
+
const result = applyRemoteOps(
|
|
9798
|
+
ops,
|
|
9799
|
+
/* fromSnapshot */
|
|
9800
|
+
true
|
|
9801
|
+
);
|
|
9584
9802
|
notify(result.updates);
|
|
9585
9803
|
} else {
|
|
9586
9804
|
context.root = LiveObject._fromItems(
|
|
@@ -9662,15 +9880,16 @@ function createRoom(options, config) {
|
|
|
9662
9880
|
);
|
|
9663
9881
|
return { opsToEmit: opsWithOpIds, reverse, updates };
|
|
9664
9882
|
}
|
|
9665
|
-
function applyRemoteOps(ops) {
|
|
9883
|
+
function applyRemoteOps(ops, fromSnapshot = false) {
|
|
9666
9884
|
return applyOps(
|
|
9667
9885
|
[],
|
|
9668
9886
|
ops,
|
|
9669
9887
|
/* isLocal */
|
|
9670
|
-
false
|
|
9888
|
+
false,
|
|
9889
|
+
fromSnapshot
|
|
9671
9890
|
);
|
|
9672
9891
|
}
|
|
9673
|
-
function applyOps(pframes, ops, isLocal) {
|
|
9892
|
+
function applyOps(pframes, ops, isLocal, fromSnapshot = false) {
|
|
9674
9893
|
const output = {
|
|
9675
9894
|
reverse: new Deque(),
|
|
9676
9895
|
storageUpdates: /* @__PURE__ */ new Map(),
|
|
@@ -9706,7 +9925,7 @@ function createRoom(options, config) {
|
|
|
9706
9925
|
} else {
|
|
9707
9926
|
source = 1 /* THEIRS */;
|
|
9708
9927
|
}
|
|
9709
|
-
const applyOpResult = applyOp(op, source);
|
|
9928
|
+
const applyOpResult = applyOp(op, source, fromSnapshot);
|
|
9710
9929
|
if (applyOpResult.modified) {
|
|
9711
9930
|
const nodeId = applyOpResult.modified.node._id;
|
|
9712
9931
|
if (!(nodeId && createdNodeIds.has(nodeId))) {
|
|
@@ -9732,7 +9951,7 @@ function createRoom(options, config) {
|
|
|
9732
9951
|
}
|
|
9733
9952
|
};
|
|
9734
9953
|
}
|
|
9735
|
-
function applyOp(op, source) {
|
|
9954
|
+
function applyOp(op, source, fromSnapshot = false) {
|
|
9736
9955
|
if (isIgnoredOp(op)) {
|
|
9737
9956
|
return { modified: false };
|
|
9738
9957
|
}
|
|
@@ -9771,7 +9990,7 @@ function createRoom(options, config) {
|
|
|
9771
9990
|
if (parentNode === void 0) {
|
|
9772
9991
|
return { modified: false };
|
|
9773
9992
|
}
|
|
9774
|
-
return parentNode._attachChild(op, source);
|
|
9993
|
+
return parentNode._attachChild(op, source, fromSnapshot);
|
|
9775
9994
|
}
|
|
9776
9995
|
}
|
|
9777
9996
|
}
|
|
@@ -9911,12 +10130,11 @@ function createRoom(options, config) {
|
|
|
9911
10130
|
}
|
|
9912
10131
|
}
|
|
9913
10132
|
function applyAndSendOfflineOps(unackedOps) {
|
|
9914
|
-
if (unackedOps.
|
|
10133
|
+
if (unackedOps.length === 0) {
|
|
9915
10134
|
return;
|
|
9916
10135
|
}
|
|
9917
10136
|
const messages = [];
|
|
9918
|
-
const
|
|
9919
|
-
const result = applyLocalOps(inOps);
|
|
10137
|
+
const result = applyLocalOps(unackedOps);
|
|
9920
10138
|
messages.push({
|
|
9921
10139
|
type: ClientMsgCode.UPDATE_STORAGE,
|
|
9922
10140
|
ops: result.opsToEmit
|
|
@@ -10140,7 +10358,7 @@ function createRoom(options, config) {
|
|
|
10140
10358
|
const storageOps = context.buffer.storageOperations;
|
|
10141
10359
|
if (storageOps.length > 0) {
|
|
10142
10360
|
for (const op of storageOps) {
|
|
10143
|
-
context.unacknowledgedOps.
|
|
10361
|
+
context.unacknowledgedOps.add(op);
|
|
10144
10362
|
}
|
|
10145
10363
|
notifyStorageStatus();
|
|
10146
10364
|
}
|
|
@@ -10367,9 +10585,9 @@ function createRoom(options, config) {
|
|
|
10367
10585
|
}
|
|
10368
10586
|
}
|
|
10369
10587
|
function processInitialStorage(nodes) {
|
|
10370
|
-
const
|
|
10588
|
+
const unacknowledgedOps2 = [...context.unacknowledgedOps.values()];
|
|
10371
10589
|
createOrUpdateRootFromMessage(nodes);
|
|
10372
|
-
applyAndSendOfflineOps(
|
|
10590
|
+
applyAndSendOfflineOps(unacknowledgedOps2);
|
|
10373
10591
|
_resolveStoragePromise?.();
|
|
10374
10592
|
notifyStorageStatus();
|
|
10375
10593
|
eventHub.storageDidLoad.notify();
|
|
@@ -10958,6 +11176,11 @@ function createRoom(options, config) {
|
|
|
10958
11176
|
connect: () => managedSocket.connect(),
|
|
10959
11177
|
reconnect: () => managedSocket.reconnect(),
|
|
10960
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
|
+
},
|
|
10961
11184
|
destroy: () => {
|
|
10962
11185
|
pendingFeedsRequests.forEach(
|
|
10963
11186
|
(request) => request.reject(new Error("Room destroyed"))
|
|
@@ -11465,6 +11688,7 @@ function createClient(options) {
|
|
|
11465
11688
|
{
|
|
11466
11689
|
enterRoom,
|
|
11467
11690
|
getRoom,
|
|
11691
|
+
_dump: () => Array.from(roomsById.values(), ({ room }) => room._dump()).join("\n\n"),
|
|
11468
11692
|
logout,
|
|
11469
11693
|
// Public inbox notifications API
|
|
11470
11694
|
getInboxNotifications: httpClient.getInboxNotifications,
|