@durable-streams/client 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,627 +1,1072 @@
1
+ //#region src/asyncIterableReadableStream.d.ts
1
2
  /**
2
- * Durable Streams TypeScript Client Types
3
- *
4
- * Following the Electric Durable Stream Protocol specification.
5
- */
3
+ * Async iterable polyfill for ReadableStream.
4
+ *
5
+ * Safari/iOS may not implement ReadableStream.prototype[Symbol.asyncIterator],
6
+ * preventing `for await...of` consumption. This module provides a soft polyfill
7
+ * that defines [Symbol.asyncIterator] on individual stream instances when missing,
8
+ * without patching the global prototype.
9
+ *
10
+ * The returned stream is still the original ReadableStream instance (not wrapped),
11
+ * so `instanceof ReadableStream` continues to work correctly.
12
+ *
13
+ * **Note on derived streams**: Streams created via `.pipeThrough()` or similar
14
+ * transformations will NOT be automatically patched. Use the exported
15
+ * `asAsyncIterableReadableStream()` helper to patch derived streams:
16
+ *
17
+ * ```typescript
18
+ * import { asAsyncIterableReadableStream } from "@durable-streams/client"
19
+ *
20
+ * const derived = res.bodyStream().pipeThrough(myTransform)
21
+ * const iterable = asAsyncIterableReadableStream(derived)
22
+ * for await (const chunk of iterable) { ... }
23
+ * ```
24
+ */
6
25
  /**
7
- * Offset string - opaque to the client.
8
- * Format: "<read-seq>_<byte-offset>"
9
- *
10
- * Always use the returned `offset` field from reads/follows as the next `offset` you pass in.
11
- */
26
+ * A ReadableStream that is guaranteed to be async-iterable.
27
+ *
28
+ * This intersection type ensures TypeScript knows the stream can be consumed
29
+ * via `for await...of` syntax.
30
+ */
31
+ type ReadableStreamAsyncIterable<T> = ReadableStream<T> & AsyncIterable<T>;
32
+ /**
33
+ * Ensure a ReadableStream is async-iterable.
34
+ *
35
+ * If the stream already has [Symbol.asyncIterator] defined (native or polyfilled),
36
+ * it is returned as-is. Otherwise, [Symbol.asyncIterator] is defined on the
37
+ * stream instance (not the prototype).
38
+ *
39
+ * The returned value is the same ReadableStream instance, so:
40
+ * - `stream instanceof ReadableStream` remains true
41
+ * - Any code relying on native branding/internal slots continues to work
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const stream = someApiReturningReadableStream();
46
+ * const iterableStream = asAsyncIterableReadableStream(stream);
47
+ *
48
+ * // Now works on Safari/iOS:
49
+ * for await (const chunk of iterableStream) {
50
+ * console.log(chunk);
51
+ * }
52
+ * ```
53
+ */
54
+ declare function asAsyncIterableReadableStream<T>(stream: ReadableStream<T>): ReadableStreamAsyncIterable<T>;
55
+
56
+ //#endregion
57
+ //#region src/fetch.d.ts
58
+ /**
59
+ * Options for configuring exponential backoff retry behavior.
60
+ */
61
+ interface BackoffOptions {
62
+ /**
63
+ * Initial delay before retrying in milliseconds.
64
+ */
65
+ initialDelay: number;
66
+ /**
67
+ * Maximum retry delay in milliseconds.
68
+ * After reaching this, delay stays constant.
69
+ */
70
+ maxDelay: number;
71
+ /**
72
+ * Multiplier for exponential backoff.
73
+ */
74
+ multiplier: number;
75
+ /**
76
+ * Callback invoked on each failed attempt.
77
+ */
78
+ onFailedAttempt?: () => void;
79
+ /**
80
+ * Enable debug logging.
81
+ */
82
+ debug?: boolean;
83
+ /**
84
+ * Maximum number of retry attempts before giving up.
85
+ * Set to Infinity for indefinite retries (useful for offline scenarios).
86
+ */
87
+ maxRetries?: number;
88
+ }
89
+ /**
90
+ * Default backoff options.
91
+ */
92
+ declare const BackoffDefaults: BackoffOptions;
93
+ /**
94
+ * Parse Retry-After header value and return delay in milliseconds.
95
+ * Supports both delta-seconds format and HTTP-date format.
96
+ * Returns 0 if header is not present or invalid.
97
+ */
98
+
99
+ /**
100
+ * Creates a fetch client that retries failed requests with exponential backoff.
101
+ *
102
+ * @param fetchClient - The base fetch client to wrap
103
+ * @param backoffOptions - Options for retry behavior
104
+ * @returns A fetch function with automatic retry
105
+ */
106
+ declare function createFetchWithBackoff(fetchClient: typeof fetch, backoffOptions?: BackoffOptions): typeof fetch;
107
+ /**
108
+ * Creates a fetch client that ensures the response body is fully consumed.
109
+ * This prevents issues with connection pooling when bodies aren't read.
110
+ *
111
+ * Uses arrayBuffer() instead of text() to preserve binary data integrity.
112
+ *
113
+ * @param fetchClient - The base fetch client to wrap
114
+ * @returns A fetch function that consumes response bodies
115
+ */
116
+ declare function createFetchWithConsumedBody(fetchClient: typeof fetch): typeof fetch;
117
+
118
+ //#endregion
119
+ //#region src/types.d.ts
120
+ /**
121
+ * Chains an AbortController to an optional source signal.
122
+ * If the source signal is aborted, the provided controller will also abort.
123
+ */
124
+ /**
125
+ * Offset string - opaque to the client.
126
+ * Format: "<read-seq>_<byte-offset>"
127
+ *
128
+ * **Special value**: `-1` means "start of stream" - use this to read from the beginning.
129
+ *
130
+ * Always use the returned `offset` field from reads/follows as the next `offset` you pass in.
131
+ */
12
132
  type Offset = string;
13
133
  /**
14
- * Type for values that can be provided immediately or resolved asynchronously.
15
- */
134
+ * Type for values that can be provided immediately or resolved asynchronously.
135
+ */
16
136
  type MaybePromise<T> = T | Promise<T>;
17
137
  /**
18
- * Auth configuration for requests.
19
- *
20
- * Supports:
21
- * - Fixed tokens with optional custom header name
22
- * - Arbitrary static headers
23
- * - Async header resolution (e.g., for short-lived tokens)
24
- */
25
- type Auth = {
26
- token: string;
27
- headerName?: string;
28
- } | {
29
- headers: Record<string, string>;
30
- } | {
31
- getHeaders: () => Promise<Record<string, string>>;
32
- };
33
- /**
34
- * Headers record where values can be static strings or async functions.
35
- * Following the @electric-sql/client pattern for dynamic headers.
36
- */
138
+ * Headers record where values can be static strings or async functions.
139
+ * Following the @electric-sql/client pattern for dynamic headers.
140
+ *
141
+ * **Important**: Functions are called **for each request**, not once per session.
142
+ * In live mode with long-polling, the same function may be called many times
143
+ * to fetch fresh values (e.g., refreshed auth tokens) for each poll.
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * headers: {
148
+ * Authorization: `Bearer ${token}`, // Static - same for all requests
149
+ * 'X-Tenant-Id': () => getCurrentTenant(), // Called per-request
150
+ * 'X-Auth': async () => await refreshToken() // Called per-request (can refresh)
151
+ * }
152
+ * ```
153
+ */
37
154
  type HeadersRecord = {
38
- [key: string]: string | (() => MaybePromise<string>);
155
+ [key: string]: string | (() => MaybePromise<string>);
39
156
  };
40
157
  /**
41
- * Params record where values can be static or async functions.
42
- * Following the @electric-sql/client pattern for dynamic params.
43
- */
158
+ * Params record where values can be static or async functions.
159
+ * Following the @electric-sql/client pattern for dynamic params.
160
+ *
161
+ * **Important**: Functions are called **for each request**, not once per session.
162
+ * In live mode, the same function may be called multiple times to fetch
163
+ * fresh parameter values for each poll.
164
+ */
44
165
  type ParamsRecord = {
45
- [key: string]: string | (() => MaybePromise<string>) | undefined;
166
+ [key: string]: string | (() => MaybePromise<string>) | undefined;
46
167
  };
47
168
  /**
48
- * Base options for all stream operations.
49
- */
169
+ * Live mode for reading from a stream.
170
+ * - false: Catch-up only, stop at first `upToDate`
171
+ * - "auto": Behavior driven by consumption method (default)
172
+ * - "long-poll": Explicit long-poll mode for live updates
173
+ * - "sse": Explicit server-sent events for live updates
174
+ */
175
+ type LiveMode = false | `auto` | `long-poll` | `sse`;
176
+ /**
177
+ * Options for the stream() function (read-only API).
178
+ */
50
179
  interface StreamOptions {
51
- /**
52
- * The full URL to the durable stream.
53
- * E.g., "https://streams.example.com/my-account/chat/room-1"
54
- */
55
- url: string;
56
- /**
57
- * Authentication configuration.
58
- * If using auth, you can provide:
59
- * - A token (sent as Bearer token in Authorization header by default)
60
- * - Custom headers
61
- * - An async function to get headers (for refreshing tokens)
62
- */
63
- auth?: Auth;
64
- /**
65
- * Additional headers to include in requests.
66
- * Values can be strings or functions (sync or async) that return strings.
67
- * Function values are resolved when needed, making this useful
68
- * for dynamic headers like authentication tokens.
69
- */
70
- headers?: HeadersRecord;
71
- /**
72
- * Additional query parameters to include in requests.
73
- * Values can be strings or functions (sync or async) that return strings.
74
- */
75
- params?: ParamsRecord;
76
- /**
77
- * Custom fetch implementation.
78
- * Defaults to globalThis.fetch.
79
- */
80
- fetch?: typeof globalThis.fetch;
81
- /**
82
- * Default AbortSignal for operations.
83
- * Individual operations can override this.
84
- */
85
- signal?: AbortSignal;
180
+ /**
181
+ * The full URL to the durable stream.
182
+ * E.g., "https://streams.example.com/my-account/chat/room-1"
183
+ */
184
+ url: string | URL;
185
+ /**
186
+ * HTTP headers to include in requests.
187
+ * Values can be strings or functions (sync or async) that return strings.
188
+ *
189
+ * **Important**: Functions are evaluated **per-request** (not per-session).
190
+ * In live mode, functions are called for each poll, allowing fresh values
191
+ * like refreshed auth tokens.
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * headers: {
196
+ * Authorization: `Bearer ${token}`, // Static
197
+ * 'X-Tenant-Id': () => getCurrentTenant(), // Evaluated per-request
198
+ * 'X-Auth': async () => await refreshToken() // Evaluated per-request
199
+ * }
200
+ * ```
201
+ */
202
+ headers?: HeadersRecord;
203
+ /**
204
+ * Query parameters to include in requests.
205
+ * Values can be strings or functions (sync or async) that return strings.
206
+ *
207
+ * **Important**: Functions are evaluated **per-request** (not per-session).
208
+ */
209
+ params?: ParamsRecord;
210
+ /**
211
+ * AbortSignal for cancellation.
212
+ */
213
+ signal?: AbortSignal;
214
+ /**
215
+ * Custom fetch implementation (for auth layers, proxies, etc.).
216
+ * Defaults to globalThis.fetch.
217
+ */
218
+ fetch?: typeof globalThis.fetch;
219
+ /**
220
+ * Backoff options for retry behavior.
221
+ * Defaults to exponential backoff with jitter.
222
+ */
223
+ backoffOptions?: BackoffOptions;
224
+ /**
225
+ * Starting offset (query param ?offset=...).
226
+ * If omitted, defaults to "-1" (start of stream).
227
+ * You can also explicitly pass "-1" to read from the beginning.
228
+ */
229
+ offset?: Offset;
230
+ /**
231
+ * Live mode behavior:
232
+ * - false: Catch-up only, stop at first `upToDate`
233
+ * - "auto" (default): Behavior driven by consumption method
234
+ * - "long-poll": Explicit long-poll mode for live updates
235
+ * - "sse": Explicit server-sent events for live updates
236
+ */
237
+ live?: LiveMode;
238
+ /**
239
+ * Hint: treat content as JSON even if Content-Type doesn't say so.
240
+ */
241
+ json?: boolean;
242
+ /**
243
+ * Error handler for recoverable errors (following Electric client pattern).
244
+ */
245
+ onError?: StreamErrorHandler;
246
+ /**
247
+ * SSE resilience options.
248
+ * When SSE connections fail repeatedly, the client can automatically
249
+ * fall back to long-polling mode.
250
+ */
251
+ sseResilience?: SSEResilienceOptions;
86
252
  }
87
253
  /**
88
- * Options for creating a new stream.
89
- */
90
- interface CreateOptions extends StreamOptions {
91
- /**
92
- * The content type for the stream.
93
- * This is set once on creation and cannot be changed.
94
- */
95
- contentType?: string;
96
- /**
97
- * Time-to-live in seconds (relative TTL).
98
- */
99
- ttlSeconds?: number;
100
- /**
101
- * Absolute expiry time (RFC3339 format).
102
- */
103
- expiresAt?: string;
104
- /**
105
- * Initial body to append on creation.
106
- */
107
- body?: BodyInit | Uint8Array | string;
254
+ * Options for SSE connection resilience.
255
+ */
256
+ interface SSEResilienceOptions {
257
+ /**
258
+ * Minimum expected SSE connection duration in milliseconds.
259
+ * Connections shorter than this are considered "short" and may indicate
260
+ * proxy buffering or server misconfiguration.
261
+ * @default 1000
262
+ */
263
+ minConnectionDuration?: number;
264
+ /**
265
+ * Maximum number of consecutive short connections before falling back to long-poll.
266
+ * @default 3
267
+ */
268
+ maxShortConnections?: number;
269
+ /**
270
+ * Base delay for exponential backoff between short connection retries (ms).
271
+ * @default 100
272
+ */
273
+ backoffBaseDelay?: number;
274
+ /**
275
+ * Maximum delay cap for exponential backoff (ms).
276
+ * @default 5000
277
+ */
278
+ backoffMaxDelay?: number;
279
+ /**
280
+ * Whether to log warnings when falling back to long-poll.
281
+ * @default true
282
+ */
283
+ logWarnings?: boolean;
108
284
  }
109
285
  /**
110
- * Options for appending data to a stream.
111
- */
112
- interface AppendOptions {
113
- /**
114
- * Writer coordination sequence (stream-seq header).
115
- * Monotonic, lexicographic sequence for coordinating multiple writers.
116
- * If lower than last appended seq, server returns 409 Conflict.
117
- * Not related to read offsets.
118
- */
119
- seq?: string;
120
- /**
121
- * Content type for this append.
122
- * Must match the stream's content type.
123
- */
124
- contentType?: string;
125
- /**
126
- * AbortSignal for this operation.
127
- */
128
- signal?: AbortSignal;
286
+ * Metadata for a JSON batch or chunk.
287
+ */
288
+ interface JsonBatchMeta {
289
+ /**
290
+ * Last Stream-Next-Offset for this batch.
291
+ */
292
+ offset: Offset;
293
+ /**
294
+ * True if this batch ends at the current end of the stream.
295
+ */
296
+ upToDate: boolean;
297
+ /**
298
+ * Last Stream-Cursor / streamCursor, if present.
299
+ */
300
+ cursor?: string;
129
301
  }
130
302
  /**
131
- * Live mode for reading from a stream.
132
- */
133
- type LiveMode = `catchup` | `long-poll` | `sse`;
134
- /**
135
- * Options for reading from a stream.
136
- */
137
- interface ReadOptions {
138
- /**
139
- * Starting offset, passed as ?offset=...
140
- * If omitted, reads from the start of the stream.
141
- */
142
- offset?: Offset;
143
- /**
144
- * Live mode behavior:
145
- * - undefined (default for follow()): Start catch-up, then auto-select SSE or long-poll
146
- * - "catchup": Only catch-up, stop after up-to-date
147
- * - "long-poll": Skip catch-up, start long-polling immediately
148
- * - "sse": Skip catch-up, start SSE immediately (throws if unsupported)
149
- */
150
- live?: LiveMode;
151
- /**
152
- * Override cursor for the request.
153
- * By default, the client echoes the last stream-cursor value.
154
- */
155
- cursor?: string;
156
- /**
157
- * AbortSignal for this operation.
158
- */
159
- signal?: AbortSignal;
303
+ * A batch of parsed JSON items with metadata.
304
+ */
305
+ interface JsonBatch<T = unknown> extends JsonBatchMeta {
306
+ /**
307
+ * The parsed JSON items in this batch.
308
+ */
309
+ items: ReadonlyArray<T>;
160
310
  }
161
311
  /**
162
- * Result from a HEAD request on a stream.
163
- */
164
- interface HeadResult {
165
- /**
166
- * Whether the stream exists.
167
- */
168
- exists: true;
169
- /**
170
- * The stream's content type.
171
- */
172
- contentType?: string;
173
- /**
174
- * The tail offset (next offset after current end of stream).
175
- * Provided by server as stream-offset header on HEAD.
176
- */
177
- offset?: Offset;
178
- /**
179
- * ETag for the stream (format: {internal_stream_id}:{end_offset}).
180
- */
181
- etag?: string;
182
- /**
183
- * Cache-Control header value.
184
- */
185
- cacheControl?: string;
312
+ * A chunk of raw bytes with metadata.
313
+ */
314
+ interface ByteChunk extends JsonBatchMeta {
315
+ /**
316
+ * The raw byte data.
317
+ */
318
+ data: Uint8Array;
186
319
  }
187
320
  /**
188
- * Result from a read operation.
189
- */
190
- interface ReadResult {
191
- /**
192
- * The data read from the stream.
193
- */
194
- data: Uint8Array;
195
- /**
196
- * Next offset to read from.
197
- * This is the HTTP stream-offset header value.
198
- */
199
- offset: Offset;
200
- /**
201
- * Cursor for CDN collapsing (stream-cursor header).
202
- */
203
- cursor?: string;
204
- /**
205
- * True if stream-up-to-date header was present.
206
- * Indicates the response ends at the current end of the stream.
207
- */
208
- upToDate: boolean;
209
- /**
210
- * ETag for caching.
211
- */
212
- etag?: string;
213
- /**
214
- * Content type of the data.
215
- */
216
- contentType?: string;
321
+ * A chunk of text with metadata.
322
+ */
323
+ interface TextChunk extends JsonBatchMeta {
324
+ /**
325
+ * The text content.
326
+ */
327
+ text: string;
217
328
  }
218
329
  /**
219
- * A chunk returned from follow() or toReadableStream().
220
- * Same structure as ReadResult.
221
- */
222
- interface StreamChunk extends ReadResult {
330
+ * Base options for StreamHandle operations.
331
+ */
332
+ interface StreamHandleOptions {
333
+ /**
334
+ * The full URL to the durable stream.
335
+ * E.g., "https://streams.example.com/my-account/chat/room-1"
336
+ */
337
+ url: string | URL;
338
+ /**
339
+ * HTTP headers to include in requests.
340
+ * Values can be strings or functions (sync or async) that return strings.
341
+ *
342
+ * Functions are evaluated **per-request** (not per-session).
343
+ */
344
+ headers?: HeadersRecord;
345
+ /**
346
+ * Query parameters to include in requests.
347
+ * Values can be strings or functions (sync or async) that return strings.
348
+ *
349
+ * Functions are evaluated **per-request** (not per-session).
350
+ */
351
+ params?: ParamsRecord;
352
+ /**
353
+ * Custom fetch implementation.
354
+ * Defaults to globalThis.fetch.
355
+ */
356
+ fetch?: typeof globalThis.fetch;
357
+ /**
358
+ * Default AbortSignal for operations.
359
+ */
360
+ signal?: AbortSignal;
361
+ /**
362
+ * The content type for the stream.
363
+ */
364
+ contentType?: string;
365
+ /**
366
+ * Error handler for recoverable errors.
367
+ */
368
+ onError?: StreamErrorHandler;
369
+ /**
370
+ * Enable automatic batching for append() calls.
371
+ * When true, multiple append() calls made while a POST is in-flight
372
+ * will be batched together into a single request.
373
+ *
374
+ * @default true
375
+ */
376
+ batching?: boolean;
223
377
  }
224
378
  /**
225
- * Error codes for DurableStreamError.
226
- */
227
- type DurableStreamErrorCode = `NOT_FOUND` | `CONFLICT_SEQ` | `CONFLICT_EXISTS` | `BAD_REQUEST` | `BUSY` | `SSE_NOT_SUPPORTED` | `UNAUTHORIZED` | `FORBIDDEN` | `RATE_LIMITED` | `UNKNOWN`;
379
+ * Options for creating a new stream.
380
+ */
381
+ interface CreateOptions extends StreamHandleOptions {
382
+ /**
383
+ * Time-to-live in seconds (relative TTL).
384
+ */
385
+ ttlSeconds?: number;
386
+ /**
387
+ * Absolute expiry time (RFC3339 format).
388
+ */
389
+ expiresAt?: string;
390
+ /**
391
+ * Initial body to append on creation.
392
+ */
393
+ body?: BodyInit | Uint8Array | string;
394
+ /**
395
+ * Enable automatic batching for append() calls.
396
+ * When true, multiple append() calls made while a POST is in-flight
397
+ * will be batched together into a single request.
398
+ *
399
+ * @default true
400
+ */
401
+ batching?: boolean;
402
+ }
228
403
  /**
229
- * Options returned from onError handler to retry with modified params/headers.
230
- * Following the Electric client pattern.
231
- */
232
- type RetryOpts = {
233
- params?: ParamsRecord;
234
- headers?: HeadersRecord;
235
- };
404
+ * Options for appending data to a stream.
405
+ */
406
+ interface AppendOptions {
407
+ /**
408
+ * Writer coordination sequence (stream-seq header).
409
+ * Monotonic, lexicographic sequence for coordinating multiple writers.
410
+ * If lower than last appended seq, server returns 409 Conflict.
411
+ * Not related to read offsets.
412
+ */
413
+ seq?: string;
414
+ /**
415
+ * Content type for this append.
416
+ * Must match the stream's content type.
417
+ */
418
+ contentType?: string;
419
+ /**
420
+ * AbortSignal for this operation.
421
+ */
422
+ signal?: AbortSignal;
423
+ }
236
424
  /**
237
- * Error handler callback type.
238
- *
239
- * Called when a recoverable error occurs during streaming.
240
- *
241
- * **Return value behavior** (following Electric client pattern):
242
- * - Return `{}` to retry with the same params/headers
243
- * - Return `{ params }` to retry with merged params (existing params are preserved)
244
- * - Return `{ headers }` to retry with merged headers (existing headers are preserved)
245
- * - Return `void`/`undefined` to stop the stream and propagate the error
246
- *
247
- * Note: Automatic retries with exponential backoff are already applied
248
- * for 5xx server errors, network errors, and 429 rate limits before
249
- * this handler is called.
250
- *
251
- * @example
252
- * ```typescript
253
- * // Retry on any error
254
- * onError: (error) => ({})
255
- *
256
- * // Refresh auth token on 401
257
- * onError: async (error) => {
258
- * if (error instanceof FetchError && error.status === 401) {
259
- * const newToken = await refreshAuthToken()
260
- * return { headers: { Authorization: `Bearer ${newToken}` } }
261
- * }
262
- * // Don't retry other errors
263
- * }
264
- * ```
265
- */
266
- type StreamErrorHandler = (error: Error) => void | RetryOpts | Promise<void | RetryOpts>;
267
-
425
+ * Legacy live mode type (internal use only).
426
+ * @internal
427
+ */
428
+ type LegacyLiveMode = `long-poll` | `sse`;
268
429
  /**
269
- * Fetch utilities with retry and backoff support.
270
- * Based on @electric-sql/client patterns.
271
- */
430
+ * Options for reading from a stream (internal iterator options).
431
+ * @internal
432
+ */
433
+ interface ReadOptions {
434
+ /**
435
+ * Starting offset, passed as ?offset=...
436
+ * If omitted, defaults to "-1" (start of stream).
437
+ */
438
+ offset?: Offset;
439
+ /**
440
+ * Live mode behavior:
441
+ * - undefined/true (default): Catch-up then auto-select SSE or long-poll for live updates
442
+ * - false: Only catch-up, stop after up-to-date (no live updates)
443
+ * - "long-poll": Use long-polling for live updates
444
+ * - "sse": Use SSE for live updates (throws if unsupported)
445
+ */
446
+ live?: boolean | LegacyLiveMode;
447
+ /**
448
+ * Override cursor for the request.
449
+ * By default, the client echoes the last stream-cursor value.
450
+ */
451
+ cursor?: string;
452
+ /**
453
+ * AbortSignal for this operation.
454
+ */
455
+ signal?: AbortSignal;
456
+ }
272
457
  /**
273
- * Options for configuring exponential backoff retry behavior.
274
- */
275
- interface BackoffOptions {
276
- /**
277
- * Initial delay before retrying in milliseconds.
278
- */
279
- initialDelay: number;
280
- /**
281
- * Maximum retry delay in milliseconds.
282
- * After reaching this, delay stays constant.
283
- */
284
- maxDelay: number;
285
- /**
286
- * Multiplier for exponential backoff.
287
- */
288
- multiplier: number;
289
- /**
290
- * Callback invoked on each failed attempt.
291
- */
292
- onFailedAttempt?: () => void;
293
- /**
294
- * Enable debug logging.
295
- */
296
- debug?: boolean;
297
- /**
298
- * Maximum number of retry attempts before giving up.
299
- * Set to Infinity for indefinite retries (useful for offline scenarios).
300
- */
301
- maxRetries?: number;
458
+ * Result from a HEAD request on a stream.
459
+ */
460
+ interface HeadResult {
461
+ /**
462
+ * Whether the stream exists.
463
+ */
464
+ exists: true;
465
+ /**
466
+ * The stream's content type.
467
+ */
468
+ contentType?: string;
469
+ /**
470
+ * The tail offset (next offset after current end of stream).
471
+ * Provided by server as stream-offset header on HEAD.
472
+ */
473
+ offset?: Offset;
474
+ /**
475
+ * ETag for the stream (format: {internal_stream_id}:{end_offset}).
476
+ */
477
+ etag?: string;
478
+ /**
479
+ * Cache-Control header value.
480
+ */
481
+ cacheControl?: string;
302
482
  }
303
483
  /**
304
- * Default backoff options.
305
- */
306
- declare const BackoffDefaults: BackoffOptions;
484
+ * Metadata extracted from a stream response.
485
+ * Contains headers and control information from the stream server.
486
+ */
487
+
307
488
  /**
308
- * Creates a fetch client that retries failed requests with exponential backoff.
309
- *
310
- * @param fetchClient - The base fetch client to wrap
311
- * @param backoffOptions - Options for retry behavior
312
- * @returns A fetch function with automatic retry
313
- */
314
- declare function createFetchWithBackoff(fetchClient: typeof fetch, backoffOptions?: BackoffOptions): typeof fetch;
489
+ * Error codes for DurableStreamError.
490
+ */
491
+ type DurableStreamErrorCode = `NOT_FOUND` | `CONFLICT_SEQ` | `CONFLICT_EXISTS` | `BAD_REQUEST` | `BUSY` | `SSE_NOT_SUPPORTED` | `UNAUTHORIZED` | `FORBIDDEN` | `RATE_LIMITED` | `ALREADY_CONSUMED` | `ALREADY_CLOSED` | `UNKNOWN`;
315
492
  /**
316
- * Creates a fetch client that ensures the response body is fully consumed.
317
- * This prevents issues with connection pooling when bodies aren't read.
318
- *
319
- * Uses arrayBuffer() instead of text() to preserve binary data integrity.
320
- *
321
- * @param fetchClient - The base fetch client to wrap
322
- * @returns A fetch function that consumes response bodies
323
- */
324
- declare function createFetchWithConsumedBody(fetchClient: typeof fetch): typeof fetch;
325
-
493
+ * Options returned from onError handler to retry with modified params/headers.
494
+ * Following the Electric client pattern.
495
+ */
496
+ type RetryOpts = {
497
+ params?: ParamsRecord;
498
+ headers?: HeadersRecord;
499
+ };
326
500
  /**
327
- * DurableStream - A handle to a remote durable stream.
328
- *
329
- * Following the Electric Durable Stream Protocol specification.
330
- */
501
+ * Error handler callback type.
502
+ *
503
+ * Called when a recoverable error occurs during streaming.
504
+ *
505
+ * **Return value behavior** (following Electric client pattern):
506
+ * - Return `{}` (empty object) → Retry immediately with same params/headers
507
+ * - Return `{ params }` → Retry with merged params (existing params preserved)
508
+ * - Return `{ headers }` → Retry with merged headers (existing headers preserved)
509
+ * - Return `void` or `undefined` → Stop stream and propagate the error
510
+ * - Return `null` → INVALID (will cause error - use `{}` instead)
511
+ *
512
+ * **Important**: To retry, you MUST return an object (can be empty `{}`).
513
+ * Returning nothing (`void`), explicitly returning `undefined`, or omitting
514
+ * a return statement all stop the stream. Do NOT return `null`.
515
+ *
516
+ * Note: Automatic retries with exponential backoff are already applied
517
+ * for 5xx server errors, network errors, and 429 rate limits before
518
+ * this handler is called.
519
+ *
520
+ * @example
521
+ * ```typescript
522
+ * // Retry on any error (returns empty object)
523
+ * onError: (error) => ({})
524
+ *
525
+ * // Refresh auth token on 401, propagate other errors
526
+ * onError: async (error) => {
527
+ * if (error instanceof FetchError && error.status === 401) {
528
+ * const newToken = await refreshAuthToken()
529
+ * return { headers: { Authorization: `Bearer ${newToken}` } }
530
+ * }
531
+ * // Implicitly returns undefined - error will propagate
532
+ * }
533
+ *
534
+ * // Conditionally retry with explicit propagation
535
+ * onError: (error) => {
536
+ * if (shouldRetry(error)) {
537
+ * return {} // Retry
538
+ * }
539
+ * return undefined // Explicitly propagate error
540
+ * }
541
+ * ```
542
+ */
543
+ type StreamErrorHandler = (error: Error) => void | RetryOpts | Promise<void | RetryOpts>;
544
+ /**
545
+ * A streaming session returned by stream() or DurableStream.stream().
546
+ *
547
+ * Represents a live session with fixed `url`, `offset`, and `live` parameters.
548
+ * Supports multiple consumption styles: Promise helpers, ReadableStreams,
549
+ * and Subscribers.
550
+ *
551
+ * @typeParam TJson - The type of JSON items in the stream.
552
+ */
553
+ interface StreamResponse<TJson = unknown> {
554
+ /**
555
+ * The stream URL.
556
+ */
557
+ readonly url: string;
558
+ /**
559
+ * The stream's content type (from first response).
560
+ */
561
+ readonly contentType?: string;
562
+ /**
563
+ * The live mode for this session.
564
+ */
565
+ readonly live: LiveMode;
566
+ /**
567
+ * The starting offset for this session.
568
+ */
569
+ readonly startOffset: Offset;
570
+ /**
571
+ * HTTP response headers from the most recent server response.
572
+ * Updated on each long-poll/SSE response.
573
+ */
574
+ readonly headers: Headers;
575
+ /**
576
+ * HTTP status code from the most recent server response.
577
+ * Updated on each long-poll/SSE response.
578
+ */
579
+ readonly status: number;
580
+ /**
581
+ * HTTP status text from the most recent server response.
582
+ * Updated on each long-poll/SSE response.
583
+ */
584
+ readonly statusText: string;
585
+ /**
586
+ * Whether the most recent response was successful (status 200-299).
587
+ * Always true for active streams (errors are thrown).
588
+ */
589
+ readonly ok: boolean;
590
+ /**
591
+ * Whether the stream is waiting for initial data.
592
+ *
593
+ * Note: Always false in current implementation because stream() awaits
594
+ * the first response before returning. A future async iterator API
595
+ * could expose this as true during initial connection.
596
+ */
597
+ readonly isLoading: boolean;
598
+ /**
599
+ * The next offset to read from (Stream-Next-Offset header).
600
+ *
601
+ * **Important**: This value advances **after data is delivered to the consumer**,
602
+ * not just after fetching from the server. The offset represents the position
603
+ * in the stream that follows the data most recently provided to your consumption
604
+ * method (body(), json(), bodyStream(), subscriber callback, etc.).
605
+ *
606
+ * Use this for resuming reads after a disconnect or saving checkpoints.
607
+ */
608
+ offset: Offset;
609
+ /**
610
+ * Stream cursor for CDN collapsing (stream-cursor header).
611
+ *
612
+ * Updated after each chunk is delivered to the consumer.
613
+ */
614
+ cursor?: string;
615
+ /**
616
+ * Whether we've reached the current end of the stream (stream-up-to-date header).
617
+ *
618
+ * Updated after each chunk is delivered to the consumer.
619
+ */
620
+ upToDate: boolean;
621
+ /**
622
+ * Accumulate raw bytes until first `upToDate` batch, then resolve.
623
+ * When used with `live: "auto"`, signals the session to stop after upToDate.
624
+ */
625
+ body: () => Promise<Uint8Array>;
626
+ /**
627
+ * Accumulate JSON *items* across batches into a single array, resolve at `upToDate`.
628
+ * Only valid in JSON-mode; throws otherwise.
629
+ * When used with `live: "auto"`, signals the session to stop after upToDate.
630
+ */
631
+ json: <T = TJson>() => Promise<Array<T>>;
632
+ /**
633
+ * Accumulate text chunks into a single string, resolve at `upToDate`.
634
+ * When used with `live: "auto"`, signals the session to stop after upToDate.
635
+ */
636
+ text: () => Promise<string>;
637
+ /**
638
+ * Raw bytes as a ReadableStream<Uint8Array>.
639
+ *
640
+ * The returned stream is guaranteed to be async-iterable, so you can use
641
+ * `for await...of` syntax even on Safari/iOS which may lack native support.
642
+ */
643
+ bodyStream: () => ReadableStreamAsyncIterable<Uint8Array>;
644
+ /**
645
+ * Individual JSON items (flattened) as a ReadableStream<TJson>.
646
+ * Built on jsonBatches().
647
+ *
648
+ * The returned stream is guaranteed to be async-iterable, so you can use
649
+ * `for await...of` syntax even on Safari/iOS which may lack native support.
650
+ */
651
+ jsonStream: () => ReadableStreamAsyncIterable<TJson>;
652
+ /**
653
+ * Text chunks as ReadableStream<string>.
654
+ *
655
+ * The returned stream is guaranteed to be async-iterable, so you can use
656
+ * `for await...of` syntax even on Safari/iOS which may lack native support.
657
+ */
658
+ textStream: () => ReadableStreamAsyncIterable<string>;
659
+ /**
660
+ * Subscribe to JSON batches as they arrive.
661
+ * Returns unsubscribe function.
662
+ */
663
+ subscribeJson: <T = TJson>(subscriber: (batch: JsonBatch<T>) => Promise<void>) => () => void;
664
+ /**
665
+ * Subscribe to raw byte chunks as they arrive.
666
+ * Returns unsubscribe function.
667
+ */
668
+ subscribeBytes: (subscriber: (chunk: ByteChunk) => Promise<void>) => () => void;
669
+ /**
670
+ * Subscribe to text chunks as they arrive.
671
+ * Returns unsubscribe function.
672
+ */
673
+ subscribeText: (subscriber: (chunk: TextChunk) => Promise<void>) => () => void;
674
+ /**
675
+ * Cancel the underlying session (abort HTTP, close SSE, stop long-polls).
676
+ */
677
+ cancel: (reason?: unknown) => void;
678
+ /**
679
+ * Resolves when the session has fully closed:
680
+ * - `live:false` and up-to-date reached,
681
+ * - manual cancellation,
682
+ * - terminal error.
683
+ */
684
+ readonly closed: Promise<void>;
685
+ } //#endregion
686
+ //#region src/stream-api.d.ts
687
+ /**
688
+ * Create a streaming session to read from a durable stream.
689
+ *
690
+ * This is a fetch-like API:
691
+ * - The promise resolves after the first network request succeeds
692
+ * - It rejects for auth/404/other protocol errors
693
+ * - Returns a StreamResponse for consuming the data
694
+ *
695
+ * @example
696
+ * ```typescript
697
+ * // Catch-up JSON:
698
+ * const res = await stream<{ message: string }>({
699
+ * url,
700
+ * auth,
701
+ * offset: "0",
702
+ * live: false,
703
+ * })
704
+ * const items = await res.json()
705
+ *
706
+ * // Live JSON:
707
+ * const live = await stream<{ message: string }>({
708
+ * url,
709
+ * auth,
710
+ * offset: savedOffset,
711
+ * live: "auto",
712
+ * })
713
+ * live.subscribeJson(async (batch) => {
714
+ * for (const item of batch.items) {
715
+ * handle(item)
716
+ * }
717
+ * })
718
+ * ```
719
+ */
720
+ declare function stream<TJson = unknown>(options: StreamOptions): Promise<StreamResponse<TJson>>;
331
721
 
722
+ //#endregion
723
+ //#region src/stream.d.ts
332
724
  /**
333
- * Options for DurableStream constructor.
334
- */
335
- interface DurableStreamOptions extends StreamOptions {
336
- /**
337
- * Backoff options for retry behavior.
338
- */
339
- backoffOptions?: BackoffOptions;
340
- /**
341
- * Error handler for recoverable errors.
342
- *
343
- * **Automatic retries**: The client automatically retries 5xx server errors, network
344
- * errors, and 429 rate limits with exponential backoff. The `onError` callback is
345
- * only invoked after these automatic retries are exhausted, or for non-retryable errors.
346
- *
347
- * **Return value behavior** (following Electric client pattern):
348
- * - Return `{}` to retry with the same params/headers
349
- * - Return `{ params }` to retry with merged params
350
- * - Return `{ headers }` to retry with merged headers
351
- * - Return `void`/`undefined` to stop the stream and propagate the error
352
- *
353
- * @example
354
- * ```typescript
355
- * // Refresh auth token on 401
356
- * onError: async (error) => {
357
- * if (error instanceof FetchError && error.status === 401) {
358
- * const newToken = await refreshAuthToken()
359
- * return { headers: { Authorization: `Bearer ${newToken}` } }
360
- * }
361
- * }
362
- * ```
363
- */
364
- onError?: StreamErrorHandler;
725
+ * Options for DurableStream constructor.
726
+ */
727
+ interface DurableStreamOptions extends StreamHandleOptions {
728
+ /**
729
+ * Additional query parameters to include in requests.
730
+ */
731
+ params?: {
732
+ [key: string]: string | (() => MaybePromise<string>) | undefined;
733
+ };
734
+ /**
735
+ * Backoff options for retry behavior.
736
+ */
737
+ backoffOptions?: BackoffOptions;
738
+ /**
739
+ * Enable automatic batching for append() calls.
740
+ * When true, multiple append() calls made while a POST is in-flight
741
+ * will be batched together into a single request.
742
+ *
743
+ * @default true
744
+ */
745
+ batching?: boolean;
365
746
  }
366
747
  /**
367
- * A handle to a remote durable stream.
368
- *
369
- * This is a lightweight, reusable handle - not a persistent connection.
370
- * It does not automatically start reading or listening.
371
- * Create sessions as needed via read(), follow(), or toReadableStream().
372
- *
373
- * @example
374
- * ```typescript
375
- * // Create a handle without any network IO
376
- * const stream = new DurableStream({
377
- * url: "https://streams.example.com/my-stream",
378
- * auth: { token: "my-token" }
379
- * });
380
- *
381
- * // One-shot read
382
- * const result = await stream.read({ offset: savedOffset });
383
- *
384
- * // Follow for live updates
385
- * for await (const chunk of stream.follow()) {
386
- * console.log(new TextDecoder().decode(chunk.data));
387
- * }
388
- * ```
389
- */
748
+ * A handle to a remote durable stream for read/write operations.
749
+ *
750
+ * This is a lightweight, reusable handle - not a persistent connection.
751
+ * It does not automatically start reading or listening.
752
+ * Create sessions as needed via stream().
753
+ *
754
+ * @example
755
+ * ```typescript
756
+ * // Create a new stream
757
+ * const stream = await DurableStream.create({
758
+ * url: "https://streams.example.com/my-stream",
759
+ * headers: { Authorization: "Bearer my-token" },
760
+ * contentType: "application/json"
761
+ * });
762
+ *
763
+ * // Write data
764
+ * await stream.append({ message: "hello" });
765
+ *
766
+ * // Read with the new API
767
+ * const res = await stream.stream<{ message: string }>();
768
+ * res.subscribeJson(async (batch) => {
769
+ * for (const item of batch.items) {
770
+ * console.log(item.message);
771
+ * }
772
+ * });
773
+ * ```
774
+ */
390
775
  declare class DurableStream {
391
- #private;
392
- /**
393
- * The URL of the durable stream.
394
- */
395
- readonly url: string;
396
- /**
397
- * The content type of the stream (populated after connect/head/read).
398
- */
399
- contentType?: string;
400
- /**
401
- * Create a cold handle to a stream.
402
- * No network IO is performed by the constructor.
403
- */
404
- constructor(opts: DurableStreamOptions);
405
- /**
406
- * Create a new stream (create-only PUT) and return a handle.
407
- * Fails with DurableStreamError(code="CONFLICT_EXISTS") if it already exists.
408
- */
409
- static create(opts: CreateOptions): Promise<DurableStream>;
410
- /**
411
- * Validate that a stream exists and fetch metadata via HEAD.
412
- * Returns a handle with contentType populated (if sent by server).
413
- */
414
- static connect(opts: StreamOptions): Promise<DurableStream>;
415
- /**
416
- * HEAD metadata for a stream without creating a handle.
417
- */
418
- static head(opts: StreamOptions): Promise<HeadResult>;
419
- /**
420
- * Delete a stream without creating a handle.
421
- */
422
- static delete(opts: StreamOptions): Promise<void>;
423
- /**
424
- * HEAD metadata for this stream.
425
- */
426
- head(opts?: {
427
- signal?: AbortSignal;
428
- }): Promise<HeadResult>;
429
- /**
430
- * Create this stream (create-only PUT) using the URL/auth from the handle.
431
- */
432
- create(opts?: Omit<CreateOptions, keyof StreamOptions>): Promise<this>;
433
- /**
434
- * Delete this stream.
435
- */
436
- delete(opts?: {
437
- signal?: AbortSignal;
438
- }): Promise<void>;
439
- /**
440
- * Append a single payload to the stream.
441
- *
442
- * - `body` may be Uint8Array, string, or any Fetch BodyInit.
443
- * - Strings are encoded as UTF-8.
444
- * - `seq` (if provided) is sent as stream-seq (writer coordination).
445
- */
446
- append(body: BodyInit | Uint8Array | string, opts?: AppendOptions): Promise<void>;
447
- /**
448
- * Append a streaming body to the stream.
449
- *
450
- * - `source` yields Uint8Array or string chunks.
451
- * - Strings are encoded as UTF-8; no delimiters are added.
452
- * - Internally uses chunked transfer or HTTP/2 streaming.
453
- */
454
- appendStream(source: ReadableStream<Uint8Array | string> | AsyncIterable<Uint8Array | string>, opts?: AppendOptions): Promise<void>;
455
- /**
456
- * One-shot read.
457
- *
458
- * Performs a single GET from the specified offset/mode and returns a chunk.
459
- * Caller is responsible for persisting the returned offset if they want to resume.
460
- */
461
- read(opts?: ReadOptions): Promise<ReadResult>;
462
- /**
463
- * Follow the stream as an AsyncIterable of chunks.
464
- *
465
- * Default behaviour:
466
- * - From `offset` (or start if omitted), repeatedly perform catch-up reads
467
- * until a chunk with upToDate=true.
468
- * - Then switch to live mode:
469
- * - SSE if content-type is text/* or application/json;
470
- * - otherwise long-poll.
471
- *
472
- * Explicit live override:
473
- * - live="catchup": only catch-up, stop at upToDate.
474
- * - live="long-poll": start long-polling immediately from offset.
475
- * - live="sse": start SSE immediately (throws if SSE not supported).
476
- */
477
- follow(opts?: ReadOptions): AsyncIterable<StreamChunk>;
478
- /**
479
- * Wrap follow() in a Web ReadableStream for piping.
480
- *
481
- * Backpressure:
482
- * - One chunk is pulled from follow() per pull() call, so standard
483
- * Web Streams backpressure semantics apply.
484
- *
485
- * Cancellation:
486
- * - rs.cancel() will stop follow() and abort any in-flight request.
487
- */
488
- toReadableStream(opts?: ReadOptions & {
489
- signal?: AbortSignal;
490
- }): ReadableStream<StreamChunk>;
491
- /**
492
- * Wrap follow() in a Web ReadableStream<Uint8Array> for piping raw bytes.
493
- *
494
- * This is the native format for many web stream APIs.
495
- */
496
- toByteStream(opts?: ReadOptions & {
497
- signal?: AbortSignal;
498
- }): ReadableStream<Uint8Array>;
499
- /**
500
- * Convenience: interpret data as JSON messages.
501
- * Parses each chunk's data as JSON and yields the parsed values.
502
- */
503
- json<T = unknown>(opts?: ReadOptions): AsyncIterable<T>;
504
- /**
505
- * Convenience: interpret data as text (UTF-8).
506
- */
507
- text(opts?: ReadOptions & {
508
- decoder?: TextDecoder;
509
- }): AsyncIterable<string>;
776
+ #private;
777
+ /**
778
+ * The URL of the durable stream.
779
+ */
780
+ readonly url: string;
781
+ /**
782
+ * The content type of the stream (populated after connect/head/read).
783
+ */
784
+ contentType?: string;
785
+ /**
786
+ * Create a cold handle to a stream.
787
+ * No network IO is performed by the constructor.
788
+ */
789
+ constructor(opts: DurableStreamOptions);
790
+ /**
791
+ * Create a new stream (create-only PUT) and return a handle.
792
+ * Fails with DurableStreamError(code="CONFLICT_EXISTS") if it already exists.
793
+ */
794
+ static create(opts: CreateOptions): Promise<DurableStream>;
795
+ /**
796
+ * Validate that a stream exists and fetch metadata via HEAD.
797
+ * Returns a handle with contentType populated (if sent by server).
798
+ *
799
+ * **Important**: This only performs a HEAD request for validation - it does
800
+ * NOT open a session or start reading data. To read from the stream, call
801
+ * `stream()` on the returned handle.
802
+ *
803
+ * @example
804
+ * ```typescript
805
+ * // Validate stream exists before reading
806
+ * const handle = await DurableStream.connect({ url })
807
+ * const res = await handle.stream() // Now actually read
808
+ * ```
809
+ */
810
+ static connect(opts: DurableStreamOptions): Promise<DurableStream>;
811
+ /**
812
+ * HEAD metadata for a stream without creating a handle.
813
+ */
814
+ static head(opts: DurableStreamOptions): Promise<HeadResult>;
815
+ /**
816
+ * Delete a stream without creating a handle.
817
+ */
818
+ static delete(opts: DurableStreamOptions): Promise<void>;
819
+ /**
820
+ * HEAD metadata for this stream.
821
+ */
822
+ head(opts?: {
823
+ signal?: AbortSignal;
824
+ }): Promise<HeadResult>;
825
+ /**
826
+ * Create this stream (create-only PUT) using the URL/auth from the handle.
827
+ */
828
+ create(opts?: Omit<CreateOptions, keyof StreamOptions>): Promise<this>;
829
+ /**
830
+ * Delete this stream.
831
+ */
832
+ delete(opts?: {
833
+ signal?: AbortSignal;
834
+ }): Promise<void>;
835
+ /**
836
+ * Append a single payload to the stream.
837
+ *
838
+ * When batching is enabled (default), multiple append() calls made while
839
+ * a POST is in-flight will be batched together into a single request.
840
+ * This significantly improves throughput for high-frequency writes.
841
+ *
842
+ * - `body` may be Uint8Array, string, or any JSON-serializable value (for JSON streams).
843
+ * - `body` may also be a Promise that resolves to any of the above types.
844
+ * - Strings are encoded as UTF-8.
845
+ * - `seq` (if provided) is sent as stream-seq (writer coordination).
846
+ *
847
+ * @example
848
+ * ```typescript
849
+ * // Direct value
850
+ * await stream.append({ message: "hello" });
851
+ *
852
+ * // Promise value - awaited before buffering
853
+ * await stream.append(fetchData());
854
+ * await stream.append(Promise.all([a, b, c]));
855
+ * ```
856
+ */
857
+ append(body: BodyInit | Uint8Array | string | unknown, opts?: AppendOptions): Promise<void>;
858
+ /**
859
+ * Append a streaming body to the stream.
860
+ *
861
+ * Supports piping from any ReadableStream or async iterable:
862
+ * - `source` yields Uint8Array or string chunks.
863
+ * - Strings are encoded as UTF-8; no delimiters are added.
864
+ * - Internally uses chunked transfer or HTTP/2 streaming.
865
+ *
866
+ * @example
867
+ * ```typescript
868
+ * // Pipe from a ReadableStream
869
+ * const readable = new ReadableStream({
870
+ * start(controller) {
871
+ * controller.enqueue("chunk 1");
872
+ * controller.enqueue("chunk 2");
873
+ * controller.close();
874
+ * }
875
+ * });
876
+ * await stream.appendStream(readable);
877
+ *
878
+ * // Pipe from an async generator
879
+ * async function* generate() {
880
+ * yield "line 1\n";
881
+ * yield "line 2\n";
882
+ * }
883
+ * await stream.appendStream(generate());
884
+ *
885
+ * // Pipe from fetch response body
886
+ * const response = await fetch("https://example.com/data");
887
+ * await stream.appendStream(response.body!);
888
+ * ```
889
+ */
890
+ appendStream(source: ReadableStream<Uint8Array | string> | AsyncIterable<Uint8Array | string>, opts?: AppendOptions): Promise<void>;
891
+ /**
892
+ * Create a writable stream that pipes data to this durable stream.
893
+ *
894
+ * Returns a WritableStream that can be used with `pipeTo()` or
895
+ * `pipeThrough()` from any ReadableStream source.
896
+ *
897
+ * @example
898
+ * ```typescript
899
+ * // Pipe from fetch response
900
+ * const response = await fetch("https://example.com/data");
901
+ * await response.body!.pipeTo(stream.writable());
902
+ *
903
+ * // Pipe through a transform
904
+ * const readable = someStream.pipeThrough(new TextEncoderStream());
905
+ * await readable.pipeTo(stream.writable());
906
+ * ```
907
+ */
908
+ writable(opts?: AppendOptions): WritableStream<Uint8Array | string>;
909
+ /**
910
+ * Start a fetch-like streaming session against this handle's URL/headers/params.
911
+ * The first request is made inside this method; it resolves when we have
912
+ * a valid first response, or rejects on errors.
913
+ *
914
+ * Call-specific headers and params are merged with handle-level ones,
915
+ * with call-specific values taking precedence.
916
+ *
917
+ * @example
918
+ * ```typescript
919
+ * const handle = await DurableStream.connect({
920
+ * url,
921
+ * headers: { Authorization: `Bearer ${token}` }
922
+ * });
923
+ * const res = await handle.stream<{ message: string }>();
924
+ *
925
+ * // Accumulate all JSON items
926
+ * const items = await res.json();
927
+ *
928
+ * // Or stream live with ReadableStream
929
+ * const reader = res.jsonStream().getReader();
930
+ * let result = await reader.read();
931
+ * while (!result.done) {
932
+ * console.log(result.value);
933
+ * result = await reader.read();
934
+ * }
935
+ *
936
+ * // Or use subscriber for backpressure-aware consumption
937
+ * res.subscribeJson(async (batch) => {
938
+ * for (const item of batch.items) {
939
+ * console.log(item);
940
+ * }
941
+ * });
942
+ * ```
943
+ */
944
+ stream<TJson = unknown>(options?: Omit<StreamOptions, `url`>): Promise<StreamResponse<TJson>>;
510
945
  }
511
946
 
947
+ //#endregion
948
+ //#region src/error.d.ts
512
949
  /**
513
- * Error thrown for transport/network errors.
514
- * Following the @electric-sql/client FetchError pattern.
515
- */
950
+ * Error thrown for transport/network errors.
951
+ * Following the @electric-sql/client FetchError pattern.
952
+ */
516
953
  declare class FetchError extends Error {
517
- url: string;
518
- status: number;
519
- text?: string;
520
- json?: object;
521
- headers: Record<string, string>;
522
- constructor(status: number, text: string | undefined, json: object | undefined, headers: Record<string, string>, url: string, message?: string);
523
- static fromResponse(response: Response, url: string): Promise<FetchError>;
954
+ url: string;
955
+ status: number;
956
+ text?: string;
957
+ json?: object;
958
+ headers: Record<string, string>;
959
+ constructor(status: number, text: string | undefined, json: object | undefined, headers: Record<string, string>, url: string, message?: string);
960
+ static fromResponse(response: Response, url: string): Promise<FetchError>;
524
961
  }
525
962
  /**
526
- * Error thrown when a fetch operation is aborted during backoff.
527
- */
963
+ * Error thrown when a fetch operation is aborted during backoff.
964
+ */
528
965
  declare class FetchBackoffAbortError extends Error {
529
- constructor();
966
+ constructor();
530
967
  }
531
968
  /**
532
- * Protocol-level error for Durable Streams operations.
533
- * Provides structured error handling with error codes.
534
- */
969
+ * Protocol-level error for Durable Streams operations.
970
+ * Provides structured error handling with error codes.
971
+ */
535
972
  declare class DurableStreamError extends Error {
536
- /**
537
- * HTTP status code, if applicable.
538
- */
539
- status?: number;
540
- /**
541
- * Structured error code for programmatic handling.
542
- */
543
- code: DurableStreamErrorCode;
544
- /**
545
- * Additional error details (e.g., raw response body).
546
- */
547
- details?: unknown;
548
- constructor(message: string, code: DurableStreamErrorCode, status?: number, details?: unknown);
549
- /**
550
- * Create a DurableStreamError from an HTTP response.
551
- */
552
- static fromResponse(response: Response, url: string): Promise<DurableStreamError>;
553
- /**
554
- * Create a DurableStreamError from a FetchError.
555
- */
556
- static fromFetchError(error: FetchError): DurableStreamError;
973
+ /**
974
+ * HTTP status code, if applicable.
975
+ */
976
+ status?: number;
977
+ /**
978
+ * Structured error code for programmatic handling.
979
+ */
980
+ code: DurableStreamErrorCode;
981
+ /**
982
+ * Additional error details (e.g., raw response body).
983
+ */
984
+ details?: unknown;
985
+ constructor(message: string, code: DurableStreamErrorCode, status?: number, details?: unknown);
986
+ /**
987
+ * Create a DurableStreamError from an HTTP response.
988
+ */
989
+ static fromResponse(response: Response, url: string): Promise<DurableStreamError>;
990
+ /**
991
+ * Create a DurableStreamError from a FetchError.
992
+ */
993
+ static fromFetchError(error: FetchError): DurableStreamError;
557
994
  }
558
995
  /**
559
- * Error thrown when stream URL is missing.
560
- */
996
+ * Error thrown when stream URL is missing.
997
+ */
561
998
  declare class MissingStreamUrlError extends Error {
562
- constructor();
999
+ constructor();
563
1000
  }
564
1001
  /**
565
- * Error thrown when signal option is invalid.
566
- */
1002
+ * Error thrown when signal option is invalid.
1003
+ */
567
1004
  declare class InvalidSignalError extends Error {
568
- constructor();
1005
+ constructor();
569
1006
  }
570
1007
 
1008
+ //#endregion
1009
+ //#region src/constants.d.ts
1010
+ /**
1011
+ * Durable Streams Protocol Constants
1012
+ *
1013
+ * Header and query parameter names following the Electric Durable Stream Protocol.
1014
+ */
1015
+ /**
1016
+ * Response header containing the next offset to read from.
1017
+ * Offsets are opaque tokens - clients MUST NOT interpret the format.
1018
+ */
1019
+ declare const STREAM_OFFSET_HEADER = "Stream-Next-Offset";
571
1020
  /**
572
- * Durable Streams Protocol Constants
573
- *
574
- * Header and query parameter names following the Electric Durable Stream Protocol.
575
- */
576
- /**
577
- * Response header containing the next offset to read from.
578
- * Format: "<read-seq>_<byte-offset>"
579
- */
580
- declare const STREAM_OFFSET_HEADER = "stream-offset";
581
- /**
582
- * Response header for cursor (used for CDN collapsing).
583
- * Echo this value in subsequent long-poll requests.
584
- */
585
- declare const STREAM_CURSOR_HEADER = "stream-cursor";
586
- /**
587
- * Presence header indicating response ends at current end of stream.
588
- * When present (any value), indicates up-to-date.
589
- */
590
- declare const STREAM_UP_TO_DATE_HEADER = "stream-up-to-date";
591
- /**
592
- * Request header for writer coordination sequence.
593
- * Monotonic, lexicographic. If lower than last appended seq -> 409 Conflict.
594
- */
595
- declare const STREAM_SEQ_HEADER = "stream-seq";
596
- /**
597
- * Request header for stream TTL in seconds (on create).
598
- */
599
- declare const STREAM_TTL_HEADER = "stream-ttl";
600
- /**
601
- * Request header for absolute stream expiry time (RFC3339, on create).
602
- */
603
- declare const STREAM_EXPIRES_AT_HEADER = "stream-expires-at";
604
- /**
605
- * Query parameter for starting offset.
606
- */
1021
+ * Response header for cursor (used for CDN collapsing).
1022
+ * Echo this value in subsequent long-poll requests.
1023
+ */
1024
+ declare const STREAM_CURSOR_HEADER = "Stream-Cursor";
1025
+ /**
1026
+ * Presence header indicating response ends at current end of stream.
1027
+ * When present (any value), indicates up-to-date.
1028
+ */
1029
+ declare const STREAM_UP_TO_DATE_HEADER = "Stream-Up-To-Date";
1030
+ /**
1031
+ * Request header for writer coordination sequence.
1032
+ * Monotonic, lexicographic. If lower than last appended seq -> 409 Conflict.
1033
+ */
1034
+ declare const STREAM_SEQ_HEADER = "Stream-Seq";
1035
+ /**
1036
+ * Request header for stream TTL in seconds (on create).
1037
+ */
1038
+ declare const STREAM_TTL_HEADER = "Stream-TTL";
1039
+ /**
1040
+ * Request header for absolute stream expiry time (RFC3339, on create).
1041
+ */
1042
+ declare const STREAM_EXPIRES_AT_HEADER = "Stream-Expires-At";
1043
+ /**
1044
+ * Query parameter for starting offset.
1045
+ */
607
1046
  declare const OFFSET_QUERY_PARAM = "offset";
608
1047
  /**
609
- * Query parameter for live mode.
610
- * Values: "long-poll", "sse"
611
- */
1048
+ * Query parameter for live mode.
1049
+ * Values: "long-poll", "sse"
1050
+ */
612
1051
  declare const LIVE_QUERY_PARAM = "live";
613
1052
  /**
614
- * Query parameter for echoing cursor (CDN collapsing).
615
- */
1053
+ * Query parameter for echoing cursor (CDN collapsing).
1054
+ */
616
1055
  declare const CURSOR_QUERY_PARAM = "cursor";
617
1056
  /**
618
- * Content types that support SSE mode.
619
- * SSE is only valid for text/* or application/json streams.
620
- */
621
- declare const SSE_COMPATIBLE_CONTENT_TYPES: string[];
1057
+ * SSE control event field for the next offset.
1058
+ * Note: Different from HTTP header name (camelCase vs Header-Case).
1059
+ */
1060
+
1061
+ /**
1062
+ * Content types that support SSE mode.
1063
+ * SSE is only valid for text/* or application/json streams.
1064
+ */
1065
+ declare const SSE_COMPATIBLE_CONTENT_TYPES: ReadonlyArray<string>;
622
1066
  /**
623
- * Protocol query parameters that should not be set by users.
624
- */
1067
+ * Protocol query parameters that should not be set by users.
1068
+ */
625
1069
  declare const DURABLE_STREAM_PROTOCOL_QUERY_PARAMS: Array<string>;
626
1070
 
627
- export { type AppendOptions, type Auth, BackoffDefaults, type BackoffOptions, CURSOR_QUERY_PARAM, type CreateOptions, DURABLE_STREAM_PROTOCOL_QUERY_PARAMS, DurableStream, DurableStreamError, type DurableStreamErrorCode, type DurableStreamOptions, FetchBackoffAbortError, FetchError, type HeadResult, type HeadersRecord, InvalidSignalError, LIVE_QUERY_PARAM, type LiveMode, type MaybePromise, MissingStreamUrlError, OFFSET_QUERY_PARAM, type Offset, type ParamsRecord, type ReadOptions, type ReadResult, type RetryOpts, SSE_COMPATIBLE_CONTENT_TYPES, STREAM_CURSOR_HEADER, STREAM_EXPIRES_AT_HEADER, STREAM_OFFSET_HEADER, STREAM_SEQ_HEADER, STREAM_TTL_HEADER, STREAM_UP_TO_DATE_HEADER, type StreamChunk, type StreamErrorHandler, type StreamOptions, createFetchWithBackoff, createFetchWithConsumedBody };
1071
+ //#endregion
1072
+ export { AppendOptions, BackoffDefaults, BackoffOptions, ByteChunk, CURSOR_QUERY_PARAM, CreateOptions, DURABLE_STREAM_PROTOCOL_QUERY_PARAMS, DurableStream, DurableStreamError, DurableStreamErrorCode, DurableStreamOptions, FetchBackoffAbortError, FetchError, HeadResult, HeadersRecord, InvalidSignalError, JsonBatch, JsonBatchMeta, LIVE_QUERY_PARAM, LegacyLiveMode, LiveMode, MaybePromise, MissingStreamUrlError, OFFSET_QUERY_PARAM, Offset, ParamsRecord, ReadOptions, ReadableStreamAsyncIterable, RetryOpts, SSEResilienceOptions, SSE_COMPATIBLE_CONTENT_TYPES, STREAM_CURSOR_HEADER, STREAM_EXPIRES_AT_HEADER, STREAM_OFFSET_HEADER, STREAM_SEQ_HEADER, STREAM_TTL_HEADER, STREAM_UP_TO_DATE_HEADER, StreamErrorHandler, StreamHandleOptions, StreamOptions, StreamResponse, TextChunk, asAsyncIterableReadableStream, createFetchWithBackoff, createFetchWithConsumedBody, stream };