@inferencesh/sdk 0.20.1 → 0.22.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 (61) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/agent/actions.js +1 -4
  3. package/dist/agent/api.d.ts +37 -6
  4. package/dist/agent/api.js +37 -3
  5. package/dist/agent/reducer.js +23 -7
  6. package/dist/agent/types.d.ts +30 -5
  7. package/dist/api/agents.d.ts +9 -3
  8. package/dist/api/agents.js +87 -65
  9. package/dist/api/api-keys.d.ts +2 -5
  10. package/dist/api/apps.d.ts +9 -2
  11. package/dist/api/apps.js +9 -1
  12. package/dist/api/chats.d.ts +13 -3
  13. package/dist/api/chats.js +13 -2
  14. package/dist/api/engines.d.ts +4 -2
  15. package/dist/api/engines.js +3 -2
  16. package/dist/api/flow-runs.d.ts +7 -2
  17. package/dist/api/flow-runs.js +7 -2
  18. package/dist/api/flows.d.ts +13 -2
  19. package/dist/api/flows.js +15 -1
  20. package/dist/api/knowledge.d.ts +7 -4
  21. package/dist/api/knowledge.js +6 -4
  22. package/dist/api/mcp-servers.d.ts +2 -1
  23. package/dist/api/projects.d.ts +5 -3
  24. package/dist/api/projects.js +3 -2
  25. package/dist/api/secrets.d.ts +2 -2
  26. package/dist/api/tasks.js +39 -60
  27. package/dist/api/teams.d.ts +4 -2
  28. package/dist/api/teams.js +3 -2
  29. package/dist/api/write-bodies.d.ts +110 -0
  30. package/dist/api/write-bodies.js +1 -0
  31. package/dist/http/client.d.ts +68 -7
  32. package/dist/http/client.js +138 -97
  33. package/dist/http/index.d.ts +2 -2
  34. package/dist/http/loopback.d.ts +8 -0
  35. package/dist/http/loopback.js +18 -0
  36. package/dist/http/stream-until-terminal.d.ts +55 -0
  37. package/dist/http/stream-until-terminal.js +88 -0
  38. package/dist/http/stream.d.ts +1 -0
  39. package/dist/http/stream.js +7 -3
  40. package/dist/http/streamable.d.ts +25 -5
  41. package/dist/http/streamable.js +28 -40
  42. package/dist/index.d.ts +4 -2
  43. package/dist/index.js +1 -0
  44. package/dist/proxy/endpoints.json +156 -0
  45. package/dist/proxy/express.d.ts +5 -6
  46. package/dist/proxy/express.js +18 -2
  47. package/dist/proxy/hono.d.ts +5 -3
  48. package/dist/proxy/hono.js +4 -3
  49. package/dist/proxy/index.d.ts +165 -6
  50. package/dist/proxy/index.js +409 -25
  51. package/dist/proxy/nextjs.d.ts +29 -17
  52. package/dist/proxy/nextjs.js +72 -62
  53. package/dist/proxy/remix.d.ts +5 -3
  54. package/dist/proxy/remix.js +4 -3
  55. package/dist/proxy/svelte.d.ts +5 -4
  56. package/dist/proxy/svelte.js +4 -4
  57. package/dist/types.d.ts +591 -271
  58. package/dist/types.js +117 -252
  59. package/dist/version.d.ts +1 -0
  60. package/dist/version.js +7 -0
  61. package/package.json +2 -1
@@ -1,5 +1,6 @@
1
1
  import { InferenceError, RequirementsNotMetException } from './errors';
2
2
  import { EventSource } from 'eventsource';
3
+ import { SDK_VERSION } from '../version';
3
4
  /**
4
5
  * Low-level HTTP client for inference.sh API
5
6
  *
@@ -16,7 +17,7 @@ export class HttpClient {
16
17
  this.baseUrl = config.baseUrl || 'https://api.inference.sh';
17
18
  this.proxyUrl = config.proxyUrl;
18
19
  this.getToken = config.getToken;
19
- this.customHeaders = { 'X-Client-Source': 'inference-sdk-js/0.5.13', ...config.headers };
20
+ this.customHeaders = { 'X-Client-Source': `inference-sdk-js/${SDK_VERSION}`, ...config.headers };
20
21
  // Bearer auth needs no cookies, and 'include' makes browsers reject the
21
22
  // response unless the API answers with Allow-Credentials — which it does not
22
23
  // for third-party origins (WebKit reports that as "Load failed"). Cookies
@@ -54,32 +55,139 @@ export class HttpClient {
54
55
  }
55
56
  return resolved;
56
57
  }
57
- /** Get authorization token */
58
- getAuthToken() {
59
- if (this.getToken) {
60
- return this.getToken();
58
+ /** The bearer token a request sent now carries; none through a proxy, which adds its own. */
59
+ async bearerToken() {
60
+ if (this.proxyUrl)
61
+ return undefined;
62
+ return (this.getToken ? await this.getToken() : this.apiKey) || undefined;
63
+ }
64
+ /** The configured headers plus what identifies the request: its bearer, or through a proxy its target. */
65
+ requestHeaders(targetUrl, token) {
66
+ const headers = this.resolveHeaders();
67
+ if (this.proxyUrl) {
68
+ headers['x-inf-target-url'] = targetUrl.toString();
69
+ }
70
+ else if (token) {
71
+ headers['Authorization'] = `Bearer ${token}`;
61
72
  }
62
- return this.apiKey;
73
+ return headers;
74
+ }
75
+ /**
76
+ * Where a request whose caller cannot set headers (EventSource) or reads
77
+ * the URL (streams) goes: the API, or the proxy with the target as a query
78
+ * parameter.
79
+ */
80
+ streamUrl(targetUrl) {
81
+ if (!this.proxyUrl)
82
+ return targetUrl.toString();
83
+ const proxyUrlWithQuery = new URL(this.proxyUrl, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
84
+ proxyUrlWithQuery.searchParams.set('__inf_target', targetUrl.toString());
85
+ return proxyUrlWithQuery.toString();
63
86
  }
64
87
  /**
65
88
  * Make an HTTP request to the API
66
89
  */
67
- async request(method, endpoint, options = {}) {
68
- const doRequest = () => this.executeRequest(method, endpoint, options);
90
+ async request(method, endpoint, { handleErrors = true, ...options } = {}) {
91
+ const send = (token) => this.executeRequest(method, endpoint, options, token);
92
+ const token = await this.bearerToken();
69
93
  try {
70
- return await doRequest();
94
+ return await send(token);
71
95
  }
72
96
  catch (error) {
73
- if (this.onError) {
74
- return await this.onError(error, doRequest);
97
+ if (this.onError && handleErrors) {
98
+ return await this.onError(error, async () => send(await this.bearerToken()), { token });
75
99
  }
76
100
  throw error;
77
101
  }
78
102
  }
103
+ /**
104
+ * Fetch an API endpoint with the client's auth, headers and credentials
105
+ * mode and resolve with the raw response, for bodies request() does not
106
+ * parse (streams, text). A failure goes through onError like one of
107
+ * request() does: a refused response, or a request that got no response.
108
+ * It resolves with the response onError's retry got; when onError resolves
109
+ * with anything else the failure stands and is thrown, the error request()
110
+ * would throw. A request aborted through `init.signal` rejects with the
111
+ * abort and is not an error for onError.
112
+ */
113
+ async fetch(endpoint, init = {}) {
114
+ const targetUrl = new URL(`${this.baseUrl}${endpoint}`);
115
+ const response = await this.fetchHandled(this.streamUrl(targetUrl), targetUrl, init, 'throw');
116
+ // What onError's retry got is handed over unread, and can be a refusal too.
117
+ if (!response.ok)
118
+ throw await this.readError(response);
119
+ return response;
120
+ }
121
+ /**
122
+ * One fetch of `url` on behalf of `targetUrl`. A refused response, or a
123
+ * request that got none, goes to onError; the result is the response its
124
+ * retry got. Without one the failure stands: the error of a request that
125
+ * got no response is thrown, and a refusal is thrown as the error its body
126
+ * was read into or, for a caller that passes the response on
127
+ * (`refused: 'return'`), handed back with its body unread.
128
+ */
129
+ async fetchHandled(url, targetUrl, init, refused) {
130
+ const send = (token) => fetch(url, {
131
+ ...init,
132
+ headers: { ...init.headers, ...this.requestHeaders(targetUrl, token) },
133
+ credentials: this.credentials,
134
+ });
135
+ const retry = async () => send(await this.bearerToken());
136
+ const token = await this.bearerToken();
137
+ let response;
138
+ try {
139
+ response = await send(token);
140
+ }
141
+ catch (error) {
142
+ // The caller aborted the request; nothing failed.
143
+ if (!this.onError || init.signal?.aborted)
144
+ throw error;
145
+ const handled = await this.onError(error, retry, { token });
146
+ if (handled instanceof globalThis.Response)
147
+ return handled;
148
+ throw error;
149
+ }
150
+ if (response.ok || (refused === 'return' && !this.onError))
151
+ return response;
152
+ // The body is read once: from a clone only when the response is handed back.
153
+ const error = await this.readError(refused === 'return' ? response.clone() : response);
154
+ const handled = await this.onError?.(error, retry, { token });
155
+ if (handled instanceof globalThis.Response)
156
+ return handled;
157
+ if (refused === 'return')
158
+ return response;
159
+ throw error;
160
+ }
161
+ /** The error a refused response stands for, read from its body. */
162
+ async readError(response) {
163
+ const responseText = await response.text().catch(() => '');
164
+ let data = null;
165
+ try {
166
+ data = JSON.parse(responseText);
167
+ }
168
+ catch {
169
+ // Not JSON
170
+ }
171
+ return this.responseError(response.status, responseText, data);
172
+ }
173
+ /** HTTP errors → RFC 9457 problem+json */
174
+ responseError(status, responseText, data) {
175
+ if (status === 412 && data && typeof data === 'object' && 'errors' in data && Array.isArray(data.errors)) {
176
+ return RequirementsNotMetException.fromResponse(data, status);
177
+ }
178
+ let errorDetail;
179
+ if (data && typeof data === 'object') {
180
+ errorDetail = this.extractErrorDetail(data) ?? JSON.stringify(data);
181
+ }
182
+ else if (responseText) {
183
+ errorDetail = responseText.slice(0, 500);
184
+ }
185
+ return new InferenceError(status, errorDetail || 'Request failed', responseText);
186
+ }
79
187
  /**
80
188
  * Execute the actual HTTP request (internal)
81
189
  */
82
- async executeRequest(method, endpoint, options = {}) {
190
+ async executeRequest(method, endpoint, options, token) {
83
191
  // Build the target URL (always points to the API)
84
192
  const targetUrl = new URL(`${this.baseUrl}${endpoint}`);
85
193
  if (options.params) {
@@ -92,23 +200,11 @@ export class HttpClient {
92
200
  });
93
201
  }
94
202
  // In proxy mode, requests go to the proxy with target URL in a header
95
- const isProxyMode = !!this.proxyUrl;
96
- const fetchUrl = isProxyMode ? this.proxyUrl : targetUrl.toString();
203
+ const fetchUrl = this.proxyUrl ?? targetUrl.toString();
97
204
  const headers = {
98
205
  'Content-Type': 'application/json',
99
- ...this.resolveHeaders(),
206
+ ...this.requestHeaders(targetUrl, token),
100
207
  };
101
- if (isProxyMode) {
102
- // Proxy mode: send target URL as header, no auth (proxy handles it)
103
- headers['x-inf-target-url'] = targetUrl.toString();
104
- }
105
- else {
106
- // Direct mode: include authorization header
107
- const token = this.getAuthToken();
108
- if (token) {
109
- headers['Authorization'] = `Bearer ${token}`;
110
- }
111
- }
112
208
  const fetchOptions = {
113
209
  method: method.toUpperCase(),
114
210
  headers,
@@ -126,19 +222,8 @@ export class HttpClient {
126
222
  catch {
127
223
  // Not JSON
128
224
  }
129
- // HTTP errors → RFC 9457 problem+json
130
225
  if (!response.ok) {
131
- if (response.status === 412 && data && 'errors' in data && Array.isArray(data.errors)) {
132
- throw RequirementsNotMetException.fromResponse(data, response.status);
133
- }
134
- let errorDetail;
135
- if (data && typeof data === 'object') {
136
- errorDetail = this.extractErrorDetail(data) ?? JSON.stringify(data);
137
- }
138
- else if (responseText) {
139
- errorDetail = responseText.slice(0, 500);
140
- }
141
- throw new InferenceError(response.status, errorDetail || 'Request failed', responseText);
226
+ throw this.responseError(response.status, responseText, data);
142
227
  }
143
228
  if (response.status === 204 || !responseText) {
144
229
  return { data: undefined, messages: [] };
@@ -156,26 +241,22 @@ export class HttpClient {
156
241
  /**
157
242
  * Get URL and headers for NDJSON streaming.
158
243
  * Returns the full URL and auth headers needed for streamable requests.
244
+ * It answers synchronously, so it throws when getToken is async.
245
+ * @deprecated Use fetch(), or a StreamRequest built on it
246
+ * (`(init) => http.fetch(endpoint, init)`): a request made from this config
247
+ * bypasses onError and carries the token of the moment the config was read.
159
248
  */
160
249
  getStreamableConfig(endpoint) {
161
250
  const targetUrl = new URL(`${this.baseUrl}${endpoint}`);
162
- const isProxyMode = !!this.proxyUrl;
163
- let url;
164
- const headers = { ...this.resolveHeaders() };
165
- if (isProxyMode) {
166
- const proxyUrlWithQuery = new URL(this.proxyUrl, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
167
- proxyUrlWithQuery.searchParams.set('__inf_target', targetUrl.toString());
168
- url = proxyUrlWithQuery.toString();
169
- headers['x-inf-target-url'] = targetUrl.toString();
170
- }
171
- else {
172
- url = targetUrl.toString();
173
- const token = this.getAuthToken();
174
- if (token) {
175
- headers['Authorization'] = `Bearer ${token}`;
176
- }
251
+ const token = this.proxyUrl ? undefined : this.getToken ? this.getToken() : this.apiKey;
252
+ if (token instanceof Promise) {
253
+ throw new Error('getStreamableConfig() cannot wait for an async getToken; use fetch() instead');
177
254
  }
178
- return { url, headers, credentials: this.credentials };
255
+ return {
256
+ url: this.streamUrl(targetUrl),
257
+ headers: this.requestHeaders(targetUrl, token || undefined),
258
+ credentials: this.credentials,
259
+ };
179
260
  }
180
261
  /**
181
262
  * Pull a human-readable detail string from a parsed problem+json / API error
@@ -189,56 +270,16 @@ export class HttpClient {
189
270
  }
190
271
  /**
191
272
  * Create an EventSource for SSE streaming
192
- * @deprecated Use getStreamableConfig() with StreamableManager instead
273
+ * @deprecated Use StreamableManager with a StreamRequest instead
193
274
  */
194
275
  createEventSource(endpoint) {
195
276
  const targetUrl = new URL(`${this.baseUrl}${endpoint}`);
196
- const isProxyMode = !!this.proxyUrl;
197
- // For proxy mode: Browser EventSource can't send custom headers,
198
- // so append target URL as query param instead
199
- let fetchUrl;
200
- if (isProxyMode) {
201
- const proxyUrlWithQuery = new URL(this.proxyUrl, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
202
- proxyUrlWithQuery.searchParams.set('__inf_target', targetUrl.toString());
203
- fetchUrl = proxyUrlWithQuery.toString();
204
- }
205
- else {
206
- fetchUrl = targetUrl.toString();
207
- }
208
- const resolvedHeaders = this.resolveHeaders();
209
277
  // The EventSource error event omits the response body, so a failed initial
210
278
  // connection (e.g. 403 otp_required) is handled here in the fetch wrapper:
211
279
  // route it through onError like request() does, and hand the retried
212
280
  // response (e.g. after OTP verification) back so the stream connects.
213
- const doFetch = async (input, init) => {
214
- const headers = {
215
- ...init?.headers,
216
- ...resolvedHeaders,
217
- };
218
- if (isProxyMode) {
219
- headers['x-inf-target-url'] = targetUrl.toString();
220
- }
221
- else {
222
- const token = this.getAuthToken();
223
- if (token) {
224
- headers['Authorization'] = `Bearer ${token}`;
225
- }
226
- }
227
- const response = await fetch(input, { ...init, headers, credentials: this.credentials });
228
- if (!response.ok && this.onError) {
229
- const body = await response.clone().text().catch(() => '');
230
- let data;
231
- try {
232
- data = JSON.parse(body);
233
- }
234
- catch { /* non-JSON body */ }
235
- const error = new InferenceError(response.status, this.extractErrorDetail(data) ?? 'Request failed', body);
236
- const handled = await this.onError(error, () => doFetch(input, init));
237
- return handled ?? response;
238
- }
239
- return response;
240
- };
241
- return Promise.resolve(new EventSource(fetchUrl, { fetch: doFetch }));
281
+ const doFetch = (input, init) => this.fetchHandled(input, targetUrl, init ?? {}, 'return');
282
+ return Promise.resolve(new EventSource(this.streamUrl(targetUrl), { fetch: doFetch }));
242
283
  }
243
284
  }
244
285
  /**
@@ -1,4 +1,4 @@
1
- export { HttpClient, type HttpClientConfig, type ErrorHandler, createHttpClient } from './client';
1
+ export { HttpClient, type HttpClientConfig, type ErrorHandler, type FailedRequest, type HttpFetchInit, createHttpClient } from './client';
2
2
  export { StreamManager, type StreamManagerOptions, type PartialDataWrapper } from './stream';
3
- export { streamable, streamableRaw, StreamableManager, type StreamableOptions, type StreamableMessage, type StreamableManagerOptions } from './streamable';
3
+ export { streamable, streamableRaw, StreamableManager, type StreamableOptions, type StreamableMessage, type StreamableManagerOptions, type StreamableSource, type StreamRequest, type StreamRequestInit } from './streamable';
4
4
  export { InferenceError, RequirementsNotMetException } from './errors';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Loopback hosts, where plain http never leaves the machine (local dev):
3
+ * `localhost`, any `*.localhost`, `[::1]` and 127.0.0.0/8. This is the one
4
+ * copy of the rule; the proxy, the app and common-js all ask it.
5
+ */
6
+ export declare function isLoopbackHost(hostname: string): boolean;
7
+ /** Whether `url` is https, or plain http to a loopback host. */
8
+ export declare function isHttpsOrLoopback(url: URL): boolean;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Loopback hosts, where plain http never leaves the machine (local dev):
3
+ * `localhost`, any `*.localhost`, `[::1]` and 127.0.0.0/8. This is the one
4
+ * copy of the rule; the proxy, the app and common-js all ask it.
5
+ */
6
+ export function isLoopbackHost(hostname) {
7
+ const h = hostname.toLowerCase();
8
+ return h === 'localhost'
9
+ || h.endsWith('.localhost')
10
+ || h === '[::1]'
11
+ || /^127(\.\d{1,3}){3}$/.test(h);
12
+ }
13
+ /** Whether `url` is https, or plain http to a loopback host. */
14
+ export function isHttpsOrLoopback(url) {
15
+ if (url.protocol === 'https:')
16
+ return true;
17
+ return url.protocol === 'http:' && isLoopbackHost(url.hostname);
18
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Follow a resource's NDJSON stream until the resource reaches a terminal
3
+ * state.
4
+ *
5
+ * StreamableManager never reconnects, and a stream can close cleanly before
6
+ * the resource is done (load balancer idle timeout, API deploy). A wait built
7
+ * on it must not hang then. This helper owns that contract for every
8
+ * "stream until done" wait: when the stream ends early it fetches the
9
+ * resource once (`reconcile`) and settles with what that shows, or rejects
10
+ * when the resource is still not terminal. sdk-py's `wait_for_completion`
11
+ * does the same final GET.
12
+ */
13
+ import { type StreamableManagerOptions, type StreamRequest } from './streamable';
14
+ /** Settles a stream-until-terminal wait. Calls after the first are ignored. */
15
+ export interface TerminalSettler<R> {
16
+ resolve(value: R): void;
17
+ reject(error: Error): void;
18
+ }
19
+ /** The stream to follow and how to tell when its resource is done. */
20
+ export type StreamUntilTerminalSpec<T> = Pick<StreamableManagerOptions<T>, 'body' | 'onData' | 'onPartialData' | 'onStart' | 'onDelta'> & {
21
+ /** Sends the stream request, e.g. `(init) => http.fetch(endpoint, init)`. */
22
+ request: StreamRequest;
23
+ /** Typed event listeners ({"event": name, "data": ...} messages). */
24
+ events?: Record<string, (data: any, fields?: string[]) => void>;
25
+ /**
26
+ * Called once when the stream ends cleanly before the wait settled. Fetch
27
+ * the resource and settle if it is terminal; returning without settling
28
+ * rejects the wait with `closedMessage`.
29
+ */
30
+ reconcile: () => Promise<void>;
31
+ /** Rejection message when the stream closed and the resource is still not terminal. */
32
+ closedMessage: string;
33
+ /**
34
+ * Called when the wait ends because `stop()` was called. Without it `done`
35
+ * stays pending.
36
+ */
37
+ onStopped?: () => void;
38
+ /**
39
+ * Keep the stream open after the wait settled so its listeners go on
40
+ * seeing updates (default false: settling stops the stream).
41
+ */
42
+ keepOpen?: boolean;
43
+ };
44
+ export interface StreamUntilTerminal<R> {
45
+ /** Settles when the resource is terminal, the stream fails, or it closed early and the resource is not terminal. */
46
+ done: Promise<R>;
47
+ /** Ends the stream on purpose; see `onStopped`. */
48
+ stop(): void;
49
+ }
50
+ /**
51
+ * `build` receives the settler and returns the stream spec; its handlers call
52
+ * `settle.resolve`/`settle.reject` once the resource is terminal. Settling
53
+ * stops the stream unless the spec says `keepOpen`.
54
+ */
55
+ export declare function streamUntilTerminal<T, R>(build: (settle: TerminalSettler<R>) => StreamUntilTerminalSpec<T>): StreamUntilTerminal<R>;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Follow a resource's NDJSON stream until the resource reaches a terminal
3
+ * state.
4
+ *
5
+ * StreamableManager never reconnects, and a stream can close cleanly before
6
+ * the resource is done (load balancer idle timeout, API deploy). A wait built
7
+ * on it must not hang then. This helper owns that contract for every
8
+ * "stream until done" wait: when the stream ends early it fetches the
9
+ * resource once (`reconcile`) and settles with what that shows, or rejects
10
+ * when the resource is still not terminal. sdk-py's `wait_for_completion`
11
+ * does the same final GET.
12
+ */
13
+ import { StreamableManager } from './streamable';
14
+ /**
15
+ * `build` receives the settler and returns the stream spec; its handlers call
16
+ * `settle.resolve`/`settle.reject` once the resource is terminal. Settling
17
+ * stops the stream unless the spec says `keepOpen`.
18
+ */
19
+ export function streamUntilTerminal(build) {
20
+ let settled = false;
21
+ let stopped = false;
22
+ let manager;
23
+ const done = new Promise((resolve, reject) => {
24
+ const settle = {
25
+ resolve: (value) => {
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
+ if (!keepOpen)
30
+ manager?.stop();
31
+ resolve(value);
32
+ },
33
+ reject: (error) => {
34
+ if (settled)
35
+ return;
36
+ settled = true;
37
+ if (!keepOpen)
38
+ manager?.stop();
39
+ reject(error);
40
+ },
41
+ };
42
+ const { events, reconcile, closedMessage, onStopped, keepOpen, ...managerOptions } = build(settle);
43
+ let streamError = null;
44
+ manager = new StreamableManager({
45
+ ...managerOptions,
46
+ onError: (error) => {
47
+ streamError = error;
48
+ },
49
+ onEnd: () => {
50
+ if (settled)
51
+ return;
52
+ if (stopped) {
53
+ onStopped?.();
54
+ return;
55
+ }
56
+ if (streamError) {
57
+ settle.reject(streamError);
58
+ return;
59
+ }
60
+ // Closed cleanly before the resource was terminal: ask the API once.
61
+ const closed = (cause) => {
62
+ if (stopped && !settled) {
63
+ onStopped?.();
64
+ return;
65
+ }
66
+ const error = new Error(closedMessage);
67
+ if (cause !== undefined)
68
+ error.cause = cause;
69
+ settle.reject(error);
70
+ };
71
+ reconcile().then(() => closed(), closed);
72
+ },
73
+ });
74
+ for (const [name, listener] of Object.entries(events ?? {})) {
75
+ manager.addEventListener(name, listener);
76
+ }
77
+ });
78
+ manager.start();
79
+ return {
80
+ done,
81
+ stop: () => {
82
+ if (stopped)
83
+ return;
84
+ stopped = true;
85
+ manager.stop();
86
+ },
87
+ };
88
+ }
@@ -24,6 +24,7 @@ export declare class StreamManager<T> {
24
24
  private initialConnectionAttempts;
25
25
  private consecutiveErrors;
26
26
  private isConnected;
27
+ private hasEverConnected;
27
28
  private isStopped;
28
29
  private eventListeners;
29
30
  constructor(options: StreamManagerOptions<T>);
@@ -17,6 +17,7 @@ export class StreamManager {
17
17
  this.initialConnectionAttempts = 0;
18
18
  this.consecutiveErrors = 0;
19
19
  this.isConnected = false;
20
+ this.hasEverConnected = false;
20
21
  this.isStopped = false;
21
22
  this.eventListeners = new Map();
22
23
  this.options = {
@@ -53,6 +54,8 @@ export class StreamManager {
53
54
  source.addEventListener(eventName, (e) => {
54
55
  if (this.isStopped)
55
56
  return;
57
+ // A delivered event proves the connection works, same as onmessage.
58
+ this.consecutiveErrors = 0;
56
59
  try {
57
60
  const parsed = JSON.parse(e.data);
58
61
  const listeners = this.eventListeners.get(eventName);
@@ -114,15 +117,15 @@ export class StreamManager {
114
117
  if (!this.options.autoReconnect || this.isStopped)
115
118
  return false;
116
119
  const maxReconnects = this.options.maxReconnects ?? 5;
117
- if (!this.isConnected && this.initialConnectionAttempts >= maxReconnects) {
120
+ if (!this.hasEverConnected && this.initialConnectionAttempts >= maxReconnects) {
118
121
  return false;
119
122
  }
120
- if (this.isConnected && this.consecutiveErrors >= maxReconnects) {
123
+ if (this.hasEverConnected && this.consecutiveErrors >= maxReconnects) {
121
124
  return false;
122
125
  }
123
126
  this.reconnectTimeout = setTimeout(() => {
124
127
  if (!this.isStopped) {
125
- if (!this.isConnected) {
128
+ if (!this.hasEverConnected) {
126
129
  this.initialConnectionAttempts++;
127
130
  }
128
131
  this.consecutiveErrors++;
@@ -142,6 +145,7 @@ export class StreamManager {
142
145
  return;
143
146
  this.eventSource = source;
144
147
  this.isConnected = true;
148
+ this.hasEverConnected = true;
145
149
  this.options.onStart?.();
146
150
  // Set up typed event listeners (for events with `event: eventName` header)
147
151
  this.setupAllEventListeners(source);
@@ -3,6 +3,19 @@
3
3
  * This is the NDJSON alternative to SSE/EventSource that avoids
4
4
  * the browser's ~6 connection limit per domain.
5
5
  */
6
+ /** What a stream asks of whatever sends its request. */
7
+ export interface StreamRequestInit {
8
+ method: 'GET' | 'POST';
9
+ headers: Record<string, string>;
10
+ body?: string;
11
+ signal?: AbortSignal;
12
+ }
13
+ /**
14
+ * Sends a stream's request in place of a plain fetch of a URL. With
15
+ * `(init) => http.fetch(endpoint, init)` the request carries the client's
16
+ * auth and a refusal goes through its error handler.
17
+ */
18
+ export type StreamRequest = (init: StreamRequestInit) => Promise<Response>;
6
19
  export interface StreamableOptions {
7
20
  /** Additional headers to include */
8
21
  headers?: Record<string, string>;
@@ -14,7 +27,7 @@ export interface StreamableOptions {
14
27
  signal?: AbortSignal;
15
28
  /** Skip heartbeat messages (default: true) */
16
29
  skipHeartbeats?: boolean;
17
- /** Request credentials mode (for cookie-based auth) */
30
+ /** Request credentials mode (for cookie-based auth); a StreamRequest brings its own */
18
31
  credentials?: RequestCredentials;
19
32
  }
20
33
  export interface StreamableMessage<T = unknown> {
@@ -30,19 +43,26 @@ export interface StreamableMessage<T = unknown> {
30
43
  /**
31
44
  * Stream NDJSON from an HTTP endpoint using fetch + ReadableStream.
32
45
  * Yields parsed JSON objects, automatically filtering heartbeats.
46
+ * `source` is the URL to fetch, or a StreamRequest that sends the request.
33
47
  */
34
- export declare function streamable<T = unknown>(url: string, options?: StreamableOptions): AsyncGenerator<T>;
48
+ export declare function streamable<T = unknown>(source: string | StreamRequest, options?: StreamableOptions): AsyncGenerator<T>;
35
49
  /**
36
50
  * StreamableManager provides a callback-based API similar to StreamManager
37
51
  * but uses fetch + ReadableStream instead of EventSource.
38
52
  */
39
- export interface StreamableManagerOptions<T> {
53
+ /** Where a StreamableManager's stream comes from: a URL it fetches, or a StreamRequest. */
54
+ export type StreamableSource = {
40
55
  /** URL to connect to */
41
56
  url: string;
42
57
  /** Additional headers */
43
58
  headers?: Record<string, string>;
44
59
  /** Request credentials mode (for cookie-based auth) */
45
60
  credentials?: RequestCredentials;
61
+ } | {
62
+ /** Sends the request, e.g. `(init) => http.fetch(endpoint, init)` */
63
+ request: StreamRequest;
64
+ };
65
+ export type StreamableManagerOptions<T> = StreamableSource & {
46
66
  /** Request body */
47
67
  body?: unknown;
48
68
  /** Called for each message */
@@ -57,7 +77,7 @@ export interface StreamableManagerOptions<T> {
57
77
  onEnd?: () => void;
58
78
  /** Called for delta events (streaming token deltas) */
59
79
  onDelta?: (delta: Record<string, any>, seq: number) => void;
60
- }
80
+ };
61
81
  export declare class StreamableManager<T> {
62
82
  private options;
63
83
  private abortController;
@@ -76,4 +96,4 @@ export declare class StreamableManager<T> {
76
96
  * Raw streamable that yields the full message including wrapper.
77
97
  * Use this when you need access to event type or fields.
78
98
  */
79
- export declare function streamableRaw<T = unknown>(url: string, options?: StreamableOptions): AsyncGenerator<StreamableMessage<T> | T>;
99
+ export declare function streamableRaw<T = unknown>(source: string | StreamRequest, options?: StreamableOptions): AsyncGenerator<StreamableMessage<T> | T>;