@liveblocks/core 3.19.2 → 3.19.4-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +426 -170
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -8
- package/dist/index.d.ts +43 -8
- package/dist/index.js +328 -72
- 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.19.
|
|
9
|
+
var PKG_VERSION = "3.19.4-rc1";
|
|
10
10
|
var PKG_FORMAT = "esm";
|
|
11
11
|
|
|
12
12
|
// src/dupe-detection.ts
|
|
@@ -701,6 +701,20 @@ var SortedList = class _SortedList {
|
|
|
701
701
|
get length() {
|
|
702
702
|
return this.#data.length;
|
|
703
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Whether the given value is present, by identity. O(log n) plus the length
|
|
706
|
+
* of any run of items that share its sort key (normally 1). Bisects on the
|
|
707
|
+
* value's own key, so it only finds values sitting at their sorted position,
|
|
708
|
+
* which is true for any item currently in the list.
|
|
709
|
+
*/
|
|
710
|
+
includes(value) {
|
|
711
|
+
for (let i = bisectRight(this.#data, value, this.#lt) - 1; i >= 0 && !this.#lt(this.#data[i], value); i--) {
|
|
712
|
+
if (this.#data[i] === value) {
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
704
718
|
*filter(predicate) {
|
|
705
719
|
for (const item of this.#data) {
|
|
706
720
|
if (predicate(item)) {
|
|
@@ -2715,6 +2729,7 @@ var HttpClient = class {
|
|
|
2715
2729
|
};
|
|
2716
2730
|
|
|
2717
2731
|
// src/lib/fsm.ts
|
|
2732
|
+
var IGNORE = /* @__PURE__ */ Symbol("fsm.ignore");
|
|
2718
2733
|
function distance(state1, state2) {
|
|
2719
2734
|
if (state1 === state2) {
|
|
2720
2735
|
return [0, 0];
|
|
@@ -2887,7 +2902,7 @@ var FSM = class {
|
|
|
2887
2902
|
this.#eventHub = {
|
|
2888
2903
|
didReceiveEvent: makeEventSource(),
|
|
2889
2904
|
willTransition: makeEventSource(),
|
|
2890
|
-
|
|
2905
|
+
didIgnoreUnexpectedEvent: makeEventSource(),
|
|
2891
2906
|
willExitState: makeEventSource(),
|
|
2892
2907
|
didEnterState: makeEventSource(),
|
|
2893
2908
|
didExitState: makeEventSource()
|
|
@@ -2895,7 +2910,7 @@ var FSM = class {
|
|
|
2895
2910
|
this.events = {
|
|
2896
2911
|
didReceiveEvent: this.#eventHub.didReceiveEvent.observable,
|
|
2897
2912
|
willTransition: this.#eventHub.willTransition.observable,
|
|
2898
|
-
|
|
2913
|
+
didIgnoreUnexpectedEvent: this.#eventHub.didIgnoreUnexpectedEvent.observable,
|
|
2899
2914
|
willExitState: this.#eventHub.willExitState.observable,
|
|
2900
2915
|
didEnterState: this.#eventHub.didEnterState.observable,
|
|
2901
2916
|
didExitState: this.#eventHub.didExitState.observable
|
|
@@ -3018,9 +3033,14 @@ var FSM = class {
|
|
|
3018
3033
|
* `context` params to conditionally decide which next state to transition
|
|
3019
3034
|
* to.
|
|
3020
3035
|
*
|
|
3021
|
-
* If you
|
|
3022
|
-
*
|
|
3023
|
-
*
|
|
3036
|
+
* If you don't define a target for a transition, the event is treated
|
|
3037
|
+
* as unhandled in this state: `didIgnoreUnexpectedEvent` fires and the
|
|
3038
|
+
* state does not change.
|
|
3039
|
+
*
|
|
3040
|
+
* To declare an event as an intentional silent no-op in this state, use
|
|
3041
|
+
* the {@link IGNORE} sentinel — either statically (`{ EVENT: IGNORE }`)
|
|
3042
|
+
* or as a return value from a target function. IGNORE'd events do not
|
|
3043
|
+
* fire `didIgnoreUnexpectedEvent`.
|
|
3024
3044
|
*/
|
|
3025
3045
|
addTransitions(nameOrPattern, mapping) {
|
|
3026
3046
|
if (this.#runningState !== 0 /* NOT_STARTED_YET */) {
|
|
@@ -3040,7 +3060,12 @@ var FSM = class {
|
|
|
3040
3060
|
}
|
|
3041
3061
|
const target = target_;
|
|
3042
3062
|
this.#knownEventTypes.add(type);
|
|
3043
|
-
if (target
|
|
3063
|
+
if (target === void 0) {
|
|
3064
|
+
continue;
|
|
3065
|
+
}
|
|
3066
|
+
if (target === IGNORE) {
|
|
3067
|
+
map.set(type, IGNORE);
|
|
3068
|
+
} else {
|
|
3044
3069
|
const targetFn = typeof target === "function" ? target : () => target;
|
|
3045
3070
|
map.set(type, targetFn);
|
|
3046
3071
|
}
|
|
@@ -3139,12 +3164,14 @@ var FSM = class {
|
|
|
3139
3164
|
if (this.#runningState === 2 /* STOPPED */) {
|
|
3140
3165
|
return;
|
|
3141
3166
|
}
|
|
3142
|
-
const
|
|
3143
|
-
if (
|
|
3144
|
-
return
|
|
3145
|
-
} else {
|
|
3146
|
-
this.#eventHub.didIgnoreEvent.notify(event);
|
|
3167
|
+
const entry = this.#getTargetFn(event.type);
|
|
3168
|
+
if (entry === IGNORE) {
|
|
3169
|
+
return;
|
|
3147
3170
|
}
|
|
3171
|
+
if (entry !== void 0) {
|
|
3172
|
+
return this.#transition(event, entry);
|
|
3173
|
+
}
|
|
3174
|
+
this.#eventHub.didIgnoreUnexpectedEvent.notify(event);
|
|
3148
3175
|
}
|
|
3149
3176
|
#transition(event, target) {
|
|
3150
3177
|
this.#eventHub.didReceiveEvent.notify(event);
|
|
@@ -3153,8 +3180,7 @@ var FSM = class {
|
|
|
3153
3180
|
const nextTarget = targetFn(event, this.#currentContext.current);
|
|
3154
3181
|
let nextState;
|
|
3155
3182
|
let effects = void 0;
|
|
3156
|
-
if (nextTarget ===
|
|
3157
|
-
this.#eventHub.didIgnoreEvent.notify(event);
|
|
3183
|
+
if (nextTarget === IGNORE) {
|
|
3158
3184
|
return;
|
|
3159
3185
|
}
|
|
3160
3186
|
if (typeof nextTarget === "string") {
|
|
@@ -3373,8 +3399,8 @@ function enableTracing(machine) {
|
|
|
3373
3399
|
machine.events.didExitState.subscribe(
|
|
3374
3400
|
({ state, durationMs }) => log2(`Exited ${state} after ${durationMs.toFixed(0)}ms`)
|
|
3375
3401
|
),
|
|
3376
|
-
machine.events.
|
|
3377
|
-
(e) => log2("Ignored event", e.type, e, "(
|
|
3402
|
+
machine.events.didIgnoreUnexpectedEvent.subscribe(
|
|
3403
|
+
(e) => log2("Ignored unexpected event", e.type, e, "(no transition declared)")
|
|
3378
3404
|
)
|
|
3379
3405
|
];
|
|
3380
3406
|
return () => {
|
|
@@ -3486,7 +3512,12 @@ function createConnectionStateMachine(delegates, options) {
|
|
|
3486
3512
|
);
|
|
3487
3513
|
const onSocketError = (event) => machine.send({ type: "EXPLICIT_SOCKET_ERROR", event });
|
|
3488
3514
|
const onSocketClose = (event) => machine.send({ type: "EXPLICIT_SOCKET_CLOSE", event });
|
|
3489
|
-
const onSocketMessage = (event) =>
|
|
3515
|
+
const onSocketMessage = (event) => {
|
|
3516
|
+
machine.send({ type: "ALIVE" });
|
|
3517
|
+
if (event.data !== "pong") {
|
|
3518
|
+
onMessage.notify(event);
|
|
3519
|
+
}
|
|
3520
|
+
};
|
|
3490
3521
|
function teardownSocket(socket) {
|
|
3491
3522
|
if (socket) {
|
|
3492
3523
|
socket.removeEventListener("error", onSocketError);
|
|
@@ -3656,7 +3687,13 @@ function createConnectionStateMachine(delegates, options) {
|
|
|
3656
3687
|
effect: [increaseBackoffDelay, logPrematureErrorOrCloseEvent(err)]
|
|
3657
3688
|
};
|
|
3658
3689
|
}
|
|
3659
|
-
)
|
|
3690
|
+
).addTransitions("@connecting.busy", {
|
|
3691
|
+
// The socket message listener is attached during @connecting.busy (see
|
|
3692
|
+
// onEnterAsync above), so server frames (most notably the actor-id
|
|
3693
|
+
// handshake) can fire onSocketMessage and emit a ALIVE before we
|
|
3694
|
+
// reach @ok.*. That's fine. Heartbeat only matters in @ok.*.
|
|
3695
|
+
ALIVE: IGNORE
|
|
3696
|
+
});
|
|
3660
3697
|
const sendHeartbeat = {
|
|
3661
3698
|
target: "@ok.awaiting-pong",
|
|
3662
3699
|
effect: (ctx) => {
|
|
@@ -3671,7 +3708,8 @@ function createConnectionStateMachine(delegates, options) {
|
|
|
3671
3708
|
machine.addTimedTransition("@ok.connected", HEARTBEAT_INTERVAL, maybeHeartbeat).addTransitions("@ok.connected", {
|
|
3672
3709
|
NAVIGATOR_OFFLINE: maybeHeartbeat,
|
|
3673
3710
|
// Don't take the browser's word for it when it says it's offline. Do a ping/pong to make sure.
|
|
3674
|
-
WINDOW_GOT_FOCUS: sendHeartbeat
|
|
3711
|
+
WINDOW_GOT_FOCUS: sendHeartbeat,
|
|
3712
|
+
ALIVE: IGNORE
|
|
3675
3713
|
});
|
|
3676
3714
|
machine.addTransitions("@idle.zombie", {
|
|
3677
3715
|
WINDOW_GOT_FOCUS: "@connecting.backoff"
|
|
@@ -3692,7 +3730,7 @@ function createConnectionStateMachine(delegates, options) {
|
|
|
3692
3730
|
clearTimeout(timerID);
|
|
3693
3731
|
onMessage.pause();
|
|
3694
3732
|
};
|
|
3695
|
-
}).addTransitions("@ok.awaiting-pong", {
|
|
3733
|
+
}).addTransitions("@ok.awaiting-pong", { ALIVE: "@ok.connected" }).addTimedTransition("@ok.awaiting-pong", PONG_TIMEOUT, {
|
|
3696
3734
|
target: "@connecting.busy",
|
|
3697
3735
|
// Log implicit connection loss and drop the current open socket
|
|
3698
3736
|
effect: log(
|
|
@@ -3705,7 +3743,7 @@ function createConnectionStateMachine(delegates, options) {
|
|
|
3705
3743
|
// not. When still OPEN, don't transition.
|
|
3706
3744
|
EXPLICIT_SOCKET_ERROR: (_, context) => {
|
|
3707
3745
|
if (context.socket?.readyState === 1) {
|
|
3708
|
-
return
|
|
3746
|
+
return IGNORE;
|
|
3709
3747
|
}
|
|
3710
3748
|
return {
|
|
3711
3749
|
target: "@connecting.backoff",
|
|
@@ -5478,6 +5516,9 @@ var OpCode = Object.freeze({
|
|
|
5478
5516
|
function isIgnoredOp(op) {
|
|
5479
5517
|
return op.type === OpCode.DELETE_CRDT && op.id === "ACK";
|
|
5480
5518
|
}
|
|
5519
|
+
function isCreateOp(op) {
|
|
5520
|
+
return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST;
|
|
5521
|
+
}
|
|
5481
5522
|
|
|
5482
5523
|
// src/protocol/StorageNode.ts
|
|
5483
5524
|
var CrdtType = Object.freeze({
|
|
@@ -5735,12 +5776,95 @@ function asPos(str) {
|
|
|
5735
5776
|
return isPos(str) ? str : convertToPos(str);
|
|
5736
5777
|
}
|
|
5737
5778
|
|
|
5779
|
+
// src/crdts/UnacknowledgedOps.ts
|
|
5780
|
+
var UnacknowledgedOps = class {
|
|
5781
|
+
// opId -> op
|
|
5782
|
+
#byOpId = /* @__PURE__ */ new Map();
|
|
5783
|
+
// position -> (opId -> Create op)
|
|
5784
|
+
#createOpsByPosition = /* @__PURE__ */ new Map();
|
|
5785
|
+
// parentId -> (opId -> Create op)
|
|
5786
|
+
#createOpsByParent = /* @__PURE__ */ new Map();
|
|
5787
|
+
#posKey(parentId, parentKey) {
|
|
5788
|
+
return `${parentId}
|
|
5789
|
+
${parentKey}`;
|
|
5790
|
+
}
|
|
5791
|
+
get size() {
|
|
5792
|
+
return this.#byOpId.size;
|
|
5793
|
+
}
|
|
5794
|
+
/**
|
|
5795
|
+
* Mark the given Op as still unacknowledged.
|
|
5796
|
+
*/
|
|
5797
|
+
add(op) {
|
|
5798
|
+
this.#byOpId.set(op.opId, op);
|
|
5799
|
+
if (isCreateOp(op)) {
|
|
5800
|
+
const posKey = this.#posKey(op.parentId, op.parentKey);
|
|
5801
|
+
let atPosition = this.#createOpsByPosition.get(posKey);
|
|
5802
|
+
if (atPosition === void 0) {
|
|
5803
|
+
atPosition = /* @__PURE__ */ new Map();
|
|
5804
|
+
this.#createOpsByPosition.set(posKey, atPosition);
|
|
5805
|
+
}
|
|
5806
|
+
atPosition.set(op.opId, op);
|
|
5807
|
+
let inParent = this.#createOpsByParent.get(op.parentId);
|
|
5808
|
+
if (inParent === void 0) {
|
|
5809
|
+
inParent = /* @__PURE__ */ new Map();
|
|
5810
|
+
this.#createOpsByParent.set(op.parentId, inParent);
|
|
5811
|
+
}
|
|
5812
|
+
inParent.set(op.opId, op);
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
/**
|
|
5816
|
+
* Drop the op with the given opId from the set, because the server has
|
|
5817
|
+
* acknowledged it (confirmed our own op, or signalled it was seen but
|
|
5818
|
+
* ignored).
|
|
5819
|
+
*/
|
|
5820
|
+
delete(opId) {
|
|
5821
|
+
const op = this.#byOpId.get(opId);
|
|
5822
|
+
if (op === void 0) {
|
|
5823
|
+
return;
|
|
5824
|
+
}
|
|
5825
|
+
this.#byOpId.delete(opId);
|
|
5826
|
+
if (isCreateOp(op)) {
|
|
5827
|
+
const posKey = this.#posKey(op.parentId, op.parentKey);
|
|
5828
|
+
const atPosition = this.#createOpsByPosition.get(posKey);
|
|
5829
|
+
atPosition?.delete(opId);
|
|
5830
|
+
if (atPosition !== void 0 && atPosition.size === 0) {
|
|
5831
|
+
this.#createOpsByPosition.delete(posKey);
|
|
5832
|
+
}
|
|
5833
|
+
const inParent = this.#createOpsByParent.get(op.parentId);
|
|
5834
|
+
inParent?.delete(opId);
|
|
5835
|
+
if (inParent !== void 0 && inParent.size === 0) {
|
|
5836
|
+
this.#createOpsByParent.delete(op.parentId);
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
/**
|
|
5841
|
+
* The still-unacknowledged Create ops with the given `parentId` and
|
|
5842
|
+
* `parentKey` (targeting one exact position), in dispatch order. O(1) lookup.
|
|
5843
|
+
* Empty if none.
|
|
5844
|
+
*/
|
|
5845
|
+
getByParentIdAndKey(parentId, parentKey) {
|
|
5846
|
+
return this.#createOpsByPosition.get(this.#posKey(parentId, parentKey))?.values() ?? [];
|
|
5847
|
+
}
|
|
5848
|
+
/**
|
|
5849
|
+
* The still-unacknowledged Create ops with the given `parentId` (across all
|
|
5850
|
+
* positions), in dispatch order. O(1) lookup. Empty if none.
|
|
5851
|
+
*/
|
|
5852
|
+
getByParentId(parentId) {
|
|
5853
|
+
return this.#createOpsByParent.get(parentId)?.values() ?? [];
|
|
5854
|
+
}
|
|
5855
|
+
/** All still-unacknowledged ops, in dispatch order. */
|
|
5856
|
+
values() {
|
|
5857
|
+
return this.#byOpId.values();
|
|
5858
|
+
}
|
|
5859
|
+
};
|
|
5860
|
+
|
|
5738
5861
|
// src/crdts/AbstractCrdt.ts
|
|
5739
5862
|
function createManagedPool(roomId, options) {
|
|
5740
5863
|
const {
|
|
5741
5864
|
getCurrentConnectionId,
|
|
5742
5865
|
onDispatch,
|
|
5743
|
-
isStorageWritable = () => true
|
|
5866
|
+
isStorageWritable = () => true,
|
|
5867
|
+
unacknowledgedOps = new UnacknowledgedOps()
|
|
5744
5868
|
} = options;
|
|
5745
5869
|
let clock = 0;
|
|
5746
5870
|
let opClock = 0;
|
|
@@ -5762,7 +5886,8 @@ function createManagedPool(roomId, options) {
|
|
|
5762
5886
|
"Cannot write to storage with a read only user, please ensure the user has write permissions"
|
|
5763
5887
|
);
|
|
5764
5888
|
}
|
|
5765
|
-
}
|
|
5889
|
+
},
|
|
5890
|
+
unacknowledgedOps
|
|
5766
5891
|
};
|
|
5767
5892
|
}
|
|
5768
5893
|
function crdtAsLiveNode(value) {
|
|
@@ -6044,11 +6169,9 @@ function childNodeLt(a, b) {
|
|
|
6044
6169
|
var LiveList = class _LiveList extends AbstractCrdt {
|
|
6045
6170
|
#items;
|
|
6046
6171
|
#implicitlyDeletedItems;
|
|
6047
|
-
#unacknowledgedSets;
|
|
6048
6172
|
constructor(items) {
|
|
6049
6173
|
super();
|
|
6050
6174
|
this.#implicitlyDeletedItems = /* @__PURE__ */ new WeakSet();
|
|
6051
|
-
this.#unacknowledgedSets = /* @__PURE__ */ new Map();
|
|
6052
6175
|
const nodes = [];
|
|
6053
6176
|
let lastPos;
|
|
6054
6177
|
for (const item of items) {
|
|
@@ -6078,12 +6201,13 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6078
6201
|
}
|
|
6079
6202
|
/**
|
|
6080
6203
|
* @internal
|
|
6081
|
-
*
|
|
6082
|
-
*
|
|
6083
|
-
*
|
|
6204
|
+
* Serializes this list (and its children) into Create ops. Each child's
|
|
6205
|
+
* create is tagged with the "set" intent (in the loop below) so that a list
|
|
6206
|
+
* created and immediately mutated doesn't transiently re-show its initial
|
|
6207
|
+
* items (flicker, https://github.com/liveblocks/liveblocks/pull/1177).
|
|
6084
6208
|
*
|
|
6085
|
-
* This is quite unintuitive and should disappear as soon as
|
|
6086
|
-
*
|
|
6209
|
+
* This is quite unintuitive and should disappear as soon as we introduce an
|
|
6210
|
+
* explicit LiveList.Set operation.
|
|
6087
6211
|
*/
|
|
6088
6212
|
_toOps(parentId, parentKey) {
|
|
6089
6213
|
if (this._id === void 0) {
|
|
@@ -6099,9 +6223,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6099
6223
|
ops.push(op);
|
|
6100
6224
|
for (const item of this.#items) {
|
|
6101
6225
|
const parentKey2 = item._getParentKeyOrThrow();
|
|
6102
|
-
const childOps =
|
|
6226
|
+
const childOps = addIntentToRootOp(
|
|
6103
6227
|
item._toOps(this._id, parentKey2),
|
|
6104
|
-
|
|
6228
|
+
"set"
|
|
6105
6229
|
);
|
|
6106
6230
|
for (const childOp of childOps) {
|
|
6107
6231
|
ops.push(childOp);
|
|
@@ -6143,6 +6267,28 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6143
6267
|
(item) => item._getParentKeyOrThrow() === position
|
|
6144
6268
|
);
|
|
6145
6269
|
}
|
|
6270
|
+
/**
|
|
6271
|
+
* The opId of this list's still-unacknowledged "set" op at the given position,
|
|
6272
|
+
* or undefined if none. Derived from the room's unacknowledgedOps (the single
|
|
6273
|
+
* source of truth) rather than tracked in a per-instance map. The pool's
|
|
6274
|
+
* position index already scopes to this list's (parentId, position); the last
|
|
6275
|
+
* match wins, matching the original last-write-wins map semantics.
|
|
6276
|
+
*/
|
|
6277
|
+
#unacknowledgedSetOpIdAt(position) {
|
|
6278
|
+
if (this._pool === void 0 || this._id === void 0) {
|
|
6279
|
+
return void 0;
|
|
6280
|
+
}
|
|
6281
|
+
let opId;
|
|
6282
|
+
for (const op of this._pool.unacknowledgedOps.getByParentIdAndKey(
|
|
6283
|
+
this._id,
|
|
6284
|
+
position
|
|
6285
|
+
)) {
|
|
6286
|
+
if (op.intent === "set") {
|
|
6287
|
+
opId = op.opId;
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6290
|
+
return opId;
|
|
6291
|
+
}
|
|
6146
6292
|
/** @internal */
|
|
6147
6293
|
_attach(id, pool) {
|
|
6148
6294
|
super._attach(id, pool);
|
|
@@ -6223,13 +6369,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6223
6369
|
if (deletedDelta) {
|
|
6224
6370
|
delta.push(deletedDelta);
|
|
6225
6371
|
}
|
|
6226
|
-
const unacknowledgedOpId = this.#
|
|
6227
|
-
if (unacknowledgedOpId !== void 0) {
|
|
6228
|
-
|
|
6229
|
-
return delta.length === 0 ? { modified: false } : { modified: makeUpdate(this, delta), reverse: [] };
|
|
6230
|
-
} else {
|
|
6231
|
-
this.#unacknowledgedSets.delete(op.parentKey);
|
|
6232
|
-
}
|
|
6372
|
+
const unacknowledgedOpId = this.#unacknowledgedSetOpIdAt(op.parentKey);
|
|
6373
|
+
if (unacknowledgedOpId !== void 0 && unacknowledgedOpId !== op.opId) {
|
|
6374
|
+
return delta.length === 0 ? { modified: false } : { modified: makeUpdate(this, delta), reverse: [] };
|
|
6233
6375
|
}
|
|
6234
6376
|
const indexOfItemWithSamePosition = this._indexOfPosition(op.parentKey);
|
|
6235
6377
|
const existingItem = this.#items.find((item) => item._id === op.id);
|
|
@@ -6310,7 +6452,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6310
6452
|
}
|
|
6311
6453
|
return result.modified.updates[0];
|
|
6312
6454
|
}
|
|
6313
|
-
#applyRemoteInsert(op) {
|
|
6455
|
+
#applyRemoteInsert(op, fromSnapshot) {
|
|
6314
6456
|
if (this._pool === void 0) {
|
|
6315
6457
|
throw new Error("Can't attach child if managed pool is not present");
|
|
6316
6458
|
}
|
|
@@ -6320,11 +6462,82 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6320
6462
|
this.#shiftItemPosition(existingItemIndex, key);
|
|
6321
6463
|
}
|
|
6322
6464
|
const { newItem, newIndex } = this.#createAttachItemAndSort(op, key);
|
|
6465
|
+
const bumpDeltas = fromSnapshot ? [] : this.#bumpUnackedPushesAbove(key);
|
|
6323
6466
|
return {
|
|
6324
|
-
modified: makeUpdate(this, [
|
|
6467
|
+
modified: makeUpdate(this, [
|
|
6468
|
+
insertDelta(newIndex, newItem),
|
|
6469
|
+
...bumpDeltas
|
|
6470
|
+
]),
|
|
6325
6471
|
reverse: []
|
|
6326
6472
|
};
|
|
6327
6473
|
}
|
|
6474
|
+
/**
|
|
6475
|
+
* This list's own still-unacknowledged pushed items (their `intent: "push"`
|
|
6476
|
+
* Create op is still pending in the room's unacknowledgedOps). Derived from
|
|
6477
|
+
* the single source of truth, so an item drops out the instant its op is
|
|
6478
|
+
* acked, with no per-instance membership to leak. Yielded in push order.
|
|
6479
|
+
*
|
|
6480
|
+
* Restricted to items currently in `#items`: a pushed node whose op is still
|
|
6481
|
+
* pending may have been pulled out of the list (e.g. implicitly deleted by a
|
|
6482
|
+
* remote set, or removed by an undo) while still living in the pool, and such
|
|
6483
|
+
* a node must not be repositioned.
|
|
6484
|
+
*/
|
|
6485
|
+
*#unackedPushNodes() {
|
|
6486
|
+
if (this._pool === void 0 || this._id === void 0) {
|
|
6487
|
+
return;
|
|
6488
|
+
}
|
|
6489
|
+
for (const op of this._pool.unacknowledgedOps.getByParentId(this._id)) {
|
|
6490
|
+
if (op.intent !== "push") {
|
|
6491
|
+
continue;
|
|
6492
|
+
}
|
|
6493
|
+
const node = this._pool.getNode(op.id);
|
|
6494
|
+
if (node !== void 0 && this.#items.includes(node)) {
|
|
6495
|
+
yield node;
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
}
|
|
6499
|
+
/**
|
|
6500
|
+
* Optimistic no-flip for pushed items. When a remote op lands at or below my
|
|
6501
|
+
* still-unacked pushed items, those items must end up *after* it: FIFO plus
|
|
6502
|
+
* the room's serial processing guarantee the remote was processed first, so
|
|
6503
|
+
* my unacked pushes belong behind it. Re-chain the whole unacked-push block,
|
|
6504
|
+
* in push order, to sit after the highest confirmed sibling, so it keeps
|
|
6505
|
+
* rendering as a contiguous tail instead of getting interleaved. Local-only;
|
|
6506
|
+
* the real acks overwrite these keys with the (identical) server keys.
|
|
6507
|
+
*/
|
|
6508
|
+
#bumpUnackedPushesAbove(remoteKey) {
|
|
6509
|
+
const pending = new Set(this.#unackedPushNodes());
|
|
6510
|
+
if (pending.size === 0) {
|
|
6511
|
+
return [];
|
|
6512
|
+
}
|
|
6513
|
+
let minPending;
|
|
6514
|
+
for (const node of pending) {
|
|
6515
|
+
const pos = node._parentPos;
|
|
6516
|
+
if (minPending === void 0 || pos < minPending) {
|
|
6517
|
+
minPending = pos;
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6520
|
+
if (remoteKey < nn(minPending)) {
|
|
6521
|
+
return [];
|
|
6522
|
+
}
|
|
6523
|
+
let base;
|
|
6524
|
+
for (const item of this.#items) {
|
|
6525
|
+
if (!pending.has(item)) {
|
|
6526
|
+
base = item._parentPos;
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
const deltas = [];
|
|
6530
|
+
for (const node of pending) {
|
|
6531
|
+
const previousIndex = this.#items.findIndex((item) => item === node);
|
|
6532
|
+
base = makePosition(base);
|
|
6533
|
+
this.#updateItemPosition(node, base);
|
|
6534
|
+
const index = this.#items.findIndex((item) => item === node);
|
|
6535
|
+
if (index !== previousIndex) {
|
|
6536
|
+
deltas.push(moveDelta(previousIndex, index, node));
|
|
6537
|
+
}
|
|
6538
|
+
}
|
|
6539
|
+
return deltas;
|
|
6540
|
+
}
|
|
6328
6541
|
#applyInsertAck(op) {
|
|
6329
6542
|
const existingItem = this.#items.find((item) => item._id === op.id);
|
|
6330
6543
|
const key = asPos(op.parentKey);
|
|
@@ -6405,7 +6618,6 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6405
6618
|
if (this._pool?.getNode(id) !== void 0) {
|
|
6406
6619
|
return { modified: false };
|
|
6407
6620
|
}
|
|
6408
|
-
this.#unacknowledgedSets.set(key, nn(op.opId));
|
|
6409
6621
|
const indexOfItemWithSameKey = this._indexOfPosition(key);
|
|
6410
6622
|
child._attach(id, nn(this._pool));
|
|
6411
6623
|
child._setParentLink(this, key);
|
|
@@ -6415,8 +6627,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6415
6627
|
existingItem._detach();
|
|
6416
6628
|
this.#items.remove(existingItem);
|
|
6417
6629
|
this.#items.add(child);
|
|
6418
|
-
const reverse =
|
|
6630
|
+
const reverse = addIntentToRootOp(
|
|
6419
6631
|
existingItem._toOps(nn(this._id), key),
|
|
6632
|
+
"set",
|
|
6420
6633
|
op.id
|
|
6421
6634
|
);
|
|
6422
6635
|
const delta = [setDelta(indexOfItemWithSameKey, child)];
|
|
@@ -6441,7 +6654,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6441
6654
|
}
|
|
6442
6655
|
}
|
|
6443
6656
|
/** @internal */
|
|
6444
|
-
_attachChild(op, source) {
|
|
6657
|
+
_attachChild(op, source, fromSnapshot = false) {
|
|
6445
6658
|
if (this._pool === void 0) {
|
|
6446
6659
|
throw new Error("Can't attach child if managed pool is not present");
|
|
6447
6660
|
}
|
|
@@ -6456,7 +6669,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6456
6669
|
}
|
|
6457
6670
|
} else {
|
|
6458
6671
|
if (source === 1 /* THEIRS */) {
|
|
6459
|
-
result = this.#applyRemoteInsert(op);
|
|
6672
|
+
result = this.#applyRemoteInsert(op, fromSnapshot);
|
|
6460
6673
|
} else if (source === 2 /* OURS */) {
|
|
6461
6674
|
result = this.#applyInsertAck(op);
|
|
6462
6675
|
} else {
|
|
@@ -6659,8 +6872,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6659
6872
|
* @param element The element to add to the end of the LiveList.
|
|
6660
6873
|
*/
|
|
6661
6874
|
push(element) {
|
|
6662
|
-
this.
|
|
6663
|
-
return this.insert(element, this.length);
|
|
6875
|
+
return this.#injectAt(element, this.length, "push");
|
|
6664
6876
|
}
|
|
6665
6877
|
/**
|
|
6666
6878
|
* Inserts one element at a specified index.
|
|
@@ -6668,6 +6880,15 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6668
6880
|
* @param index The index at which you want to insert the element.
|
|
6669
6881
|
*/
|
|
6670
6882
|
insert(element, index) {
|
|
6883
|
+
return this.#injectAt(element, index, "insert");
|
|
6884
|
+
}
|
|
6885
|
+
/**
|
|
6886
|
+
* Shared implementation of `insert` and `push`. A `"push"` intent leaves the
|
|
6887
|
+
* client-computed position untouched (so optimistic rendering is unchanged),
|
|
6888
|
+
* but tags the Op so the server appends it to the true end of the list
|
|
6889
|
+
* instead of resolving its position against the client's stale view.
|
|
6890
|
+
*/
|
|
6891
|
+
#injectAt(element, index, intent) {
|
|
6671
6892
|
this._pool?.assertStorageIsWritable();
|
|
6672
6893
|
if (index < 0 || index > this.#items.length) {
|
|
6673
6894
|
throw new Error(
|
|
@@ -6683,8 +6904,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6683
6904
|
if (this._pool && this._id) {
|
|
6684
6905
|
const id = this._pool.generateId();
|
|
6685
6906
|
value._attach(id, this._pool);
|
|
6907
|
+
const ops = value._toOpsWithOpId(this._id, position, this._pool);
|
|
6686
6908
|
this._pool.dispatch(
|
|
6687
|
-
|
|
6909
|
+
intent === "push" ? addIntentToRootOp(ops, "push") : ops,
|
|
6688
6910
|
[{ type: OpCode.DELETE_CRDT, id }],
|
|
6689
6911
|
/* @__PURE__ */ new Map([
|
|
6690
6912
|
[this._id, makeUpdate(this, [insertDelta(index, value)])]
|
|
@@ -6842,13 +7064,14 @@ var LiveList = class _LiveList extends AbstractCrdt {
|
|
|
6842
7064
|
value._attach(id, this._pool);
|
|
6843
7065
|
const storageUpdates = /* @__PURE__ */ new Map();
|
|
6844
7066
|
storageUpdates.set(this._id, makeUpdate(this, [setDelta(index, value)]));
|
|
6845
|
-
const ops =
|
|
7067
|
+
const ops = addIntentToRootOp(
|
|
6846
7068
|
value._toOpsWithOpId(this._id, position, this._pool),
|
|
7069
|
+
"set",
|
|
6847
7070
|
existingId
|
|
6848
7071
|
);
|
|
6849
|
-
|
|
6850
|
-
const reverseOps = HACK_addIntentAndDeletedIdToOperation(
|
|
7072
|
+
const reverseOps = addIntentToRootOp(
|
|
6851
7073
|
existingItem._toOps(this._id, position),
|
|
7074
|
+
"set",
|
|
6852
7075
|
id
|
|
6853
7076
|
);
|
|
6854
7077
|
this._pool.dispatch(ops, reverseOps, storageUpdates);
|
|
@@ -7049,15 +7272,11 @@ function moveDelta(previousIndex, index, item) {
|
|
|
7049
7272
|
previousIndex
|
|
7050
7273
|
};
|
|
7051
7274
|
}
|
|
7052
|
-
function
|
|
7275
|
+
function addIntentToRootOp(ops, intent, deletedId) {
|
|
7053
7276
|
return ops.map((op, index) => {
|
|
7054
7277
|
if (index === 0) {
|
|
7055
7278
|
const firstOp = op;
|
|
7056
|
-
return {
|
|
7057
|
-
...firstOp,
|
|
7058
|
-
intent: "set",
|
|
7059
|
-
deletedId
|
|
7060
|
-
};
|
|
7279
|
+
return { ...firstOp, intent, deletedId };
|
|
7061
7280
|
} else {
|
|
7062
7281
|
return op;
|
|
7063
7282
|
}
|
|
@@ -8297,6 +8516,31 @@ function lsonToLiveNode(value) {
|
|
|
8297
8516
|
return new LiveRegister(value);
|
|
8298
8517
|
}
|
|
8299
8518
|
}
|
|
8519
|
+
function dumpPool(pool) {
|
|
8520
|
+
const rows = Array.from(pool.nodes.values(), (node) => {
|
|
8521
|
+
const parent = node.parent;
|
|
8522
|
+
const parentId = parent.type === "HasParent" ? parent.node._id ?? "?" : parent.type === "Orphaned" ? "<orphaned>" : "-";
|
|
8523
|
+
let value;
|
|
8524
|
+
if (node instanceof LiveRegister) {
|
|
8525
|
+
value = stringifyOrLog(node.data);
|
|
8526
|
+
} else if (node instanceof LiveList) {
|
|
8527
|
+
value = "<LiveList>";
|
|
8528
|
+
} else if (node instanceof LiveMap) {
|
|
8529
|
+
value = "<LiveMap>";
|
|
8530
|
+
} else {
|
|
8531
|
+
value = "<LiveObject>";
|
|
8532
|
+
}
|
|
8533
|
+
return { id: nn(node._id), parentId, key: node._parentKey ?? "", value };
|
|
8534
|
+
});
|
|
8535
|
+
rows.sort((a, b) => {
|
|
8536
|
+
if (a.parentId !== b.parentId) return a.parentId < b.parentId ? -1 : 1;
|
|
8537
|
+
if (a.key !== b.key) return a.key < b.key ? -1 : 1;
|
|
8538
|
+
return 0;
|
|
8539
|
+
});
|
|
8540
|
+
return rows.map(
|
|
8541
|
+
(r) => ` ${r.id} parent=${r.parentId} key=${r.key || "\u2014"} ${r.value}`
|
|
8542
|
+
).join("\n");
|
|
8543
|
+
}
|
|
8300
8544
|
function getTreesDiffOperations(currentItems, newItems) {
|
|
8301
8545
|
const ops = [];
|
|
8302
8546
|
currentItems.forEach((_, id) => {
|
|
@@ -9306,6 +9550,7 @@ function createRoom(options, config) {
|
|
|
9306
9550
|
delegates,
|
|
9307
9551
|
config.enableDebugLogging
|
|
9308
9552
|
);
|
|
9553
|
+
const unacknowledgedOps = new UnacknowledgedOps();
|
|
9309
9554
|
const context = {
|
|
9310
9555
|
buffer: {
|
|
9311
9556
|
flushTimerID: void 0,
|
|
@@ -9333,14 +9578,15 @@ function createRoom(options, config) {
|
|
|
9333
9578
|
pool: createManagedPool(roomId, {
|
|
9334
9579
|
getCurrentConnectionId,
|
|
9335
9580
|
onDispatch,
|
|
9336
|
-
isStorageWritable
|
|
9581
|
+
isStorageWritable,
|
|
9582
|
+
unacknowledgedOps
|
|
9337
9583
|
}),
|
|
9338
9584
|
root: void 0,
|
|
9339
9585
|
undoStack: [],
|
|
9340
9586
|
redoStack: [],
|
|
9341
9587
|
pausedHistory: null,
|
|
9342
9588
|
activeBatch: null,
|
|
9343
|
-
unacknowledgedOps
|
|
9589
|
+
unacknowledgedOps
|
|
9344
9590
|
};
|
|
9345
9591
|
const nodeMapBuffer = makeNodeMapBuffer();
|
|
9346
9592
|
const stopwatch = config.enableDebugLogging ? makeStopWatch() : void 0;
|
|
@@ -9548,7 +9794,11 @@ function createRoom(options, config) {
|
|
|
9548
9794
|
currentItems.set(id, crdt._serialize());
|
|
9549
9795
|
}
|
|
9550
9796
|
const ops = getTreesDiffOperations(currentItems, nodes);
|
|
9551
|
-
const result = applyRemoteOps(
|
|
9797
|
+
const result = applyRemoteOps(
|
|
9798
|
+
ops,
|
|
9799
|
+
/* fromSnapshot */
|
|
9800
|
+
true
|
|
9801
|
+
);
|
|
9552
9802
|
notify(result.updates);
|
|
9553
9803
|
} else {
|
|
9554
9804
|
context.root = LiveObject._fromItems(
|
|
@@ -9630,15 +9880,16 @@ function createRoom(options, config) {
|
|
|
9630
9880
|
);
|
|
9631
9881
|
return { opsToEmit: opsWithOpIds, reverse, updates };
|
|
9632
9882
|
}
|
|
9633
|
-
function applyRemoteOps(ops) {
|
|
9883
|
+
function applyRemoteOps(ops, fromSnapshot = false) {
|
|
9634
9884
|
return applyOps(
|
|
9635
9885
|
[],
|
|
9636
9886
|
ops,
|
|
9637
9887
|
/* isLocal */
|
|
9638
|
-
false
|
|
9888
|
+
false,
|
|
9889
|
+
fromSnapshot
|
|
9639
9890
|
);
|
|
9640
9891
|
}
|
|
9641
|
-
function applyOps(pframes, ops, isLocal) {
|
|
9892
|
+
function applyOps(pframes, ops, isLocal, fromSnapshot = false) {
|
|
9642
9893
|
const output = {
|
|
9643
9894
|
reverse: new Deque(),
|
|
9644
9895
|
storageUpdates: /* @__PURE__ */ new Map(),
|
|
@@ -9674,7 +9925,7 @@ function createRoom(options, config) {
|
|
|
9674
9925
|
} else {
|
|
9675
9926
|
source = 1 /* THEIRS */;
|
|
9676
9927
|
}
|
|
9677
|
-
const applyOpResult = applyOp(op, source);
|
|
9928
|
+
const applyOpResult = applyOp(op, source, fromSnapshot);
|
|
9678
9929
|
if (applyOpResult.modified) {
|
|
9679
9930
|
const nodeId = applyOpResult.modified.node._id;
|
|
9680
9931
|
if (!(nodeId && createdNodeIds.has(nodeId))) {
|
|
@@ -9700,7 +9951,7 @@ function createRoom(options, config) {
|
|
|
9700
9951
|
}
|
|
9701
9952
|
};
|
|
9702
9953
|
}
|
|
9703
|
-
function applyOp(op, source) {
|
|
9954
|
+
function applyOp(op, source, fromSnapshot = false) {
|
|
9704
9955
|
if (isIgnoredOp(op)) {
|
|
9705
9956
|
return { modified: false };
|
|
9706
9957
|
}
|
|
@@ -9739,7 +9990,7 @@ function createRoom(options, config) {
|
|
|
9739
9990
|
if (parentNode === void 0) {
|
|
9740
9991
|
return { modified: false };
|
|
9741
9992
|
}
|
|
9742
|
-
return parentNode._attachChild(op, source);
|
|
9993
|
+
return parentNode._attachChild(op, source, fromSnapshot);
|
|
9743
9994
|
}
|
|
9744
9995
|
}
|
|
9745
9996
|
}
|
|
@@ -9879,12 +10130,11 @@ function createRoom(options, config) {
|
|
|
9879
10130
|
}
|
|
9880
10131
|
}
|
|
9881
10132
|
function applyAndSendOfflineOps(unackedOps) {
|
|
9882
|
-
if (unackedOps.
|
|
10133
|
+
if (unackedOps.length === 0) {
|
|
9883
10134
|
return;
|
|
9884
10135
|
}
|
|
9885
10136
|
const messages = [];
|
|
9886
|
-
const
|
|
9887
|
-
const result = applyLocalOps(inOps);
|
|
10137
|
+
const result = applyLocalOps(unackedOps);
|
|
9888
10138
|
messages.push({
|
|
9889
10139
|
type: ClientMsgCode.UPDATE_STORAGE,
|
|
9890
10140
|
ops: result.opsToEmit
|
|
@@ -10108,7 +10358,7 @@ function createRoom(options, config) {
|
|
|
10108
10358
|
const storageOps = context.buffer.storageOperations;
|
|
10109
10359
|
if (storageOps.length > 0) {
|
|
10110
10360
|
for (const op of storageOps) {
|
|
10111
|
-
context.unacknowledgedOps.
|
|
10361
|
+
context.unacknowledgedOps.add(op);
|
|
10112
10362
|
}
|
|
10113
10363
|
notifyStorageStatus();
|
|
10114
10364
|
}
|
|
@@ -10335,9 +10585,9 @@ function createRoom(options, config) {
|
|
|
10335
10585
|
}
|
|
10336
10586
|
}
|
|
10337
10587
|
function processInitialStorage(nodes) {
|
|
10338
|
-
const
|
|
10588
|
+
const unacknowledgedOps2 = [...context.unacknowledgedOps.values()];
|
|
10339
10589
|
createOrUpdateRootFromMessage(nodes);
|
|
10340
|
-
applyAndSendOfflineOps(
|
|
10590
|
+
applyAndSendOfflineOps(unacknowledgedOps2);
|
|
10341
10591
|
_resolveStoragePromise?.();
|
|
10342
10592
|
notifyStorageStatus();
|
|
10343
10593
|
eventHub.storageDidLoad.notify();
|
|
@@ -10926,6 +11176,11 @@ function createRoom(options, config) {
|
|
|
10926
11176
|
connect: () => managedSocket.connect(),
|
|
10927
11177
|
reconnect: () => managedSocket.reconnect(),
|
|
10928
11178
|
disconnect: () => managedSocket.disconnect(),
|
|
11179
|
+
_dump: () => {
|
|
11180
|
+
const n = context.pool.nodes.size;
|
|
11181
|
+
return `Room "${roomId}" (${n} node${n === 1 ? "" : "s"}):
|
|
11182
|
+
${dumpPool(context.pool)}`;
|
|
11183
|
+
},
|
|
10929
11184
|
destroy: () => {
|
|
10930
11185
|
pendingFeedsRequests.forEach(
|
|
10931
11186
|
(request) => request.reject(new Error("Room destroyed"))
|
|
@@ -11433,6 +11688,7 @@ function createClient(options) {
|
|
|
11433
11688
|
{
|
|
11434
11689
|
enterRoom,
|
|
11435
11690
|
getRoom,
|
|
11691
|
+
_dump: () => Array.from(roomsById.values(), ({ room }) => room._dump()).join("\n\n"),
|
|
11436
11692
|
logout,
|
|
11437
11693
|
// Public inbox notifications API
|
|
11438
11694
|
getInboxNotifications: httpClient.getInboxNotifications,
|