@craftedxp/voice-js 0.2.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.
@@ -0,0 +1,277 @@
1
+ type CallState = 'idle' | 'connecting' | 'listening' | 'user_speaking' | 'agent_speaking' | 'ended' | 'error';
2
+ type TranscriptEntry = {
3
+ id: string;
4
+ role: 'user';
5
+ text: string;
6
+ committed: boolean;
7
+ } | {
8
+ id: string;
9
+ role: 'agent';
10
+ text: string;
11
+ interrupted?: boolean;
12
+ } | {
13
+ id: string;
14
+ role: 'tool';
15
+ text: string;
16
+ } | {
17
+ id: string;
18
+ role: 'system';
19
+ text: string;
20
+ };
21
+ type CallErrorCode = 'missing_credentials' | 'forbidden' | 'mic_denied' | 'mic_start_failed' | 'audio_session_failed' | 'token_expired' | 'token_invalid' | 'unauthorized' | 'network_unreachable' | 'socket_error' | 'payment_required' | 'not_found' | 'silence_timeout' | 'server_error';
22
+ interface CallError {
23
+ code: CallErrorCode;
24
+ message: string;
25
+ }
26
+ type CallEndReason = 'agent_ended' | 'user_hangup' | 'timeout' | 'error';
27
+ interface CallEndEvent {
28
+ reason: CallEndReason;
29
+ errorCode?: CallErrorCode;
30
+ durationMs: number;
31
+ }
32
+ interface VolumeEvent {
33
+ input: number;
34
+ output: number;
35
+ }
36
+ type ServerMessage = Record<string, unknown> & {
37
+ type?: string;
38
+ };
39
+ interface ProtocolState {
40
+ state: CallState;
41
+ transcript: TranscriptEntry[];
42
+ agentBubbleId: string | null;
43
+ idCounter: number;
44
+ endReason: CallEndReason | null;
45
+ }
46
+ declare const createProtocolState: () => ProtocolState;
47
+ interface ProtocolCallbacks {
48
+ onState: (next: CallState) => void;
49
+ onTranscript: (entries: TranscriptEntry[]) => void;
50
+ onError: (err: CallError) => void;
51
+ onInterrupt: () => void;
52
+ onAgentTurnStart: () => void;
53
+ onCallEnd: (reason: CallEndReason) => void;
54
+ }
55
+ declare function handleServerMessage(raw: string, state: ProtocolState, cb: ProtocolCallbacks): void;
56
+ interface BuildWsUrlArgs {
57
+ apiBase: string;
58
+ agentId: string;
59
+ token: string;
60
+ bargeIn?: boolean;
61
+ }
62
+ declare function buildWsUrl(args: BuildWsUrlArgs): string;
63
+
64
+ interface FetchTokenArgs {
65
+ /** The agent the SDK is about to call. */
66
+ agentId: string;
67
+ /**
68
+ * Optional consumer-side user identifier. Round-tripped to the server
69
+ * as `contactId` for Phase 11 contact memory. The SDK does not
70
+ * inspect this; your backend uses it to scope the token mint.
71
+ */
72
+ userId?: string;
73
+ /**
74
+ * Per-call structured context lowered into the agent's effective
75
+ * system prompt server-side at session open. Opaque to the SDK.
76
+ */
77
+ context?: Record<string, unknown>;
78
+ /**
79
+ * String key/value pairs round-tripped on the `call.ended` webhook.
80
+ * Capped at 1 KB total server-side. NOT lowered into the system prompt.
81
+ */
82
+ metadata?: Record<string, string>;
83
+ }
84
+ type FetchToken = (args: FetchTokenArgs) => Promise<string>;
85
+ interface VoiceClientConfig {
86
+ /**
87
+ * Full HTTPS URL of the Voxline server. The WebSocket scheme is
88
+ * derived: `https` → `wss`, `http` → `ws`. No trailing slash needed.
89
+ */
90
+ apiBase: string;
91
+ /**
92
+ * Called by the SDK whenever it needs a fresh `ct_` token (initial
93
+ * connect; mid-call refresh on `token_expired`). Your implementation
94
+ * should hit YOUR backend, which holds the `sk_` API key and mints
95
+ * via `POST /v1/call-tokens` (or `client.callTokens.mint` from
96
+ * @craftedxp/sdk-node). Never embed `sk_` in JS code that ships to a
97
+ * client.
98
+ */
99
+ fetchToken: FetchToken;
100
+ /**
101
+ * Optional metadata applied to EVERY startCall. Per-call `metadata`
102
+ * in `startCall` is merged on top (per-call wins on key conflicts).
103
+ * Useful for dashboard-wide tags like `{ surface: 'web', appVersion }`.
104
+ */
105
+ defaultMetadata?: Record<string, string>;
106
+ /**
107
+ * Optional context applied to EVERY startCall. Per-call `context` in
108
+ * `startCall` is merged on top. Useful for cross-call invariants like
109
+ * the signed-in user's locale.
110
+ */
111
+ defaultContext?: Record<string, unknown>;
112
+ }
113
+ interface StartCallOptions {
114
+ /** The agent to call. */
115
+ agentId: string;
116
+ /** Per-call user identifier. Round-tripped to fetchToken as `userId`. */
117
+ userId?: string;
118
+ /**
119
+ * Per-call structured context. Merged on top of `defaultContext`
120
+ * configured at factory time.
121
+ */
122
+ context?: Record<string, unknown>;
123
+ /**
124
+ * Per-call metadata. Merged on top of `defaultMetadata` configured
125
+ * at factory time.
126
+ */
127
+ metadata?: Record<string, string>;
128
+ /**
129
+ * When false, the SDK + server stay full-duplex but barge-in is
130
+ * suppressed. Useful for alarm-style flows where the user shouldn't
131
+ * accidentally interrupt the script. Default true.
132
+ */
133
+ bargeIn?: boolean;
134
+ /**
135
+ * Test-only escape hatch — pass a pre-minted `ct_` directly and skip
136
+ * the `fetchToken` call. Don't use this in production code: tokens
137
+ * expire and the SDK can't re-mint without the callback.
138
+ */
139
+ token?: string;
140
+ onStateChange?: (state: CallState) => void;
141
+ onTranscript?: (entries: TranscriptEntry[]) => void;
142
+ onError?: (err: CallError) => void;
143
+ onEnd?: (end: CallEndEvent) => void;
144
+ /** Volume-meter event for VU UIs. ~10 Hz cadence (browser bundle only). */
145
+ onVolume?: (vol: VolumeEvent) => void;
146
+ }
147
+ interface Call {
148
+ /** Current state. Snapshot — subscribe via onStateChange for live updates. */
149
+ readonly state: CallState;
150
+ /** Full transcript so far. Snapshot — subscribe via onTranscript for live updates. */
151
+ readonly transcript: TranscriptEntry[];
152
+ /** True after `mute()` and before `unmute()`. */
153
+ readonly isMuted: boolean;
154
+ /** End the call locally. Closes the WS, stops the mic, fires onEnd. Idempotent. */
155
+ end: () => void;
156
+ /** Mute mic frames. Wire stays active so server endpointing doesn't false-positive. Idempotent. */
157
+ mute: () => void;
158
+ /** Unmute mic frames. Idempotent. */
159
+ unmute: () => void;
160
+ }
161
+ interface VoiceClientFactory {
162
+ /** Read back the resolved config (post trailing-slash normalisation). */
163
+ readonly config: VoiceClientConfig;
164
+ /**
165
+ * Open a fresh call. Returns when the WS is open; rejects on
166
+ * pre-flight failure (missing config, fetchToken throw, etc). Mid-
167
+ * call failures arrive via the per-call `onError` callback — they
168
+ * don't reject this promise.
169
+ */
170
+ startCall: (options: StartCallOptions) => Promise<Call>;
171
+ }
172
+
173
+ type OnChunk = (pcm: ArrayBuffer) => void;
174
+ type OnVolume$1 = (rms01: number) => void;
175
+ type OnError = (err: Error) => void;
176
+ interface CaptureOptions {
177
+ onChunk: OnChunk;
178
+ onVolume?: OnVolume$1;
179
+ onError?: OnError;
180
+ }
181
+ interface CaptureController {
182
+ start: () => Promise<void>;
183
+ stop: () => void;
184
+ mute: (muted: boolean) => void;
185
+ isCapturing: () => boolean;
186
+ }
187
+ declare const createAudioCapture: (options: CaptureOptions) => CaptureController;
188
+
189
+ type OnVolume = (rms01: number) => void;
190
+ type OnAgentSpeakingChange = (speaking: boolean) => void;
191
+ interface PlaybackOptions {
192
+ sampleRate?: number;
193
+ onVolume?: OnVolume;
194
+ onSpeakingChange?: OnAgentSpeakingChange;
195
+ }
196
+ interface PlaybackController {
197
+ enqueue: (pcm: ArrayBuffer) => void;
198
+ flush: () => void;
199
+ close: () => void;
200
+ resume: () => Promise<void>;
201
+ }
202
+ declare const createAudioPlayback: (options?: PlaybackOptions) => PlaybackController;
203
+
204
+ type RWSEvent = {
205
+ type: 'open';
206
+ } | {
207
+ type: 'reconnected';
208
+ } | {
209
+ type: 'message';
210
+ data: string | ArrayBuffer;
211
+ } | {
212
+ type: 'close';
213
+ code: number;
214
+ reason: string;
215
+ permanent: boolean;
216
+ } | {
217
+ type: 'error';
218
+ error: Error;
219
+ };
220
+ interface WebSocketLike {
221
+ binaryType: string;
222
+ readyState: number;
223
+ onopen: ((ev: unknown) => void) | null;
224
+ onmessage: ((ev: {
225
+ data: string | ArrayBuffer;
226
+ }) => void) | null;
227
+ onerror: ((ev: unknown) => void) | null;
228
+ onclose: ((ev: {
229
+ code: number;
230
+ reason: string;
231
+ }) => void) | null;
232
+ send: (data: string | ArrayBuffer | ArrayBufferView) => void;
233
+ close: (code?: number, reason?: string) => void;
234
+ }
235
+ type WebSocketFactory = (url: string) => WebSocketLike;
236
+ interface RWSOptions {
237
+ url: string;
238
+ wsFactory: WebSocketFactory;
239
+ maxRetries?: number;
240
+ initialBackoffMs?: number;
241
+ maxBackoffMs?: number;
242
+ }
243
+ declare const createReconnectingWebSocket: (options: RWSOptions, onEvent: (ev: RWSEvent) => void) => {
244
+ send: (data: string | ArrayBuffer | ArrayBufferView) => void;
245
+ close: (code?: number, reason?: string) => void;
246
+ readyState: () => number;
247
+ };
248
+ type ReconnectingWebSocket = ReturnType<typeof createReconnectingWebSocket>;
249
+
250
+ /**
251
+ * One-time SDK setup. Returns a factory you call `startCall` on for
252
+ * every voice call.
253
+ *
254
+ * Example:
255
+ * const voice = configureVoiceClient({
256
+ * apiBase: 'https://api.your-server.com',
257
+ * fetchToken: async ({ agentId }) => {
258
+ * const r = await fetch('/api/voice-token', {
259
+ * method: 'POST',
260
+ * body: JSON.stringify({ agentId }),
261
+ * })
262
+ * return (await r.json()).token
263
+ * },
264
+ * })
265
+ *
266
+ * // Per call (typically inside a click handler):
267
+ * const call = await voice.startCall({
268
+ * agentId: 'agt_xxx',
269
+ * onTranscript: (entries) => render(entries),
270
+ * onEnd: ({ reason }) => log(reason),
271
+ * })
272
+ * call.mute()
273
+ * call.end()
274
+ */
275
+ declare function configureVoiceClient(config: VoiceClientConfig): VoiceClientFactory;
276
+
277
+ export { type Call, type CallEndEvent, type CallEndReason, type CallError, type CallErrorCode, type CallState, type CaptureController, type CaptureOptions, type FetchToken, type FetchTokenArgs, type OnAgentSpeakingChange, type OnChunk, type OnError, type OnVolume$1 as OnVolume, type PlaybackController, type PlaybackOptions, type ProtocolCallbacks, type ProtocolState, type RWSEvent, type RWSOptions, type ReconnectingWebSocket, type ServerMessage, type StartCallOptions, type TranscriptEntry, type VoiceClientConfig, type VoiceClientFactory, type VolumeEvent, type WebSocketFactory, type WebSocketLike, buildWsUrl, configureVoiceClient, createAudioCapture, createAudioPlayback, createProtocolState, createReconnectingWebSocket, handleServerMessage };
@@ -0,0 +1,277 @@
1
+ type CallState = 'idle' | 'connecting' | 'listening' | 'user_speaking' | 'agent_speaking' | 'ended' | 'error';
2
+ type TranscriptEntry = {
3
+ id: string;
4
+ role: 'user';
5
+ text: string;
6
+ committed: boolean;
7
+ } | {
8
+ id: string;
9
+ role: 'agent';
10
+ text: string;
11
+ interrupted?: boolean;
12
+ } | {
13
+ id: string;
14
+ role: 'tool';
15
+ text: string;
16
+ } | {
17
+ id: string;
18
+ role: 'system';
19
+ text: string;
20
+ };
21
+ type CallErrorCode = 'missing_credentials' | 'forbidden' | 'mic_denied' | 'mic_start_failed' | 'audio_session_failed' | 'token_expired' | 'token_invalid' | 'unauthorized' | 'network_unreachable' | 'socket_error' | 'payment_required' | 'not_found' | 'silence_timeout' | 'server_error';
22
+ interface CallError {
23
+ code: CallErrorCode;
24
+ message: string;
25
+ }
26
+ type CallEndReason = 'agent_ended' | 'user_hangup' | 'timeout' | 'error';
27
+ interface CallEndEvent {
28
+ reason: CallEndReason;
29
+ errorCode?: CallErrorCode;
30
+ durationMs: number;
31
+ }
32
+ interface VolumeEvent {
33
+ input: number;
34
+ output: number;
35
+ }
36
+ type ServerMessage = Record<string, unknown> & {
37
+ type?: string;
38
+ };
39
+ interface ProtocolState {
40
+ state: CallState;
41
+ transcript: TranscriptEntry[];
42
+ agentBubbleId: string | null;
43
+ idCounter: number;
44
+ endReason: CallEndReason | null;
45
+ }
46
+ declare const createProtocolState: () => ProtocolState;
47
+ interface ProtocolCallbacks {
48
+ onState: (next: CallState) => void;
49
+ onTranscript: (entries: TranscriptEntry[]) => void;
50
+ onError: (err: CallError) => void;
51
+ onInterrupt: () => void;
52
+ onAgentTurnStart: () => void;
53
+ onCallEnd: (reason: CallEndReason) => void;
54
+ }
55
+ declare function handleServerMessage(raw: string, state: ProtocolState, cb: ProtocolCallbacks): void;
56
+ interface BuildWsUrlArgs {
57
+ apiBase: string;
58
+ agentId: string;
59
+ token: string;
60
+ bargeIn?: boolean;
61
+ }
62
+ declare function buildWsUrl(args: BuildWsUrlArgs): string;
63
+
64
+ interface FetchTokenArgs {
65
+ /** The agent the SDK is about to call. */
66
+ agentId: string;
67
+ /**
68
+ * Optional consumer-side user identifier. Round-tripped to the server
69
+ * as `contactId` for Phase 11 contact memory. The SDK does not
70
+ * inspect this; your backend uses it to scope the token mint.
71
+ */
72
+ userId?: string;
73
+ /**
74
+ * Per-call structured context lowered into the agent's effective
75
+ * system prompt server-side at session open. Opaque to the SDK.
76
+ */
77
+ context?: Record<string, unknown>;
78
+ /**
79
+ * String key/value pairs round-tripped on the `call.ended` webhook.
80
+ * Capped at 1 KB total server-side. NOT lowered into the system prompt.
81
+ */
82
+ metadata?: Record<string, string>;
83
+ }
84
+ type FetchToken = (args: FetchTokenArgs) => Promise<string>;
85
+ interface VoiceClientConfig {
86
+ /**
87
+ * Full HTTPS URL of the Voxline server. The WebSocket scheme is
88
+ * derived: `https` → `wss`, `http` → `ws`. No trailing slash needed.
89
+ */
90
+ apiBase: string;
91
+ /**
92
+ * Called by the SDK whenever it needs a fresh `ct_` token (initial
93
+ * connect; mid-call refresh on `token_expired`). Your implementation
94
+ * should hit YOUR backend, which holds the `sk_` API key and mints
95
+ * via `POST /v1/call-tokens` (or `client.callTokens.mint` from
96
+ * @craftedxp/sdk-node). Never embed `sk_` in JS code that ships to a
97
+ * client.
98
+ */
99
+ fetchToken: FetchToken;
100
+ /**
101
+ * Optional metadata applied to EVERY startCall. Per-call `metadata`
102
+ * in `startCall` is merged on top (per-call wins on key conflicts).
103
+ * Useful for dashboard-wide tags like `{ surface: 'web', appVersion }`.
104
+ */
105
+ defaultMetadata?: Record<string, string>;
106
+ /**
107
+ * Optional context applied to EVERY startCall. Per-call `context` in
108
+ * `startCall` is merged on top. Useful for cross-call invariants like
109
+ * the signed-in user's locale.
110
+ */
111
+ defaultContext?: Record<string, unknown>;
112
+ }
113
+ interface StartCallOptions {
114
+ /** The agent to call. */
115
+ agentId: string;
116
+ /** Per-call user identifier. Round-tripped to fetchToken as `userId`. */
117
+ userId?: string;
118
+ /**
119
+ * Per-call structured context. Merged on top of `defaultContext`
120
+ * configured at factory time.
121
+ */
122
+ context?: Record<string, unknown>;
123
+ /**
124
+ * Per-call metadata. Merged on top of `defaultMetadata` configured
125
+ * at factory time.
126
+ */
127
+ metadata?: Record<string, string>;
128
+ /**
129
+ * When false, the SDK + server stay full-duplex but barge-in is
130
+ * suppressed. Useful for alarm-style flows where the user shouldn't
131
+ * accidentally interrupt the script. Default true.
132
+ */
133
+ bargeIn?: boolean;
134
+ /**
135
+ * Test-only escape hatch — pass a pre-minted `ct_` directly and skip
136
+ * the `fetchToken` call. Don't use this in production code: tokens
137
+ * expire and the SDK can't re-mint without the callback.
138
+ */
139
+ token?: string;
140
+ onStateChange?: (state: CallState) => void;
141
+ onTranscript?: (entries: TranscriptEntry[]) => void;
142
+ onError?: (err: CallError) => void;
143
+ onEnd?: (end: CallEndEvent) => void;
144
+ /** Volume-meter event for VU UIs. ~10 Hz cadence (browser bundle only). */
145
+ onVolume?: (vol: VolumeEvent) => void;
146
+ }
147
+ interface Call {
148
+ /** Current state. Snapshot — subscribe via onStateChange for live updates. */
149
+ readonly state: CallState;
150
+ /** Full transcript so far. Snapshot — subscribe via onTranscript for live updates. */
151
+ readonly transcript: TranscriptEntry[];
152
+ /** True after `mute()` and before `unmute()`. */
153
+ readonly isMuted: boolean;
154
+ /** End the call locally. Closes the WS, stops the mic, fires onEnd. Idempotent. */
155
+ end: () => void;
156
+ /** Mute mic frames. Wire stays active so server endpointing doesn't false-positive. Idempotent. */
157
+ mute: () => void;
158
+ /** Unmute mic frames. Idempotent. */
159
+ unmute: () => void;
160
+ }
161
+ interface VoiceClientFactory {
162
+ /** Read back the resolved config (post trailing-slash normalisation). */
163
+ readonly config: VoiceClientConfig;
164
+ /**
165
+ * Open a fresh call. Returns when the WS is open; rejects on
166
+ * pre-flight failure (missing config, fetchToken throw, etc). Mid-
167
+ * call failures arrive via the per-call `onError` callback — they
168
+ * don't reject this promise.
169
+ */
170
+ startCall: (options: StartCallOptions) => Promise<Call>;
171
+ }
172
+
173
+ type OnChunk = (pcm: ArrayBuffer) => void;
174
+ type OnVolume$1 = (rms01: number) => void;
175
+ type OnError = (err: Error) => void;
176
+ interface CaptureOptions {
177
+ onChunk: OnChunk;
178
+ onVolume?: OnVolume$1;
179
+ onError?: OnError;
180
+ }
181
+ interface CaptureController {
182
+ start: () => Promise<void>;
183
+ stop: () => void;
184
+ mute: (muted: boolean) => void;
185
+ isCapturing: () => boolean;
186
+ }
187
+ declare const createAudioCapture: (options: CaptureOptions) => CaptureController;
188
+
189
+ type OnVolume = (rms01: number) => void;
190
+ type OnAgentSpeakingChange = (speaking: boolean) => void;
191
+ interface PlaybackOptions {
192
+ sampleRate?: number;
193
+ onVolume?: OnVolume;
194
+ onSpeakingChange?: OnAgentSpeakingChange;
195
+ }
196
+ interface PlaybackController {
197
+ enqueue: (pcm: ArrayBuffer) => void;
198
+ flush: () => void;
199
+ close: () => void;
200
+ resume: () => Promise<void>;
201
+ }
202
+ declare const createAudioPlayback: (options?: PlaybackOptions) => PlaybackController;
203
+
204
+ type RWSEvent = {
205
+ type: 'open';
206
+ } | {
207
+ type: 'reconnected';
208
+ } | {
209
+ type: 'message';
210
+ data: string | ArrayBuffer;
211
+ } | {
212
+ type: 'close';
213
+ code: number;
214
+ reason: string;
215
+ permanent: boolean;
216
+ } | {
217
+ type: 'error';
218
+ error: Error;
219
+ };
220
+ interface WebSocketLike {
221
+ binaryType: string;
222
+ readyState: number;
223
+ onopen: ((ev: unknown) => void) | null;
224
+ onmessage: ((ev: {
225
+ data: string | ArrayBuffer;
226
+ }) => void) | null;
227
+ onerror: ((ev: unknown) => void) | null;
228
+ onclose: ((ev: {
229
+ code: number;
230
+ reason: string;
231
+ }) => void) | null;
232
+ send: (data: string | ArrayBuffer | ArrayBufferView) => void;
233
+ close: (code?: number, reason?: string) => void;
234
+ }
235
+ type WebSocketFactory = (url: string) => WebSocketLike;
236
+ interface RWSOptions {
237
+ url: string;
238
+ wsFactory: WebSocketFactory;
239
+ maxRetries?: number;
240
+ initialBackoffMs?: number;
241
+ maxBackoffMs?: number;
242
+ }
243
+ declare const createReconnectingWebSocket: (options: RWSOptions, onEvent: (ev: RWSEvent) => void) => {
244
+ send: (data: string | ArrayBuffer | ArrayBufferView) => void;
245
+ close: (code?: number, reason?: string) => void;
246
+ readyState: () => number;
247
+ };
248
+ type ReconnectingWebSocket = ReturnType<typeof createReconnectingWebSocket>;
249
+
250
+ /**
251
+ * One-time SDK setup. Returns a factory you call `startCall` on for
252
+ * every voice call.
253
+ *
254
+ * Example:
255
+ * const voice = configureVoiceClient({
256
+ * apiBase: 'https://api.your-server.com',
257
+ * fetchToken: async ({ agentId }) => {
258
+ * const r = await fetch('/api/voice-token', {
259
+ * method: 'POST',
260
+ * body: JSON.stringify({ agentId }),
261
+ * })
262
+ * return (await r.json()).token
263
+ * },
264
+ * })
265
+ *
266
+ * // Per call (typically inside a click handler):
267
+ * const call = await voice.startCall({
268
+ * agentId: 'agt_xxx',
269
+ * onTranscript: (entries) => render(entries),
270
+ * onEnd: ({ reason }) => log(reason),
271
+ * })
272
+ * call.mute()
273
+ * call.end()
274
+ */
275
+ declare function configureVoiceClient(config: VoiceClientConfig): VoiceClientFactory;
276
+
277
+ export { type Call, type CallEndEvent, type CallEndReason, type CallError, type CallErrorCode, type CallState, type CaptureController, type CaptureOptions, type FetchToken, type FetchTokenArgs, type OnAgentSpeakingChange, type OnChunk, type OnError, type OnVolume$1 as OnVolume, type PlaybackController, type PlaybackOptions, type ProtocolCallbacks, type ProtocolState, type RWSEvent, type RWSOptions, type ReconnectingWebSocket, type ServerMessage, type StartCallOptions, type TranscriptEntry, type VoiceClientConfig, type VoiceClientFactory, type VolumeEvent, type WebSocketFactory, type WebSocketLike, buildWsUrl, configureVoiceClient, createAudioCapture, createAudioPlayback, createProtocolState, createReconnectingWebSocket, handleServerMessage };