statewire 0.9.1 → 0.10.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.
Files changed (53) hide show
  1. package/dist/client/connection.d.ts +5 -2
  2. package/dist/client/connection.d.ts.map +1 -1
  3. package/dist/client/connection.js +123 -23
  4. package/dist/client/connection.js.map +1 -1
  5. package/dist/client/session.d.ts +7 -4
  6. package/dist/client/session.d.ts.map +1 -1
  7. package/dist/client/session.js +62 -15
  8. package/dist/client/session.js.map +1 -1
  9. package/dist/client/sessionStore.d.ts +5 -14
  10. package/dist/client/sessionStore.d.ts.map +1 -1
  11. package/dist/client/sessionStore.js +4 -1
  12. package/dist/client/sessionStore.js.map +1 -1
  13. package/dist/client/timing.d.ts +2 -0
  14. package/dist/client/timing.d.ts.map +1 -1
  15. package/dist/client/timing.js +8 -2
  16. package/dist/client/timing.js.map +1 -1
  17. package/dist/host/http.d.ts +7 -0
  18. package/dist/host/http.d.ts.map +1 -0
  19. package/dist/host/http.js +175 -0
  20. package/dist/host/http.js.map +1 -0
  21. package/dist/host/socket.d.ts +102 -1
  22. package/dist/host/socket.d.ts.map +1 -1
  23. package/dist/host/socket.js +48 -27
  24. package/dist/host/socket.js.map +1 -1
  25. package/dist/host-internal.d.ts +5 -0
  26. package/dist/host-internal.d.ts.map +1 -1
  27. package/dist/host.d.ts +1 -1
  28. package/dist/host.d.ts.map +1 -1
  29. package/dist/statewire.d.ts +34 -8
  30. package/dist/statewire.d.ts.map +1 -1
  31. package/dist/statewire.js +9 -2
  32. package/dist/statewire.js.map +1 -1
  33. package/dist/transports/sse.d.ts.map +1 -1
  34. package/dist/transports/sse.js +6 -4
  35. package/dist/transports/sse.js.map +1 -1
  36. package/dist/transports/ws.d.ts.map +1 -1
  37. package/dist/transports/ws.js +5 -9
  38. package/dist/transports/ws.js.map +1 -1
  39. package/dist/types.d.ts +1 -1
  40. package/dist/types.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/client/connection.ts +212 -40
  43. package/src/client/session.ts +90 -24
  44. package/src/client/sessionStore.ts +8 -15
  45. package/src/client/timing.ts +10 -2
  46. package/src/host/http.ts +280 -0
  47. package/src/host/socket.ts +120 -57
  48. package/src/host-internal.ts +5 -0
  49. package/src/host.ts +1 -0
  50. package/src/statewire.ts +65 -9
  51. package/src/transports/sse.ts +6 -5
  52. package/src/transports/ws.ts +4 -9
  53. package/src/types.ts +1 -1
@@ -42,6 +42,8 @@ type Conn = {
42
42
  hasSnapshot: boolean;
43
43
  lease: string | null;
44
44
  attempt: number;
45
+ authDenials: number;
46
+ liveAt: number | null;
45
47
  episode: Episode | null;
46
48
  awaitingHello: boolean;
47
49
  held: StatewireCmd[];
@@ -54,7 +56,8 @@ type Conn = {
54
56
  notify: (connection: StatewireClient.Connection) => void;
55
57
  reconnect: () => void;
56
58
  };
57
- declare const handleEnvelope: (c: Conn, env: StatewireEnvelope) => AttemptOutcome | null;
59
+ declare const validateRefusalSyn: (value: unknown) => StatewireSyn;
60
+ declare const handleEnvelope: (c: Conn, value: unknown) => AttemptOutcome | null;
58
61
  declare const useStatewireConn: (props: {
59
62
  url: string;
60
63
  onChange?: ((connection: StatewireClient.Connection) => void) | undefined;
@@ -63,5 +66,5 @@ declare const useStatewireConn: (props: {
63
66
  }) => Conn;
64
67
  declare const classify: (timeout: AbortSignal) => Error;
65
68
  //#endregion
66
- export { AttemptOutcome, Conn, Episode, Gate, StatewireCmd, StatewireEnvelope, StatewireSyn, classify, handleEnvelope, useStatewireConn };
69
+ export { AttemptOutcome, Conn, Episode, Gate, StatewireCmd, StatewireEnvelope, StatewireSyn, classify, handleEnvelope, useStatewireConn, validateRefusalSyn };
67
70
  //# sourceMappingURL=connection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","names":[],"sources":["../../src/client/connection.ts"],"mappings":";;;;KAQY;EACV;EACA;;KAGU;EACV;EACA;EACA;EACA;EACA;EACA;;EAEA;;KAGU;EACV;EACA;EACA;EACA;EACA;EACA;;KAGU;EACV;EACA,MAAM;EACN;EACA;EACA,MAAM;EACN,MAAM,cAAc;;KAGV;EAAS;EAAkB;;KAoC3B;KAEA;EACV,YAAY,gBAAgB;EAC5B;EACA;EACA;EACA,SAAS;EACT;EACA,MAAM;EACN,MAAM;EACN,OAAO;EACP,UAAU;EACV,KAAK;EACL,WAAW,YAAY,gBAAgB;EACvC,cAAc,OAAO,oBAAoB,QAAQ;EACjD,SAAS,YAAY,gBAAgB;EACrC;;cAyGW,iBACX,GAAG,MACH,KAAK,sBACJ;cAwFU,mBAAoB;EAC/B;EACA,aAAa,YAAY,gBAAgB;EACzC,KAAK;EACL,cAAc,GAAG,MAAM,OAAO,oBAAoB,QAAQ;MACxD;cAkDS,WAAY,SAAS,gBAAW"}
1
+ {"version":3,"file":"connection.d.ts","names":[],"sources":["../../src/client/connection.ts"],"mappings":";;;;KAQY;EACV;EACA;;KAGU;EACV;EACA;EACA;EACA;EACA;EACA;;EAEA;;KAGU;EACV;EACA;EACA;EACA;EACA;EACA;;KAGU;EACV;EACA,MAAM;EACN;EACA;EACA,MAAM;EACN,MAAM,cAAc;;KAGV;EAAS;EAAkB;;KAoC3B;KAEA;EACV,YAAY,gBAAgB;EAC5B;EACA;EACA;EACA;EACA;EACA,SAAS;EACT;EACA,MAAM;EACN,MAAM;EACN,OAAO;EACP,UAAU;EACV,KAAK;EACL,WAAW,YAAY,gBAAgB;EACvC,cAAc,OAAO,oBAAoB,QAAQ;EACjD,SAAS,YAAY,gBAAgB;EACrC;;cA8CW,qBAAsB,mBAAiB;cAiOvC,iBACX,GAAG,MACH,mBACC;cAwFU,mBAAoB;EAC/B;EACA,aAAa,YAAY,gBAAgB;EACzC,KAAK;EACL,cAAc,GAAG,MAAM,OAAO,oBAAoB,QAAQ;MACxD;cAsDS,WAAY,SAAS,gBAAW"}
@@ -1,5 +1,5 @@
1
1
  import { reviveOps, reviveValue } from "../wire/meta.js";
2
- import { backoff } from "./timing.js";
2
+ import { backoff, timing } from "./timing.js";
3
3
  import { useEffect, useRef, useState } from "@assistant-ui/tap/react-shim";
4
4
  import { flushTapSync } from "@assistant-ui/tap";
5
5
  //#region src/client/connection.ts
@@ -45,6 +45,58 @@ const setStatus = (c, next) => {
45
45
  const protocolError = (message) => {
46
46
  throw new Error(`statewire: protocol error: ${message}`);
47
47
  };
48
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49
+ const isNonnegativeSafeInteger = (value) => Number.isSafeInteger(value) && value >= 0;
50
+ const isPositiveSafeInteger = (value) => Number.isSafeInteger(value) && value > 0;
51
+ const validateOptionalString = (value, field) => {
52
+ if (value !== void 0 && typeof value !== "string") protocolError(`${field} must be a string`);
53
+ };
54
+ const validateSynScalars = (syn) => {
55
+ if (syn["seq"] !== void 0 && !isNonnegativeSafeInteger(syn["seq"])) protocolError("syn.seq must be a nonnegative safe integer");
56
+ if (syn["ctxSeq"] !== void 0 && !isNonnegativeSafeInteger(syn["ctxSeq"])) protocolError("syn.ctxSeq must be a nonnegative safe integer");
57
+ if (syn["retry"] !== void 0 && !isNonnegativeSafeInteger(syn["retry"])) protocolError("syn.retry must be a nonnegative safe integer");
58
+ validateOptionalString(syn["id"], "syn.id");
59
+ if (syn["idle"] !== void 0 && typeof syn["idle"] !== "boolean") protocolError("syn.idle must be a boolean");
60
+ validateOptionalString(syn["record"], "syn.record");
61
+ };
62
+ const validateRefusalSyn = (value) => {
63
+ if (!isObject(value)) return protocolError("syn must be an object");
64
+ validateSynScalars(value);
65
+ if (value["id"] !== void 0 || value["idle"] !== void 0 || value["record"] !== void 0) protocolError("invalid refusal syn");
66
+ if (value["seq"] === void 0 && value["ctxSeq"] === void 0 && value["retry"] === void 0) protocolError("refusal syn has no watermarks or retry");
67
+ return value;
68
+ };
69
+ const validateCmd = (value) => {
70
+ if (!isObject(value)) return protocolError("cmd entry must be an object");
71
+ if (!isPositiveSafeInteger(value["seq"])) protocolError("cmd.seq must be a positive safe integer");
72
+ validateOptionalString(value["type"], "cmd.type");
73
+ validateOptionalString(value["code"], "cmd.code");
74
+ validateOptionalString(value["message"], "cmd.message");
75
+ if (value["dur"] !== void 0 && value["dur"] !== true) protocolError("cmd.dur must be true");
76
+ return true;
77
+ };
78
+ const validateFin = (value) => {
79
+ if (!isObject(value)) return protocolError("fin must be an object");
80
+ if (typeof value["reason"] !== "string") protocolError("fin.reason must be a string");
81
+ validateOptionalString(value["message"], "fin.message");
82
+ return true;
83
+ };
84
+ const validateEnvelope = (value) => {
85
+ if (!isObject(value)) return protocolError("packet must be an object");
86
+ if (value["ops"] !== void 0 && !Array.isArray(value["ops"])) protocolError("ops must be an array");
87
+ if (value["cmd"] !== void 0) {
88
+ const cmd = value["cmd"];
89
+ if (!Array.isArray(cmd)) return protocolError("cmd must be an array");
90
+ for (const entry of cmd) validateCmd(entry);
91
+ }
92
+ if (value["idle"] !== void 0 && typeof value["idle"] !== "boolean") protocolError("idle must be a boolean");
93
+ if (value["next"] !== void 0) {
94
+ if (value["next"] !== true) protocolError("next must be true");
95
+ if (value["cmd"] === void 0 || value["ops"] !== void 0 || value["fin"] !== void 0) protocolError("next must ride a cmd-only packet");
96
+ }
97
+ if (value["fin"] !== void 0) validateFin(value["fin"]);
98
+ return value;
99
+ };
48
100
  const releaseHeld = (c) => {
49
101
  if (c.held.length === 0) return;
50
102
  const held = c.held;
@@ -53,29 +105,57 @@ const releaseHeld = (c) => {
53
105
  };
54
106
  const cmdsOf = (env) => {
55
107
  if (env.cmd === void 0) return [];
56
- if (!Array.isArray(env.cmd)) return protocolError("cmd must be an array");
57
108
  return env.cmd;
58
109
  };
110
+ const snapshotOf = (env) => {
111
+ const ops = env.ops;
112
+ if (ops === void 0) return protocolError("hello missing snapshot");
113
+ if (ops.length !== 1 || !isObject(ops[0])) return protocolError("hello snapshot must be one root replace op");
114
+ const op = ops[0];
115
+ if (op["op"] !== "replace" || !Array.isArray(op["path"]) || op["path"].length !== 0 || !Object.hasOwn(op, "value")) return protocolError("hello snapshot must be one root replace op");
116
+ return reviveOps(ops);
117
+ };
118
+ const helloSynOf = (env) => {
119
+ if (!isObject(env.syn)) return protocolError("expected hello");
120
+ validateSynScalars(env.syn);
121
+ if (env.syn["retry"] !== void 0) protocolError("hello syn cannot carry retry");
122
+ return env.syn;
123
+ };
59
124
  const handleHello = (c, env) => {
60
- const syn = env.syn;
61
- if (syn === void 0) return protocolError("expected hello");
62
- c.awaitingHello = false;
63
- c.hasSnapshot = true;
125
+ const syn = helloSynOf(env);
126
+ const ops = snapshotOf(env);
64
127
  c.ctx.onHello({
65
- ops: env.ops === void 0 ? [] : reviveOps(env.ops),
128
+ ops,
66
129
  syn
67
130
  });
68
131
  c.ctx.onIdle(syn.idle ?? false);
69
- c.attempt = 0;
132
+ c.awaitingHello = false;
133
+ c.hasSnapshot = true;
134
+ c.liveAt = Date.now();
135
+ c.authDenials = 0;
70
136
  c.episode = null;
71
137
  setStatus(c, { status: "live" });
72
138
  };
73
- const deliverEnvelope = (c, env) => {
74
- if (env.syn !== void 0) {
75
- const syn = env.syn;
76
- if (syn.seq !== void 0 || syn.ctxSeq !== void 0 || syn.retry !== void 0) c.ctx.onRefusal(syn);
77
- else if (syn.id === void 0) protocolError("invalid mid-stream syn");
139
+ let warnedUnknownSynFact = false;
140
+ const warnUnknownSynFact = () => {
141
+ if (warnedUnknownSynFact) return;
142
+ warnedUnknownSynFact = true;
143
+ console.warn("statewire: ignoring unknown syn fact");
144
+ };
145
+ const deliverSyn = (c, value) => {
146
+ if (!isObject(value)) return protocolError("syn must be an object");
147
+ validateSynScalars(value);
148
+ if (value["record"] !== void 0 || value["idle"] !== void 0) protocolError("invalid mid-stream syn");
149
+ if (value["seq"] !== void 0 || value["ctxSeq"] !== void 0 || value["retry"] !== void 0) {
150
+ c.ctx.onRefusal(validateRefusalSyn(value));
151
+ return;
78
152
  }
153
+ if (value["id"] !== void 0) return;
154
+ if (Object.keys(value).length === 0) protocolError("invalid mid-stream syn");
155
+ warnUnknownSynFact();
156
+ };
157
+ const deliverEnvelope = (c, env) => {
158
+ if (env.syn !== void 0) deliverSyn(c, env.syn);
79
159
  if (env.ops !== void 0) {
80
160
  const held = c.held;
81
161
  c.held = [];
@@ -90,7 +170,14 @@ const deliverEnvelope = (c, env) => {
90
170
  }
91
171
  if (typeof env.idle === "boolean") c.ctx.onIdle(env.idle);
92
172
  };
93
- const isTransientFinish = (reason) => reason === "evicted" || reason === "error";
173
+ const transientCauseOf = (reason) => {
174
+ switch (reason) {
175
+ case "evicted": return "evicted";
176
+ case "error": return "error";
177
+ case "unauthorized": return "unauthorized";
178
+ default: return "error";
179
+ }
180
+ };
94
181
  const settleFin = (c, fin) => {
95
182
  let finish = fin;
96
183
  if (finish.meta !== void 0) {
@@ -108,12 +195,19 @@ const settleFin = (c, fin) => {
108
195
  setStatus(c, { status: "idle" });
109
196
  return "idle";
110
197
  }
111
- c.ctx.onFinish(finish);
112
- if (isTransientFinish(finish.reason)) {
198
+ const denial = finish.reason === "unauthorized";
199
+ const terminal = finish.reason === "gone" || finish.reason === "superseded" || denial && c.authDenials > 0;
200
+ if (denial) c.authDenials += 1;
201
+ c.ctx.onFinish(terminal ? finish : {
202
+ ...finish,
203
+ reason: "error"
204
+ });
205
+ if (!terminal) {
113
206
  c.episode = {
114
- cause: finish.reason,
207
+ cause: transientCauseOf(finish.reason),
115
208
  ...finish.message !== void 0 && { message: finish.message }
116
209
  };
210
+ if (denial) openGate(c.gate);
117
211
  return "reattach";
118
212
  }
119
213
  setStatus(c, {
@@ -123,8 +217,8 @@ const settleFin = (c, fin) => {
123
217
  });
124
218
  return "stop";
125
219
  };
126
- const handleEnvelope = (c, env) => {
127
- if (typeof env !== "object" || env === null) return protocolError("packet must be an object");
220
+ const handleEnvelope = (c, value) => {
221
+ const env = validateEnvelope(value);
128
222
  if (c.awaitingHello) {
129
223
  if (env.fin !== void 0) return settleFin(c, env.fin);
130
224
  handleHello(c, env);
@@ -140,10 +234,12 @@ const handleEnvelope = (c, env) => {
140
234
  return null;
141
235
  };
142
236
  const scheduleRetry = (c, failure, immediate) => {
237
+ if (c.liveAt !== null && Date.now() - c.liveAt >= timing.backoffResetMs) c.attempt = 0;
238
+ c.liveAt = null;
143
239
  c.attempt += 1;
144
240
  c.episode ??= { cause: "dropped" };
145
241
  const delay = immediate ? 0 : backoff(c.attempt);
146
- if (c.hasSnapshot) setStatus(c, {
242
+ setStatus(c, {
147
243
  status: "reconnecting",
148
244
  ...c.episode,
149
245
  attempt: c.attempt,
@@ -156,10 +252,10 @@ const reconnectConn = (c) => {
156
252
  if (!c.lifetime) return;
157
253
  if (c.connection.status === "gone" || c.connection.status === "idle") {
158
254
  c.attempt = 0;
255
+ c.authDenials = 0;
159
256
  c.episode = { cause: "dropped" };
160
257
  flushTapSync(() => {
161
- if (c.hasSnapshot) scheduleRetry(c, void 0, true);
162
- else setStatus(c, { status: "connecting" });
258
+ scheduleRetry(c, void 0, true);
163
259
  });
164
260
  }
165
261
  openGate(c.gate);
@@ -205,6 +301,8 @@ const useStatewireConn = (props) => {
205
301
  hasSnapshot: false,
206
302
  lease: null,
207
303
  attempt: 0,
304
+ authDenials: 0,
305
+ liveAt: null,
208
306
  episode: null,
209
307
  awaitingHello: true,
210
308
  held: [],
@@ -229,6 +327,8 @@ const useStatewireConn = (props) => {
229
327
  c.hasSnapshot = false;
230
328
  c.lease = null;
231
329
  c.attempt = 0;
330
+ c.authDenials = 0;
331
+ c.liveAt = null;
232
332
  c.episode = null;
233
333
  if (c.connection.status !== "connecting") setStatus(c, { status: "connecting" });
234
334
  const lifetime = new AbortController();
@@ -246,6 +346,6 @@ const useStatewireConn = (props) => {
246
346
  };
247
347
  const classify = (timeout) => /* @__PURE__ */ new Error(timeout.aborted ? "timeout" : "network");
248
348
  //#endregion
249
- export { classify, handleEnvelope, useStatewireConn };
349
+ export { classify, handleEnvelope, useStatewireConn, validateRefusalSyn };
250
350
 
251
351
  //# sourceMappingURL=connection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"connection.js","names":["useEffect","useRef","useState","flushTapSync","reviveOps","reviveValue","backoff","ConnectionState","StatewireRemoteProps","StatewireClient","StatewireHost","Episode","cause","message","StatewireCmd","seq","type","code","payload","meta","dur","StatewireSyn","ctxSeq","record","id","idle","retry","StatewireEnvelope","ops","cmd","next","syn","fin","Fin","Gate","latched","wake","newGate","undefined","takeGate","g","was","waitGate","timeoutMs","Promise","resolve","timer","setTimeout","clearTimeout","openGate","AttemptOutcome","Conn","connection","Connection","hasSnapshot","lease","attempt","episode","awaitingHello","held","gate","abort","AbortController","lifetime","ctx","onChange","attemptOnce","notify","reconnect","setStatus","c","changed","status","protocolError","Error","releaseHeld","length","onCmds","cmdsOf","env","Array","isArray","handleHello","onHello","onIdle","deliverEnvelope","onRefusal","onOps","cmds","push","isTransientFinish","reason","settleFin","finish","rest","onFinish","handleEnvelope","scheduleRetry","failure","immediate","delay","nextRetryAt","Date","now","lastError","reconnectConn","run","alive","outcome","error","signal","aborted","onDetach","useStatewireConn","props","url","latest","current","setConnection","conn","classify","timeout","AbortSignal"],"sources":["../../src/client/connection.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport { flushTapSync } from \"@assistant-ui/tap\";\nimport { reviveOps, reviveValue } from \"../wire/meta\";\nimport { backoff } from \"./timing\";\nimport type { ConnectionState, StatewireRemoteProps } from \"../transports/sse\";\nimport type { StatewireClient } from \"../statewire\";\nimport type { StatewireHost } from \"../host\";\n\nexport type Episode = {\n cause: \"dropped\" | \"evicted\" | \"error\" | \"unauthorized\";\n message?: string;\n};\n\nexport type StatewireCmd = {\n seq: number;\n type?: \"applied\" | \"result\" | \"rejected\" | \"failed\" | \"crashed\" | \"expired\";\n code?: string;\n message?: string;\n payload?: unknown;\n meta?: unknown;\n /** Cumulative: resending through this seq will never be needed. */\n dur?: boolean;\n};\n\nexport type StatewireSyn = {\n seq?: number;\n ctxSeq?: number;\n record?: \"fresh\" | \"resumed\" | \"ephemeral\";\n id?: string;\n idle?: boolean;\n retry?: number;\n};\n\nexport type StatewireEnvelope = {\n ops?: unknown[];\n cmd?: StatewireCmd[];\n next?: true;\n idle?: boolean;\n syn?: StatewireSyn;\n fin?: StatewireHost.Fin;\n};\n\nexport type Gate = { latched: boolean; wake: (() => void) | undefined };\n\nconst newGate = (): Gate => ({ latched: false, wake: undefined });\n\nconst takeGate = (g: Gate) => {\n const was = g.latched;\n g.latched = false;\n return was;\n};\n\nconst waitGate = (g: Gate, timeoutMs?: number) => {\n if (g.latched) {\n g.latched = false;\n return Promise.resolve();\n }\n return new Promise<void>((resolve) => {\n const timer =\n timeoutMs === undefined\n ? undefined\n : setTimeout(() => {\n g.wake = undefined;\n resolve();\n }, timeoutMs);\n g.wake = () => {\n clearTimeout(timer);\n g.wake = undefined;\n resolve();\n };\n });\n};\n\nconst openGate = (g: Gate) => {\n if (g.wake) g.wake();\n else g.latched = true;\n};\n\nexport type AttemptOutcome = \"stop\" | \"reattach\" | \"ended\" | \"idle\";\n\nexport type Conn = {\n connection: StatewireClient.Connection;\n hasSnapshot: boolean;\n lease: string | null;\n attempt: number;\n episode: Episode | null;\n awaitingHello: boolean;\n held: StatewireCmd[];\n gate: Gate;\n abort: AbortController | null;\n lifetime: AbortController | null;\n ctx: StatewireRemoteProps;\n onChange: (connection: StatewireClient.Connection) => void;\n attemptOnce: (abort: AbortController) => Promise<AttemptOutcome>;\n notify: (connection: StatewireClient.Connection) => void;\n reconnect: () => void;\n};\n\nconst setStatus = (c: Conn, next: ConnectionState) => {\n const changed = next.status !== c.connection.status;\n c.connection = { ...next, reconnect: c.reconnect };\n c.notify(c.connection);\n if (changed) c.onChange(c.connection);\n};\n\nconst protocolError = (message: string): never => {\n throw new Error(`statewire: protocol error: ${message}`);\n};\n\n// A hold's release delivers, never settles: fin and bare close both drain it.\nconst releaseHeld = (c: Conn) => {\n if (c.held.length === 0) return;\n const held = c.held;\n c.held = [];\n c.ctx.onCmds(held);\n};\n\nconst cmdsOf = (env: StatewireEnvelope): StatewireCmd[] => {\n if (env.cmd === undefined) return [];\n if (!Array.isArray(env.cmd)) return protocolError(\"cmd must be an array\");\n return env.cmd;\n};\n\nconst handleHello = (c: Conn, env: StatewireEnvelope) => {\n const syn = env.syn;\n if (syn === undefined) return protocolError(\"expected hello\");\n c.awaitingHello = false;\n c.hasSnapshot = true;\n c.ctx.onHello({\n ops: env.ops === undefined ? [] : reviveOps(env.ops),\n syn,\n });\n c.ctx.onIdle(syn.idle ?? false);\n c.attempt = 0;\n c.episode = null;\n setStatus(c, { status: \"live\" });\n};\n\nconst deliverEnvelope = (c: Conn, env: StatewireEnvelope) => {\n if (env.syn !== undefined) {\n const syn = env.syn;\n if (\n syn.seq !== undefined ||\n syn.ctxSeq !== undefined ||\n syn.retry !== undefined\n ) {\n c.ctx.onRefusal(syn);\n } else if (syn.id === undefined) {\n protocolError(\"invalid mid-stream syn\");\n }\n }\n if (env.ops !== undefined) {\n const held = c.held;\n c.held = [];\n c.ctx.onOps({\n ops: reviveOps(env.ops),\n cmds: [...held, ...cmdsOf(env)],\n });\n } else if (env.cmd !== undefined) {\n const cmds = cmdsOf(env);\n if (env.next === true) c.held.push(...cmds);\n else c.ctx.onCmds(cmds);\n }\n if (typeof env.idle === \"boolean\") c.ctx.onIdle(env.idle);\n};\n\nconst isTransientFinish = (\n reason: StatewireHost.Fin[\"reason\"],\n): reason is \"evicted\" | \"error\" => reason === \"evicted\" || reason === \"error\";\n\nconst settleFin = (c: Conn, fin: StatewireHost.Fin): AttemptOutcome => {\n let finish = fin;\n if (finish.meta !== undefined) {\n const { meta, ...rest } = finish;\n finish = { ...rest, payload: reviveValue(rest.payload, meta) };\n }\n releaseHeld(c);\n if (finish.reason === \"idle\") {\n c.ctx.onIdle(true);\n c.attempt = 0;\n c.episode = null;\n setStatus(c, { status: \"idle\" });\n return \"idle\";\n }\n c.ctx.onFinish(finish);\n if (isTransientFinish(finish.reason)) {\n c.episode = {\n cause: finish.reason,\n ...(finish.message !== undefined && { message: finish.message }),\n };\n return \"reattach\";\n }\n setStatus(c, {\n status: \"gone\",\n ...(finish.message !== undefined && { message: finish.message }),\n ...(finish.payload !== undefined && { payload: finish.payload }),\n });\n return \"stop\";\n};\n\nexport const handleEnvelope = (\n c: Conn,\n env: StatewireEnvelope,\n): AttemptOutcome | null => {\n if (typeof env !== \"object\" || env === null) {\n return protocolError(\"packet must be an object\");\n }\n if (c.awaitingHello) {\n if (env.fin !== undefined) return settleFin(c, env.fin);\n handleHello(c, env);\n return null;\n }\n deliverEnvelope(c, env);\n if (env.fin !== undefined) return settleFin(c, env.fin);\n if (c.connection.status !== \"live\") {\n c.attempt = 0;\n c.episode = null;\n setStatus(c, { status: \"live\" });\n }\n return null;\n};\n\nconst scheduleRetry = (\n c: Conn,\n failure: string | undefined,\n immediate: boolean,\n) => {\n c.attempt += 1;\n c.episode ??= { cause: \"dropped\" };\n const delay = immediate ? 0 : backoff(c.attempt);\n if (c.hasSnapshot) {\n setStatus(c, {\n status: \"reconnecting\",\n ...c.episode,\n attempt: c.attempt,\n nextRetryAt: Date.now() + delay,\n ...(failure !== undefined && { lastError: failure }),\n });\n }\n return delay;\n};\n\nconst reconnectConn = (c: Conn) => {\n if (!c.lifetime) return;\n if (c.connection.status === \"gone\" || c.connection.status === \"idle\") {\n c.attempt = 0;\n c.episode = { cause: \"dropped\" };\n flushTapSync(() => {\n if (c.hasSnapshot) scheduleRetry(c, undefined, true);\n else setStatus(c, { status: \"connecting\" });\n });\n }\n openGate(c.gate);\n c.abort?.abort();\n};\n\nconst run = async (c: Conn, lifetime: AbortController, gate: Gate) => {\n const alive = () => c.lifetime === lifetime;\n while (alive()) {\n const abort = new AbortController();\n c.abort = abort;\n c.awaitingHello = true;\n c.held = [];\n let outcome: AttemptOutcome | undefined;\n let failure: string | undefined;\n try {\n outcome = await c.attemptOnce(abort);\n } catch (error) {\n if (!abort.signal.aborted) {\n failure =\n error instanceof Error && error.message ? error.message : \"network\";\n }\n }\n abort.abort();\n if (!alive()) return;\n flushTapSync(() => {\n releaseHeld(c);\n c.ctx.onDetach();\n });\n if (!alive()) return;\n if (outcome === \"stop\" || outcome === \"idle\") {\n await waitGate(gate);\n continue;\n }\n const immediate = takeGate(gate);\n const delay = flushTapSync(() => scheduleRetry(c, failure, immediate));\n if (delay === 0) continue;\n await waitGate(gate, delay);\n }\n};\n\nexport const useStatewireConn = (props: {\n url: string;\n onChange?: ((connection: StatewireClient.Connection) => void) | undefined;\n ctx: StatewireRemoteProps;\n attemptOnce: (c: Conn, abort: AbortController) => Promise<AttemptOutcome>;\n}): Conn => {\n const latest = useRef(props);\n latest.current = props;\n const [, setConnection] = useState<StatewireClient.Connection | null>(null);\n const [c] = useState((): Conn => {\n const conn: Conn = {\n hasSnapshot: false,\n lease: null,\n attempt: 0,\n episode: null,\n awaitingHello: true,\n held: [],\n abort: null,\n gate: newGate(),\n lifetime: null,\n get ctx() {\n return latest.current.ctx;\n },\n onChange: (connection) => latest.current.onChange?.(connection),\n attemptOnce: (abort) => latest.current.attemptOnce(conn, abort),\n notify: setConnection,\n reconnect: () => reconnectConn(conn),\n connection: { status: \"connecting\", reconnect: () => conn.reconnect() },\n };\n return conn;\n });\n\n useEffect(() => {\n c.hasSnapshot = false;\n c.lease = null;\n c.attempt = 0;\n c.episode = null;\n if (c.connection.status !== \"connecting\") {\n setStatus(c, { status: \"connecting\" });\n }\n const lifetime = new AbortController();\n c.lifetime = lifetime;\n c.gate = newGate();\n void run(c, lifetime, c.gate);\n return () => {\n c.abort?.abort();\n lifetime.abort();\n if (c.lifetime === lifetime) c.lifetime = null;\n openGate(c.gate);\n };\n }, [c, props.url]);\n\n return c;\n};\n\nexport const classify = (timeout: AbortSignal) =>\n new Error(timeout.aborted ? \"timeout\" : \"network\");\n"],"mappings":";;;;;AA4CA,MAAMqC,iBAAuB;CAAEF,SAAS;CAAOC,MAAME,KAAAA;AAAU;AAE/D,MAAMC,YAAYC,MAAY;CAC5B,MAAMC,MAAMD,EAAEL;CACdK,EAAEL,UAAU;CACZ,OAAOM;AACT;AAEA,MAAMC,YAAYF,GAASG,cAAuB;CAChD,IAAIH,EAAEL,SAAS;EACbK,EAAEL,UAAU;EACZ,OAAOS,QAAQC,QAAQ;CACzB;CACA,OAAO,IAAID,SAAeC,YAAY;EACpC,MAAMC,QACJH,cAAcL,KAAAA,IACVA,KAAAA,IACAS,iBAAiB;GACfP,EAAEJ,OAAOE,KAAAA;GACTO,QAAQ;EACV,GAAGF,SAAS;EAClBH,EAAEJ,aAAa;GACbY,aAAaF,KAAK;GAClBN,EAAEJ,OAAOE,KAAAA;GACTO,QAAQ;EACV;CACF,CAAC;AACH;AAEA,MAAMI,YAAYT,MAAY;CAC5B,IAAIA,EAAEJ,MAAMI,EAAEJ,KAAK;MACdI,EAAEL,UAAU;AACnB;AAsBA,MAAMkC,aAAaC,GAASxC,SAA0B;CACpD,MAAMyC,UAAUzC,KAAK0C,WAAWF,EAAElB,WAAWoB;CAC7CF,EAAElB,aAAa;EAAE,GAAGtB;EAAMsC,WAAWE,EAAEF;CAAU;CACjDE,EAAEH,OAAOG,EAAElB,UAAU;CACrB,IAAImB,SAASD,EAAEL,SAASK,EAAElB,UAAU;AACtC;AAEA,MAAMqB,iBAAiB5D,YAA2B;CAChD,MAAM,IAAI6D,MAAM,8BAA8B7D,SAAS;AACzD;AAGA,MAAM8D,eAAeL,MAAY;CAC/B,IAAIA,EAAEX,KAAKiB,WAAW,GAAG;CACzB,MAAMjB,OAAOW,EAAEX;CACfW,EAAEX,OAAO,CAAA;CACTW,EAAEN,IAAIa,OAAOlB,IAAI;AACnB;AAEA,MAAMmB,UAAUC,QAA2C;CACzD,IAAIA,IAAIlD,QAAQS,KAAAA,GAAW,OAAO,CAAA;CAClC,IAAI,CAAC0C,MAAMC,QAAQF,IAAIlD,GAAG,GAAG,OAAO4C,cAAc,sBAAsB;CACxE,OAAOM,IAAIlD;AACb;AAEA,MAAMqD,eAAeZ,GAASS,QAA2B;CACvD,MAAMhD,MAAMgD,IAAIhD;CAChB,IAAIA,QAAQO,KAAAA,GAAW,OAAOmC,cAAc,gBAAgB;CAC5DH,EAAEZ,gBAAgB;CAClBY,EAAEhB,cAAc;CAChBgB,EAAEN,IAAImB,QAAQ;EACZvD,KAAKmD,IAAInD,QAAQU,KAAAA,IAAY,CAAA,IAAKlC,UAAU2E,IAAInD,GAAG;EACnDG;CACF,CAAC;CACDuC,EAAEN,IAAIoB,OAAOrD,IAAIN,QAAQ,KAAK;CAC9B6C,EAAEd,UAAU;CACZc,EAAEb,UAAU;CACZY,UAAUC,GAAG,EAAEE,QAAQ,OAAO,CAAC;AACjC;AAEA,MAAMa,mBAAmBf,GAASS,QAA2B;CAC3D,IAAIA,IAAIhD,QAAQO,KAAAA,GAAW;EACzB,MAAMP,MAAMgD,IAAIhD;EAChB,IACEA,IAAIhB,QAAQuB,KAAAA,KACZP,IAAIT,WAAWgB,KAAAA,KACfP,IAAIL,UAAUY,KAAAA,GAEdgC,EAAEN,IAAIsB,UAAUvD,GAAG;OACd,IAAIA,IAAIP,OAAOc,KAAAA,GACpBmC,cAAc,wBAAwB;CAE1C;CACA,IAAIM,IAAInD,QAAQU,KAAAA,GAAW;EACzB,MAAMqB,OAAOW,EAAEX;EACfW,EAAEX,OAAO,CAAA;EACTW,EAAEN,IAAIuB,MAAM;GACV3D,KAAKxB,UAAU2E,IAAInD,GAAG;GACtB4D,MAAM,CAAC,GAAG7B,MAAM,GAAGmB,OAAOC,GAAG,CAAC;EAChC,CAAC;CACH,OAAO,IAAIA,IAAIlD,QAAQS,KAAAA,GAAW;EAChC,MAAMkD,OAAOV,OAAOC,GAAG;EACvB,IAAIA,IAAIjD,SAAS,MAAMwC,EAAEX,KAAK8B,KAAK,GAAGD,IAAI;OACrClB,EAAEN,IAAIa,OAAOW,IAAI;CACxB;CACA,IAAI,OAAOT,IAAItD,SAAS,WAAW6C,EAAEN,IAAIoB,OAAOL,IAAItD,IAAI;AAC1D;AAEA,MAAMiE,qBACJC,WACkCA,WAAW,aAAaA,WAAW;AAEvE,MAAMC,aAAatB,GAAStC,QAA2C;CACrE,IAAI6D,SAAS7D;CACb,IAAI6D,OAAO1E,SAASmB,KAAAA,GAAW;EAC7B,MAAM,EAAEnB,MAAM,GAAG2E,SAASD;EAC1BA,SAAS;GAAE,GAAGC;GAAM5E,SAASb,YAAYyF,KAAK5E,SAASC,IAAI;EAAE;CAC/D;CACAwD,YAAYL,CAAC;CACb,IAAIuB,OAAOF,WAAW,QAAQ;EAC5BrB,EAAEN,IAAIoB,OAAO,IAAI;EACjBd,EAAEd,UAAU;EACZc,EAAEb,UAAU;EACZY,UAAUC,GAAG,EAAEE,QAAQ,OAAO,CAAC;EAC/B,OAAO;CACT;CACAF,EAAEN,IAAI+B,SAASF,MAAM;CACrB,IAAIH,kBAAkBG,OAAOF,MAAM,GAAG;EACpCrB,EAAEb,UAAU;GACV7C,OAAOiF,OAAOF;GACd,GAAIE,OAAOhF,YAAYyB,KAAAA,KAAa,EAAEzB,SAASgF,OAAOhF,QAAQ;EAChE;EACA,OAAO;CACT;CACAwD,UAAUC,GAAG;EACXE,QAAQ;EACR,GAAIqB,OAAOhF,YAAYyB,KAAAA,KAAa,EAAEzB,SAASgF,OAAOhF,QAAQ;EAC9D,GAAIgF,OAAO3E,YAAYoB,KAAAA,KAAa,EAAEpB,SAAS2E,OAAO3E,QAAQ;CAChE,CAAC;CACD,OAAO;AACT;AAEA,MAAa8E,kBACX1B,GACAS,QAC0B;CAC1B,IAAI,OAAOA,QAAQ,YAAYA,QAAQ,MACrC,OAAON,cAAc,0BAA0B;CAEjD,IAAIH,EAAEZ,eAAe;EACnB,IAAIqB,IAAI/C,QAAQM,KAAAA,GAAW,OAAOsD,UAAUtB,GAAGS,IAAI/C,GAAG;EACtDkD,YAAYZ,GAAGS,GAAG;EAClB,OAAO;CACT;CACAM,gBAAgBf,GAAGS,GAAG;CACtB,IAAIA,IAAI/C,QAAQM,KAAAA,GAAW,OAAOsD,UAAUtB,GAAGS,IAAI/C,GAAG;CACtD,IAAIsC,EAAElB,WAAWoB,WAAW,QAAQ;EAClCF,EAAEd,UAAU;EACZc,EAAEb,UAAU;EACZY,UAAUC,GAAG,EAAEE,QAAQ,OAAO,CAAC;CACjC;CACA,OAAO;AACT;AAEA,MAAMyB,iBACJ3B,GACA4B,SACAC,cACG;CACH7B,EAAEd,WAAW;CACbc,EAAEb,YAAY,EAAE7C,OAAO,UAAU;CACjC,MAAMwF,QAAQD,YAAY,IAAI7F,QAAQgE,EAAEd,OAAO;CAC/C,IAAIc,EAAEhB,aACJe,UAAUC,GAAG;EACXE,QAAQ;EACR,GAAGF,EAAEb;EACLD,SAASc,EAAEd;EACX6C,aAAaC,KAAKC,IAAI,IAAIH;EAC1B,GAAIF,YAAY5D,KAAAA,KAAa,EAAEkE,WAAWN,QAAQ;CACpD,CAAC;CAEH,OAAOE;AACT;AAEA,MAAMK,iBAAiBnC,MAAY;CACjC,IAAI,CAACA,EAAEP,UAAU;CACjB,IAAIO,EAAElB,WAAWoB,WAAW,UAAUF,EAAElB,WAAWoB,WAAW,QAAQ;EACpEF,EAAEd,UAAU;EACZc,EAAEb,UAAU,EAAE7C,OAAO,UAAU;EAC/BT,mBAAmB;GACjB,IAAImE,EAAEhB,aAAa2C,cAAc3B,GAAGhC,KAAAA,GAAW,IAAI;QAC9C+B,UAAUC,GAAG,EAAEE,QAAQ,aAAa,CAAC;EAC5C,CAAC;CACH;CACAvB,SAASqB,EAAEV,IAAI;CACfU,EAAET,OAAOA,MAAM;AACjB;AAEA,MAAM6C,MAAM,OAAOpC,GAASP,UAA2BH,SAAe;CACpE,MAAM+C,cAAcrC,EAAEP,aAAaA;CACnC,OAAO4C,MAAM,GAAG;EACd,MAAM9C,QAAQ,IAAIC,gBAAgB;EAClCQ,EAAET,QAAQA;EACVS,EAAEZ,gBAAgB;EAClBY,EAAEX,OAAO,CAAA;EACT,IAAIiD;EACJ,IAAIV;EACJ,IAAI;GACFU,UAAU,MAAMtC,EAAEJ,YAAYL,KAAK;EACrC,SAASgD,OAAO;GACd,IAAI,CAAChD,MAAMiD,OAAOC,SAChBb,UACEW,iBAAiBnC,SAASmC,MAAMhG,UAAUgG,MAAMhG,UAAU;EAEhE;EACAgD,MAAMA,MAAM;EACZ,IAAI,CAAC8C,MAAM,GAAG;EACdxG,mBAAmB;GACjBwE,YAAYL,CAAC;GACbA,EAAEN,IAAIgD,SAAS;EACjB,CAAC;EACD,IAAI,CAACL,MAAM,GAAG;EACd,IAAIC,YAAY,UAAUA,YAAY,QAAQ;GAC5C,MAAMlE,SAASkB,IAAI;GACnB;EACF;EACA,MAAMuC,YAAY5D,SAASqB,IAAI;EAC/B,MAAMwC,QAAQjG,mBAAmB8F,cAAc3B,GAAG4B,SAASC,SAAS,CAAC;EACrE,IAAIC,UAAU,GAAG;EACjB,MAAM1D,SAASkB,MAAMwC,KAAK;CAC5B;AACF;AAEA,MAAaa,oBAAoBC,UAKrB;CACV,MAAME,SAASnH,OAAOiH,KAAK;CAC3BE,OAAOC,UAAUH;CACjB,MAAM,GAAGI,iBAAiBpH,SAA4C,IAAI;CAC1E,MAAM,CAACoE,KAAKpE,eAAqB;EAC/B,MAAMqH,OAAa;GACjBjE,aAAa;GACbC,OAAO;GACPC,SAAS;GACTC,SAAS;GACTC,eAAe;GACfC,MAAM,CAAA;GACNE,OAAO;GACPD,MAAMvB,QAAQ;GACd0B,UAAU;GACV,IAAIC,MAAM;IACR,OAAOoD,OAAOC,QAAQrD;GACxB;GACAC,WAAWb,eAAegE,OAAOC,QAAQpD,WAAWb,UAAU;GAC9Dc,cAAcL,UAAUuD,OAAOC,QAAQnD,YAAYqD,MAAM1D,KAAK;GAC9DM,QAAQmD;GACRlD,iBAAiBqC,cAAcc,IAAI;GACnCnE,YAAY;IAAEoB,QAAQ;IAAcJ,iBAAiBmD,KAAKnD,UAAU;GAAE;EACxE;EACA,OAAOmD;CACT,CAAC;CAEDvH,gBAAgB;EACdsE,EAAEhB,cAAc;EAChBgB,EAAEf,QAAQ;EACVe,EAAEd,UAAU;EACZc,EAAEb,UAAU;EACZ,IAAIa,EAAElB,WAAWoB,WAAW,cAC1BH,UAAUC,GAAG,EAAEE,QAAQ,aAAa,CAAC;EAEvC,MAAMT,WAAW,IAAID,gBAAgB;EACrCQ,EAAEP,WAAWA;EACbO,EAAEV,OAAOvB,QAAQ;EACjB,IAASiC,GAAGP,UAAUO,EAAEV,IAAI;EAC5B,aAAa;GACXU,EAAET,OAAOA,MAAM;GACfE,SAASF,MAAM;GACf,IAAIS,EAAEP,aAAaA,UAAUO,EAAEP,WAAW;GAC1Cd,SAASqB,EAAEV,IAAI;EACjB;CACF,GAAG,CAACU,GAAG4C,MAAMC,GAAG,CAAC;CAEjB,OAAO7C;AACT;AAEA,MAAakD,YAAYC,4BACvB,IAAI/C,MAAM+C,QAAQV,UAAU,YAAY,SAAS"}
1
+ {"version":3,"file":"connection.js","names":["useEffect","useRef","useState","flushTapSync","reviveOps","reviveValue","backoff","timing","ConnectionState","StatewireRemoteProps","StatewireClient","StatewireHost","Episode","cause","message","StatewireCmd","seq","type","code","payload","meta","dur","StatewireSyn","ctxSeq","record","id","idle","retry","StatewireEnvelope","ops","cmd","next","syn","fin","Fin","Gate","latched","wake","newGate","undefined","takeGate","g","was","waitGate","timeoutMs","Promise","resolve","timer","setTimeout","clearTimeout","openGate","AttemptOutcome","Conn","connection","Connection","hasSnapshot","lease","attempt","authDenials","liveAt","episode","awaitingHello","held","gate","abort","AbortController","lifetime","ctx","onChange","attemptOnce","notify","reconnect","setStatus","c","changed","status","protocolError","Error","isObject","value","Record","Array","isArray","isNonnegativeSafeInteger","Number","isSafeInteger","isPositiveSafeInteger","validateOptionalString","field","validateSynScalars","validateRefusalSyn","validateCmd","validateFin","validateEnvelope","entry","releaseHeld","length","onCmds","cmdsOf","env","snapshotOf","op","Object","hasOwn","helloSynOf","handleHello","onHello","onIdle","Date","now","warnedUnknownSynFact","warnUnknownSynFact","console","warn","deliverSyn","onRefusal","keys","deliverEnvelope","onOps","cmds","push","transientCauseOf","reason","settleFin","finish","rest","denial","terminal","onFinish","handleEnvelope","scheduleRetry","failure","immediate","backoffResetMs","delay","nextRetryAt","lastError","reconnectConn","run","alive","outcome","error","signal","aborted","onDetach","useStatewireConn","props","url","latest","current","setConnection","conn","classify","timeout","AbortSignal"],"sources":["../../src/client/connection.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport { flushTapSync } from \"@assistant-ui/tap\";\nimport { reviveOps, reviveValue } from \"../wire/meta\";\nimport { backoff, timing } from \"./timing\";\nimport type { ConnectionState, StatewireRemoteProps } from \"../transports/sse\";\nimport type { StatewireClient } from \"../statewire\";\nimport type { StatewireHost } from \"../host\";\n\nexport type Episode = {\n cause: \"dropped\" | \"evicted\" | \"error\" | \"unauthorized\";\n message?: string;\n};\n\nexport type StatewireCmd = {\n seq: number;\n type?: \"applied\" | \"result\" | \"rejected\" | \"failed\" | \"crashed\" | \"expired\";\n code?: string;\n message?: string;\n payload?: unknown;\n meta?: unknown;\n /** Cumulative: resending through this seq will never be needed. */\n dur?: boolean;\n};\n\nexport type StatewireSyn = {\n seq?: number;\n ctxSeq?: number;\n record?: \"fresh\" | \"resumed\" | \"ephemeral\";\n id?: string;\n idle?: boolean;\n retry?: number;\n};\n\nexport type StatewireEnvelope = {\n ops?: unknown[];\n cmd?: StatewireCmd[];\n next?: true;\n idle?: boolean;\n syn?: StatewireSyn;\n fin?: StatewireHost.Fin;\n};\n\nexport type Gate = { latched: boolean; wake: (() => void) | undefined };\n\nconst newGate = (): Gate => ({ latched: false, wake: undefined });\n\nconst takeGate = (g: Gate) => {\n const was = g.latched;\n g.latched = false;\n return was;\n};\n\nconst waitGate = (g: Gate, timeoutMs?: number) => {\n if (g.latched) {\n g.latched = false;\n return Promise.resolve();\n }\n return new Promise<void>((resolve) => {\n const timer =\n timeoutMs === undefined\n ? undefined\n : setTimeout(() => {\n g.wake = undefined;\n resolve();\n }, timeoutMs);\n g.wake = () => {\n clearTimeout(timer);\n g.wake = undefined;\n resolve();\n };\n });\n};\n\nconst openGate = (g: Gate) => {\n if (g.wake) g.wake();\n else g.latched = true;\n};\n\nexport type AttemptOutcome = \"stop\" | \"reattach\" | \"ended\" | \"idle\";\n\nexport type Conn = {\n connection: StatewireClient.Connection;\n hasSnapshot: boolean;\n lease: string | null;\n attempt: number;\n authDenials: number;\n liveAt: number | null;\n episode: Episode | null;\n awaitingHello: boolean;\n held: StatewireCmd[];\n gate: Gate;\n abort: AbortController | null;\n lifetime: AbortController | null;\n ctx: StatewireRemoteProps;\n onChange: (connection: StatewireClient.Connection) => void;\n attemptOnce: (abort: AbortController) => Promise<AttemptOutcome>;\n notify: (connection: StatewireClient.Connection) => void;\n reconnect: () => void;\n};\n\nconst setStatus = (c: Conn, next: ConnectionState) => {\n const changed = next.status !== c.connection.status;\n c.connection = { ...next, reconnect: c.reconnect };\n c.notify(c.connection);\n if (changed) c.onChange(c.connection);\n};\n\nconst protocolError = (message: string): never => {\n throw new Error(`statewire: protocol error: ${message}`);\n};\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isNonnegativeSafeInteger = (value: unknown): value is number =>\n Number.isSafeInteger(value) && (value as number) >= 0;\n\nconst isPositiveSafeInteger = (value: unknown): value is number =>\n Number.isSafeInteger(value) && (value as number) > 0;\n\nconst validateOptionalString = (value: unknown, field: string): void => {\n if (value !== undefined && typeof value !== \"string\") {\n protocolError(`${field} must be a string`);\n }\n};\n\nconst validateSynScalars = (syn: Record<string, unknown>) => {\n if (syn[\"seq\"] !== undefined && !isNonnegativeSafeInteger(syn[\"seq\"])) {\n protocolError(\"syn.seq must be a nonnegative safe integer\");\n }\n if (syn[\"ctxSeq\"] !== undefined && !isNonnegativeSafeInteger(syn[\"ctxSeq\"])) {\n protocolError(\"syn.ctxSeq must be a nonnegative safe integer\");\n }\n if (syn[\"retry\"] !== undefined && !isNonnegativeSafeInteger(syn[\"retry\"])) {\n protocolError(\"syn.retry must be a nonnegative safe integer\");\n }\n validateOptionalString(syn[\"id\"], \"syn.id\");\n if (syn[\"idle\"] !== undefined && typeof syn[\"idle\"] !== \"boolean\") {\n protocolError(\"syn.idle must be a boolean\");\n }\n validateOptionalString(syn[\"record\"], \"syn.record\");\n};\n\nexport const validateRefusalSyn = (value: unknown): StatewireSyn => {\n if (!isObject(value)) return protocolError(\"syn must be an object\");\n validateSynScalars(value);\n if (\n value[\"id\"] !== undefined ||\n value[\"idle\"] !== undefined ||\n value[\"record\"] !== undefined\n ) {\n protocolError(\"invalid refusal syn\");\n }\n if (\n value[\"seq\"] === undefined &&\n value[\"ctxSeq\"] === undefined &&\n value[\"retry\"] === undefined\n ) {\n protocolError(\"refusal syn has no watermarks or retry\");\n }\n return value as StatewireSyn;\n};\n\nconst validateCmd = (value: unknown): value is StatewireCmd => {\n if (!isObject(value)) return protocolError(\"cmd entry must be an object\");\n if (!isPositiveSafeInteger(value[\"seq\"])) {\n protocolError(\"cmd.seq must be a positive safe integer\");\n }\n validateOptionalString(value[\"type\"], \"cmd.type\");\n validateOptionalString(value[\"code\"], \"cmd.code\");\n validateOptionalString(value[\"message\"], \"cmd.message\");\n if (value[\"dur\"] !== undefined && value[\"dur\"] !== true) {\n protocolError(\"cmd.dur must be true\");\n }\n return true;\n};\n\nconst validateFin = (value: unknown): value is StatewireHost.Fin => {\n if (!isObject(value)) return protocolError(\"fin must be an object\");\n if (typeof value[\"reason\"] !== \"string\") {\n protocolError(\"fin.reason must be a string\");\n }\n validateOptionalString(value[\"message\"], \"fin.message\");\n return true;\n};\n\nconst validateEnvelope = (value: unknown): StatewireEnvelope => {\n if (!isObject(value)) return protocolError(\"packet must be an object\");\n if (value[\"ops\"] !== undefined && !Array.isArray(value[\"ops\"])) {\n protocolError(\"ops must be an array\");\n }\n if (value[\"cmd\"] !== undefined) {\n const cmd = value[\"cmd\"];\n if (!Array.isArray(cmd)) return protocolError(\"cmd must be an array\");\n for (const entry of cmd) validateCmd(entry);\n }\n if (value[\"idle\"] !== undefined && typeof value[\"idle\"] !== \"boolean\") {\n protocolError(\"idle must be a boolean\");\n }\n if (value[\"next\"] !== undefined) {\n if (value[\"next\"] !== true) protocolError(\"next must be true\");\n if (\n value[\"cmd\"] === undefined ||\n value[\"ops\"] !== undefined ||\n value[\"fin\"] !== undefined\n ) {\n protocolError(\"next must ride a cmd-only packet\");\n }\n }\n if (value[\"fin\"] !== undefined) validateFin(value[\"fin\"]);\n return value as StatewireEnvelope;\n};\n\n// A hold's release delivers, never settles: fin and bare close both drain it.\nconst releaseHeld = (c: Conn) => {\n if (c.held.length === 0) return;\n const held = c.held;\n c.held = [];\n c.ctx.onCmds(held);\n};\n\nconst cmdsOf = (env: StatewireEnvelope): StatewireCmd[] => {\n if (env.cmd === undefined) return [];\n return env.cmd;\n};\n\nconst snapshotOf = (env: StatewireEnvelope): unknown[] => {\n const ops = env.ops;\n if (ops === undefined) return protocolError(\"hello missing snapshot\");\n if (ops.length !== 1 || !isObject(ops[0])) {\n return protocolError(\"hello snapshot must be one root replace op\");\n }\n const op = ops[0];\n if (\n op[\"op\"] !== \"replace\" ||\n !Array.isArray(op[\"path\"]) ||\n op[\"path\"].length !== 0 ||\n !Object.hasOwn(op, \"value\")\n ) {\n return protocolError(\"hello snapshot must be one root replace op\");\n }\n return reviveOps(ops);\n};\n\nconst helloSynOf = (env: StatewireEnvelope): StatewireSyn => {\n if (!isObject(env.syn)) return protocolError(\"expected hello\");\n validateSynScalars(env.syn);\n if (env.syn[\"retry\"] !== undefined) {\n protocolError(\"hello syn cannot carry retry\");\n }\n return env.syn as StatewireSyn;\n};\n\nconst handleHello = (c: Conn, env: StatewireEnvelope) => {\n const syn = helloSynOf(env);\n const ops = snapshotOf(env);\n c.ctx.onHello({\n ops,\n syn,\n });\n c.ctx.onIdle(syn.idle ?? false);\n c.awaitingHello = false;\n c.hasSnapshot = true;\n c.liveAt = Date.now();\n c.authDenials = 0;\n c.episode = null;\n setStatus(c, { status: \"live\" });\n};\n\nlet warnedUnknownSynFact = false;\n\nconst warnUnknownSynFact = () => {\n if (warnedUnknownSynFact) return;\n warnedUnknownSynFact = true;\n console.warn(\"statewire: ignoring unknown syn fact\");\n};\n\nconst deliverSyn = (c: Conn, value: unknown) => {\n if (!isObject(value)) return protocolError(\"syn must be an object\");\n validateSynScalars(value);\n if (value[\"record\"] !== undefined || value[\"idle\"] !== undefined) {\n protocolError(\"invalid mid-stream syn\");\n }\n if (\n value[\"seq\"] !== undefined ||\n value[\"ctxSeq\"] !== undefined ||\n value[\"retry\"] !== undefined\n ) {\n c.ctx.onRefusal(validateRefusalSyn(value));\n return;\n }\n if (value[\"id\"] !== undefined) return;\n if (Object.keys(value).length === 0) {\n protocolError(\"invalid mid-stream syn\");\n }\n warnUnknownSynFact();\n};\n\nconst deliverEnvelope = (c: Conn, env: StatewireEnvelope) => {\n if (env.syn !== undefined) deliverSyn(c, env.syn);\n if (env.ops !== undefined) {\n const held = c.held;\n c.held = [];\n c.ctx.onOps({\n ops: reviveOps(env.ops),\n cmds: [...held, ...cmdsOf(env)],\n });\n } else if (env.cmd !== undefined) {\n const cmds = cmdsOf(env);\n if (env.next === true) c.held.push(...cmds);\n else c.ctx.onCmds(cmds);\n }\n if (typeof env.idle === \"boolean\") c.ctx.onIdle(env.idle);\n};\n\nconst transientCauseOf = (\n reason: StatewireHost.Fin[\"reason\"],\n): Episode[\"cause\"] => {\n switch (reason) {\n case \"evicted\":\n return \"evicted\";\n case \"error\":\n return \"error\";\n case \"unauthorized\":\n return \"unauthorized\";\n default:\n return \"error\";\n }\n};\n\nconst settleFin = (c: Conn, fin: StatewireHost.Fin): AttemptOutcome => {\n let finish = fin;\n if (finish.meta !== undefined) {\n const { meta, ...rest } = finish;\n finish = { ...rest, payload: reviveValue(rest.payload, meta) };\n }\n releaseHeld(c);\n if (finish.reason === \"idle\") {\n c.ctx.onIdle(true);\n c.attempt = 0;\n c.episode = null;\n setStatus(c, { status: \"idle\" });\n return \"idle\";\n }\n // An auth denial gets one immediate redial; a consecutive repeat parks as gone.\n const denial = finish.reason === \"unauthorized\";\n const terminal =\n finish.reason === \"gone\" ||\n finish.reason === \"superseded\" ||\n (denial && c.authDenials > 0);\n if (denial) c.authDenials += 1;\n c.ctx.onFinish(terminal ? finish : { ...finish, reason: \"error\" });\n if (!terminal) {\n c.episode = {\n cause: transientCauseOf(finish.reason),\n ...(finish.message !== undefined && { message: finish.message }),\n };\n if (denial) openGate(c.gate);\n return \"reattach\";\n }\n setStatus(c, {\n status: \"gone\",\n ...(finish.message !== undefined && { message: finish.message }),\n ...(finish.payload !== undefined && { payload: finish.payload }),\n });\n return \"stop\";\n};\n\nexport const handleEnvelope = (\n c: Conn,\n value: unknown,\n): AttemptOutcome | null => {\n const env = validateEnvelope(value);\n if (c.awaitingHello) {\n if (env.fin !== undefined) return settleFin(c, env.fin);\n handleHello(c, env);\n return null;\n }\n deliverEnvelope(c, env);\n if (env.fin !== undefined) return settleFin(c, env.fin);\n if (c.connection.status !== \"live\") {\n c.attempt = 0;\n c.episode = null;\n setStatus(c, { status: \"live\" });\n }\n return null;\n};\n\nconst scheduleRetry = (\n c: Conn,\n failure: string | undefined,\n immediate: boolean,\n) => {\n if (c.liveAt !== null && Date.now() - c.liveAt >= timing.backoffResetMs) {\n c.attempt = 0;\n }\n c.liveAt = null;\n c.attempt += 1;\n c.episode ??= { cause: \"dropped\" };\n const delay = immediate ? 0 : backoff(c.attempt);\n setStatus(c, {\n status: \"reconnecting\",\n ...c.episode,\n attempt: c.attempt,\n nextRetryAt: Date.now() + delay,\n ...(failure !== undefined && { lastError: failure }),\n });\n return delay;\n};\n\nconst reconnectConn = (c: Conn) => {\n if (!c.lifetime) return;\n if (c.connection.status === \"gone\" || c.connection.status === \"idle\") {\n c.attempt = 0;\n c.authDenials = 0;\n c.episode = { cause: \"dropped\" };\n flushTapSync(() => {\n scheduleRetry(c, undefined, true);\n });\n }\n openGate(c.gate);\n c.abort?.abort();\n};\n\nconst run = async (c: Conn, lifetime: AbortController, gate: Gate) => {\n const alive = () => c.lifetime === lifetime;\n while (alive()) {\n const abort = new AbortController();\n c.abort = abort;\n c.awaitingHello = true;\n c.held = [];\n let outcome: AttemptOutcome | undefined;\n let failure: string | undefined;\n try {\n outcome = await c.attemptOnce(abort);\n } catch (error) {\n if (!abort.signal.aborted) {\n failure =\n error instanceof Error && error.message ? error.message : \"network\";\n }\n }\n abort.abort();\n if (!alive()) return;\n flushTapSync(() => {\n releaseHeld(c);\n c.ctx.onDetach();\n });\n if (!alive()) return;\n if (outcome === \"stop\" || outcome === \"idle\") {\n await waitGate(gate);\n continue;\n }\n const immediate = takeGate(gate);\n const delay = flushTapSync(() => scheduleRetry(c, failure, immediate));\n if (delay === 0) continue;\n await waitGate(gate, delay);\n }\n};\n\nexport const useStatewireConn = (props: {\n url: string;\n onChange?: ((connection: StatewireClient.Connection) => void) | undefined;\n ctx: StatewireRemoteProps;\n attemptOnce: (c: Conn, abort: AbortController) => Promise<AttemptOutcome>;\n}): Conn => {\n const latest = useRef(props);\n latest.current = props;\n const [, setConnection] = useState<StatewireClient.Connection | null>(null);\n const [c] = useState((): Conn => {\n const conn: Conn = {\n hasSnapshot: false,\n lease: null,\n attempt: 0,\n authDenials: 0,\n liveAt: null,\n episode: null,\n awaitingHello: true,\n held: [],\n abort: null,\n gate: newGate(),\n lifetime: null,\n get ctx() {\n return latest.current.ctx;\n },\n onChange: (connection) => latest.current.onChange?.(connection),\n attemptOnce: (abort) => latest.current.attemptOnce(conn, abort),\n notify: setConnection,\n reconnect: () => reconnectConn(conn),\n connection: { status: \"connecting\", reconnect: () => conn.reconnect() },\n };\n return conn;\n });\n\n useEffect(() => {\n c.hasSnapshot = false;\n c.lease = null;\n c.attempt = 0;\n c.authDenials = 0;\n c.liveAt = null;\n c.episode = null;\n if (c.connection.status !== \"connecting\") {\n setStatus(c, { status: \"connecting\" });\n }\n const lifetime = new AbortController();\n c.lifetime = lifetime;\n c.gate = newGate();\n void run(c, lifetime, c.gate);\n return () => {\n c.abort?.abort();\n lifetime.abort();\n if (c.lifetime === lifetime) c.lifetime = null;\n openGate(c.gate);\n };\n }, [c, props.url]);\n\n return c;\n};\n\nexport const classify = (timeout: AbortSignal) =>\n new Error(timeout.aborted ? \"timeout\" : \"network\");\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { StatewireSessionRecord, StatewireSessionStore, StatewireSessionStoreOptions } from "./sessionStore.js";
1
+ import { StatewireClient } from "../statewire.js";
2
2
  import { Statewire, StatewireSendError } from "../types.js";
3
3
  import { StatewirePath } from "../wire/ops.js";
4
4
  import { CommandsInvocation, CommandsListener } from "../host/host.js";
@@ -28,15 +28,18 @@ type CtxPending = {
28
28
  posted: boolean;
29
29
  };
30
30
  type AttachState = "detached" | "attached" | "finished";
31
- type StatewireSessionOptions = StatewireSessionStoreOptions & {
31
+ type StatewireSessionOptions = {
32
+ sessionStore?: StatewireClient.SessionStorage | undefined;
32
33
  onDone?: (() => void) | undefined;
33
34
  };
34
35
  type Session = {
35
- record: StatewireSessionRecord;
36
+ record: StatewireClient.SessionRecord;
36
37
  ctx: StatewireRemoteProps;
37
38
  recordPersisted: boolean;
38
- store: StatewireSessionStore;
39
+ store: StatewireClient.SessionStorage;
39
40
  ephemeral: boolean;
41
+ /** An ephemeral record died with its attach: the storage is off for good. */
42
+ deactivated: boolean;
40
43
  sessionKnown: boolean;
41
44
  durableSeq: number;
42
45
  resumeAt: number;
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","names":[],"sources":["../../src/client/session.ts"],"mappings":";;;;;;KAyBK;EACH;EACA;EACA;;KAGG;WACM;WACA,SAAS;WACT,KAAK;;EAEd;;EAEA;;EAEA;EACA,UAAU;EACV,SAAS,OAAO;;KAGb;WACM;WACA;EACT;;KAiBG;KAEO,0BAA0B;EACpC;;KAGG;EACH,QAAQ;EACR,KAAK;EACL;EACA,OAAO;EACP;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YAAY;EACZ;EACA,0BAA0B,qBAAqB;EAC/C;EACA,SAAS;EACT;EACA;EACA,SAAS;EACT,QAAQ,UAAU;EAClB,QAAQ;EACR;EACA;EACA;EACA,WAAW;EACX;EACA,WAAW;EACX;EACA,UAAU,mBAAmB,sBAAsB;;cA0d/C,eAAgB,GAAG,SAAS,WAAW;cA6DvC,aAAc,GAAG,SAAS,OAAO;cAoB1B,sBACX,SAAS,4BACR"}
1
+ {"version":3,"file":"session.d.ts","names":[],"sources":["../../src/client/session.ts"],"mappings":";;;;;;KA4BK;EACH;EACA;EACA;;KAGG;WACM;WACA,SAAS;WACT,KAAK;;EAEd;;EAEA;;EAEA;EACA,UAAU;EACV,SAAS,OAAO;;KAGb;WACM;WACA;EACT;;KAiBG;KAEO;EACV,eAAe,gBAAgB;EAC/B;;KAGG;EACH,QAAQ,gBAAgB;EACxB,KAAK;EACL;EACA,OAAO,gBAAgB;EACvB;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YAAY;EACZ;EACA,0BAA0B,qBAAqB;EAC/C;EACA,SAAS;EACT;EACA;EACA,SAAS;EACT,QAAQ,UAAU;EAClB,QAAQ;EACR;EACA;EACA;EACA,WAAW;EACX;EACA,WAAW;EACX;EACA,UAAU,mBAAmB,sBAAsB;;cA2e/C,eAAgB,GAAG,SAAS,WAAW;cA2FvC,aAAc,GAAG,SAAS,OAAO;cAoB1B,sBACX,SAAS,4BACR"}
@@ -2,11 +2,11 @@ import { applyOps } from "../wire/ops.js";
2
2
  import { createMethodProxy } from "../util/methodProxy.js";
3
3
  import { StatewireSendError } from "../types.js";
4
4
  import { bindCommandsHost } from "../host/host.js";
5
- import { encodeParams, encodeValue, reviveResponse } from "../wire/meta.js";
5
+ import { encodeParams, encodeValue, reviveResponse, reviveValue } from "../wire/meta.js";
6
+ import { SessionStorageContext, createMemorySessionStore } from "./sessionStore.js";
6
7
  import { backoff } from "./timing.js";
7
- import { createMemorySessionStore } from "./sessionStore.js";
8
8
  import { randomId } from "../util/randomId.js";
9
- import { useEffect, useState } from "@assistant-ui/tap/react-shim";
9
+ import { useContext, useEffect, useState } from "@assistant-ui/tap/react-shim";
10
10
  //#region src/client/session.ts
11
11
  const protocolError = (message) => {
12
12
  throw new Error(`statewire: protocol error: ${message}`);
@@ -41,7 +41,19 @@ const sleepUntilWake = (s, delayMs) => new Promise((resolve) => {
41
41
  });
42
42
  const setRecord = (s, record) => {
43
43
  s.record = record;
44
- if (!s.ephemeral) s.store.save(record);
44
+ if (!s.deactivated) s.store.save(record);
45
+ };
46
+ const backlog = (s) => s.pending.filter((p) => p.seq > s.durableSeq).map((p) => ({
47
+ seq: p.seq,
48
+ ...p.command
49
+ }));
50
+ const syncBacklog = (s) => {
51
+ const commands = backlog(s);
52
+ if (commands.length === s.record.commands.length && commands.every((c, i) => c.seq === s.record.commands[i].seq)) return;
53
+ setRecord(s, {
54
+ ...s.record,
55
+ commands
56
+ });
45
57
  };
46
58
  const setIdle = (s, level) => {
47
59
  if (level === s.idle) return;
@@ -134,7 +146,8 @@ const resetRecord = (s, clientId) => {
134
146
  setRecord(s, {
135
147
  clientId,
136
148
  nextSeq: 1,
137
- ctxNextSeq: 1
149
+ ctxNextSeq: 1,
150
+ commands: []
138
151
  });
139
152
  for (const old of survivors) {
140
153
  s.lifecycle?.onCovered?.(old.inv);
@@ -150,17 +163,16 @@ const ephemeralDetach = (s) => {
150
163
  resetRecord(s, newClientId());
151
164
  };
152
165
  const rejectedFailure = (cmd) => {
153
- const code = cmd.code === void 0 || cmd.code === "unknown-command" || cmd.code === "invalid-params" ? cmd.code : protocolError(`unknown rejected code: ${cmd.code}`);
154
166
  return {
155
167
  fate: "rejected",
156
168
  cause: "server",
157
- ...code !== void 0 && { code },
169
+ ...cmd.code !== void 0 && { code: cmd.code },
158
170
  ...cmd.message !== void 0 && { message: cmd.message },
159
171
  ...cmd.payload !== void 0 && { payload: cmd.payload }
160
172
  };
161
173
  };
162
174
  const handleCmd = (s, cmd) => {
163
- if (typeof cmd.seq !== "number") protocolError("cmd entry missing seq");
175
+ if (!Number.isSafeInteger(cmd.seq) || cmd.seq <= 0) protocolError("cmd.seq must be a positive safe integer");
164
176
  if (cmd.dur === true) {
165
177
  s.durableSeq = Math.max(s.durableSeq, cmd.seq);
166
178
  for (const entry of s.pending) if (entry.seq <= s.durableSeq) entry.posted = true;
@@ -203,6 +215,7 @@ const handleCmd = (s, cmd) => {
203
215
  };
204
216
  const handleCmds = (s, cmds) => {
205
217
  for (const cmd of cmds) handleCmd(s, cmd);
218
+ syncBacklog(s);
206
219
  wake(s);
207
220
  };
208
221
  const ingestOps = (s, ops) => {
@@ -249,6 +262,7 @@ const handleHello = (s, { ops, syn }) => {
249
262
  s.frameFailures = 0;
250
263
  s.resumeAt = 0;
251
264
  s.ephemeral = record === "ephemeral";
265
+ if (s.ephemeral) s.deactivated = true;
252
266
  const [attachCmd, attachCtx] = s.attachLaneSeqs;
253
267
  s.attachLaneSeqs = [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()];
254
268
  if (record === "resumed") {
@@ -257,6 +271,7 @@ const handleHello = (s, { ops, syn }) => {
257
271
  s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxWatermark);
258
272
  for (const c of s.ctxPending) c.posted = attachCtx.has(c.seq);
259
273
  snapClocks(s, syn.seq ?? 0, ctxWatermark);
274
+ syncBacklog(s);
260
275
  } else resetRecord(s, s.record.clientId);
261
276
  s.sessionKnown = true;
262
277
  publishIngested(s);
@@ -265,6 +280,7 @@ const handleHello = (s, { ops, syn }) => {
265
280
  const handleOps = (s, { ops, cmds }) => {
266
281
  ingestOps(s, ops);
267
282
  for (const cmd of cmds) handleCmd(s, cmd);
283
+ syncBacklog(s);
268
284
  publishIngested(s);
269
285
  };
270
286
  const handleRefusal = (s, syn) => {
@@ -277,7 +293,7 @@ const handleRefusal = (s, syn) => {
277
293
  s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxSeq);
278
294
  for (const c of s.ctxPending) c.posted = false;
279
295
  }
280
- if (typeof syn.retry === "number") s.resumeAt = Date.now() + Math.max(0, syn.retry);
296
+ if (typeof syn.retry === "number") s.resumeAt = Date.now() + syn.retry;
281
297
  const firstCmd = s.pending.find((p) => !p.posted);
282
298
  const firstCtx = s.ctxPending.find((c) => !c.posted);
283
299
  if (typeof syn.seq === "number" && firstCmd !== void 0 && firstCmd.seq > Math.max(syn.seq, s.durableSeq) + 1 || typeof syn.ctxSeq === "number" && firstCtx !== void 0 && firstCtx.seq > syn.ctxSeq + 1) s.transport?.connection.reconnect();
@@ -401,6 +417,7 @@ const start = (s) => {
401
417
  s.recordPersisted = true;
402
418
  s.store.save(s.record);
403
419
  }
420
+ resume(s);
404
421
  s.running = true;
405
422
  pump(s);
406
423
  };
@@ -416,10 +433,6 @@ const setTransport = (s, transport) => {
416
433
  };
417
434
  const enqueue = (s, command, params, resolve, reject) => {
418
435
  const seq = s.record.nextSeq;
419
- setRecord(s, {
420
- ...s.record,
421
- nextSeq: seq + 1
422
- });
423
436
  const entry = {
424
437
  seq,
425
438
  command,
@@ -437,9 +450,38 @@ const enqueue = (s, command, params, resolve, reject) => {
437
450
  reject
438
451
  };
439
452
  s.pending.push(entry);
453
+ setRecord(s, {
454
+ ...s.record,
455
+ nextSeq: seq + 1,
456
+ commands: backlog(s)
457
+ });
440
458
  s.lifecycle?.onStart(entry.inv);
441
459
  pump(s);
442
460
  };
461
+ const resume = (s) => {
462
+ const stored = [...s.record.commands].sort((a, b) => a.seq - b.seq);
463
+ for (const { seq, ...command } of stored) {
464
+ const params = command.meta === void 0 ? command.params : reviveValue([...command.params], command.meta);
465
+ const entry = {
466
+ seq,
467
+ command,
468
+ inv: {
469
+ method: command.method,
470
+ params,
471
+ ts: Date.now(),
472
+ seq,
473
+ clientId: s.record.clientId
474
+ },
475
+ attempted: true,
476
+ posted: false,
477
+ applied: false,
478
+ resolve: () => {},
479
+ reject: () => {}
480
+ };
481
+ s.pending.push(entry);
482
+ s.lifecycle?.onStart(entry.inv);
483
+ }
484
+ };
443
485
  const sendCommand = (s, method, params) => {
444
486
  if (!s.running) return rejectedSend({
445
487
  fate: "aborted",
@@ -484,14 +526,18 @@ const setContext = (s, value) => {
484
526
  };
485
527
  const useStatewireSession = (options) => {
486
528
  const [defaultStore] = useState(createMemorySessionStore);
487
- const store = options.sessionStore ?? defaultStore;
529
+ const mounted = useContext(SessionStorageContext);
530
+ 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
+ const store = options.sessionStore ?? mounted ?? defaultStore;
488
532
  const [s] = useState(() => {
489
533
  const stored = store.load();
534
+ if (stored !== null && !Array.isArray(stored.commands)) throw new Error("statewire: session storage loaded a record without a commands array");
490
535
  const session = {
491
536
  record: stored ?? {
492
537
  clientId: newClientId(),
493
538
  nextSeq: 1,
494
- ctxNextSeq: 1
539
+ ctxNextSeq: 1,
540
+ commands: []
495
541
  },
496
542
  ctx: {
497
543
  get clientId() {
@@ -509,6 +555,7 @@ const useStatewireSession = (options) => {
509
555
  recordPersisted: stored !== null,
510
556
  store,
511
557
  ephemeral: false,
558
+ deactivated: false,
512
559
  sessionKnown: false,
513
560
  durableSeq: 0,
514
561
  resumeAt: 0,