statewire 0.10.0 → 0.12.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/README.md +2 -2
- package/dist/client/connection.d.ts +6 -4
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +10 -2
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts +1 -1
- package/dist/client/session.d.ts +10 -2
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +246 -97
- package/dist/client/session.js.map +1 -1
- package/dist/host/host.d.ts +1 -1
- package/dist/host/http.js +4 -3
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +6 -5
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +12 -20
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +2 -1
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +2 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/statewire.d.ts +29 -3
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js.map +1 -1
- package/dist/transports/fake-sse.js +4 -4
- package/dist/transports/fake-sse.js.map +1 -1
- package/dist/transports/fixtures.d.ts +1 -1
- package/dist/transports/remote.d.ts +1 -1
- package/dist/transports/remote.js +1 -0
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -2
- package/dist/transports/testing.d.ts +1 -1
- package/dist/transports/testing.js +2 -4
- package/dist/transports/testing.js.map +1 -1
- package/dist/wire/meta.d.ts +5 -3
- package/dist/wire/meta.d.ts.map +1 -1
- package/dist/wire/meta.js +7 -4
- package/dist/wire/meta.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +12 -6
- package/src/client/session.ts +332 -114
- package/src/host/http.ts +10 -9
- package/src/host/socket.ts +18 -28
- package/src/host-internal.ts +2 -1
- package/src/host.ts +0 -1
- package/src/statewire.ts +33 -3
- package/src/transports/fake-sse.ts +6 -6
- package/src/transports/remote.ts +1 -0
- package/src/transports/testing.ts +2 -7
- package/src/wire/meta.ts +12 -4
package/dist/client/session.js
CHANGED
|
@@ -5,6 +5,7 @@ import { bindCommandsHost } from "../host/host.js";
|
|
|
5
5
|
import { encodeParams, encodeValue, reviveResponse, reviveValue } from "../wire/meta.js";
|
|
6
6
|
import { SessionStorageContext, createMemorySessionStore } from "./sessionStore.js";
|
|
7
7
|
import { backoff } from "./timing.js";
|
|
8
|
+
import { TERMINAL } from "./connection.js";
|
|
8
9
|
import { randomId } from "../util/randomId.js";
|
|
9
10
|
import { useContext, useEffect, useState } from "@assistant-ui/tap/react-shim";
|
|
10
11
|
//#region src/client/session.ts
|
|
@@ -47,14 +48,43 @@ const backlog = (s) => s.pending.filter((p) => p.seq > s.durableSeq).map((p) =>
|
|
|
47
48
|
seq: p.seq,
|
|
48
49
|
...p.command
|
|
49
50
|
}));
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
51
|
+
const mergeBacklog = (s) => {
|
|
52
|
+
const own = backlog(s);
|
|
53
|
+
if (!s.shared) return own;
|
|
54
|
+
const known = new Set(s.pending.map((p) => p.seq));
|
|
55
|
+
return [...s.record.commands.filter((c) => !known.has(c.seq) && c.seq > s.durableSeq), ...own].sort((a, b) => a.seq - b.seq);
|
|
56
|
+
};
|
|
57
|
+
const syncBacklogInner = (s) => {
|
|
58
|
+
const parked = s.shared ? s.shared.parked.splice(0) : [];
|
|
59
|
+
const parkedSeqs = new Set(parked.map((v) => v.seq));
|
|
60
|
+
const commands = mergeBacklog(s).filter((c) => !parkedSeqs.has(c.seq));
|
|
61
|
+
const verdicts = parked.length > 0 ? [...s.record.verdicts ?? [], ...parked].slice(-64) : s.record.verdicts;
|
|
62
|
+
if (verdicts === s.record.verdicts && commands.length === s.record.commands.length && commands.every((c, i) => c.seq === s.record.commands[i].seq)) return;
|
|
53
63
|
setRecord(s, {
|
|
54
64
|
...s.record,
|
|
55
|
-
commands
|
|
65
|
+
commands,
|
|
66
|
+
...verdicts !== void 0 && { verdicts }
|
|
56
67
|
});
|
|
57
68
|
};
|
|
69
|
+
const syncBacklog = (s) => {
|
|
70
|
+
if (!s.shared) return syncBacklogInner(s);
|
|
71
|
+
transact(s, () => syncBacklogInner(s));
|
|
72
|
+
};
|
|
73
|
+
const transact = (s, fn) => {
|
|
74
|
+
if (!s.shared || s.deactivated) {
|
|
75
|
+
fn();
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
}
|
|
78
|
+
const shared = s.shared;
|
|
79
|
+
const run = shared.txq.then(() => s.store.lock(async () => {
|
|
80
|
+
const stored = s.store.load();
|
|
81
|
+
if (stored !== null && stored.clientId !== s.record.clientId) adoptReset(s, stored);
|
|
82
|
+
else if (stored !== null) s.record = stored;
|
|
83
|
+
fn();
|
|
84
|
+
}));
|
|
85
|
+
shared.txq = run.then(() => {}, () => {});
|
|
86
|
+
return run;
|
|
87
|
+
};
|
|
58
88
|
const setIdle = (s, level) => {
|
|
59
89
|
if (level === s.idle) return;
|
|
60
90
|
s.idle = level;
|
|
@@ -84,11 +114,7 @@ const settleEntry = (s, entry, outcome) => {
|
|
|
84
114
|
}
|
|
85
115
|
};
|
|
86
116
|
const settleAll = (s, detach) => {
|
|
87
|
-
const cause =
|
|
88
|
-
unmount: "unmount",
|
|
89
|
-
gone: "stream-gone",
|
|
90
|
-
superseded: "superseded"
|
|
91
|
-
}[detach];
|
|
117
|
+
const cause = detach === "unmount" ? "unmount" : "stream-gone";
|
|
92
118
|
const batch = s.pending;
|
|
93
119
|
const detachFailure = (entry) => {
|
|
94
120
|
if (!entry.attempted) {
|
|
@@ -129,9 +155,8 @@ const restateContext = (s) => {
|
|
|
129
155
|
posted: false
|
|
130
156
|
});
|
|
131
157
|
};
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
for (const entry of [...s.pending]) if (entry.attempted || entry.applied) {
|
|
158
|
+
const settleLost = (s, entries) => {
|
|
159
|
+
for (const entry of entries) {
|
|
135
160
|
s.lifecycle?.onCovered?.(entry.inv);
|
|
136
161
|
settleEntry(s, entry, {
|
|
137
162
|
ok: false,
|
|
@@ -140,23 +165,33 @@ const resetRecord = (s, clientId) => {
|
|
|
140
165
|
cause: "session-lost"
|
|
141
166
|
}
|
|
142
167
|
});
|
|
143
|
-
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const rebaseRecord = (s, adopt) => {
|
|
171
|
+
const held = [...s.pending];
|
|
172
|
+
const survivors = held.filter((p) => !p.attempted && !p.applied);
|
|
173
|
+
settleLost(s, held.filter((p) => p.attempted || p.applied));
|
|
144
174
|
s.pending = [];
|
|
145
175
|
s.durableSeq = 0;
|
|
146
|
-
|
|
147
|
-
clientId,
|
|
148
|
-
nextSeq: 1,
|
|
149
|
-
ctxNextSeq: 1,
|
|
150
|
-
commands: []
|
|
151
|
-
});
|
|
176
|
+
adopt();
|
|
152
177
|
for (const old of survivors) {
|
|
153
178
|
s.lifecycle?.onCovered?.(old.inv);
|
|
154
|
-
|
|
179
|
+
enqueueInner(s, old.command, old.inv.params, old.resolve, old.reject);
|
|
155
180
|
}
|
|
156
181
|
restateContext(s);
|
|
157
182
|
publish(s);
|
|
158
183
|
wake(s);
|
|
184
|
+
pump(s);
|
|
159
185
|
};
|
|
186
|
+
const resetRecord = (s, clientId) => rebaseRecord(s, () => setRecord(s, {
|
|
187
|
+
clientId,
|
|
188
|
+
nextSeq: 1,
|
|
189
|
+
ctxNextSeq: 1,
|
|
190
|
+
commands: []
|
|
191
|
+
}));
|
|
192
|
+
const adoptReset = (s, stored) => rebaseRecord(s, () => {
|
|
193
|
+
s.record = stored;
|
|
194
|
+
});
|
|
160
195
|
const ephemeralDetach = (s) => {
|
|
161
196
|
s.ephemeral = false;
|
|
162
197
|
s.sessionKnown = false;
|
|
@@ -171,6 +206,53 @@ const rejectedFailure = (cmd) => {
|
|
|
171
206
|
...cmd.payload !== void 0 && { payload: cmd.payload }
|
|
172
207
|
};
|
|
173
208
|
};
|
|
209
|
+
const outcomeOf = (cmd) => {
|
|
210
|
+
switch (cmd.type) {
|
|
211
|
+
case "result": return {
|
|
212
|
+
ok: true,
|
|
213
|
+
payload: reviveResponse(cmd).payload
|
|
214
|
+
};
|
|
215
|
+
case "rejected": return {
|
|
216
|
+
ok: false,
|
|
217
|
+
failure: rejectedFailure(reviveResponse(cmd))
|
|
218
|
+
};
|
|
219
|
+
case "failed": return {
|
|
220
|
+
ok: false,
|
|
221
|
+
failure: {
|
|
222
|
+
fate: "failed",
|
|
223
|
+
cause: "server",
|
|
224
|
+
...cmd.message !== void 0 && { message: cmd.message }
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
case "crashed": return {
|
|
228
|
+
ok: false,
|
|
229
|
+
failure: {
|
|
230
|
+
fate: "crashed",
|
|
231
|
+
cause: "server"
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
case "expired": return {
|
|
235
|
+
ok: false,
|
|
236
|
+
failure: {
|
|
237
|
+
fate: "expired",
|
|
238
|
+
cause: "server"
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
default: return protocolError(`unknown cmd type: ${String(cmd.type)}`);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const TERMINAL_CMD_TYPES = new Set(TERMINAL);
|
|
245
|
+
const parkVerdict = (s, cmd) => {
|
|
246
|
+
if (!s.shared || !TERMINAL_CMD_TYPES.has(cmd.type)) return;
|
|
247
|
+
s.shared.parked.push({
|
|
248
|
+
seq: cmd.seq,
|
|
249
|
+
type: cmd.type,
|
|
250
|
+
...cmd.code !== void 0 && { code: cmd.code },
|
|
251
|
+
...cmd.message !== void 0 && { message: cmd.message },
|
|
252
|
+
...cmd.payload !== void 0 && { payload: cmd.payload },
|
|
253
|
+
...cmd.meta !== void 0 && { meta: cmd.meta }
|
|
254
|
+
});
|
|
255
|
+
};
|
|
174
256
|
const handleCmd = (s, cmd) => {
|
|
175
257
|
if (!Number.isSafeInteger(cmd.seq) || cmd.seq <= 0) protocolError("cmd.seq must be a positive safe integer");
|
|
176
258
|
if (cmd.dur === true) {
|
|
@@ -183,35 +265,29 @@ const handleCmd = (s, cmd) => {
|
|
|
183
265
|
if (entry) cover(s, entry);
|
|
184
266
|
return;
|
|
185
267
|
}
|
|
268
|
+
parkVerdict(s, cmd);
|
|
186
269
|
if (!entry) return;
|
|
187
270
|
cover(s, entry);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
ok: false,
|
|
209
|
-
failure: {
|
|
210
|
-
fate: "expired",
|
|
211
|
-
cause: "server"
|
|
212
|
-
}
|
|
213
|
-
} : protocolError(`unknown cmd type: ${String(cmd.type)}`);
|
|
214
|
-
settleEntry(s, entry, outcome);
|
|
271
|
+
settleEntry(s, entry, outcomeOf(cmd));
|
|
272
|
+
};
|
|
273
|
+
const consumeVerdicts = (s) => {
|
|
274
|
+
const verdicts = s.record.verdicts ?? [];
|
|
275
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
276
|
+
for (const verdict of verdicts) {
|
|
277
|
+
const entry = s.pending.find((p) => p.seq === verdict.seq);
|
|
278
|
+
if (!entry) continue;
|
|
279
|
+
consumed.add(verdict.seq);
|
|
280
|
+
cover(s, entry);
|
|
281
|
+
settleEntry(s, entry, outcomeOf(verdict));
|
|
282
|
+
}
|
|
283
|
+
if (consumed.size === 0) return;
|
|
284
|
+
setRecord(s, {
|
|
285
|
+
...s.record,
|
|
286
|
+
commands: s.record.commands.filter((c) => !consumed.has(c.seq)),
|
|
287
|
+
verdicts: verdicts.filter((v) => !consumed.has(v.seq))
|
|
288
|
+
});
|
|
289
|
+
publish(s);
|
|
290
|
+
wake(s);
|
|
215
291
|
};
|
|
216
292
|
const handleCmds = (s, cmds) => {
|
|
217
293
|
for (const cmd of cmds) handleCmd(s, cmd);
|
|
@@ -227,24 +303,7 @@ const publishIngested = (s) => {
|
|
|
227
303
|
publish(s);
|
|
228
304
|
wake(s);
|
|
229
305
|
};
|
|
230
|
-
const
|
|
231
|
-
"fresh",
|
|
232
|
-
"resumed",
|
|
233
|
-
"ephemeral"
|
|
234
|
-
];
|
|
235
|
-
const snapClocks = (s, seqWm, ctxWm) => {
|
|
236
|
-
const tail = s.pending.filter((p) => p.seq > seqWm);
|
|
237
|
-
if (!(tail.every((p, i) => p.seq === seqWm + 1 + i) && s.record.nextSeq === seqWm + tail.length + 1)) {
|
|
238
|
-
s.pending = s.pending.filter((p) => p.seq <= seqWm);
|
|
239
|
-
setRecord(s, {
|
|
240
|
-
...s.record,
|
|
241
|
-
nextSeq: seqWm + 1
|
|
242
|
-
});
|
|
243
|
-
for (const old of tail) {
|
|
244
|
-
s.lifecycle?.onCovered?.(old.inv);
|
|
245
|
-
enqueue(s, old.command, old.inv.params, old.resolve, old.reject);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
306
|
+
const snapCtxClock = (s, ctxWm) => {
|
|
248
307
|
if (!(s.ctxPending.every((c, i) => c.seq === ctxWm + 1 + i) && s.record.ctxNextSeq === ctxWm + s.ctxPending.length + 1)) {
|
|
249
308
|
setRecord(s, {
|
|
250
309
|
...s.record,
|
|
@@ -253,29 +312,54 @@ const snapClocks = (s, seqWm, ctxWm) => {
|
|
|
253
312
|
restateContext(s);
|
|
254
313
|
}
|
|
255
314
|
};
|
|
315
|
+
const sessionLostInner = (s, seqWm, ctxWm) => {
|
|
316
|
+
const held = [...s.pending];
|
|
317
|
+
settleLost(s, held);
|
|
318
|
+
s.pending = [];
|
|
319
|
+
s.durableSeq = seqWm;
|
|
320
|
+
setRecord(s, {
|
|
321
|
+
clientId: s.record.clientId,
|
|
322
|
+
nextSeq: seqWm + 1,
|
|
323
|
+
ctxNextSeq: ctxWm + 1,
|
|
324
|
+
commands: []
|
|
325
|
+
});
|
|
326
|
+
restateContext(s);
|
|
327
|
+
if (held.length > 0 && s.onSessionLost?.(held.map((p) => p.inv)) === "requeue") for (const old of held) enqueueInner(s, old.command, old.inv.params, () => {}, () => {});
|
|
328
|
+
publish(s);
|
|
329
|
+
wake(s);
|
|
330
|
+
};
|
|
256
331
|
const handleHello = (s, { ops, syn }) => {
|
|
257
|
-
|
|
258
|
-
if (!RECORDS.includes(record)) protocolError(`unknown record: ${String(record)}`);
|
|
332
|
+
if (syn.record !== void 0 && syn.record !== "ephemeral") protocolError(`unknown record: ${String(syn.record)}`);
|
|
259
333
|
ingestOps(s, ops);
|
|
260
334
|
s.attach = "attached";
|
|
261
335
|
s.attachEpoch += 1;
|
|
262
336
|
s.frameFailures = 0;
|
|
263
337
|
s.resumeAt = 0;
|
|
264
|
-
s.ephemeral = record === "ephemeral";
|
|
338
|
+
s.ephemeral = syn.record === "ephemeral";
|
|
265
339
|
if (s.ephemeral) s.deactivated = true;
|
|
266
340
|
const [attachCmd, attachCtx] = s.attachLaneSeqs;
|
|
267
341
|
s.attachLaneSeqs = [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()];
|
|
268
|
-
|
|
342
|
+
const reconcile = () => {
|
|
343
|
+
if (s.shared) adoptStored(s);
|
|
344
|
+
s.durableSeq = syn.seq ?? 0;
|
|
269
345
|
for (const entry of s.pending) entry.posted = attachCmd.has(entry.seq);
|
|
270
346
|
const ctxWatermark = syn.ctxSeq ?? 0;
|
|
271
347
|
s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxWatermark);
|
|
272
348
|
for (const c of s.ctxPending) c.posted = attachCtx.has(c.seq);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
349
|
+
snapCtxClock(s, ctxWatermark);
|
|
350
|
+
syncBacklogInner(s);
|
|
351
|
+
s.sessionKnown = true;
|
|
352
|
+
};
|
|
353
|
+
if (!s.shared || s.deactivated) {
|
|
354
|
+
reconcile();
|
|
355
|
+
publishIngested(s);
|
|
356
|
+
pump(s);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
transact(s, reconcile).then(() => {
|
|
360
|
+
publishIngested(s);
|
|
361
|
+
pump(s);
|
|
362
|
+
});
|
|
279
363
|
};
|
|
280
364
|
const handleOps = (s, { ops, cmds }) => {
|
|
281
365
|
ingestOps(s, ops);
|
|
@@ -293,10 +377,26 @@ const handleRefusal = (s, syn) => {
|
|
|
293
377
|
s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxSeq);
|
|
294
378
|
for (const c of s.ctxPending) c.posted = false;
|
|
295
379
|
}
|
|
296
|
-
if (typeof syn.retry === "number")
|
|
380
|
+
if (typeof syn.retry === "number") {
|
|
381
|
+
s.resumeAt = Date.now() + syn.retry;
|
|
382
|
+
wake(s);
|
|
383
|
+
pump(s);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
297
386
|
const firstCmd = s.pending.find((p) => !p.posted);
|
|
298
387
|
const firstCtx = s.ctxPending.find((c) => !c.posted);
|
|
299
|
-
|
|
388
|
+
const cmdGap = typeof seq === "number" && firstCmd !== void 0 && firstCmd.seq > Math.max(seq, s.durableSeq) + 1;
|
|
389
|
+
const ctxGap = typeof ctxSeq === "number" && firstCtx !== void 0 && firstCtx.seq > ctxSeq + 1;
|
|
390
|
+
if (cmdGap || ctxGap) {
|
|
391
|
+
transact(s, () => {
|
|
392
|
+
if (cmdGap) sessionLostInner(s, seq, ctxSeq ?? 0);
|
|
393
|
+
else snapCtxClock(s, ctxSeq);
|
|
394
|
+
}).then(() => {
|
|
395
|
+
wake(s);
|
|
396
|
+
pump(s);
|
|
397
|
+
});
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
300
400
|
wake(s);
|
|
301
401
|
pump(s);
|
|
302
402
|
};
|
|
@@ -308,7 +408,7 @@ const handleDetach = (s) => {
|
|
|
308
408
|
const handleFinish = (s, { reason }) => {
|
|
309
409
|
if (reason === "evicted" || reason === "error") return;
|
|
310
410
|
s.attach = "finished";
|
|
311
|
-
settleAll(s,
|
|
411
|
+
settleAll(s, "gone");
|
|
312
412
|
publish(s);
|
|
313
413
|
wake(s);
|
|
314
414
|
};
|
|
@@ -376,7 +476,8 @@ const pump = async (s) => {
|
|
|
376
476
|
for (const entry of lanes.cmdEntries) entry.attempted = true;
|
|
377
477
|
let result;
|
|
378
478
|
try {
|
|
379
|
-
|
|
479
|
+
const send = () => transport.send(lanes.frame);
|
|
480
|
+
result = await (s.shared && !s.deactivated ? s.store.lock(send) : send());
|
|
380
481
|
} catch {
|
|
381
482
|
if (!s.running || isFinished(s)) return;
|
|
382
483
|
s.frameFailures += 1;
|
|
@@ -413,16 +514,33 @@ const pump = async (s) => {
|
|
|
413
514
|
}
|
|
414
515
|
};
|
|
415
516
|
const start = (s) => {
|
|
517
|
+
if (s.shared) {
|
|
518
|
+
s.shared.unsubscribe = s.store.subscribe(() => {
|
|
519
|
+
transact(s, () => consumeVerdicts(s));
|
|
520
|
+
});
|
|
521
|
+
s.running = true;
|
|
522
|
+
transact(s, () => {
|
|
523
|
+
if (s.store.load() === null) s.store.save(s.record);
|
|
524
|
+
s.recordPersisted = true;
|
|
525
|
+
adoptStored(s);
|
|
526
|
+
consumeVerdicts(s);
|
|
527
|
+
}).then(() => void pump(s));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
416
530
|
if (!s.recordPersisted) {
|
|
417
531
|
s.recordPersisted = true;
|
|
418
532
|
s.store.save(s.record);
|
|
419
533
|
}
|
|
420
|
-
|
|
534
|
+
adoptStored(s);
|
|
421
535
|
s.running = true;
|
|
422
536
|
pump(s);
|
|
423
537
|
};
|
|
424
538
|
const stop = (s) => {
|
|
425
539
|
s.running = false;
|
|
540
|
+
if (s.shared) {
|
|
541
|
+
s.shared.unsubscribe?.();
|
|
542
|
+
s.shared.unsubscribe = null;
|
|
543
|
+
}
|
|
426
544
|
wake(s);
|
|
427
545
|
settleAll(s, "unmount");
|
|
428
546
|
};
|
|
@@ -431,7 +549,7 @@ const setTransport = (s, transport) => {
|
|
|
431
549
|
s.transport = transport;
|
|
432
550
|
wake(s);
|
|
433
551
|
};
|
|
434
|
-
const
|
|
552
|
+
const enqueueInner = (s, command, params, resolve, reject) => {
|
|
435
553
|
const seq = s.record.nextSeq;
|
|
436
554
|
const entry = {
|
|
437
555
|
seq,
|
|
@@ -453,13 +571,22 @@ const enqueue = (s, command, params, resolve, reject) => {
|
|
|
453
571
|
setRecord(s, {
|
|
454
572
|
...s.record,
|
|
455
573
|
nextSeq: seq + 1,
|
|
456
|
-
commands:
|
|
574
|
+
commands: mergeBacklog(s)
|
|
457
575
|
});
|
|
458
576
|
s.lifecycle?.onStart(entry.inv);
|
|
459
|
-
pump(s);
|
|
460
577
|
};
|
|
461
|
-
const
|
|
462
|
-
|
|
578
|
+
const enqueue = (s, command, params, resolve, reject) => {
|
|
579
|
+
if (!s.shared) {
|
|
580
|
+
enqueueInner(s, command, params, resolve, reject);
|
|
581
|
+
pump(s);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
transact(s, () => enqueueInner(s, command, params, resolve, reject)).then(() => void pump(s));
|
|
585
|
+
};
|
|
586
|
+
const adoptStored = (s) => {
|
|
587
|
+
const known = new Set(s.pending.map((p) => p.seq));
|
|
588
|
+
const stored = s.record.commands.filter((c) => !known.has(c.seq));
|
|
589
|
+
if (stored.length === 0) return;
|
|
463
590
|
for (const { seq, ...command } of stored) {
|
|
464
591
|
const params = command.meta === void 0 ? command.params : reviveValue([...command.params], command.meta);
|
|
465
592
|
const entry = {
|
|
@@ -481,6 +608,7 @@ const resume = (s) => {
|
|
|
481
608
|
s.pending.push(entry);
|
|
482
609
|
s.lifecycle?.onStart(entry.inv);
|
|
483
610
|
}
|
|
611
|
+
s.pending.sort((a, b) => a.seq - b.seq);
|
|
484
612
|
};
|
|
485
613
|
const sendCommand = (s, method, params) => {
|
|
486
614
|
if (!s.running) return rejectedSend({
|
|
@@ -504,31 +632,44 @@ const sendCommand = (s, method, params) => {
|
|
|
504
632
|
};
|
|
505
633
|
const setContext = (s, value) => {
|
|
506
634
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("statewire: context must be a plain object");
|
|
635
|
+
if (!s.running || isFinished(s)) return;
|
|
507
636
|
const { value: encoded, meta } = encodeValue(value);
|
|
508
|
-
|
|
637
|
+
const ops = [{
|
|
509
638
|
op: "replace",
|
|
510
639
|
path: [],
|
|
511
640
|
value: encoded,
|
|
512
641
|
...meta !== void 0 && { meta }
|
|
513
642
|
}];
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
643
|
+
s.ctxOps = ops;
|
|
644
|
+
const mint = () => {
|
|
645
|
+
const seq = s.record.ctxNextSeq;
|
|
646
|
+
setRecord(s, {
|
|
647
|
+
...s.record,
|
|
648
|
+
ctxNextSeq: seq + 1
|
|
649
|
+
});
|
|
650
|
+
s.ctxPending.push({
|
|
651
|
+
seq,
|
|
652
|
+
ops,
|
|
653
|
+
posted: false
|
|
654
|
+
});
|
|
655
|
+
};
|
|
656
|
+
if (!s.shared) {
|
|
657
|
+
mint();
|
|
658
|
+
wake(s);
|
|
659
|
+
pump(s);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
transact(s, mint).then(() => {
|
|
663
|
+
wake(s);
|
|
664
|
+
pump(s);
|
|
523
665
|
});
|
|
524
|
-
wake(s);
|
|
525
|
-
pump(s);
|
|
526
666
|
};
|
|
527
667
|
const useStatewireSession = (options) => {
|
|
528
668
|
const [defaultStore] = useState(createMemorySessionStore);
|
|
529
669
|
const mounted = useContext(SessionStorageContext);
|
|
530
670
|
if (options.sessionStore !== void 0 && mounted !== void 0) throw new Error("statewire: session storage is set twice — drop the transport's sessionStore or the client's storage.session");
|
|
531
671
|
const store = options.sessionStore ?? mounted ?? defaultStore;
|
|
672
|
+
if (store.lock === void 0 !== (store.subscribe === void 0)) throw new Error("statewire: session storage must provide lock and subscribe together");
|
|
532
673
|
const [s] = useState(() => {
|
|
533
674
|
const stored = store.load();
|
|
534
675
|
if (stored !== null && !Array.isArray(stored.commands)) throw new Error("statewire: session storage loaded a record without a commands array");
|
|
@@ -554,12 +695,18 @@ const useStatewireSession = (options) => {
|
|
|
554
695
|
},
|
|
555
696
|
recordPersisted: stored !== null,
|
|
556
697
|
store,
|
|
698
|
+
shared: store.lock !== void 0 ? {
|
|
699
|
+
txq: Promise.resolve(),
|
|
700
|
+
parked: [],
|
|
701
|
+
unsubscribe: null
|
|
702
|
+
} : false,
|
|
557
703
|
ephemeral: false,
|
|
558
704
|
deactivated: false,
|
|
559
705
|
sessionKnown: false,
|
|
560
706
|
durableSeq: 0,
|
|
561
707
|
resumeAt: 0,
|
|
562
708
|
idle: false,
|
|
709
|
+
onSessionLost: void 0,
|
|
563
710
|
onDone: void 0,
|
|
564
711
|
ctxOps: void 0,
|
|
565
712
|
ctxPending: [],
|
|
@@ -596,7 +743,9 @@ const useStatewireSession = (options) => {
|
|
|
596
743
|
} });
|
|
597
744
|
return session;
|
|
598
745
|
});
|
|
746
|
+
if (store.lock !== void 0 !== (s.shared !== false)) throw new Error("statewire: session storage changed sharedness across renders — lock/subscribe must be present from the first render or never");
|
|
599
747
|
s.store = store;
|
|
748
|
+
s.onSessionLost = options.onSessionLost;
|
|
600
749
|
s.onDone = options.onDone;
|
|
601
750
|
useEffect(() => {
|
|
602
751
|
start(s);
|