@inferencesh/sdk 0.6.51 → 0.6.53

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/dist/index.d.ts CHANGED
@@ -5,9 +5,10 @@ export { StreamManager, type StreamManagerOptions, type PartialDataWrapper } fro
5
5
  export { StreamableManager, type StreamableManagerOptions, type StreamableMessage, streamable, streamableRaw } from './http/streamable';
6
6
  export { PollManager, type PollManagerOptions } from './http/poll';
7
7
  export { InferenceError, RequirementsNotMetException, SessionError, SessionNotFoundError, SessionExpiredError, SessionEndedError, WorkerLostError, isRequirementsNotMetException, isInferenceError, isSessionError, } from './http/errors';
8
- export { TasksAPI, type RunOptions } from './api/tasks';
8
+ export { TasksAPI, type RunOptions, type WatchOptions, type TaskWatch } from './api/tasks';
9
+ export { SocketsAPI, type SocketTarget, type OpenSocketOptions } from './api/sockets';
9
10
  export { FilesAPI, type UploadFileOptions } from './api/files';
10
- export { AgentsAPI, Agent, type AgentOptions, type SendMessageOptions, type AgentRunOptions } from './api/agents';
11
+ export { AgentsAPI, Agent, type AgentOptions, type SendMessageOptions, type AgentRunOptions, type AgentDelta } from './api/agents';
11
12
  export { SessionsAPI } from './api/sessions';
12
13
  export { AppsAPI } from './api/apps';
13
14
  export { ChatsAPI } from './api/chats';
@@ -23,6 +24,10 @@ export { IntegrationsAPI } from './api/integrations';
23
24
  export { SearchAPI } from './api/search';
24
25
  export { ProjectsAPI } from './api/projects';
25
26
  export { MCPServersAPI } from './api/mcp-servers';
27
+ export { LiveSession } from './live/session';
28
+ export type { LiveState, LiveEnd, LiveHandlers, LiveSessionOptions, WebSocketLike, WebSocketConstructor } from './live/session';
29
+ export { STREAM_FORMAT, isLiveField, parseMediaType, pcmFormat, splitLiveSchema, binaryLiveField, alternativeLabel, } from './live/schema';
30
+ export type { JsonSchema, MediaType, PCMFormat, LiveField } from './live/schema';
26
31
  export { tool, appTool, agentTool, webhookTool, httpTool, callTool, mcpTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
27
32
  export type { ClientTool, ClientToolHandler } from './tool-builder';
28
33
  export { lifecycleHook } from './hook-builder';
@@ -32,8 +37,12 @@ export { parseStatus, isTerminalStatus, isChatBusy, pendingApprovals } from './u
32
37
  export type { PendingApproval } from './utils';
33
38
  export * from './types';
34
39
  export type { TaskDTO as Task } from './types';
40
+ export type { CredentialDTO as IntegrationDTO, CredentialConfigDTO as IntegrationConfigDTO, CredentialConnectRequest as IntegrationConnectRequest, CredentialConnectResponse as IntegrationConnectResponse, CredentialCompleteOAuthRequest as IntegrationCompleteOAuthRequest, CredentialRequirement as IntegrationRequirement, CredentialStatus as IntegrationStatus, CredentialScope as IntegrationScope, CredentialGrant as IntegrationGrant, CredentialType as IntegrationAuthType, CredentialProvider as IntegrationProvider, } from './types';
35
41
  import { HttpClient, type HttpClientConfig } from './http/client';
36
42
  import { TasksAPI, RunOptions } from './api/tasks';
43
+ import { SocketsAPI } from './api/sockets';
44
+ import { LiveSession, type LiveHandlers } from './live/session';
45
+ import type { OpenSocketOptions } from './api/sockets';
37
46
  import { FilesAPI, UploadFileOptions } from './api/files';
38
47
  import { AgentsAPI, Agent, AgentOptions } from './api/agents';
39
48
  import { SessionsAPI } from './api/sessions';
@@ -107,6 +116,7 @@ export declare class Inference {
107
116
  readonly search: SearchAPI;
108
117
  readonly projects: ProjectsAPI;
109
118
  readonly mcpServers: MCPServersAPI;
119
+ readonly sockets: SocketsAPI;
110
120
  constructor(config: InferenceConfig | HttpClientConfig);
111
121
  /** @internal */
112
122
  _request<T>(method: 'get' | 'post' | 'put' | 'delete', endpoint: string, options?: {
@@ -119,6 +129,24 @@ export declare class Inference {
119
129
  * Run a task on inference.sh
120
130
  */
121
131
  run(params: ApiAppRunRequest, options?: RunOptions): Promise<Task>;
132
+ /**
133
+ * Start a stream function and open its socket. The task runs until the
134
+ * session is closed (or the app returns); `session.ended` settles then.
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const { session } = await client.live({ app: 'infsh/voice-loop', function: 'stream', input: { effect: 'robot' } }, {
139
+ * onBinary: (pcm) => speaker.write(pcm),
140
+ * onPatch: (patch) => console.log(patch),
141
+ * });
142
+ * session.sendBinary(micFrame);
143
+ * session.close();
144
+ * ```
145
+ */
146
+ live(params: ApiAppRunRequest, handlers?: LiveHandlers, options?: OpenSocketOptions): Promise<{
147
+ task: Task;
148
+ session: LiveSession;
149
+ }>;
122
150
  /**
123
151
  * Upload a file
124
152
  */
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export { InferenceError, RequirementsNotMetException, SessionError, SessionNotFo
8
8
  isRequirementsNotMetException, isInferenceError, isSessionError, } from './http/errors';
9
9
  // API modules
10
10
  export { TasksAPI } from './api/tasks';
11
+ export { SocketsAPI } from './api/sockets';
11
12
  export { FilesAPI } from './api/files';
12
13
  export { AgentsAPI, Agent } from './api/agents';
13
14
  export { SessionsAPI } from './api/sessions';
@@ -25,6 +26,9 @@ export { IntegrationsAPI } from './api/integrations';
25
26
  export { SearchAPI } from './api/search';
26
27
  export { ProjectsAPI } from './api/projects';
27
28
  export { MCPServersAPI } from './api/mcp-servers';
29
+ // Live: the socket of a stream task and the live fields of its schemas
30
+ export { LiveSession } from './live/session';
31
+ export { STREAM_FORMAT, isLiveField, parseMediaType, pcmFormat, splitLiveSchema, binaryLiveField, alternativeLabel, } from './live/schema';
28
32
  // Tool Builder (fluent API)
29
33
  export { tool, appTool, agentTool, webhookTool, httpTool, callTool, mcpTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
30
34
  // Hook Builder (fluent API)
@@ -40,6 +44,7 @@ export * from './types';
40
44
  // =============================================================================
41
45
  import { HttpClient } from './http/client';
42
46
  import { TasksAPI } from './api/tasks';
47
+ import { SocketsAPI } from './api/sockets';
43
48
  import { FilesAPI } from './api/files';
44
49
  import { AgentsAPI } from './api/agents';
45
50
  import { SessionsAPI } from './api/sessions';
@@ -100,6 +105,7 @@ export class Inference {
100
105
  this.search = new SearchAPI(this.http);
101
106
  this.projects = new ProjectsAPI(this.http);
102
107
  this.mcpServers = new MCPServersAPI(this.http);
108
+ this.sockets = new SocketsAPI(this.http, this.tasks);
103
109
  }
104
110
  // Legacy methods for backward compatibility
105
111
  /** @internal */
@@ -117,6 +123,25 @@ export class Inference {
117
123
  const processedInput = await this.files.processInput(params.input);
118
124
  return this.tasks.run(params, processedInput, options);
119
125
  }
126
+ /**
127
+ * Start a stream function and open its socket. The task runs until the
128
+ * session is closed (or the app returns); `session.ended` settles then.
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const { session } = await client.live({ app: 'infsh/voice-loop', function: 'stream', input: { effect: 'robot' } }, {
133
+ * onBinary: (pcm) => speaker.write(pcm),
134
+ * onPatch: (patch) => console.log(patch),
135
+ * });
136
+ * session.sendBinary(micFrame);
137
+ * session.close();
138
+ * ```
139
+ */
140
+ async live(params, handlers = {}, options = {}) {
141
+ const task = await this.run(params, { wait: false });
142
+ const session = await this.sockets.open(task, handlers, options);
143
+ return { task, session };
144
+ }
120
145
  /**
121
146
  * Upload a file
122
147
  */
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Live fields of a stream function.
3
+ *
4
+ * A stream function's input and output schemas are ordinary JSON Schemas in
5
+ * which some properties are live: `{"type": "array", "format": "stream",
6
+ * "items": ...}`. Their values travel over the task's socket while it runs,
7
+ * instead of in the request body or the final output. `format: "stream"` is
8
+ * the sibling of `format: "file"`: the same media, live instead of by
9
+ * reference.
10
+ *
11
+ * On the wire a binary frame is one item of the schema's binary live field
12
+ * (there is at most one per direction), and a JSON text frame is a partial
13
+ * object keyed by property name: an item of a live field, or a new value for
14
+ * an ordinary one.
15
+ *
16
+ * The helpers are generic over the schema type so a caller with a richer
17
+ * JSON Schema type keeps it.
18
+ */
19
+ export declare const STREAM_FORMAT = "stream";
20
+ /** The part of JSON Schema these helpers read. */
21
+ export interface JsonSchema {
22
+ $ref?: string;
23
+ $defs?: Record<string, JsonSchema>;
24
+ type?: string | string[];
25
+ format?: string;
26
+ title?: string;
27
+ description?: string;
28
+ contentMediaType?: string;
29
+ const?: unknown;
30
+ items?: JsonSchema | JsonSchema[];
31
+ properties?: Record<string, JsonSchema>;
32
+ required?: string[];
33
+ anyOf?: JsonSchema[];
34
+ oneOf?: JsonSchema[];
35
+ }
36
+ export declare function isLiveField(schema: JsonSchema | undefined | null): boolean;
37
+ export interface MediaType {
38
+ /** e.g. "audio/pcm" */
39
+ type: string;
40
+ /** e.g. {format: "s16le", rate: "16000", channels: "1"} */
41
+ params: Record<string, string>;
42
+ }
43
+ export declare function parseMediaType(value: string | undefined): MediaType | null;
44
+ export interface PCMFormat {
45
+ sampleRate: number;
46
+ channels: number;
47
+ }
48
+ /** The PCM format of a media type, or null when it is not 16-bit PCM audio. */
49
+ export declare function pcmFormat(media: MediaType | null): PCMFormat | null;
50
+ export interface LiveField<S extends JsonSchema = JsonSchema> {
51
+ key: string;
52
+ title: string;
53
+ description?: string;
54
+ /** Items are binary frames. */
55
+ binary: boolean;
56
+ /** Set when binary. */
57
+ media: MediaType | null;
58
+ /**
59
+ * What one item can be, references resolved: the alternatives of an anyOf,
60
+ * or the single item schema. Empty for a binary field.
61
+ */
62
+ alternatives: S[];
63
+ }
64
+ /**
65
+ * Splits a function schema into what a form renders (the ordinary
66
+ * properties) and what the socket carries (the live ones).
67
+ */
68
+ export declare function splitLiveSchema<S extends JsonSchema>(schema: S | undefined | null): {
69
+ ordinary: S | null;
70
+ live: LiveField<S>[];
71
+ };
72
+ /** The schema's one binary live field. A binary frame carries no field name. */
73
+ export declare function binaryLiveField<S extends JsonSchema>(live: LiveField<S>[]): LiveField<S> | null;
74
+ /** A label for one alternative of a JSON live field: its `type` const, else its title. */
75
+ export declare function alternativeLabel(schema: JsonSchema, index: number): string;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Live fields of a stream function.
3
+ *
4
+ * A stream function's input and output schemas are ordinary JSON Schemas in
5
+ * which some properties are live: `{"type": "array", "format": "stream",
6
+ * "items": ...}`. Their values travel over the task's socket while it runs,
7
+ * instead of in the request body or the final output. `format: "stream"` is
8
+ * the sibling of `format: "file"`: the same media, live instead of by
9
+ * reference.
10
+ *
11
+ * On the wire a binary frame is one item of the schema's binary live field
12
+ * (there is at most one per direction), and a JSON text frame is a partial
13
+ * object keyed by property name: an item of a live field, or a new value for
14
+ * an ordinary one.
15
+ *
16
+ * The helpers are generic over the schema type so a caller with a richer
17
+ * JSON Schema type keeps it.
18
+ */
19
+ export const STREAM_FORMAT = 'stream';
20
+ export function isLiveField(schema) {
21
+ return !!schema && schema.format === STREAM_FORMAT;
22
+ }
23
+ export function parseMediaType(value) {
24
+ if (!value)
25
+ return null;
26
+ const [type, ...rest] = value.split(';').map((part) => part.trim());
27
+ if (!type)
28
+ return null;
29
+ const params = {};
30
+ for (const part of rest) {
31
+ const eq = part.indexOf('=');
32
+ if (eq > 0)
33
+ params[part.slice(0, eq).trim().toLowerCase()] = part.slice(eq + 1).trim();
34
+ }
35
+ return { type: type.toLowerCase(), params };
36
+ }
37
+ /** The PCM format of a media type, or null when it is not 16-bit PCM audio. */
38
+ export function pcmFormat(media) {
39
+ if (!media || media.type !== 'audio/pcm')
40
+ return null;
41
+ if (media.params.format && media.params.format !== 's16le')
42
+ return null;
43
+ const sampleRate = Number(media.params.rate ?? 16000);
44
+ const channels = Number(media.params.channels ?? 1);
45
+ if (!Number.isFinite(sampleRate) || sampleRate <= 0 || !Number.isFinite(channels) || channels <= 0)
46
+ return null;
47
+ return { sampleRate, channels };
48
+ }
49
+ /** Resolves a `#/$defs/` reference against the root, recursively. */
50
+ function deref(schema, root) {
51
+ if (!schema.$ref || !root.$defs)
52
+ return schema;
53
+ const name = schema.$ref.replace('#/$defs/', '');
54
+ const target = root.$defs[name];
55
+ if (!target)
56
+ return schema;
57
+ // The reference's own fields (title, description) win over the target's.
58
+ return { ...deref(target, root), ...schema };
59
+ }
60
+ function itemAlternatives(items, root) {
61
+ const resolved = deref(items, root);
62
+ const options = (resolved.anyOf ?? resolved.oneOf);
63
+ if (options?.length)
64
+ return options.map((option) => deref(option, root));
65
+ return [resolved];
66
+ }
67
+ /**
68
+ * Splits a function schema into what a form renders (the ordinary
69
+ * properties) and what the socket carries (the live ones).
70
+ */
71
+ export function splitLiveSchema(schema) {
72
+ if (!schema?.properties)
73
+ return { ordinary: schema ?? null, live: [] };
74
+ const ordinary = {};
75
+ const live = [];
76
+ for (const [key, property] of Object.entries(schema.properties)) {
77
+ if (!isLiveField(property)) {
78
+ ordinary[key] = property;
79
+ continue;
80
+ }
81
+ const items = ((Array.isArray(property.items) ? property.items[0] : property.items) ?? {});
82
+ const resolvedItems = deref(items, schema);
83
+ const binary = resolvedItems.format === 'binary';
84
+ live.push({
85
+ key,
86
+ title: property.title ?? key,
87
+ description: property.description,
88
+ binary,
89
+ media: binary ? parseMediaType(resolvedItems.contentMediaType) : null,
90
+ alternatives: binary ? [] : itemAlternatives(items, schema),
91
+ });
92
+ }
93
+ return {
94
+ ordinary: { ...schema, properties: ordinary, required: schema.required?.filter((key) => key in ordinary) },
95
+ live,
96
+ };
97
+ }
98
+ /** The schema's one binary live field. A binary frame carries no field name. */
99
+ export function binaryLiveField(live) {
100
+ return live.find((field) => field.binary) ?? null;
101
+ }
102
+ /** A label for one alternative of a JSON live field: its `type` const, else its title. */
103
+ export function alternativeLabel(schema, index) {
104
+ const tag = schema.properties?.type?.const;
105
+ if (typeof tag === 'string')
106
+ return tag;
107
+ return schema.title ?? `option ${index + 1}`;
108
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * One end of a stream task's socket, as the caller holds it.
3
+ *
4
+ * The task's run response carries where to dial and a short-lived credential
5
+ * (SocketAccess). The relay pairs this connection with the worker's. Until the
6
+ * app's first frame arrives nobody may be on the other end yet (the worker can
7
+ * still be pulling an image), so the session is `waiting`, not `live`.
8
+ *
9
+ * Frames follow the function's schemas (see ./schema): a binary frame is one
10
+ * item of the binary live field, a text frame is a JSON object keyed by field
11
+ * name.
12
+ */
13
+ import type { SocketAccess } from '../types';
14
+ export type LiveState = 'connecting' | 'waiting' | 'live' | 'ended';
15
+ export interface LiveEnd {
16
+ /** The WebSocket close code, or 1006 when the session ended without one. */
17
+ code: number;
18
+ reason: string;
19
+ /** The caller asked for it. */
20
+ byCaller: boolean;
21
+ /** The task ended before the app connected, so the session gave up waiting. */
22
+ taskEnded: boolean;
23
+ }
24
+ export interface LiveHandlers {
25
+ onState?: (state: LiveState, end?: LiveEnd) => void;
26
+ /** An item of the output's binary live field. */
27
+ onBinary?: (data: ArrayBuffer) => void;
28
+ /** A partial output object keyed by field name. */
29
+ onPatch?: (patch: Record<string, unknown>) => void;
30
+ }
31
+ /**
32
+ * Follows the task the socket belongs to. `done` settles when the task ends
33
+ * (rejected when it failed or was cancelled); `stop` ends the watch early and
34
+ * leaves `done` pending.
35
+ */
36
+ export interface TaskWatch {
37
+ done: Promise<unknown>;
38
+ stop(): void;
39
+ }
40
+ /** The part of the WebSocket API the session uses; browsers, Node 22+ and `ws` all provide it. */
41
+ export interface WebSocketLike {
42
+ binaryType: string;
43
+ readyState: number;
44
+ onopen: ((event: unknown) => void) | null;
45
+ onmessage: ((event: {
46
+ data: unknown;
47
+ }) => void) | null;
48
+ onclose: ((event: {
49
+ code: number;
50
+ reason: string;
51
+ }) => void) | null;
52
+ send(data: string | ArrayBuffer | ArrayBufferView): void;
53
+ close(code?: number, reason?: string): void;
54
+ }
55
+ export type WebSocketConstructor = new (url: string) => WebSocketLike;
56
+ export interface LiveSessionOptions {
57
+ access: SocketAccess;
58
+ handlers?: LiveHandlers;
59
+ /** Issues a fresh credential (POST /sockets/{id}/access) for a redial. */
60
+ renew?: () => Promise<SocketAccess>;
61
+ /**
62
+ * Ends the session when the task ends before the app connected. Without it
63
+ * a task that fails before its worker dials leaves the caller waiting on
64
+ * the relay until the pair timeout.
65
+ */
66
+ task?: TaskWatch;
67
+ /** The WebSocket to dial with; defaults to the runtime's global one. */
68
+ webSocket?: WebSocketConstructor;
69
+ }
70
+ export declare class LiveSession {
71
+ private ws;
72
+ private current;
73
+ private closedByCaller;
74
+ private redials;
75
+ private access;
76
+ private readonly handlers;
77
+ private readonly renew;
78
+ private readonly task;
79
+ private readonly WebSocket;
80
+ private resolveEnded;
81
+ /** Settles when the session has ended, however it ended. */
82
+ readonly ended: Promise<LiveEnd>;
83
+ constructor(options: LiveSessionOptions);
84
+ get state(): LiveState;
85
+ get isOpen(): boolean;
86
+ /** Dials the relay. The session reports its progress through onState. */
87
+ connect(): void;
88
+ private watched;
89
+ private watchTask;
90
+ private redial;
91
+ private setState;
92
+ private end;
93
+ private closeSocket;
94
+ /** One item of the input's binary live field. */
95
+ sendBinary(data: ArrayBuffer | ArrayBufferView): void;
96
+ /** A partial input object keyed by field name. */
97
+ sendPatch(patch: Record<string, unknown>): void;
98
+ /** Ends the stream; the function returns and the task completes. */
99
+ close(): void;
100
+ }
@@ -0,0 +1,139 @@
1
+ const WS_OPEN = 1;
2
+ // 1012: the relay is restarting and closed an end that still waited for its
3
+ // peer. 1013: the peer did not come in time. Both mean "dial again" while the
4
+ // task is alive, and neither means anything once frames have flowed.
5
+ const REDIAL_CODES = new Set([1012, 1013]);
6
+ const MAX_REDIALS = 5;
7
+ function globalWebSocket() {
8
+ const ctor = globalThis.WebSocket;
9
+ if (!ctor) {
10
+ throw new Error('no WebSocket in this runtime: pass one (e.g. from the "ws" package) as webSocket');
11
+ }
12
+ return ctor;
13
+ }
14
+ export class LiveSession {
15
+ constructor(options) {
16
+ this.ws = null;
17
+ this.current = 'connecting';
18
+ this.closedByCaller = false;
19
+ this.redials = 0;
20
+ this.watched = false;
21
+ this.access = options.access;
22
+ this.handlers = options.handlers ?? {};
23
+ this.renew = options.renew;
24
+ this.task = options.task;
25
+ this.WebSocket = options.webSocket ?? globalWebSocket();
26
+ this.ended = new Promise((resolve) => {
27
+ this.resolveEnded = resolve;
28
+ });
29
+ }
30
+ get state() {
31
+ return this.current;
32
+ }
33
+ get isOpen() {
34
+ return this.ws?.readyState === WS_OPEN;
35
+ }
36
+ /** Dials the relay. The session reports its progress through onState. */
37
+ connect() {
38
+ this.setState('connecting');
39
+ this.watchTask();
40
+ // A browser cannot set headers on a WebSocket, so the credential rides in
41
+ // the query, as the relay documents.
42
+ const ws = new this.WebSocket(`${this.access.url}?access_token=${encodeURIComponent(this.access.token)}`);
43
+ ws.binaryType = 'arraybuffer';
44
+ this.ws = ws;
45
+ ws.onopen = () => this.setState('waiting');
46
+ ws.onmessage = (event) => {
47
+ if (this.current !== 'live') {
48
+ this.setState('live');
49
+ this.task?.stop(); // the app is there; the task's fate now shows on the socket
50
+ }
51
+ if (typeof event.data === 'string') {
52
+ let patch;
53
+ try {
54
+ patch = JSON.parse(event.data);
55
+ }
56
+ catch {
57
+ patch = { text: event.data };
58
+ }
59
+ if (patch && typeof patch === 'object' && !Array.isArray(patch)) {
60
+ this.handlers.onPatch?.(patch);
61
+ }
62
+ }
63
+ else {
64
+ this.handlers.onBinary?.(event.data);
65
+ }
66
+ };
67
+ ws.onclose = (event) => {
68
+ if (this.ws !== ws)
69
+ return;
70
+ this.ws = null;
71
+ const waiting = this.current !== 'live';
72
+ if (!this.closedByCaller && waiting && REDIAL_CODES.has(event.code) && this.redials < MAX_REDIALS) {
73
+ this.redials += 1;
74
+ void this.redial();
75
+ return;
76
+ }
77
+ this.end({ code: event.code, reason: event.reason, byCaller: this.closedByCaller, taskEnded: false });
78
+ };
79
+ }
80
+ watchTask() {
81
+ if (!this.task || this.watched)
82
+ return;
83
+ this.watched = true;
84
+ const endedBeforeLive = (reason) => {
85
+ if (this.current === 'ended' || this.current === 'live')
86
+ return;
87
+ this.closeSocket(1000, 'task ended');
88
+ this.end({ code: 1000, reason, byCaller: false, taskEnded: true });
89
+ };
90
+ this.task.done.then(() => endedBeforeLive('the task ended before the app connected'), (err) => endedBeforeLive(err instanceof Error ? err.message : String(err)));
91
+ }
92
+ async redial() {
93
+ try {
94
+ if (this.renew)
95
+ this.access = await this.renew();
96
+ if (this.current !== 'ended' && !this.closedByCaller)
97
+ this.connect();
98
+ }
99
+ catch (err) {
100
+ this.end({ code: 1006, reason: err instanceof Error ? err.message : 'could not redial', byCaller: false, taskEnded: false });
101
+ }
102
+ }
103
+ setState(state, end) {
104
+ this.current = state;
105
+ this.handlers.onState?.(state, end);
106
+ }
107
+ end(end) {
108
+ if (this.current === 'ended')
109
+ return;
110
+ this.task?.stop();
111
+ this.setState('ended', end);
112
+ this.resolveEnded(end);
113
+ }
114
+ closeSocket(code, reason) {
115
+ const ws = this.ws;
116
+ this.ws = null; // its onclose must not end the session a second time
117
+ ws?.close(code, reason);
118
+ }
119
+ /** One item of the input's binary live field. */
120
+ sendBinary(data) {
121
+ if (this.isOpen)
122
+ this.ws.send(data);
123
+ }
124
+ /** A partial input object keyed by field name. */
125
+ sendPatch(patch) {
126
+ if (this.isOpen)
127
+ this.ws.send(JSON.stringify(patch));
128
+ }
129
+ /** Ends the stream; the function returns and the task completes. */
130
+ close() {
131
+ this.closedByCaller = true;
132
+ if (this.ws) {
133
+ this.ws.close(1000, 'done'); // onclose ends the session
134
+ }
135
+ else {
136
+ this.end({ code: 1000, reason: 'done', byCaller: true, taskEnded: false });
137
+ }
138
+ }
139
+ }