statewire 0.0.3 → 0.0.5
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 +3 -1
- package/dist/client/connection.d.ts +1 -1
- package/dist/client/session.d.ts +9 -4
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +51 -20
- package/dist/client/session.js.map +1 -1
- package/dist/host/host.d.ts +1 -1
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +3 -0
- package/dist/host/host.js.map +1 -1
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +1 -0
- package/dist/host/socket.js.map +1 -1
- package/dist/host.d.ts +6 -4
- package/dist/host.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/statewire.d.ts +30 -2
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js.map +1 -1
- package/dist/transports/fixtures.d.ts +1 -1
- package/dist/transports/remote.d.ts +3 -3
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +4 -1
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +3 -18
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +1 -1
- package/dist/transports/testing.js.map +1 -1
- package/package.json +1 -1
- package/src/client/session.ts +63 -19
- package/src/host/host.ts +3 -0
- package/src/host/socket.ts +1 -0
- package/src/host.ts +5 -3
- package/src/statewire.ts +27 -7
- package/src/transports/remote.ts +9 -3
- package/src/transports/sse.ts +1 -12
package/src/client/session.ts
CHANGED
|
@@ -147,6 +147,12 @@ const rejectedSend = (
|
|
|
147
147
|
|
|
148
148
|
type AttachState = "detached" | "attached" | "finished";
|
|
149
149
|
|
|
150
|
+
export type StatewireSessionOptions = StatewireSessionStoreOptions & {
|
|
151
|
+
onCommandSessionLost?:
|
|
152
|
+
| ((command: StatewireClient.SessionLostCommand) => "replay" | void)
|
|
153
|
+
| undefined;
|
|
154
|
+
};
|
|
155
|
+
|
|
150
156
|
type Session = {
|
|
151
157
|
record: StatewireSessionRecord;
|
|
152
158
|
ctx: StatewireRemoteProps;
|
|
@@ -166,13 +172,14 @@ type Session = {
|
|
|
166
172
|
transport: RemoteLink | null;
|
|
167
173
|
waiters: (() => void)[];
|
|
168
174
|
lifecycle: CommandsListener | null;
|
|
175
|
+
onCommandSessionLost: StatewireSessionOptions["onCommandSessionLost"];
|
|
169
176
|
notify: () => void;
|
|
170
177
|
emitEvent: (event: unknown) => void;
|
|
171
178
|
send: (command: StatewireWireCommand) => Promise<unknown>;
|
|
172
179
|
commands: Record<string, (...params: unknown[]) => Promise<unknown>>;
|
|
173
180
|
};
|
|
174
181
|
|
|
175
|
-
const createSession = (options:
|
|
182
|
+
const createSession = (options: StatewireSessionOptions): Session => {
|
|
176
183
|
const store = options.sessionStore ?? createMemorySessionStore();
|
|
177
184
|
const stored = store.load();
|
|
178
185
|
const record = stored ?? { clientId: newClientId(), nextSeq: 1 };
|
|
@@ -212,6 +219,7 @@ const createSession = (options: StatewireSessionStoreOptions): Session => {
|
|
|
212
219
|
transport: null,
|
|
213
220
|
waiters: [],
|
|
214
221
|
lifecycle: null,
|
|
222
|
+
onCommandSessionLost: options.onCommandSessionLost,
|
|
215
223
|
notify: () => {},
|
|
216
224
|
emitEvent: () => {},
|
|
217
225
|
send: (command) => sendCommand(s, command),
|
|
@@ -274,10 +282,12 @@ const settleAll = (
|
|
|
274
282
|
if (state.at === "awaiting-result")
|
|
275
283
|
return { fate: "result-unavailable", cause };
|
|
276
284
|
if (state.at === "queued") {
|
|
277
|
-
const
|
|
285
|
+
const predecessorLost = batch.some(
|
|
278
286
|
(p) => p.seq < entry.seq && p.state.at !== "queued",
|
|
279
287
|
);
|
|
280
|
-
return
|
|
288
|
+
return predecessorLost
|
|
289
|
+
? { fate: "aborted", cause, predecessorLost }
|
|
290
|
+
: { fate: "aborted", cause };
|
|
281
291
|
}
|
|
282
292
|
return { fate: "lost", cause };
|
|
283
293
|
};
|
|
@@ -288,6 +298,27 @@ const settleAll = (
|
|
|
288
298
|
}
|
|
289
299
|
};
|
|
290
300
|
|
|
301
|
+
// Offers every unsettled real command for replay, in seq order. Not caught:
|
|
302
|
+
// a throwing callback aborts the eviction before any entry settles; the
|
|
303
|
+
// attach retry evicts again and re-offers.
|
|
304
|
+
const claimReplays = (s: Session): Pending[] => {
|
|
305
|
+
const handler = s.onCommandSessionLost;
|
|
306
|
+
if (!handler) return [];
|
|
307
|
+
const claimed = new Set<Pending>();
|
|
308
|
+
for (const entry of s.pending) {
|
|
309
|
+
if (!entry.inv || isResolved(entry.state)) continue;
|
|
310
|
+
const decision = handler({
|
|
311
|
+
method: entry.inv.method,
|
|
312
|
+
params: [...entry.inv.params],
|
|
313
|
+
seq: entry.seq,
|
|
314
|
+
});
|
|
315
|
+
if (decision === "replay") claimed.add(entry);
|
|
316
|
+
}
|
|
317
|
+
if (claimed.size === 0) return [];
|
|
318
|
+
s.pending = s.pending.filter((p) => !claimed.has(p));
|
|
319
|
+
return [...claimed];
|
|
320
|
+
};
|
|
321
|
+
|
|
291
322
|
const evict = (
|
|
292
323
|
s: Session,
|
|
293
324
|
verdict?: {
|
|
@@ -295,6 +326,7 @@ const evict = (
|
|
|
295
326
|
failure: StatewireClient.SendFailure;
|
|
296
327
|
},
|
|
297
328
|
) => {
|
|
329
|
+
const replays = claimReplays(s);
|
|
298
330
|
settleAll(s, "evicted", verdict);
|
|
299
331
|
s.record = { clientId: newClientId(), nextSeq: 1 };
|
|
300
332
|
s.store.save(s.record);
|
|
@@ -302,6 +334,10 @@ const evict = (
|
|
|
302
334
|
// The server dropped this client's record and its stored context with it:
|
|
303
335
|
// the new session opens with a fresh full root replace.
|
|
304
336
|
if (s.context !== undefined) void s.send({ context: s.context });
|
|
337
|
+
for (const old of replays) {
|
|
338
|
+
if (old.inv) s.lifecycle?.onCovered?.(old.inv);
|
|
339
|
+
enqueue(s, old.command, old.resolve, old.reject);
|
|
340
|
+
}
|
|
305
341
|
publish(s);
|
|
306
342
|
wake(s);
|
|
307
343
|
};
|
|
@@ -645,16 +681,12 @@ const setTransport = (s: Session, transport: RemoteLink) => {
|
|
|
645
681
|
wake(s);
|
|
646
682
|
};
|
|
647
683
|
|
|
648
|
-
const
|
|
684
|
+
const enqueue = (
|
|
649
685
|
s: Session,
|
|
650
686
|
command: StatewireWireCommand,
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
if (isFinished(s)) {
|
|
656
|
-
return rejectedSend({ fate: "aborted", cause: "stream-gone" });
|
|
657
|
-
}
|
|
687
|
+
resolve: (payload: unknown) => void,
|
|
688
|
+
reject: (error: StatewireSendError) => void,
|
|
689
|
+
) => {
|
|
658
690
|
const seq = s.record.nextSeq;
|
|
659
691
|
s.record = { ...s.record, nextSeq: seq + 1 };
|
|
660
692
|
s.store.save(s.record);
|
|
@@ -672,17 +704,28 @@ const sendCommand = (
|
|
|
672
704
|
clientId: s.record.clientId,
|
|
673
705
|
},
|
|
674
706
|
state: { at: "queued", pendingMarked: false },
|
|
675
|
-
resolve
|
|
676
|
-
reject
|
|
707
|
+
resolve,
|
|
708
|
+
reject,
|
|
677
709
|
};
|
|
678
|
-
const result = new Promise<unknown>((resolve, reject) => {
|
|
679
|
-
entry.resolve = resolve;
|
|
680
|
-
entry.reject = reject;
|
|
681
|
-
});
|
|
682
|
-
result.catch(() => {});
|
|
683
710
|
s.pending.push(entry);
|
|
684
711
|
if (entry.inv) s.lifecycle?.onStart(entry.inv);
|
|
685
712
|
void pump(s);
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
const sendCommand = (
|
|
716
|
+
s: Session,
|
|
717
|
+
command: StatewireWireCommand,
|
|
718
|
+
): Promise<unknown> => {
|
|
719
|
+
if (!s.running) {
|
|
720
|
+
return rejectedSend({ fate: "aborted", cause: "unmount" });
|
|
721
|
+
}
|
|
722
|
+
if (isFinished(s)) {
|
|
723
|
+
return rejectedSend({ fate: "aborted", cause: "stream-gone" });
|
|
724
|
+
}
|
|
725
|
+
const result = new Promise<unknown>((resolve, reject) => {
|
|
726
|
+
enqueue(s, command, resolve, reject);
|
|
727
|
+
});
|
|
728
|
+
result.catch(() => {});
|
|
686
729
|
return result;
|
|
687
730
|
};
|
|
688
731
|
|
|
@@ -698,10 +741,11 @@ const setContext = (
|
|
|
698
741
|
};
|
|
699
742
|
|
|
700
743
|
export const useStatewireSession = (
|
|
701
|
-
options:
|
|
744
|
+
options: StatewireSessionOptions,
|
|
702
745
|
emit: (event: unknown) => void,
|
|
703
746
|
): Session => {
|
|
704
747
|
const [s] = useState(() => createSession(options));
|
|
748
|
+
s.onCommandSessionLost = options.onCommandSessionLost;
|
|
705
749
|
useEffect(() => {
|
|
706
750
|
s.emitEvent = emit;
|
|
707
751
|
return () => {
|
package/src/host/host.ts
CHANGED
|
@@ -250,6 +250,9 @@ const STALL_WARN_MS = 5000;
|
|
|
250
250
|
|
|
251
251
|
// In-process dispatch has no wire client: never any ambient context, trivially attached.
|
|
252
252
|
const LOCAL_CALLER: StatewireHost.Caller = Object.freeze({
|
|
253
|
+
get clientId(): string {
|
|
254
|
+
throw new Error("statewire: in-process dispatch has no client id");
|
|
255
|
+
},
|
|
253
256
|
context: null,
|
|
254
257
|
isConnected: true,
|
|
255
258
|
});
|
package/src/host/socket.ts
CHANGED
package/src/host.ts
CHANGED
|
@@ -48,11 +48,13 @@ export namespace StatewireHost {
|
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* The submitting client, bound as `this` of a command handler. Holdable
|
|
51
|
-
* across a long run: `
|
|
52
|
-
*
|
|
53
|
-
* client
|
|
51
|
+
* across a long run: `clientId` is the client's `Statewire-Client-Id`
|
|
52
|
+
* (in-process dispatch has none — reading it throws), `context` reads the
|
|
53
|
+
* client's current ambient context live (null until the client sends one),
|
|
54
|
+
* `isConnected` reports whether the client has an active stream attachment.
|
|
54
55
|
*/
|
|
55
56
|
export type Caller = {
|
|
57
|
+
readonly clientId: string;
|
|
56
58
|
readonly context: Record<string, unknown> | null;
|
|
57
59
|
readonly isConnected: boolean;
|
|
58
60
|
};
|
package/src/statewire.ts
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
import type { ResourceElement } from "@assistant-ui/tap";
|
|
9
9
|
import { LIVE_CONNECTION, useStatewireCore } from "./client/core";
|
|
10
10
|
import type { Statewire } from "./types";
|
|
11
|
-
import type { ConnectionState } from "./transports/sse";
|
|
12
11
|
import type { StatewireSessionStoreOptions } from "./client/sessionStore";
|
|
13
12
|
|
|
14
13
|
export const useStatewire = <
|
|
@@ -136,6 +135,18 @@ export namespace StatewireClient {
|
|
|
136
135
|
setContext: (value: Record<string, unknown>) => Promise<unknown>;
|
|
137
136
|
};
|
|
138
137
|
|
|
138
|
+
export type ConnectionState =
|
|
139
|
+
| { status: "connecting" }
|
|
140
|
+
| { status: "live" }
|
|
141
|
+
| {
|
|
142
|
+
status: "reconnecting";
|
|
143
|
+
cause: "dropped" | "evicted" | "error";
|
|
144
|
+
attempt: number;
|
|
145
|
+
nextRetryAt: number;
|
|
146
|
+
lastError?: string;
|
|
147
|
+
message?: string;
|
|
148
|
+
}
|
|
149
|
+
| { status: "gone"; message?: string; payload?: unknown };
|
|
139
150
|
export type Connection = ConnectionState & { reconnect: () => void };
|
|
140
151
|
export type ConnectionStatus = Connection["status"];
|
|
141
152
|
|
|
@@ -183,23 +194,31 @@ export namespace StatewireClient {
|
|
|
183
194
|
}
|
|
184
195
|
| {
|
|
185
196
|
fate: "aborted";
|
|
186
|
-
cause:
|
|
187
|
-
|
|
188
|
-
| "unmount"
|
|
189
|
-
| "stream-gone"
|
|
190
|
-
| "session-lost"
|
|
191
|
-
| "superseded";
|
|
197
|
+
cause: "unmount" | "stream-gone" | "session-lost" | "superseded";
|
|
198
|
+
predecessorLost?: true;
|
|
192
199
|
}
|
|
193
200
|
| {
|
|
194
201
|
fate: "lost";
|
|
195
202
|
cause: "unmount" | "stream-gone" | "session-lost" | "superseded";
|
|
196
203
|
};
|
|
197
204
|
|
|
205
|
+
/**
|
|
206
|
+
* A pending command offered for replay while the session is being evicted.
|
|
207
|
+
* Return "replay" to re-enqueue it under the rotated client id instead of
|
|
208
|
+
* settling it as a session-lost failure.
|
|
209
|
+
*/
|
|
210
|
+
export type SessionLostCommand = {
|
|
211
|
+
method: string;
|
|
212
|
+
params: unknown[];
|
|
213
|
+
seq: number;
|
|
214
|
+
};
|
|
215
|
+
|
|
198
216
|
export type SSEOptions = StatewireSessionStoreOptions & {
|
|
199
217
|
url: string;
|
|
200
218
|
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
201
219
|
fetch?: typeof globalThis.fetch;
|
|
202
220
|
onChange?: (connection: Connection) => void;
|
|
221
|
+
onCommandSessionLost?: (command: SessionLostCommand) => "replay" | void;
|
|
203
222
|
};
|
|
204
223
|
|
|
205
224
|
export type WSOptions = StatewireSessionStoreOptions & {
|
|
@@ -210,5 +229,6 @@ export namespace StatewireClient {
|
|
|
210
229
|
| (() => Record<string, string> | Promise<Record<string, string>>);
|
|
211
230
|
webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;
|
|
212
231
|
onChange?: (connection: Connection) => void;
|
|
232
|
+
onCommandSessionLost?: (command: SessionLostCommand) => "replay" | void;
|
|
213
233
|
};
|
|
214
234
|
}
|
package/src/transports/remote.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
setTransport,
|
|
7
7
|
useStatewireSession,
|
|
8
8
|
} from "../client/session";
|
|
9
|
-
import type {
|
|
9
|
+
import type { StatewireSessionOptions } from "../client/session";
|
|
10
10
|
import type { StatewireClient } from "../statewire";
|
|
11
11
|
import type { RemoteLink, StatewireRemoteProps } from "./sse";
|
|
12
12
|
|
|
@@ -14,13 +14,19 @@ export const useStatewireRemote = <
|
|
|
14
14
|
TOptions extends {
|
|
15
15
|
url: string;
|
|
16
16
|
onChange?: (connection: StatewireClient.Connection) => void;
|
|
17
|
-
} &
|
|
17
|
+
} & StatewireSessionOptions,
|
|
18
18
|
>(
|
|
19
19
|
options: TOptions,
|
|
20
20
|
useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink,
|
|
21
21
|
): Statewire<any, any, any> => {
|
|
22
22
|
const emit = useStatewireEmit<any>();
|
|
23
|
-
const s = useStatewireSession(
|
|
23
|
+
const s = useStatewireSession(
|
|
24
|
+
{
|
|
25
|
+
sessionStore: options.sessionStore,
|
|
26
|
+
onCommandSessionLost: options.onCommandSessionLost,
|
|
27
|
+
},
|
|
28
|
+
emit,
|
|
29
|
+
);
|
|
24
30
|
const conn = useLink(options, s.ctx);
|
|
25
31
|
useEffect(() => setTransport(s, conn), [s, conn]);
|
|
26
32
|
|
package/src/transports/sse.ts
CHANGED
|
@@ -58,18 +58,7 @@ export type StatewireSubmitResult =
|
|
|
58
58
|
| { type: "unknown-client" }
|
|
59
59
|
| { type: "stale-lease" };
|
|
60
60
|
|
|
61
|
-
export type ConnectionState =
|
|
62
|
-
| { status: "connecting" }
|
|
63
|
-
| { status: "live" }
|
|
64
|
-
| {
|
|
65
|
-
status: "reconnecting";
|
|
66
|
-
cause: "dropped" | "evicted" | "error";
|
|
67
|
-
attempt: number;
|
|
68
|
-
nextRetryAt: number;
|
|
69
|
-
lastError?: string;
|
|
70
|
-
message?: string;
|
|
71
|
-
}
|
|
72
|
-
| { status: "gone"; message?: string; payload?: unknown };
|
|
61
|
+
export type ConnectionState = StatewireClient.ConnectionState;
|
|
73
62
|
|
|
74
63
|
/** The live wire a remote session drives: connection status plus command POST. */
|
|
75
64
|
export type RemoteLink = {
|