@alexkroman1/aai-ui 0.14.0 → 0.14.2

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.
@@ -0,0 +1,121 @@
1
+ /**
2
+ * One browser session's CONNECTION — the socket, the handshake deadline over
3
+ * it, and the server's idle retirement — as a statechart.
4
+ *
5
+ * `link` is `closed → open`, and `open` is `dialing → awaitingHandshake → live`:
6
+ *
7
+ * - **The socket belongs to `open`.** Its entry dials and attaches the
8
+ * listeners; its exit is the ONE teardown (detach, release the pre-connect
9
+ * mic, tear the audio path down, close the socket). A hang-up, a terminal
10
+ * close, an exhausted handshake and a fresh `connect()` all leave `open`, so
11
+ * none of them restates it, and exit actions run before the transition's, so
12
+ * the teardown always precedes the snapshot write reporting the end.
13
+ * - **The handshake deadline is an `after` on `awaitingHandshake`** (see
14
+ * `session/handshake.ts` for why there is one). Leaving the state disarms it,
15
+ * so a `config` frame, a close or a teardown cannot leave it armed against
16
+ * the next attempt — there is no timer to clear by hand on abort.
17
+ * - **partysocket owns the reconnect backoff.** A close that partysocket will
18
+ * retry goes back to `dialing` and waits for its next `open`; this machine
19
+ * never schedules an attempt. The deadline forces one only for a socket
20
+ * partysocket thinks is healthy (`forceReconnect`).
21
+ *
22
+ * `retired` is a second region because it outlives a socket: the server's
23
+ * `session.timedOut` marks the close that follows as EXPECTED, so it is not
24
+ * retried. Without it the automatic reconnect would re-open the session the
25
+ * server just reclaimed — a tab left open would cycle forever and the guest
26
+ * would never see zero sessions, which is the point of the timeout. Only a
27
+ * fresh `connect()` (the user asking again) clears it.
28
+ *
29
+ * The machine owns WHEN; every HOW — the socket, the snapshot, the audio path —
30
+ * is an injected {@link ConnectionEffects} call, as in `session/audio-state.ts`.
31
+ */
32
+ import type { SessionConfigMessage } from "./messages.ts";
33
+ /** The socket shape the session speaks (a partysocket, or an injected `WebSocket`). */
34
+ type Socket = WebSocket;
35
+ /** The session core's side of the machine. */
36
+ export type ConnectionEffects = {
37
+ /** Open the socket for a fresh `connect()` and make it the session's. */
38
+ dial(): Socket;
39
+ /**
40
+ * Let go of everything one connection holds: the pre-connect mic, the audio
41
+ * path and `socket`. Its listeners are already detached, so the close this
42
+ * causes cannot re-enter the machine.
43
+ */
44
+ release(socket: Socket): void;
45
+ /** The caller's `connect({ signal })` aborted: hang up. */
46
+ abandon(): void;
47
+ /** Interpret a frame; the `config` payload when it completes the handshake. */
48
+ receive(data: unknown): SessionConfigMessage | undefined;
49
+ /** The socket opened; the handshake has not completed yet. */
50
+ opened(): void;
51
+ /** The handshake completed. */
52
+ configured(config: SessionConfigMessage): void;
53
+ /** Is the session fatally over (`session/state.ts`)? Then no close is retried. */
54
+ fatal(): boolean;
55
+ /**
56
+ * Will partysocket retry the close being dispatched on `socket`? Asked
57
+ * inside the `close` listener: partysocket schedules its retry before it
58
+ * dispatches.
59
+ */
60
+ retryPending(socket: Socket): boolean;
61
+ /** Does the socket have reconnect machinery? An injected `WebSocket` does not. */
62
+ canRedial(): boolean;
63
+ /** Force a fresh attempt on a socket that opened but never became a session. */
64
+ redial(): void;
65
+ /** An attempt failed and another is coming: drop the audio path, show `connecting`. */
66
+ retrying(): void;
67
+ /** The socket closed for good — `failure` is what to report, null for a clean end. */
68
+ closed(failure: string | null): void;
69
+ /** The handshake budget is spent. */
70
+ exhausted(): void;
71
+ };
72
+ /** Everything that happens to the connection. */
73
+ export type ConnectionEvent =
74
+ /** `connect()`: tear down whatever is there and dial afresh. */
75
+ {
76
+ type: "DIAL";
77
+ signal?: AbortSignal | undefined;
78
+ }
79
+ /** The caller hung up (`disconnect()`, `end()`). */
80
+ | {
81
+ type: "HANG_UP";
82
+ }
83
+ /** The server retired the session for idleness (`session.timedOut`). */
84
+ | {
85
+ type: "RETIRED";
86
+ }
87
+ /** The socket opened. */
88
+ | {
89
+ type: "OPENED";
90
+ }
91
+ /** The `config` frame arrived. */
92
+ | {
93
+ type: "CONFIGURED";
94
+ config: SessionConfigMessage;
95
+ }
96
+ /** The socket errored; a `close` always follows. */
97
+ | {
98
+ type: "ERRORED";
99
+ }
100
+ /** The socket closed; `reconnecting` is whether partysocket will retry it. */
101
+ | {
102
+ type: "DROPPED";
103
+ event: CloseEvent;
104
+ reconnecting: boolean;
105
+ };
106
+ /** Where the connection is. */
107
+ export type ConnectionPhase = "closed" | "dialing" | "awaitingHandshake" | "live";
108
+ /** The session core's handle on its connection. */
109
+ export type Connection = {
110
+ /** Tear down whatever is there and dial afresh; `signal` aborting hangs up. */
111
+ dial(signal?: AbortSignal): void;
112
+ /** Hang up: the teardown, and no retry. */
113
+ hangUp(): void;
114
+ /** The server retired the session: the close that follows is not retried. */
115
+ retire(): void;
116
+ /** Where the connection is — for specs. */
117
+ phase(): ConnectionPhase;
118
+ };
119
+ /** Create the connection machine for one browser session. */
120
+ export declare function createConnection(effects: ConnectionEffects): Connection;
121
+ export {};
@@ -17,44 +17,31 @@
17
17
  * nothing on screen to say so. Measured against a server that accepts and
18
18
  * then says nothing: `ready` at 34ms, still `ready` and errorless when the
19
19
  * probe gave up.
20
+ *
21
+ * The deadline itself is the `awaitingHandshake` state's `after` delay in
22
+ * `session/connection.ts`, so leaving the state — a `config` frame, a close, a
23
+ * teardown — is what disarms it. This module holds its numbers and its error.
20
24
  */
21
25
  import type { SessionError } from "../types.ts";
22
26
  /** What the session reports once the budget below is spent. */
23
27
  export declare const HANDSHAKE_ERROR: SessionError;
24
- export type HandshakeGuard = {
25
- /** Start the deadline for the attempt that just opened. */
26
- arm(): void;
27
- /** Stop it — this socket is closing, or the connection is being torn down. */
28
- disarm(): void;
29
- /**
30
- * The `config` frame arrived: stop the deadline AND spend nothing.
31
- *
32
- * Separate from {@link HandshakeGuard.disarm} because the budget is
33
- * CONSECUTIVE, and only a completed handshake proves the peer is healthy. One
34
- * guard covers a whole `connect()`, partysocket's retries included, so with a
35
- * plain disarm the count survived every successful session in between: an
36
- * hour-long call whose socket dropped three times, each drop timing out once
37
- * before the next attempt succeeded, surfaced the permanent
38
- * "Agent did not complete the session handshake" error against a peer that
39
- * had answered every time. A close must NOT reset it — a wedged peer closes
40
- * and reopens on its own, and resetting there is the unbounded re-dial loop
41
- * the budget exists to bound.
42
- */
43
- succeeded(): void;
44
- };
28
+ /** How long an OPEN socket may go without a `config` frame. */
29
+ export declare const HANDSHAKE_TIMEOUT_MS = 10000;
45
30
  /**
46
- * Watch one connection's handshake.
31
+ * How many consecutive handshake timeouts to ride out before giving up.
47
32
  *
48
- * `onRetry` fires when a timed-out attempt has been re-dialed (the sandbox
49
- * behind the endpoint may have been replaced, and the URL provider re-brokers
50
- * on the next attempt); `onExhausted` when the budget is spent, or when the
51
- * socket has no reconnect machinery at all (an injected `options.WebSocket`).
33
+ * `forceReconnect` restarts partysocket's own retry budget, so this is the
34
+ * budget for this failure mode — without one, a permanently wedged peer would
35
+ * be re-dialed every ~10s forever, which is the unbounded retry loop
36
+ * `RECONNECT_OPTIONS.maxRetries` exists to prevent.
52
37
  *
53
- * @internal
38
+ * CONSECUTIVE is the whole of it: only a completed handshake resets the count.
39
+ * One `connect()` spans partysocket's retries, so a count that survived a
40
+ * successful session in between turned an hour-long call whose socket dropped
41
+ * three times — each drop timing out once before the next attempt succeeded —
42
+ * into the permanent "did not complete the session handshake" error against a
43
+ * peer that had answered every time. A close must NOT reset it: a wedged peer
44
+ * closes and reopens on its own, and resetting there is the unbounded re-dial
45
+ * loop the budget exists to bound.
54
46
  */
55
- export declare function createHandshakeGuard(opts: {
56
- socket: unknown;
57
- signal: AbortSignal;
58
- onRetry: () => void;
59
- onExhausted: () => void;
60
- }): HandshakeGuard;
47
+ export declare const MAX_HANDSHAKE_TIMEOUTS = 3;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `createBrowserSession` and the types its handle is described by are what the
5
5
  * package builds on; every other module in `session/` (the dialer, the
6
- * reconnecting socket, the handshake guard, the two statecharts, the message
6
+ * reconnecting socket, the handshake deadline, the three statecharts, the message
7
7
  * handlers, the audio path's bring-up) is private to this directory. Private
8
8
  * means NOT re-exported here: guard-invariants rule 37 fails an import from
9
9
  * outside `session/` that names any module but this one.
@@ -1,2 +1,2 @@
1
- import { a as ticketCarriage, i as resolveSessionTokenSync, n as CLEARED_SESSION_STATE, o as loadAudioModules, r as resolveSessionToken, t as createBrowserSession } from "../session-D9jM5-TF.js";
1
+ import { a as ticketCarriage, i as resolveSessionTokenSync, n as CLEARED_SESSION_STATE, o as loadAudioModules, r as resolveSessionToken, t as createBrowserSession } from "../session-CYdjRvbl.js";
2
2
  export { CLEARED_SESSION_STATE, createBrowserSession, loadAudioModules, resolveSessionToken, resolveSessionTokenSync, ticketCarriage };
@@ -37,6 +37,12 @@ type MessageHandlerDeps = {
37
37
  * module's.
38
38
  */
39
39
  audio: AudioPath;
40
+ /**
41
+ * The server retired the session for idleness: the close that follows is
42
+ * EXPECTED and must not be retried (the `retired` region of
43
+ * `session/connection.ts`).
44
+ */
45
+ retire: () => void;
40
46
  };
41
47
  type MessageHandlers = {
42
48
  /**
@@ -8,6 +8,11 @@ import type { WebSocketConstructor } from "../types.ts";
8
8
  * tick, for each attempt.
9
9
  */
10
10
  export declare function openReconnectingSocket(urlProvider: () => Promise<string>, protocolsProvider?: () => Promise<string[] | null>): InstanceType<WebSocketConstructor>;
11
+ /**
12
+ * Does `socket` have reconnect machinery? False for an injected
13
+ * `options.WebSocket` (tests), which a failure then ends for good.
14
+ */
15
+ export declare function canReconnect(socket: unknown): boolean;
11
16
  /**
12
17
  * Force a fresh connection attempt on a reconnecting socket, reporting
13
18
  * whether it was one. Used for a failure partysocket cannot see: a socket it
@@ -411,22 +411,12 @@ export type BrowserSession = {
411
411
  * from anywhere. They are the audio path's own statechart now — see
412
412
  * `session/audio-state.ts`.
413
413
  *
414
- * What is left is what the CONNECTION really owns: the socket, the server's
415
- * idle retirement, and the turn boundary.
414
+ * What is left is the socket the senders write to and the turn boundary. The
415
+ * socket's lifecycle and the server's idle retirement are the connection's own
416
+ * statechart (`session/connection.ts`), which is the only writer of `ws`.
416
417
  */
417
418
  export type ConnState = {
418
419
  ws: InstanceType<WebSocketConstructor> | null;
419
- /**
420
- * The server retired this session for idleness (`idle_timeout`), so the
421
- * close that follows is EXPECTED and must not be retried.
422
- *
423
- * Without this the automatic reconnect would immediately re-open the
424
- * session the server just reclaimed — a tab left open would cycle forever
425
- * and the guest would never see zero sessions, which is the whole point of
426
- * the timeout. Reconnecting is the user's call from here (the controls
427
- * reconnect on demand).
428
- */
429
- retiredByServer: boolean;
430
420
  /**
431
421
  * Turn epoch, bumped at every turn boundary — a committed user turn, a
432
422
  * barge-in, a reset, AND every audio-path teardown (`cleanupAudio`).