@inferencesh/sdk 0.22.1 → 0.24.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 (78) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/agent/actions.d.ts +2 -2
  3. package/dist/agent/actions.js +131 -28
  4. package/dist/agent/api.d.ts +4 -4
  5. package/dist/agent/api.js +6 -3
  6. package/dist/agent/context.d.ts +1 -1
  7. package/dist/agent/hooks.d.ts +2 -2
  8. package/dist/agent/hooks.js +1 -1
  9. package/dist/agent/index.d.ts +6 -6
  10. package/dist/agent/index.js +5 -5
  11. package/dist/agent/mcp-input.d.ts +1 -1
  12. package/dist/agent/provider.d.ts +1 -1
  13. package/dist/agent/provider.js +5 -5
  14. package/dist/agent/reducer.d.ts +1 -1
  15. package/dist/agent/reducer.js +2 -2
  16. package/dist/agent/types.d.ts +20 -7
  17. package/dist/api/agents.d.ts +16 -14
  18. package/dist/api/agents.js +63 -40
  19. package/dist/api/api-keys.d.ts +3 -3
  20. package/dist/api/apps.d.ts +4 -4
  21. package/dist/api/artifacts.d.ts +3 -3
  22. package/dist/api/chats.d.ts +4 -4
  23. package/dist/api/credentials.d.ts +3 -3
  24. package/dist/api/engines.d.ts +4 -4
  25. package/dist/api/files.d.ts +3 -3
  26. package/dist/api/flow-runs.d.ts +3 -3
  27. package/dist/api/flows.d.ts +4 -4
  28. package/dist/api/index.d.ts +4 -4
  29. package/dist/api/index.js +4 -4
  30. package/dist/api/knowledge.d.ts +4 -4
  31. package/dist/api/mcp-servers.d.ts +4 -4
  32. package/dist/api/projects.d.ts +4 -4
  33. package/dist/api/search.d.ts +3 -3
  34. package/dist/api/secrets.d.ts +3 -3
  35. package/dist/api/sessions.d.ts +3 -3
  36. package/dist/api/sockets.d.ts +6 -6
  37. package/dist/api/sockets.js +2 -2
  38. package/dist/api/tasks.d.ts +3 -3
  39. package/dist/api/tasks.js +4 -4
  40. package/dist/api/teams.d.ts +4 -4
  41. package/dist/api/write-bodies.d.ts +1 -1
  42. package/dist/delta.d.ts +1 -1
  43. package/dist/delta.js +1 -1
  44. package/dist/hook-builder.d.ts +1 -1
  45. package/dist/hook-builder.js +1 -1
  46. package/dist/http/client.d.ts +2 -2
  47. package/dist/http/client.js +2 -2
  48. package/dist/http/errors.d.ts +1 -1
  49. package/dist/http/index.d.ts +4 -4
  50. package/dist/http/index.js +4 -4
  51. package/dist/http/response.d.ts +1 -1
  52. package/dist/http/stream-until-terminal.d.ts +22 -8
  53. package/dist/http/stream-until-terminal.js +17 -9
  54. package/dist/http/streamable.d.ts +63 -7
  55. package/dist/http/streamable.js +277 -119
  56. package/dist/index.d.ts +65 -65
  57. package/dist/index.js +53 -53
  58. package/dist/live/schema.js +1 -1
  59. package/dist/live/session.d.ts +3 -3
  60. package/dist/live/session.js +2 -2
  61. package/dist/proxy/express.d.ts +1 -1
  62. package/dist/proxy/express.js +1 -1
  63. package/dist/proxy/hono.d.ts +1 -1
  64. package/dist/proxy/hono.js +1 -1
  65. package/dist/proxy/index.js +0 -2
  66. package/dist/proxy/nextjs.d.ts +3 -3
  67. package/dist/proxy/nextjs.js +2 -2
  68. package/dist/proxy/remix.d.ts +1 -1
  69. package/dist/proxy/remix.js +1 -1
  70. package/dist/proxy/svelte.d.ts +1 -1
  71. package/dist/proxy/svelte.js +1 -1
  72. package/dist/tool-builder.d.ts +1 -1
  73. package/dist/tool-builder.js +1 -1
  74. package/dist/types.d.ts +182 -41
  75. package/dist/types.js +33 -9
  76. package/dist/utils.d.ts +2 -2
  77. package/dist/utils.js +2 -2
  78. package/package.json +2 -1
@@ -46,6 +46,28 @@ export interface StreamableMessage<T = unknown> {
46
46
  * `source` is the URL to fetch, or a StreamRequest that sends the request.
47
47
  */
48
48
  export declare function streamable<T = unknown>(source: string | StreamRequest, options?: StreamableOptions): AsyncGenerator<T>;
49
+ /**
50
+ * Raw streamable that yields the full message including wrapper.
51
+ * Use this when you need access to event type or fields.
52
+ */
53
+ export declare function streamableRaw<T = unknown>(source: string | StreamRequest, options?: StreamableOptions): AsyncGenerator<StreamableMessage<T> | T>;
54
+ /** How a StreamableManager reconnects after its stream fails. */
55
+ export interface StreamReconnectOptions {
56
+ /**
57
+ * Failed connection attempts in a row before the manager gives up and calls
58
+ * `onError` (default 10). Any line received, heartbeats included, resets the count.
59
+ */
60
+ maxAttempts?: number;
61
+ /** Delay before the first retry in ms (default 1000). It doubles per attempt, with jitter. */
62
+ baseDelayMs?: number;
63
+ /** Longest delay between retries in ms (default 30000). */
64
+ maxDelayMs?: number;
65
+ /**
66
+ * Reconnect when nothing, not even a heartbeat, arrives for this many ms
67
+ * (default 30000; the API sends a heartbeat every 10s). 0 turns it off.
68
+ */
69
+ idleTimeoutMs?: number;
70
+ }
49
71
  /**
50
72
  * StreamableManager provides a callback-based API similar to StreamManager
51
73
  * but uses fetch + ReadableStream instead of EventSource.
@@ -69,19 +91,46 @@ export type StreamableManagerOptions<T> = StreamableSource & {
69
91
  onData?: (data: T) => void;
70
92
  /** Called for partial updates with fields list */
71
93
  onPartialData?: (data: T, fields: string[]) => void;
72
- /** Called on error */
94
+ /**
95
+ * Called when the stream fails. With `reconnect`, only once the manager
96
+ * gives up: a refusal no retry would change (4xx other than 408/429), or
97
+ * `maxAttempts` failures in a row.
98
+ */
73
99
  onError?: (error: Error) => void;
74
100
  /** Called when stream starts */
75
101
  onStart?: () => void;
76
- /** Called when stream ends */
102
+ /**
103
+ * Called once when the manager is done: the stream ended cleanly, stop()
104
+ * was called, or it failed and will not retry.
105
+ */
77
106
  onEnd?: () => void;
78
107
  /** Called for delta events (streaming token deltas) */
79
108
  onDelta?: (delta: Record<string, any>, seq: number) => void;
109
+ /**
110
+ * Reconnect after the stream fails or goes silent (default false: a failure
111
+ * ends the stream). `true` uses the StreamReconnectOptions defaults. A clean
112
+ * end of the stream is not retried.
113
+ */
114
+ reconnect?: boolean | StreamReconnectOptions;
115
+ /** With `reconnect`: the stream failed and the manager retries after `delayMs`. */
116
+ onRetry?: (error: Error, attempt: number, delayMs: number) => void;
117
+ /**
118
+ * With `reconnect`: a retry got a new connection. Events sent while the
119
+ * stream was down are lost, so this is where the caller fetches the current
120
+ * state. The manager reads the new stream only after the returned promise
121
+ * settles, so what it reads is newer than what the caller fetched; a
122
+ * rejection counts as a failed attempt.
123
+ */
124
+ onReconnect?: (attempt: number) => void | Promise<void>;
80
125
  };
81
126
  export declare class StreamableManager<T> {
82
127
  private options;
83
128
  private abortController;
84
129
  private isRunning;
130
+ /** Bumped by start() and stop(), so a stopped run never acts again. */
131
+ private runId;
132
+ /** Ends a pending retry wait early (stop, online, tab visible). */
133
+ private wakeRetry;
85
134
  private eventListeners;
86
135
  constructor(options: StreamableManagerOptions<T>);
87
136
  /**
@@ -90,10 +139,17 @@ export declare class StreamableManager<T> {
90
139
  */
91
140
  addEventListener<E = unknown>(eventName: string, callback: (data: E, fields?: string[]) => void): () => void;
92
141
  start(): Promise<void>;
142
+ /**
143
+ * One connection: open the stream and dispatch its messages until it ends.
144
+ * Resolves on a clean end, throws when the request or the read fails.
145
+ */
146
+ private connect;
147
+ /** Route one message to onDelta, a typed event listener, onPartialData or onData. */
148
+ private dispatch;
149
+ /**
150
+ * Wait `delayMs` before the next attempt. In a browser, coming back online
151
+ * or the tab becoming visible retries at once; stop() ends the wait.
152
+ */
153
+ private waitToRetry;
93
154
  stop(): void;
94
155
  }
95
- /**
96
- * Raw streamable that yields the full message including wrapper.
97
- * Use this when you need access to event type or fields.
98
- */
99
- export declare function streamableRaw<T = unknown>(source: string | StreamRequest, options?: StreamableOptions): AsyncGenerator<StreamableMessage<T> | T>;
@@ -28,19 +28,46 @@ async function openStream(source, { headers = {}, body, method = body ? 'POST' :
28
28
  }
29
29
  return res.body;
30
30
  }
31
+ /** The error a stream read sees when its signal aborts. */
32
+ function abortError() {
33
+ const error = new Error('The operation was aborted');
34
+ error.name = 'AbortError';
35
+ return error;
36
+ }
31
37
  /**
32
- * Stream NDJSON from an HTTP endpoint using fetch + ReadableStream.
33
- * Yields parsed JSON objects, automatically filtering heartbeats.
34
- * `source` is the URL to fetch, or a StreamRequest that sends the request.
38
+ * Resolves with `promise`, or rejects with an AbortError once `signal`
39
+ * aborts. A request or body that ignores the signal cannot hold the caller.
35
40
  */
36
- export async function* streamable(source, options = {}) {
37
- const { skipHeartbeats = true } = options;
38
- const reader = (await openStream(source, options)).getReader();
41
+ function untilAborted(promise, signal) {
42
+ if (!signal)
43
+ return promise;
44
+ if (signal.aborted)
45
+ return Promise.reject(abortError());
46
+ return new Promise((resolve, reject) => {
47
+ const onAbort = () => reject(abortError());
48
+ signal.addEventListener('abort', onAbort, { once: true });
49
+ promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
50
+ });
51
+ }
52
+ /** Run a cleanup that may throw or reject; its failure changes nothing. */
53
+ function quietly(cleanup) {
54
+ try {
55
+ cleanup()?.catch(() => { });
56
+ }
57
+ catch {
58
+ // Already closed.
59
+ }
60
+ }
61
+ /** Every parsed NDJSON line of `body`, heartbeats included. Aborting `signal` ends the read with an AbortError. */
62
+ async function* readLines(body, signal) {
63
+ const reader = body.getReader();
39
64
  const decoder = new TextDecoder();
40
65
  let buffer = '';
66
+ const cancel = () => quietly(() => reader.cancel?.());
67
+ signal?.addEventListener('abort', cancel, { once: true });
41
68
  try {
42
69
  while (true) {
43
- const { done, value } = await reader.read();
70
+ const { done, value } = await untilAborted(reader.read(), signal);
44
71
  if (done)
45
72
  break;
46
73
  buffer += decoder.decode(value, { stream: true });
@@ -49,42 +76,91 @@ export async function* streamable(source, options = {}) {
49
76
  for (const line of lines) {
50
77
  if (!line.trim())
51
78
  continue;
52
- const parsed = JSON.parse(line);
53
- // Skip heartbeats
54
- if (skipHeartbeats && parsed.type === 'heartbeat') {
55
- continue;
56
- }
57
- // If it's a wrapped message with event/data/fields, yield the data
58
- // Otherwise yield the entire parsed object
59
- if ('data' in parsed && parsed.data !== undefined) {
60
- yield parsed.data;
61
- }
62
- else {
63
- yield parsed;
64
- }
79
+ yield JSON.parse(line);
65
80
  }
66
81
  }
67
82
  // Handle remaining buffer
68
83
  if (buffer.trim()) {
69
- const parsed = JSON.parse(buffer);
70
- if (!(skipHeartbeats && parsed.type === 'heartbeat')) {
71
- if ('data' in parsed && parsed.data !== undefined) {
72
- yield parsed.data;
73
- }
74
- else {
75
- yield parsed;
76
- }
77
- }
84
+ yield JSON.parse(buffer);
78
85
  }
79
86
  }
80
87
  finally {
81
- reader.releaseLock();
88
+ signal?.removeEventListener('abort', cancel);
89
+ try {
90
+ reader.releaseLock();
91
+ }
92
+ catch {
93
+ // A read still pending after an abort can make releaseLock throw.
94
+ }
95
+ }
96
+ }
97
+ const isHeartbeat = (parsed) => typeof parsed === 'object' && parsed !== null && parsed.type === 'heartbeat';
98
+ /**
99
+ * Stream NDJSON from an HTTP endpoint using fetch + ReadableStream.
100
+ * Yields parsed JSON objects, automatically filtering heartbeats.
101
+ * `source` is the URL to fetch, or a StreamRequest that sends the request.
102
+ */
103
+ export async function* streamable(source, options = {}) {
104
+ for await (const parsed of streamableRaw(source, options)) {
105
+ const message = parsed;
106
+ // If it's a wrapped message with event/data/fields, yield the data
107
+ // Otherwise yield the entire parsed object
108
+ if (typeof message === 'object' && message !== null && 'data' in message && message.data !== undefined) {
109
+ yield message.data;
110
+ }
111
+ else {
112
+ yield parsed;
113
+ }
82
114
  }
83
115
  }
116
+ /**
117
+ * Raw streamable that yields the full message including wrapper.
118
+ * Use this when you need access to event type or fields.
119
+ */
120
+ export async function* streamableRaw(source, options = {}) {
121
+ const { skipHeartbeats = true } = options;
122
+ for await (const parsed of readLines(await openStream(source, options), options.signal)) {
123
+ if (skipHeartbeats && isHeartbeat(parsed))
124
+ continue;
125
+ yield parsed;
126
+ }
127
+ }
128
+ const RECONNECT_DEFAULTS = {
129
+ maxAttempts: 10,
130
+ baseDelayMs: 1000,
131
+ maxDelayMs: 30000,
132
+ idleTimeoutMs: 30000,
133
+ };
134
+ /** The HTTP status an error carries, if any. */
135
+ function errorStatus(error) {
136
+ const { statusCode, status } = error;
137
+ if (typeof statusCode === 'number')
138
+ return statusCode;
139
+ if (typeof status === 'number')
140
+ return status;
141
+ const match = /^HTTP (\d{3})\b/.exec(error.message);
142
+ return match ? Number(match[1]) : undefined;
143
+ }
144
+ /** A refusal that a retry would get again: any 4xx except 408 (timeout) and 429 (rate limit). */
145
+ function isRetryable(error) {
146
+ const status = errorStatus(error);
147
+ if (status === undefined)
148
+ return true;
149
+ return !(status >= 400 && status < 500 && status !== 408 && status !== 429);
150
+ }
151
+ /** Exponential backoff for retry `attempt` (1-based), jittered to [50%, 100%] of the step. */
152
+ function backoffDelay(policy, attempt) {
153
+ const step = Math.min(policy.maxDelayMs, policy.baseDelayMs * 2 ** (attempt - 1));
154
+ return Math.round(step / 2 + Math.random() * (step / 2));
155
+ }
84
156
  export class StreamableManager {
85
157
  constructor(options) {
86
158
  this.abortController = null;
87
159
  this.isRunning = false;
160
+ /** Bumped by start() and stop(), so a stopped run never acts again. */
161
+ this.runId = 0;
162
+ /** Ends a pending retry wait early (stop, online, tab visible). */
163
+ this.wakeRetry = null;
88
164
  this.eventListeners = new Map();
89
165
  this.options = options;
90
166
  }
@@ -111,116 +187,198 @@ export class StreamableManager {
111
187
  if (this.isRunning)
112
188
  return;
113
189
  this.isRunning = true;
114
- this.abortController = new AbortController();
190
+ const run = ++this.runId;
191
+ const active = () => this.runId === run;
192
+ const { reconnect } = this.options;
193
+ const policy = reconnect
194
+ ? { ...RECONNECT_DEFAULTS, ...(reconnect === true ? {} : reconnect) }
195
+ : null;
196
+ // Failed attempts in a row; any line received resets it.
197
+ let failures = 0;
198
+ let attempts = 0;
115
199
  try {
116
200
  this.options.onStart?.();
117
- const { options } = this;
118
- const stream = 'request' in options
119
- ? streamableRaw(options.request, { body: options.body, signal: this.abortController.signal })
120
- : streamableRaw(options.url, {
121
- headers: options.headers,
122
- body: options.body,
123
- signal: this.abortController.signal,
124
- credentials: options.credentials,
125
- });
126
- for await (const message of stream) {
127
- if (!this.isRunning)
128
- break;
129
- const wrapper = message;
130
- // Handle delta events
131
- if (wrapper.event === 'delta' && this.options.onDelta) {
132
- const payload = wrapper.data;
133
- if (payload && payload.delta) {
134
- this.options.onDelta(payload.delta, payload.seq);
135
- }
136
- continue;
201
+ while (active()) {
202
+ const retry = attempts++ > 0 ? failures : 0;
203
+ const idle = { timedOut: false };
204
+ let error = null;
205
+ try {
206
+ await this.connect(active, policy?.idleTimeoutMs ?? 0, idle, retry, () => {
207
+ failures = 0;
208
+ });
137
209
  }
138
- // Handle typed events ({"event": "...", "data": ...})
139
- if (wrapper.event && this.eventListeners.has(wrapper.event)) {
140
- const listeners = this.eventListeners.get(wrapper.event);
141
- const eventData = wrapper.data !== undefined ? wrapper.data : message;
142
- const fields = wrapper.fields;
143
- listeners.forEach(callback => callback(eventData, fields));
144
- continue;
210
+ catch (err) {
211
+ error = err instanceof Error ? err : new Error(String(err));
145
212
  }
146
- // Check if it's a partial update
147
- if (typeof message === 'object' &&
148
- message !== null &&
149
- 'data' in message &&
150
- 'fields' in message &&
151
- Array.isArray(wrapper.fields)) {
152
- if (this.options.onPartialData && wrapper.data !== undefined) {
153
- this.options.onPartialData(wrapper.data, wrapper.fields);
154
- }
155
- else if (wrapper.data !== undefined) {
156
- this.options.onData?.(wrapper.data);
157
- }
213
+ if (!active())
214
+ break;
215
+ if (idle.timedOut) {
216
+ error = new Error(`Stream received nothing for ${policy.idleTimeoutMs}ms`);
158
217
  }
159
- else if (wrapper.data !== undefined) {
160
- // Has data wrapper but no fields - unwrap
161
- this.options.onData?.(wrapper.data);
218
+ if (!error)
219
+ break; // ended cleanly
220
+ if (!policy) {
221
+ if (error.name !== 'AbortError')
222
+ this.options.onError?.(error);
223
+ break;
162
224
  }
163
- else {
164
- this.options.onData?.(message);
225
+ if (!isRetryable(error) || failures >= policy.maxAttempts) {
226
+ this.options.onError?.(error);
227
+ break;
165
228
  }
229
+ failures++;
230
+ const delay = backoffDelay(policy, failures);
231
+ this.options.onRetry?.(error, failures, delay);
232
+ await this.waitToRetry(delay);
166
233
  }
167
234
  }
168
235
  catch (err) {
169
- if (err instanceof Error && err.name === 'AbortError') {
170
- // Intentional stop, not an error
171
- }
172
- else {
236
+ // A callback threw outside a connection.
237
+ if (active())
173
238
  this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
174
- }
175
239
  }
176
240
  finally {
177
- this.isRunning = false;
241
+ if (active()) {
242
+ this.isRunning = false;
243
+ this.abortController = null;
244
+ }
178
245
  this.options.onEnd?.();
179
246
  }
180
247
  }
181
- stop() {
182
- this.isRunning = false;
183
- this.abortController?.abort();
184
- this.abortController = null;
185
- }
186
- }
187
- /**
188
- * Raw streamable that yields the full message including wrapper.
189
- * Use this when you need access to event type or fields.
190
- */
191
- export async function* streamableRaw(source, options = {}) {
192
- const { skipHeartbeats = true } = options;
193
- const reader = (await openStream(source, options)).getReader();
194
- const decoder = new TextDecoder();
195
- let buffer = '';
196
- try {
197
- while (true) {
198
- const { done, value } = await reader.read();
199
- if (done)
200
- break;
201
- buffer += decoder.decode(value, { stream: true });
202
- const lines = buffer.split('\n');
203
- buffer = lines.pop();
204
- for (const line of lines) {
205
- if (!line.trim())
206
- continue;
207
- const parsed = JSON.parse(line);
208
- // Skip heartbeats
209
- if (skipHeartbeats && parsed.type === 'heartbeat') {
210
- continue;
248
+ /**
249
+ * One connection: open the stream and dispatch its messages until it ends.
250
+ * Resolves on a clean end, throws when the request or the read fails.
251
+ */
252
+ async connect(active, idleTimeoutMs, idle, retry, onActivity) {
253
+ const controller = new AbortController();
254
+ this.abortController = controller;
255
+ const { signal } = controller;
256
+ let watchdog;
257
+ const disarm = () => {
258
+ if (watchdog !== undefined)
259
+ clearTimeout(watchdog);
260
+ watchdog = undefined;
261
+ };
262
+ const arm = () => {
263
+ if (idleTimeoutMs <= 0)
264
+ return;
265
+ disarm();
266
+ watchdog = setTimeout(() => {
267
+ idle.timedOut = true;
268
+ controller.abort();
269
+ }, idleTimeoutMs);
270
+ };
271
+ try {
272
+ arm();
273
+ const { options } = this;
274
+ const body = await untilAborted('request' in options
275
+ ? openStream(options.request, { body: options.body, signal })
276
+ : openStream(options.url, {
277
+ headers: options.headers,
278
+ body: options.body,
279
+ signal,
280
+ credentials: options.credentials,
281
+ }), signal);
282
+ if (retry > 0 && this.options.onReconnect) {
283
+ disarm();
284
+ try {
285
+ await this.options.onReconnect(retry);
286
+ }
287
+ catch (err) {
288
+ quietly(() => body.cancel?.());
289
+ throw err;
290
+ }
291
+ if (!active()) {
292
+ quietly(() => body.cancel?.());
293
+ return;
211
294
  }
212
- yield parsed;
295
+ arm();
296
+ }
297
+ for await (const message of readLines(body, signal)) {
298
+ if (!active())
299
+ break;
300
+ arm();
301
+ onActivity();
302
+ if (isHeartbeat(message))
303
+ continue;
304
+ this.dispatch(message);
213
305
  }
214
306
  }
215
- // Handle remaining buffer
216
- if (buffer.trim()) {
217
- const parsed = JSON.parse(buffer);
218
- if (!(skipHeartbeats && parsed.type === 'heartbeat')) {
219
- yield parsed;
307
+ finally {
308
+ disarm();
309
+ }
310
+ }
311
+ /** Route one message to onDelta, a typed event listener, onPartialData or onData. */
312
+ dispatch(message) {
313
+ const wrapper = message;
314
+ // Handle delta events
315
+ if (wrapper.event === 'delta' && this.options.onDelta) {
316
+ const payload = wrapper.data;
317
+ if (payload && payload.delta) {
318
+ this.options.onDelta(payload.delta, payload.seq);
319
+ }
320
+ return;
321
+ }
322
+ // Handle typed events ({"event": "...", "data": ...})
323
+ if (wrapper.event && this.eventListeners.has(wrapper.event)) {
324
+ const listeners = this.eventListeners.get(wrapper.event);
325
+ const eventData = wrapper.data !== undefined ? wrapper.data : message;
326
+ const fields = wrapper.fields;
327
+ listeners.forEach(callback => callback(eventData, fields));
328
+ return;
329
+ }
330
+ // Check if it's a partial update
331
+ if (typeof message === 'object' &&
332
+ message !== null &&
333
+ 'data' in message &&
334
+ 'fields' in message &&
335
+ Array.isArray(wrapper.fields)) {
336
+ if (this.options.onPartialData && wrapper.data !== undefined) {
337
+ this.options.onPartialData(wrapper.data, wrapper.fields);
338
+ }
339
+ else if (wrapper.data !== undefined) {
340
+ this.options.onData?.(wrapper.data);
220
341
  }
221
342
  }
343
+ else if (wrapper.data !== undefined) {
344
+ // Has data wrapper but no fields - unwrap
345
+ this.options.onData?.(wrapper.data);
346
+ }
347
+ else {
348
+ this.options.onData?.(message);
349
+ }
222
350
  }
223
- finally {
224
- reader.releaseLock();
351
+ /**
352
+ * Wait `delayMs` before the next attempt. In a browser, coming back online
353
+ * or the tab becoming visible retries at once; stop() ends the wait.
354
+ */
355
+ waitToRetry(delayMs) {
356
+ return new Promise((resolve) => {
357
+ const win = typeof window !== 'undefined' && typeof window.addEventListener === 'function' ? window : undefined;
358
+ const doc = typeof document !== 'undefined' && typeof document.addEventListener === 'function' ? document : undefined;
359
+ const onVisible = () => {
360
+ if (doc?.visibilityState === 'visible')
361
+ wake();
362
+ };
363
+ const wake = () => {
364
+ clearTimeout(timer);
365
+ win?.removeEventListener('online', wake);
366
+ doc?.removeEventListener('visibilitychange', onVisible);
367
+ if (this.wakeRetry === wake)
368
+ this.wakeRetry = null;
369
+ resolve();
370
+ };
371
+ const timer = setTimeout(wake, delayMs);
372
+ win?.addEventListener('online', wake);
373
+ doc?.addEventListener('visibilitychange', onVisible);
374
+ this.wakeRetry = wake;
375
+ });
376
+ }
377
+ stop() {
378
+ this.isRunning = false;
379
+ this.runId++;
380
+ this.abortController?.abort();
381
+ this.abortController = null;
382
+ this.wakeRetry?.();
225
383
  }
226
384
  }