@panphora/clayjs 1.13.1 → 1.15.0
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/THIRD-PARTY-NOTICES.md +19 -0
- package/dist/clay.standalone.js +69385 -40992
- package/dist/server/checkpoint.cjs +1 -0
- package/dist/server/checkpoint.mjs +8 -0
- package/dist/server/core.cjs +72210 -0
- package/dist/server/rooms.cjs +1 -0
- package/dist/server/rooms.mjs +66 -0
- package/docs/guide.md +127 -99
- package/entries/clay.js +22 -0
- package/entries/sap.js +5 -5
- package/package.json +16 -2
- package/packed-contract.json +18 -3
- package/src/core/boot-phase.js +6 -0
- package/src/core/conflict-download.js +12 -3
- package/src/core/host-meta.js +75 -0
- package/src/core/persist.js +11 -0
- package/src/core/save-core.js +266 -31
- package/src/core/save.js +154 -36
- package/src/core/snapshot.js +51 -13
- package/src/core/style-comparisons.js +37 -0
- package/src/dom/insert-style-tag.js +11 -12
- package/src/lib/derived.js +116 -0
- package/src/lib/effect.js +130 -0
- package/src/lib/mutation.js +3 -1
- package/src/lib/style-adoption.js +9 -0
- package/src/lib/style.js +47 -0
- package/src/live/fast-lane.js +347 -60
- package/src/live/local-lane.js +32 -6
- package/src/live/ops.js +77 -0
- package/src/live/provenance.js +129 -0
- package/src/live/save-history.js +188 -0
- package/src/live/server-lane.js +9 -2
- package/src/live/store.js +194 -16
- package/src/loader-logic.js +16 -4
- package/src/loader.js +37 -1
- package/src/page/address.js +20 -0
- package/src/page/adopt-boot.js +189 -0
- package/src/page/adopt.js +240 -0
- package/src/page/ai-tool.js +188 -0
- package/src/page/announce.js +21 -0
- package/src/page/blocks.js +1170 -0
- package/src/page/blocksel.js +269 -0
- package/src/page/board-drag.js +226 -0
- package/src/page/board-panel.js +353 -0
- package/src/page/board-style.js +146 -0
- package/src/page/board-view.js +447 -0
- package/src/page/board.js +41 -0
- package/src/page/chart.js +220 -0
- package/src/page/checklist.js +143 -0
- package/src/page/chrome.js +587 -0
- package/src/page/csv-import.js +169 -0
- package/src/page/drag.js +202 -0
- package/src/page/drop.js +112 -0
- package/src/page/editor.js +386 -0
- package/src/page/highlight.js +268 -0
- package/src/page/inline.js +60 -1
- package/src/page/items.js +159 -0
- package/src/page/link.js +247 -0
- package/src/page/markdown.js +71 -0
- package/src/page/media.js +570 -0
- package/src/page/merge.js +130 -0
- package/src/page/model.js +71 -9
- package/src/page/placeholder.js +23 -0
- package/src/page/projection.js +647 -0
- package/src/page/recover.js +193 -0
- package/src/page/rich/cell.js +16 -0
- package/src/page/rich/chart.js +104 -0
- package/src/page/rich/csv.js +118 -0
- package/src/page/rich/page-tool.js +86 -0
- package/src/page/rich/tool-runtime.js +195 -0
- package/src/page/save-projection.js +53 -0
- package/src/page/save-states.js +85 -0
- package/src/page/schema.js +12 -5
- package/src/page/table-kinds.js +20 -0
- package/src/page/table-paste.js +57 -0
- package/src/page/table-select.js +244 -0
- package/src/page/table-style.js +25 -0
- package/src/page/table.js +650 -0
- package/src/page/themes/content.css +46 -0
- package/src/page/themes/light-1.css +269 -0
- package/src/page/themes/light-2.css +211 -0
- package/src/page/themes/light-3.css +130 -0
- package/src/page/themes/light-4.css +299 -0
- package/src/page/tool-data.js +25 -0
- package/src/page/tool-view.js +68 -0
- package/src/page/tools.js +133 -0
- package/src/page/view.js +136 -0
- package/src/page/ybind/author.js +25 -0
- package/src/page/ybind/binding.js +92 -0
- package/src/page/ybind/board-commands.js +170 -0
- package/src/page/ybind/board.js +89 -0
- package/src/page/ybind/bridge.js +382 -0
- package/src/page/ybind/conversion.js +26 -0
- package/src/page/ybind/core.js +7 -0
- package/src/page/ybind/decode.js +208 -0
- package/src/page/ybind/doc.js +58 -0
- package/src/page/ybind/external.js +190 -0
- package/src/page/ybind/group.js +124 -0
- package/src/page/ybind/index.js +12 -0
- package/src/page/ybind/layout.js +27 -0
- package/src/page/ybind/marks.js +18 -0
- package/src/page/ybind/migrate.js +152 -0
- package/src/page/ybind/moves.js +288 -0
- package/src/page/ybind/origins.js +257 -0
- package/src/page/ybind/parts.js +44 -0
- package/src/page/ybind/projection-map.js +92 -0
- package/src/page/ybind/provider.js +285 -0
- package/src/page/ybind/recovery.js +411 -0
- package/src/page/ybind/seed.js +27 -0
- package/src/page/ybind/table.js +166 -0
- package/src/page/ybind/tokens.js +93 -0
- package/src/page/ybind/walk.js +333 -0
- package/src/page/ybind/writer.js +307 -0
- package/src/plugins/derive.js +12 -0
- package/src/plugins/indicator.js +16 -6
- package/src/plugins/live.js +742 -65
- package/src/plugins/page.js +547 -0
- package/src/plugins/undo.js +62 -4
- package/src/server/checkpoint/index.js +218 -0
- package/src/server/rooms/index.js +59 -0
- package/src/server/rooms/live-data-ops.js +369 -0
- package/src/server/rooms/live-data-rooms.js +965 -0
- package/src/server/rooms/live-data-tags.js +49 -0
- package/src/server/rooms/live-page-history.js +41 -0
- package/src/server/rooms/live-provenance.js +142 -0
- package/src/server/rooms/page-admission.js +79 -0
- package/src/server/rooms/page-conversion.js +62 -0
- package/src/server/rooms/page-rebase.js +108 -0
- package/src/server/rooms/page-state.js +91 -0
- package/src/server/rooms/page-write-admission.js +63 -0
- package/src/server/rooms/page-write-guard.js +23 -0
- package/src/server/rooms/recent.js +18 -0
- package/src/server/rooms/spec-wire.js +17 -0
- package/src/standalone.js +22 -0
- package/src/sync/conflict-revert.js +2 -0
- package/src/sync/conflicts.js +2 -2
- package/src/sync/live-sync.js +36 -8
- package/src/sync/stream.js +62 -16
- package/src/ui/index.js +4 -0
- package/src/ui/toast.js +21 -5
- package/src/vendor/pierre/pierre.vendor.js +3 -0
- package/src/vendor/sap-core.vendor.js +9 -0
- package/src/vendor/sap-ownership.vendor.js +111 -0
- package/src/vendor/yjs.vendor.js +7 -0
- package/src/page/binding.js +0 -647
- package/src/page/replay.js +0 -578
package/src/live/fast-lane.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// the same frames through a transport:
|
|
5
5
|
// start({ open, close, frame, state, writer }) -> stop function
|
|
6
6
|
// send(clientId, { name, n, ops, epoch }) -> Promise<{ status, body, retryAfter }>
|
|
7
|
-
// fetchState(clientId) -> Promise<{ epoch, seq, writer, stores } | { status } | null>
|
|
7
|
+
// fetchState(clientId, since, { signal }) -> Promise<{ epoch, seq, writer, stores } | { status } | null>
|
|
8
|
+
// answered within STATE_TIMEOUT_MS or abandoned: its signal aborts and the read is made again
|
|
8
9
|
// A new room (a new epoch) holds some of this tab's frames still in flight; see
|
|
9
10
|
// sortAwaiting for how each is sorted.
|
|
10
11
|
//
|
|
@@ -13,12 +14,17 @@
|
|
|
13
14
|
// the room's log rebuilds the value; -1: rewrite now). Both are numbers in the
|
|
14
15
|
// current epoch, taken from the room and never carried across epochs.
|
|
15
16
|
|
|
16
|
-
import {
|
|
17
|
+
import { unionRanges, contains, frameSet, hasNumber } from "./provenance.js";
|
|
17
18
|
|
|
18
19
|
export const MIN_GAP_MS = 40;
|
|
19
20
|
export const MAX_OPS = 128;
|
|
20
21
|
export const RETRY_MS = 500;
|
|
21
|
-
export const
|
|
22
|
+
export const STATE_TIMEOUT_MS = 10_000;
|
|
23
|
+
export const FRAME_BYTES = 63 * 1024;
|
|
24
|
+
// What a host still on the old 16 KB body cap takes: a multi-op frame it
|
|
25
|
+
// answers 413 is packed again at this size for the rest of the session.
|
|
26
|
+
export const OLD_FRAME_BYTES = 15 * 1024;
|
|
27
|
+
const PAGE_STORE = /^page(\.[0-9]+)?$/;
|
|
22
28
|
const BUFFER_MAX = 512;
|
|
23
29
|
const RETRY_MAX_MS = 8000;
|
|
24
30
|
const REVOKED = new Set([401, 403]);
|
|
@@ -42,6 +48,12 @@ const unhandled = (status) => status === 429 || status === 503;
|
|
|
42
48
|
* @param {(name: string) => boolean} [options.held] - a save still on the wire carries some of
|
|
43
49
|
* this store's pending ops in file mode: nothing of the store is framed until release(name),
|
|
44
50
|
* so those ops are framed in order if the save does not land, and never if it does
|
|
51
|
+
* @param {(name: string) => number[]} [options.heldIds] - the ids of those ops: a room state
|
|
52
|
+
* cannot say whether they are in its value, so it is not applied to the store until the
|
|
53
|
+
* room's receipts hold their frames or hold(name, ids) lets go of them
|
|
54
|
+
* @param {(name: string, ids: number[]) => boolean} [options.hold] - true to keep waiting for
|
|
55
|
+
* the save's answer; false when the ops left pending (the caller gave up on them)
|
|
56
|
+
* @param {(name: string) => void} [options.taken] - the store took a room state
|
|
45
57
|
* @param {() => void} [options.onStop] - the lane stopped for good
|
|
46
58
|
*/
|
|
47
59
|
export function createFastLane({
|
|
@@ -54,6 +66,9 @@ export function createFastLane({
|
|
|
54
66
|
onLanded = () => {},
|
|
55
67
|
onCheckpoint = () => {},
|
|
56
68
|
held = () => false,
|
|
69
|
+
heldIds = () => [],
|
|
70
|
+
hold = () => true,
|
|
71
|
+
taken = () => {},
|
|
57
72
|
onStop = () => {},
|
|
58
73
|
now = () => Date.now(),
|
|
59
74
|
setTimer = setTimeout,
|
|
@@ -63,6 +78,9 @@ export function createFastLane({
|
|
|
63
78
|
let connected = false;
|
|
64
79
|
let synced = false;
|
|
65
80
|
let epoch = null;
|
|
81
|
+
let receiptGeneration = null;
|
|
82
|
+
let pageGeneration = null;
|
|
83
|
+
let provenanceVersion = 0;
|
|
66
84
|
let seq = 0;
|
|
67
85
|
let writer = null;
|
|
68
86
|
let roomNames = new Set();
|
|
@@ -75,7 +93,9 @@ export function createFastLane({
|
|
|
75
93
|
let lastSentAt = -Infinity;
|
|
76
94
|
let holdUntil = 0;
|
|
77
95
|
let timer = null;
|
|
96
|
+
let readTimer = null;
|
|
78
97
|
let retryDelay = RETRY_MS;
|
|
98
|
+
let frameBytes = FRAME_BYTES;
|
|
79
99
|
let resyncDelay = RETRY_MS;
|
|
80
100
|
// Bumped when the stream opens or closes, and when a state is taken: a state
|
|
81
101
|
// request answered after either is obsolete.
|
|
@@ -85,13 +105,19 @@ export function createFastLane({
|
|
|
85
105
|
let conn;
|
|
86
106
|
let stateCount = 0;
|
|
87
107
|
let fetchAgain = false;
|
|
108
|
+
let reads = 0;
|
|
109
|
+
// A store left the room, so frames went by that its value never took: state
|
|
110
|
+
// reads ask for every store whole, never for the moves since seq, until a
|
|
111
|
+
// state is applied.
|
|
112
|
+
let missed = false;
|
|
88
113
|
let unsubscribe = null;
|
|
89
114
|
// Per store: the next n, ops not yet in a frame, frames sent but not yet seen
|
|
90
115
|
// back (each { n, ops, through, seq, sent, refused, uncertain }), and the
|
|
91
116
|
// newest op id ever put in a frame.
|
|
92
117
|
const lanes = new Map();
|
|
118
|
+
const abandoned = new Set();
|
|
93
119
|
|
|
94
|
-
const tell = (name, event) => entries.get(name)?.store.deliver?.({ code: null, n: null, seq: null, through: null, retryAfter: null, ...event });
|
|
120
|
+
const tell = (name, event) => entries.get(name)?.store.deliver?.({ code: null, n: null, seq: null, through: null, retryAfter: null, client: clientId, ...event });
|
|
95
121
|
|
|
96
122
|
function laneOf(name) {
|
|
97
123
|
let lane = lanes.get(name);
|
|
@@ -100,30 +126,49 @@ export function createFastLane({
|
|
|
100
126
|
}
|
|
101
127
|
|
|
102
128
|
// The store's ops still to frame: pending, past what was framed.
|
|
129
|
+
// The first frame that reaches an adoption's last op carries its marker.
|
|
130
|
+
function markAdopted(lane, frame) {
|
|
131
|
+
if (!lane.adopted || frame.through < lane.adopted.through) return;
|
|
132
|
+
frame.adopted = lane.adopted.rev;
|
|
133
|
+
lane.adopted = null;
|
|
134
|
+
}
|
|
135
|
+
|
|
103
136
|
function unframed(name, lane) {
|
|
104
137
|
const store = entries.get(name)?.store;
|
|
105
138
|
if (!store) return [];
|
|
106
139
|
return store.pendingEntries().filter((pending) => pending.id > lane.framed);
|
|
107
140
|
}
|
|
108
141
|
|
|
142
|
+
// A frame none of whose ops is pending any more (a save at save speed took
|
|
143
|
+
// them, and the room merges that file) leaves the lane: never sent again.
|
|
144
|
+
function prune(name, lane) {
|
|
145
|
+
const store = entries.get(name)?.store;
|
|
146
|
+
if (!store) return;
|
|
147
|
+
const live = new Set(store.pendingEntries().map((pending) => pending.id));
|
|
148
|
+
lane.awaiting = lane.awaiting.filter((frame) => frame === flying || !frame.entries || frame.entries.some((e) => live.has(e.id)));
|
|
149
|
+
}
|
|
150
|
+
|
|
109
151
|
// Never applied: every attempt was refused before the room handled it.
|
|
110
152
|
const unapplied = (frame) => frame.refused === true && !frame.uncertain;
|
|
111
153
|
|
|
112
|
-
// Stop sending this store: it leaves the room.
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
154
|
+
// Stop sending this store: it leaves the room. Every op the room has not
|
|
155
|
+
// confirmed stays pending, so the next save writes it: an op never framed, a
|
|
156
|
+
// frame refused, a frame that landed but has not come back, and the frame on
|
|
157
|
+
// the wire, whose answer decides it (decide). The frames stay as records: a
|
|
158
|
+
// rejoin settles each by the room's answer or receipt, or sends it again.
|
|
159
|
+
// Leaving wakes the checkpoints (onApplied): what is pending now saves at
|
|
160
|
+
// save speed. kept: the room still holds the store, so its frames go by
|
|
161
|
+
// while this tab is out, and the rejoin reads it whole (missed). A store the
|
|
162
|
+
// room does not hold has no frames to miss.
|
|
163
|
+
function leaveRoom(name, lane, kept = true) {
|
|
117
164
|
const store = entries.get(name)?.store;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
165
|
+
for (const frame of lane.awaiting) if (frame === flying) frame.undecided = true;
|
|
166
|
+
if (kept) missed = true;
|
|
167
|
+
lane.out = true;
|
|
121
168
|
onLeave(name, { epoch, seq });
|
|
122
169
|
lane.unsent = [];
|
|
123
|
-
lane.awaiting = [];
|
|
124
|
-
// What is left pending is framed afresh if the store comes back.
|
|
125
|
-
lane.framed = through;
|
|
126
170
|
store?.setMode("file");
|
|
171
|
+
onApplied();
|
|
127
172
|
}
|
|
128
173
|
|
|
129
174
|
function schedule() {
|
|
@@ -140,30 +185,25 @@ export function createFastLane({
|
|
|
140
185
|
// oldest unsent ops of one store; stores take turns.
|
|
141
186
|
function nextFrame() {
|
|
142
187
|
for (const [name, lane] of lanes) {
|
|
188
|
+
if (!roomNames.has(name) || held(name)) continue;
|
|
143
189
|
const retry = lane.awaiting.find((frame) => !frame.sent);
|
|
144
190
|
if (retry) return { name, lane, frame: retry };
|
|
145
191
|
}
|
|
146
192
|
for (const [name, lane] of lanes) {
|
|
147
193
|
if (!lane.unsent.length || !roomNames.has(name) || held(name)) continue;
|
|
148
|
-
// Packed by bytes as well as count
|
|
194
|
+
// Packed by bytes as well as count, so a host still on a 64 KB body cap
|
|
195
|
+
// takes every frame; one op larger than a frame goes in a frame of its own.
|
|
149
196
|
let size = byteSize({ name, n: lane.n + 1, ops: [], epoch });
|
|
150
197
|
let count = 0;
|
|
151
198
|
while (count < lane.unsent.length && count < MAX_OPS) {
|
|
152
199
|
const more = byteSize(lane.unsent[count].op) + 1;
|
|
153
|
-
if (size + more >
|
|
200
|
+
if (count && size + more > frameBytes) break;
|
|
154
201
|
size += more;
|
|
155
202
|
count++;
|
|
156
203
|
}
|
|
157
|
-
if (count === 0) {
|
|
158
|
-
const [entry] = lane.unsent.splice(0, 1);
|
|
159
|
-
console.warn(`clay.live("${name}"): one update was larger than ${FRAME_BYTES} bytes, so it was undone`);
|
|
160
|
-
const store = entries.get(name)?.store;
|
|
161
|
-
store?.rebase(store.authoritative, { source: "state", through: entry.id, cause: "undone" });
|
|
162
|
-
tell(name, { phase: "undone", code: "op-too-large", through: entry.id });
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
204
|
const take = lane.unsent.splice(0, count);
|
|
166
|
-
const frame = { n: ++lane.n, ops: take.map((entry) => entry.op), through: take[take.length - 1].id, seq: null, sent: false };
|
|
205
|
+
const frame = { n: ++lane.n, ops: take.map((entry) => entry.op), entries: take, first: take[0].id, through: take[take.length - 1].id, seq: null, sent: false, receiptGeneration, pageGeneration };
|
|
206
|
+
markAdopted(lane, frame);
|
|
167
207
|
lane.framed = Math.max(lane.framed, frame.through);
|
|
168
208
|
lane.awaiting.push(frame);
|
|
169
209
|
return { name, lane, frame };
|
|
@@ -179,9 +219,11 @@ export function createFastLane({
|
|
|
179
219
|
lanes.delete(name);
|
|
180
220
|
lanes.set(name, lane);
|
|
181
221
|
const sentEpoch = epoch;
|
|
222
|
+
frame.epoch = sentEpoch;
|
|
223
|
+
frame.client = clientId;
|
|
182
224
|
frame.sentOn = connection;
|
|
183
225
|
frame.sent = true;
|
|
184
|
-
tell(name, { phase: "sent", n: frame.n, through: frame.through });
|
|
226
|
+
tell(name, { phase: "sent", n: frame.n, first: frame.first, through: frame.through });
|
|
185
227
|
// Refusal is evidence about earlier attempts only: this one may apply.
|
|
186
228
|
frame.refused = false;
|
|
187
229
|
inFlight = true;
|
|
@@ -189,7 +231,7 @@ export function createFastLane({
|
|
|
189
231
|
lastSentAt = now();
|
|
190
232
|
let result;
|
|
191
233
|
try {
|
|
192
|
-
result = await transport.send(clientId, { name, n: frame.n, ops: frame.ops, epoch: sentEpoch, ...(conn !== undefined && { conn }) });
|
|
234
|
+
result = await transport.send(clientId, { name, n: frame.n, ops: frame.ops, epoch: sentEpoch, ...(conn !== undefined && { conn }), ...(frame.receiptGeneration !== null && { receiptGeneration: frame.receiptGeneration }), ...(frame.pageGeneration !== null && PAGE_STORE.test(name) && { pageGeneration: frame.pageGeneration }), ...(frame.adopted && { adopted: frame.adopted }) });
|
|
193
235
|
} catch {
|
|
194
236
|
result = null;
|
|
195
237
|
}
|
|
@@ -212,6 +254,19 @@ export function createFastLane({
|
|
|
212
254
|
frame.refused = true;
|
|
213
255
|
frame.sent = false;
|
|
214
256
|
frame.seq = null;
|
|
257
|
+
if (frame.epoch !== epoch) frame.receiptGeneration = receiptGeneration;
|
|
258
|
+
if (frame.epoch !== epoch) frame.pageGeneration = pageGeneration;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Its store left the room meanwhile: its ops stay pending for the save,
|
|
262
|
+
// and the frame waits for the rejoin, which settles it by its seq or the
|
|
263
|
+
// room's receipt.
|
|
264
|
+
if (!roomNames.has(name)) {
|
|
265
|
+
if (status >= 200 && status < 300 && Number.isInteger(body?.seq) && !body?.stale) frame.seq = body.seq;
|
|
266
|
+
else {
|
|
267
|
+
frame.uncertain = true;
|
|
268
|
+
frame.uncertainSince ??= frame.sentOn;
|
|
269
|
+
}
|
|
215
270
|
return;
|
|
216
271
|
}
|
|
217
272
|
lane.awaiting.splice(lane.awaiting.indexOf(frame), 1);
|
|
@@ -223,6 +278,7 @@ export function createFastLane({
|
|
|
223
278
|
// authoritative values now, whether its answer or its event said so.
|
|
224
279
|
function settleAwaiting() {
|
|
225
280
|
for (const [name, lane] of lanes) {
|
|
281
|
+
if (!roomNames.has(name)) continue;
|
|
226
282
|
let through;
|
|
227
283
|
while (lane.awaiting.length && lane.awaiting[0].seq !== null && lane.awaiting[0].seq <= seq) {
|
|
228
284
|
through = lane.awaiting.shift().through;
|
|
@@ -303,24 +359,41 @@ export function createFastLane({
|
|
|
303
359
|
console.warn(`clay.live("${name}"): the room has no such store, so it stays at save speed (${reason})`);
|
|
304
360
|
frame.refused = true;
|
|
305
361
|
roomNames.delete(name);
|
|
306
|
-
leaveRoom(name, lane);
|
|
362
|
+
leaveRoom(name, lane, false);
|
|
307
363
|
return;
|
|
308
364
|
}
|
|
309
365
|
lane.awaiting.splice(lane.awaiting.indexOf(frame), 1);
|
|
366
|
+
// A host still on the old body cap: pack its ops again, smaller, and send
|
|
367
|
+
// them again. Only an op too large on its own is refused. That host's 413
|
|
368
|
+
// comes from its body parser, with no code; the room's own size refusal
|
|
369
|
+
// (a store or room cap, code "too-large") is a refusal like any other.
|
|
370
|
+
if (status === 413 && !body?.code && frame.entries?.length > 1 && frameBytes > OLD_FRAME_BYTES) {
|
|
371
|
+
frameBytes = OLD_FRAME_BYTES;
|
|
372
|
+
console.warn(`clay.live("${name}"): the host takes smaller updates, so they go in frames of ${frameBytes} bytes`);
|
|
373
|
+
lane.unsent.unshift(...frame.entries);
|
|
374
|
+
if (frame.adopted) lane.adopted = { rev: frame.adopted, through: frame.through };
|
|
375
|
+
// Unframed again: a new room's state frames them afresh (unframed()).
|
|
376
|
+
lane.framed = frame.first - 1;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
310
379
|
const store = entries.get(name)?.store;
|
|
311
|
-
|
|
380
|
+
const code = body?.code ?? (status === 413 && frame.entries?.length === 1 ? "too-large" : "refused");
|
|
381
|
+
tell(name, { phase: "refused", code, n: frame.n, from: frame.first, through: frame.through, ...(body?.code === "receipt-expired" && { ops: frame.ops }) });
|
|
312
382
|
console.warn(`clay.live("${name}"): the host refused an update, so it was undone (${reason})`);
|
|
313
|
-
store?.rebase(store.authoritative, { source: "state", through: frame.through, cause: "refused" });
|
|
383
|
+
store?.rebase(store.authoritative, { source: "state", drop: frame.first === undefined ? undefined : [frame.first, frame.through], through: frame.first === undefined ? frame.through : undefined, cause: "refused" });
|
|
384
|
+
if (body?.code === "receipt-expired") resync();
|
|
314
385
|
}
|
|
315
386
|
|
|
316
387
|
function apply(frame) {
|
|
317
388
|
seq = frame.seq;
|
|
318
389
|
const store = entries.get(frame.name)?.store;
|
|
319
|
-
if (!store ||
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
390
|
+
if (!store || abandoned.has(frame.name)) return;
|
|
391
|
+
if (!roomNames.has(frame.name)) {
|
|
392
|
+
missed = true;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const ops = store.checkOps(frame.ops);
|
|
396
|
+
if (!ops) {
|
|
324
397
|
resync();
|
|
325
398
|
return;
|
|
326
399
|
}
|
|
@@ -335,7 +408,7 @@ export function createFastLane({
|
|
|
335
408
|
lane.awaiting.splice(0, i + 1);
|
|
336
409
|
}
|
|
337
410
|
}
|
|
338
|
-
store.
|
|
411
|
+
store.rebaseOps(ops, { source: own ? "state" : "remote", through, frame: { seq: frame.seq, from: frame.from, n: frame.n, ops: frame.ops } });
|
|
339
412
|
entries.get(frame.name).last = frame.seq;
|
|
340
413
|
onApplied();
|
|
341
414
|
if (frame.checkpoint === true) onCheckpoint();
|
|
@@ -374,20 +447,37 @@ export function createFastLane({
|
|
|
374
447
|
function sortAwaiting(awaiting, room) {
|
|
375
448
|
const seen = room.seen ? (room.seen[clientId] ?? 0) : null;
|
|
376
449
|
const resend = [];
|
|
377
|
-
|
|
450
|
+
const dropIds = [];
|
|
378
451
|
for (const frame of awaiting) {
|
|
379
|
-
if (seen !== null && frame.n <= seen)
|
|
452
|
+
if ((seen !== null && frame.n <= seen) || hasNumber(room.saved, frame.n)) dropIds.push(...frame.entries.map((e) => e.id));
|
|
380
453
|
else if (frame.sent && frame === flying) {
|
|
381
454
|
// Still on the wire: its answer decides.
|
|
382
455
|
frame.undecided = true;
|
|
383
456
|
resend.push(frame);
|
|
384
457
|
} else if (unapplied(frame)) resend.push(frame);
|
|
385
|
-
else
|
|
458
|
+
else dropIds.push(...frame.entries.map((e) => e.id));
|
|
459
|
+
}
|
|
460
|
+
return { resend, dropIds };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function waiting(name, lane, room, fresh) {
|
|
464
|
+
const ids = heldIds(name);
|
|
465
|
+
if (!ids.length) return false;
|
|
466
|
+
if (!fresh) {
|
|
467
|
+
const covered = new Set();
|
|
468
|
+
for (const frame of lane.awaiting) {
|
|
469
|
+
if (hasNumber(room.saved, frame.n)) for (const op of frame.entries ?? []) covered.add(op.id);
|
|
470
|
+
}
|
|
471
|
+
if (ids.every((id) => covered.has(id))) return false;
|
|
386
472
|
}
|
|
387
|
-
|
|
473
|
+
if (!hold(name, ids)) {
|
|
474
|
+
prune(name, lane);
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
return true;
|
|
388
478
|
}
|
|
389
479
|
|
|
390
|
-
function applyState(state) {
|
|
480
|
+
function applyState(state, read = 0) {
|
|
391
481
|
if (stopped || !state || typeof state.epoch !== "string") return;
|
|
392
482
|
stateCount++;
|
|
393
483
|
// Older than what this tab already applied: keep what it has.
|
|
@@ -396,44 +486,103 @@ export function createFastLane({
|
|
|
396
486
|
return;
|
|
397
487
|
}
|
|
398
488
|
const fresh = state.epoch !== epoch;
|
|
489
|
+
provenanceVersion = Number.isInteger(state.provenanceVersion) ? state.provenanceVersion : 0;
|
|
490
|
+
receiptGeneration = Number.isSafeInteger(state.receiptGeneration) ? state.receiptGeneration : null;
|
|
491
|
+
const priorGeneration = pageGeneration;
|
|
492
|
+
pageGeneration = typeof state.pageTransition?.id === "string" ? state.pageTransition.id : null;
|
|
493
|
+
const replaced = pageGeneration !== priorGeneration;
|
|
399
494
|
let joined = false;
|
|
495
|
+
// Every store is read whole here.
|
|
496
|
+
missed = false;
|
|
400
497
|
epoch = state.epoch;
|
|
401
498
|
seq = Number.isInteger(state.seq) ? state.seq : 0;
|
|
402
499
|
writer = state.writer ?? null;
|
|
403
500
|
if ("conn" in state) conn = state.conn;
|
|
404
501
|
const stores = state.stores || {};
|
|
405
502
|
roomNames = new Set(Object.keys(stores));
|
|
503
|
+
for (const name of abandoned) roomNames.delete(name);
|
|
504
|
+
let skipped = false;
|
|
505
|
+
let readDue = false;
|
|
406
506
|
for (const [name, entry] of entries) {
|
|
507
|
+
if (abandoned.has(name)) continue;
|
|
407
508
|
const room = stores[name];
|
|
408
509
|
const lane = laneOf(name);
|
|
510
|
+
prune(name, lane);
|
|
511
|
+
const held = lane.room;
|
|
512
|
+
lane.room = !!room;
|
|
409
513
|
if (!room) {
|
|
410
|
-
|
|
514
|
+
// Not in the room (too large, too many stores, or opened after it):
|
|
515
|
+
// there are no frames of it to miss. Already out: its value still
|
|
516
|
+
// descends from where it left. A store the room held and has now
|
|
517
|
+
// dropped was replaced by a document without it, which a 404 to this
|
|
518
|
+
// tab's next frame would also say. A replacement always installs a new page
|
|
519
|
+
// transition; a store dropped for size leaves it, and leaves quietly.
|
|
520
|
+
if (!lane.out) {
|
|
521
|
+
if (held && replaced) tell(name, { phase: "left", code: "no-store" });
|
|
522
|
+
leaveRoom(name, lane, false);
|
|
523
|
+
}
|
|
524
|
+
// A read the barrier waited for says no room holds this store: the file is its authority.
|
|
525
|
+
if (lane.after !== undefined && read >= lane.after) {
|
|
526
|
+
lane.after = undefined;
|
|
527
|
+
lane.deferred = false;
|
|
528
|
+
taken(name);
|
|
529
|
+
}
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const behind = lane.after !== undefined && read < lane.after;
|
|
533
|
+
if (behind || waiting(name, lane, room, fresh || lane.stale === true)) {
|
|
534
|
+
roomNames.delete(name);
|
|
535
|
+
lane.deferred = true;
|
|
536
|
+
if (fresh) lane.stale = true;
|
|
537
|
+
if (behind) readDue = true;
|
|
538
|
+
skipped = true;
|
|
411
539
|
continue;
|
|
412
540
|
}
|
|
541
|
+
const wholly = fresh || lane.stale === true;
|
|
542
|
+
lane.deferred = false;
|
|
543
|
+
lane.stale = false;
|
|
544
|
+
lane.after = undefined;
|
|
413
545
|
const rejoined = entry.store.handle.status.mode === "file";
|
|
546
|
+
lane.out = false;
|
|
414
547
|
entry.store.setMode("fast");
|
|
415
548
|
entry.last = Number.isInteger(room.last) ? room.last : 0;
|
|
416
549
|
entry.landed = Number.isInteger(room.landed) ? room.landed : 0;
|
|
417
|
-
|
|
418
|
-
|
|
550
|
+
const expired = [];
|
|
551
|
+
if (!wholly && receiptGeneration !== null) {
|
|
552
|
+
const seen = room.seen?.[clientId];
|
|
553
|
+
lane.awaiting = lane.awaiting.filter((frame) => {
|
|
554
|
+
const known = hasNumber(room.saved, frame.n) || (seen !== undefined && frame.n <= seen) || (frame.seq !== null && frame.seq <= seq);
|
|
555
|
+
if (known || (frame.receiptGeneration ?? 0) === receiptGeneration) return true;
|
|
556
|
+
expired.push(...frame.entries.map((e) => e.id));
|
|
557
|
+
tell(name, { phase: "refused", code: "receipt-expired", n: frame.n, from: frame.first, through: frame.through, ops: frame.ops });
|
|
558
|
+
console.warn(`clay.live("${name}"): the room retired an update receipt, so the update was not replayed. Check the saved value before restoring it.`);
|
|
559
|
+
return false;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
if (wholly) {
|
|
563
|
+
const { resend, dropIds } = sortAwaiting(lane.awaiting, room);
|
|
419
564
|
for (const frame of resend) {
|
|
420
565
|
if (frame.undecided) continue;
|
|
421
566
|
frame.sent = false;
|
|
422
567
|
frame.seq = null;
|
|
568
|
+
frame.receiptGeneration = receiptGeneration;
|
|
569
|
+
frame.pageGeneration = pageGeneration;
|
|
423
570
|
}
|
|
424
571
|
lane.awaiting = resend;
|
|
425
|
-
entry.store.rebase(room.value, { source: "state",
|
|
572
|
+
entry.store.rebase(room.value, { source: "state", dropIds, room: { seq, fresh: true, moves: state.moves?.[name] ?? null, floor: Number.isInteger(room.floor) ? room.floor : null } });
|
|
426
573
|
lane.unsent = unframed(name, lane);
|
|
427
574
|
} else {
|
|
428
575
|
const seen = room.seen ? room.seen[clientId] : undefined;
|
|
429
576
|
const next = [];
|
|
430
|
-
|
|
577
|
+
const dropIds = [...expired];
|
|
431
578
|
for (const frame of lane.awaiting) {
|
|
432
|
-
if (frame
|
|
579
|
+
if (hasNumber(room.saved, frame.n)) {
|
|
580
|
+
dropIds.push(...frame.entries.map((e) => e.id));
|
|
581
|
+
} else if (frame === flying) {
|
|
433
582
|
next.push(frame);
|
|
434
583
|
} else if ((frame.seq !== null && frame.seq <= seq) || (seen !== undefined && frame.n <= seen)) {
|
|
435
584
|
// In this state, by its answer or by the room's receipt.
|
|
436
|
-
|
|
585
|
+
dropIds.push(...frame.entries.map((e) => e.id));
|
|
437
586
|
} else if (frame.seq !== null) {
|
|
438
587
|
// Answered as applied after this state was read: its event is on its way.
|
|
439
588
|
next.push(frame);
|
|
@@ -446,22 +595,25 @@ export function createFastLane({
|
|
|
446
595
|
next.push(frame);
|
|
447
596
|
} else {
|
|
448
597
|
// Unanswered, and the stream reconnected since: its receipt may be gone.
|
|
449
|
-
|
|
598
|
+
dropIds.push(...frame.entries.map((e) => e.id));
|
|
450
599
|
}
|
|
451
600
|
}
|
|
452
601
|
lane.awaiting = next;
|
|
453
|
-
entry.store.rebase(room.value, { source: "state",
|
|
602
|
+
entry.store.rebase(room.value, { source: "state", dropIds, room: { seq, fresh: false, moves: state.moves?.[name] ?? null, floor: Number.isInteger(room.floor) ? room.floor : null } });
|
|
454
603
|
// Back in the room after a spell at save speed: frame what was made meanwhile.
|
|
455
604
|
if (rejoined) {
|
|
456
605
|
lane.unsent = unframed(name, lane);
|
|
457
606
|
joined = true;
|
|
458
607
|
}
|
|
459
608
|
}
|
|
609
|
+
taken(name);
|
|
460
610
|
}
|
|
611
|
+
if (skipped) missed = true;
|
|
461
612
|
if (fresh || joined) onEpoch(epoch);
|
|
462
613
|
onApplied();
|
|
463
614
|
onWriter();
|
|
464
615
|
drain();
|
|
616
|
+
if (readDue) resync();
|
|
465
617
|
}
|
|
466
618
|
|
|
467
619
|
// Back in step: apply what arrived while the state was being read.
|
|
@@ -484,11 +636,21 @@ export function createFastLane({
|
|
|
484
636
|
fetching = true;
|
|
485
637
|
fetchAgain = false;
|
|
486
638
|
synced = false;
|
|
487
|
-
const asked = { connection, stateCount };
|
|
639
|
+
const asked = { connection, stateCount, read: ++reads };
|
|
488
640
|
let state = null;
|
|
641
|
+
const abort = typeof AbortController === "function" ? new AbortController() : null;
|
|
642
|
+
const expired = new Promise((resolve) => {
|
|
643
|
+
readTimer = setTimer(() => {
|
|
644
|
+
readTimer = null;
|
|
645
|
+
abort?.abort();
|
|
646
|
+
resolve(null);
|
|
647
|
+
}, STATE_TIMEOUT_MS);
|
|
648
|
+
});
|
|
489
649
|
try {
|
|
490
|
-
state = await transport.fetchState(clientId, epoch !== null && seq > 0 ? { epoch, seq } : null);
|
|
650
|
+
state = await Promise.race([transport.fetchState(clientId, epoch !== null && seq > 0 && !missed ? { epoch, seq } : null, abort ? { signal: abort.signal } : {}), expired]);
|
|
491
651
|
} catch {}
|
|
652
|
+
if (readTimer !== null) clearTimer(readTimer);
|
|
653
|
+
readTimer = null;
|
|
492
654
|
fetching = false;
|
|
493
655
|
if (stopped) return;
|
|
494
656
|
if (asked.connection !== connection || fetchAgain) {
|
|
@@ -501,13 +663,37 @@ export function createFastLane({
|
|
|
501
663
|
}
|
|
502
664
|
if (state && typeof state.epoch === "string") {
|
|
503
665
|
resyncDelay = RETRY_MS;
|
|
504
|
-
applyState(state);
|
|
666
|
+
applyState(state, asked.read);
|
|
505
667
|
return;
|
|
506
668
|
}
|
|
507
669
|
if (state && STATE_GONE.has(state.status)) {
|
|
508
670
|
stop();
|
|
509
671
|
return;
|
|
510
672
|
}
|
|
673
|
+
// The host has no room for this document now: it holds as much live data
|
|
674
|
+
// as it can (503 busy), or it is not answering (any other 503). Every
|
|
675
|
+
// store goes at save speed, with its own pending ops, so a save never
|
|
676
|
+
// writes the room's stale value. The lane asks again after Retry-After, a
|
|
677
|
+
// minute for busy, or its usual backoff.
|
|
678
|
+
if (state && state.status === 503) {
|
|
679
|
+
const busy = state.code === "busy";
|
|
680
|
+
for (const [name, entry] of entries) {
|
|
681
|
+
if (entry.store.handle.status.mode !== "fast") continue;
|
|
682
|
+
tell(name, { phase: "left", code: busy ? "busy" : "unavailable" });
|
|
683
|
+
console.warn(`clay.live("${name}"): the host has no room for this document now, so it stays at save speed for now`);
|
|
684
|
+
leaveRoom(name, laneOf(name));
|
|
685
|
+
}
|
|
686
|
+
roomNames = new Set();
|
|
687
|
+
if (!connected) return;
|
|
688
|
+
if (busy || state.retryAfter > 0) {
|
|
689
|
+
resyncDelay = RETRY_MS;
|
|
690
|
+
setTimer(resync, (state.retryAfter > 0 ? state.retryAfter : 60) * 1000);
|
|
691
|
+
} else {
|
|
692
|
+
setTimer(resync, resyncDelay);
|
|
693
|
+
resyncDelay = Math.min(resyncDelay * 2, RETRY_MAX_MS);
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
511
697
|
if (connected) {
|
|
512
698
|
setTimer(resync, resyncDelay);
|
|
513
699
|
resyncDelay = Math.min(resyncDelay * 2, RETRY_MAX_MS);
|
|
@@ -519,9 +705,12 @@ export function createFastLane({
|
|
|
519
705
|
stopped = true;
|
|
520
706
|
if (timer !== null) clearTimer(timer);
|
|
521
707
|
timer = null;
|
|
708
|
+
if (readTimer !== null) clearTimer(readTimer);
|
|
709
|
+
readTimer = null;
|
|
522
710
|
unsubscribe?.();
|
|
523
711
|
for (const [name, entry] of entries) {
|
|
524
712
|
const lane = lanes.get(name);
|
|
713
|
+
if (lane?.out) continue;
|
|
525
714
|
if (lane) leaveRoom(name, lane);
|
|
526
715
|
else {
|
|
527
716
|
onLeave(name, { epoch, seq });
|
|
@@ -545,7 +734,12 @@ export function createFastLane({
|
|
|
545
734
|
buffered = [];
|
|
546
735
|
},
|
|
547
736
|
frame: onFrame,
|
|
548
|
-
state
|
|
737
|
+
// A broadcast state carries no moves. While a store is out, the rejoin
|
|
738
|
+
// goes through this tab's own read, which carries them.
|
|
739
|
+
state(state) {
|
|
740
|
+
if (missed && state && !("moves" in state)) resync();
|
|
741
|
+
else applyState(state);
|
|
742
|
+
},
|
|
549
743
|
// The room's writer, and how far the file reaches when a save landed.
|
|
550
744
|
writer(data) {
|
|
551
745
|
if (!data || data.epoch !== epoch) return;
|
|
@@ -578,13 +772,103 @@ export function createFastLane({
|
|
|
578
772
|
ops.forEach((op, i) => lane.unsent.push({ id: first + i, op }));
|
|
579
773
|
schedule();
|
|
580
774
|
},
|
|
775
|
+
// The store's ops through `through` merged the file article with revision
|
|
776
|
+
// rev into the page: the frame that carries them tells the room, so its
|
|
777
|
+
// checkpoint knows the room holds that edit. A frame already sent leaves it
|
|
778
|
+
// to the next one.
|
|
779
|
+
// The store's history is not the room's (its page was rebuilt while this tab was
|
|
780
|
+
// away): nothing of it is sent again, and the room's frames for it are ignored.
|
|
781
|
+
abandon(name) {
|
|
782
|
+
if (abandoned.has(name)) return;
|
|
783
|
+
abandoned.add(name);
|
|
784
|
+
roomNames.delete(name);
|
|
785
|
+
const lane = laneOf(name);
|
|
786
|
+
lane.awaiting = lane.awaiting.filter((frame) => frame === flying);
|
|
787
|
+
lane.unsent = [];
|
|
788
|
+
lane.adopted = null;
|
|
789
|
+
},
|
|
790
|
+
adopted(name, rev, through) {
|
|
791
|
+
if (stopped || !roomNames.has(name)) return;
|
|
792
|
+
const lane = laneOf(name);
|
|
793
|
+
const waiting = lane.awaiting.find((frame) => !frame.sent && frame.through >= through);
|
|
794
|
+
if (waiting) waiting.adopted = rev;
|
|
795
|
+
else lane.adopted = { rev, through };
|
|
796
|
+
},
|
|
581
797
|
// The save that held this store ended: frame whatever is still pending past
|
|
582
|
-
// what was framed (an accepted save's ops have left pending).
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
798
|
+
// what was framed (an accepted save's ops have left pending). A state it
|
|
799
|
+
// kept from the store, or a value the caller kept (read), is replaced by a
|
|
800
|
+
// read of the room, which frames what is pending when the store takes it.
|
|
801
|
+
release(name, { read = false } = {}) {
|
|
802
|
+
if (stopped) return;
|
|
803
|
+
const lane = laneOf(name);
|
|
804
|
+
// A read release arms the barrier even before the first epoch exists: the
|
|
805
|
+
// state that may apply has to be one read after this, and a lane that has
|
|
806
|
+
// not joined a room yet has nothing to fetch.
|
|
807
|
+
if (lane.deferred || read) {
|
|
808
|
+
lane.after = reads + 1;
|
|
809
|
+
if (epoch !== null && connected) resync();
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (epoch === null) return;
|
|
813
|
+
if (!roomNames.has(name)) return;
|
|
814
|
+
prune(name, lane);
|
|
815
|
+
lane.unsent = unframed(name, lane);
|
|
586
816
|
schedule();
|
|
587
817
|
},
|
|
818
|
+
// Lifetime frame counter retained for the legacy fixture harness.
|
|
819
|
+
framed(name) {
|
|
820
|
+
return lanes.get(name)?.n ?? 0;
|
|
821
|
+
},
|
|
822
|
+
pendingFrames(name, at, { reserve = false } = {}) {
|
|
823
|
+
const lane = lanes.get(name);
|
|
824
|
+
if (reserve && lane && clientId && epoch === at?.epoch) {
|
|
825
|
+
const pending = unframed(name, lane);
|
|
826
|
+
while (pending.length) {
|
|
827
|
+
let size = byteSize({ name, n: lane.n + 1, ops: [], epoch });
|
|
828
|
+
let count = 0;
|
|
829
|
+
while (count < pending.length && count < MAX_OPS) {
|
|
830
|
+
const more = byteSize(pending[count].op) + 1;
|
|
831
|
+
if (count && size + more > frameBytes) break;
|
|
832
|
+
size += more;
|
|
833
|
+
count++;
|
|
834
|
+
}
|
|
835
|
+
const take = pending.splice(0, count);
|
|
836
|
+
const frame = { n: ++lane.n, ops: take.map((e) => e.op), entries: take,
|
|
837
|
+
first: take[0].id, through: take[take.length - 1].id, seq: null,
|
|
838
|
+
sent: false, refused: true, epoch, client: clientId, receiptGeneration, pageGeneration };
|
|
839
|
+
markAdopted(lane, frame);
|
|
840
|
+
lane.awaiting.push(frame);
|
|
841
|
+
lane.framed = frame.through;
|
|
842
|
+
}
|
|
843
|
+
lane.unsent = [];
|
|
844
|
+
}
|
|
845
|
+
const ids = new Set(entries.get(name)?.store.pendingEntries().map((e) => e.id) ?? []);
|
|
846
|
+
let frames = [];
|
|
847
|
+
let complete = true;
|
|
848
|
+
for (const frame of lane?.awaiting ?? []) {
|
|
849
|
+
const present = frame.entries.filter((e) => ids.has(e.id));
|
|
850
|
+
if (!present.length) continue;
|
|
851
|
+
if (present.length !== frame.entries.length || frame.epoch !== at?.epoch || !frame.client) {
|
|
852
|
+
complete = false;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
frames = unionRanges(frames, [[frame.client, [[frame.n, frame.n]]]]);
|
|
856
|
+
}
|
|
857
|
+
return { frames, complete };
|
|
858
|
+
},
|
|
859
|
+
captured(name, at, by) {
|
|
860
|
+
const lane = lanes.get(name);
|
|
861
|
+
if (!lane || !at || at.epoch !== epoch) return [];
|
|
862
|
+
const included = frameSet(by);
|
|
863
|
+
const dropIds = [];
|
|
864
|
+
lane.awaiting = lane.awaiting.filter((frame) => {
|
|
865
|
+
const held = frame.epoch === at.epoch &&
|
|
866
|
+
(contains(included, frame.client, frame.n) || (frame.seq !== null && frame.seq <= at.seq));
|
|
867
|
+
if (held) dropIds.push(...frame.entries.map((e) => e.id));
|
|
868
|
+
return !held;
|
|
869
|
+
});
|
|
870
|
+
return dropIds;
|
|
871
|
+
},
|
|
588
872
|
// Read the room again: a store opened after this tab joined, or a save the
|
|
589
873
|
// room merged whose state may not have arrived yet.
|
|
590
874
|
refresh() {
|
|
@@ -593,11 +877,14 @@ export function createFastLane({
|
|
|
593
877
|
storeOpened() {
|
|
594
878
|
if (epoch !== null && connected) resync();
|
|
595
879
|
},
|
|
880
|
+
roomBound(name) {
|
|
881
|
+
return !stopped && lanes.get(name)?.room === true;
|
|
882
|
+
},
|
|
596
883
|
isWriter() {
|
|
597
884
|
return writer !== null && writer === clientId;
|
|
598
885
|
},
|
|
599
886
|
get status() {
|
|
600
|
-
return { connected, synced, epoch, seq, writer, clientId, stopped };
|
|
887
|
+
return { connected, synced, epoch, seq, writer, clientId, stopped, provenanceVersion, receiptGeneration };
|
|
601
888
|
},
|
|
602
889
|
stop,
|
|
603
890
|
};
|