@durable-streams/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stream.ts","../src/error.ts","../src/constants.ts","../src/fetch.ts"],"sourcesContent":["/**\n * DurableStream - A handle to a remote durable stream.\n *\n * Following the Electric Durable Stream Protocol specification.\n */\n\nimport { fetchEventSource } from \"@microsoft/fetch-event-source\"\nimport {\n DurableStreamError,\n FetchBackoffAbortError,\n InvalidSignalError,\n MissingStreamUrlError,\n} from \"./error\"\nimport {\n CURSOR_QUERY_PARAM,\n LIVE_QUERY_PARAM,\n OFFSET_QUERY_PARAM,\n SSE_COMPATIBLE_CONTENT_TYPES,\n STREAM_CURSOR_HEADER,\n STREAM_EXPIRES_AT_HEADER,\n STREAM_OFFSET_HEADER,\n STREAM_SEQ_HEADER,\n STREAM_TTL_HEADER,\n STREAM_UP_TO_DATE_HEADER,\n} from \"./constants\"\nimport {\n BackoffDefaults,\n chainAborter,\n createFetchWithBackoff,\n createFetchWithConsumedBody,\n} from \"./fetch\"\nimport type { EventSourceMessage } from \"@microsoft/fetch-event-source\"\nimport type {\n AppendOptions,\n CreateOptions,\n HeadResult,\n MaybePromise,\n Offset,\n ReadOptions,\n ReadResult,\n StreamChunk,\n StreamErrorHandler,\n StreamOptions,\n} from \"./types\"\n\nimport type { BackoffOptions } from \"./fetch\"\n\n/**\n * Options for DurableStream constructor.\n */\nexport interface DurableStreamOptions extends StreamOptions {\n /**\n * Backoff options for retry behavior.\n */\n backoffOptions?: BackoffOptions\n\n /**\n * Error handler for recoverable errors.\n *\n * **Automatic retries**: The client automatically retries 5xx server errors, network\n * errors, and 429 rate limits with exponential backoff. The `onError` callback is\n * only invoked after these automatic retries are exhausted, or for non-retryable errors.\n *\n * **Return value behavior** (following Electric client pattern):\n * - Return `{}` to retry with the same params/headers\n * - Return `{ params }` to retry with merged params\n * - Return `{ headers }` to retry with merged headers\n * - Return `void`/`undefined` to stop the stream and propagate the error\n *\n * @example\n * ```typescript\n * // Refresh auth token on 401\n * onError: async (error) => {\n * if (error instanceof FetchError && error.status === 401) {\n * const newToken = await refreshAuthToken()\n * return { headers: { Authorization: `Bearer ${newToken}` } }\n * }\n * }\n * ```\n */\n onError?: StreamErrorHandler\n}\n\n/**\n * A handle to a remote durable stream.\n *\n * This is a lightweight, reusable handle - not a persistent connection.\n * It does not automatically start reading or listening.\n * Create sessions as needed via read(), follow(), or toReadableStream().\n *\n * @example\n * ```typescript\n * // Create a handle without any network IO\n * const stream = new DurableStream({\n * url: \"https://streams.example.com/my-stream\",\n * auth: { token: \"my-token\" }\n * });\n *\n * // One-shot read\n * const result = await stream.read({ offset: savedOffset });\n *\n * // Follow for live updates\n * for await (const chunk of stream.follow()) {\n * console.log(new TextDecoder().decode(chunk.data));\n * }\n * ```\n */\nexport class DurableStream {\n /**\n * The URL of the durable stream.\n */\n readonly url: string\n\n /**\n * The content type of the stream (populated after connect/head/read).\n */\n contentType?: string\n\n #options: DurableStreamOptions\n readonly #fetchClient: typeof fetch\n readonly #sseFetchClient: typeof fetch\n #onError?: StreamErrorHandler\n\n /**\n * Create a cold handle to a stream.\n * No network IO is performed by the constructor.\n */\n constructor(opts: DurableStreamOptions) {\n validateOptions(opts)\n this.url = opts.url\n this.#options = opts\n this.#onError = opts.onError\n\n const baseFetchClient =\n opts.fetch ?? ((...args: Parameters<typeof fetch>) => fetch(...args))\n\n const backOffOpts = {\n ...(opts.backoffOptions ?? BackoffDefaults),\n }\n\n const fetchWithBackoffClient = createFetchWithBackoff(\n baseFetchClient,\n backOffOpts\n )\n\n this.#sseFetchClient = fetchWithBackoffClient\n this.#fetchClient = createFetchWithConsumedBody(fetchWithBackoffClient)\n }\n\n // ============================================================================\n // Static convenience methods\n // ============================================================================\n\n /**\n * Create a new stream (create-only PUT) and return a handle.\n * Fails with DurableStreamError(code=\"CONFLICT_EXISTS\") if it already exists.\n */\n static async create(opts: CreateOptions): Promise<DurableStream> {\n const stream = new DurableStream(opts)\n await stream.create({\n contentType: opts.contentType,\n ttlSeconds: opts.ttlSeconds,\n expiresAt: opts.expiresAt,\n body: opts.body,\n })\n return stream\n }\n\n /**\n * Validate that a stream exists and fetch metadata via HEAD.\n * Returns a handle with contentType populated (if sent by server).\n */\n static async connect(opts: StreamOptions): Promise<DurableStream> {\n const stream = new DurableStream(opts)\n await stream.head()\n return stream\n }\n\n /**\n * HEAD metadata for a stream without creating a handle.\n */\n static async head(opts: StreamOptions): Promise<HeadResult> {\n const stream = new DurableStream(opts)\n return stream.head()\n }\n\n /**\n * Delete a stream without creating a handle.\n */\n static async delete(opts: StreamOptions): Promise<void> {\n const stream = new DurableStream(opts)\n return stream.delete()\n }\n\n // ============================================================================\n // Instance methods\n // ============================================================================\n\n /**\n * HEAD metadata for this stream.\n */\n async head(opts?: { signal?: AbortSignal }): Promise<HeadResult> {\n const { requestHeaders, fetchUrl } = await this.#buildRequest()\n\n const response = await this.#fetchClient(fetchUrl.toString(), {\n method: `HEAD`,\n headers: requestHeaders,\n signal: opts?.signal ?? this.#options.signal,\n })\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new DurableStreamError(\n `Stream not found: ${this.url}`,\n `NOT_FOUND`,\n 404\n )\n }\n throw await DurableStreamError.fromResponse(response, this.url)\n }\n\n const contentType = response.headers.get(`content-type`) ?? undefined\n const offset = response.headers.get(STREAM_OFFSET_HEADER) ?? undefined\n const etag = response.headers.get(`etag`) ?? undefined\n const cacheControl = response.headers.get(`cache-control`) ?? undefined\n\n // Update instance contentType\n if (contentType) {\n this.contentType = contentType\n }\n\n return {\n exists: true,\n contentType,\n offset,\n etag,\n cacheControl,\n }\n }\n\n /**\n * Create this stream (create-only PUT) using the URL/auth from the handle.\n */\n async create(opts?: Omit<CreateOptions, keyof StreamOptions>): Promise<this> {\n const { requestHeaders, fetchUrl } = await this.#buildRequest()\n\n if (opts?.contentType) {\n requestHeaders[`content-type`] = opts.contentType\n }\n if (opts?.ttlSeconds !== undefined) {\n requestHeaders[STREAM_TTL_HEADER] = String(opts.ttlSeconds)\n }\n if (opts?.expiresAt) {\n requestHeaders[STREAM_EXPIRES_AT_HEADER] = opts.expiresAt\n }\n\n const body = encodeBody(opts?.body)\n\n const response = await this.#fetchClient(fetchUrl.toString(), {\n method: `PUT`,\n headers: requestHeaders,\n body,\n signal: this.#options.signal,\n })\n\n if (!response.ok) {\n if (response.status === 409) {\n throw new DurableStreamError(\n `Stream already exists: ${this.url}`,\n `CONFLICT_EXISTS`,\n 409\n )\n }\n throw await DurableStreamError.fromResponse(response, this.url)\n }\n\n // Update content type from response or options\n const responseContentType = response.headers.get(`content-type`)\n if (responseContentType) {\n this.contentType = responseContentType\n } else if (opts?.contentType) {\n this.contentType = opts.contentType\n }\n\n return this\n }\n\n /**\n * Delete this stream.\n */\n async delete(opts?: { signal?: AbortSignal }): Promise<void> {\n const { requestHeaders, fetchUrl } = await this.#buildRequest()\n\n const response = await this.#fetchClient(fetchUrl.toString(), {\n method: `DELETE`,\n headers: requestHeaders,\n signal: opts?.signal ?? this.#options.signal,\n })\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new DurableStreamError(\n `Stream not found: ${this.url}`,\n `NOT_FOUND`,\n 404\n )\n }\n throw await DurableStreamError.fromResponse(response, this.url)\n }\n }\n\n /**\n * Append a single payload to the stream.\n *\n * - `body` may be Uint8Array, string, or any Fetch BodyInit.\n * - Strings are encoded as UTF-8.\n * - `seq` (if provided) is sent as stream-seq (writer coordination).\n */\n async append(\n body: BodyInit | Uint8Array | string,\n opts?: AppendOptions\n ): Promise<void> {\n const { requestHeaders, fetchUrl } = await this.#buildRequest()\n\n if (opts?.contentType) {\n requestHeaders[`content-type`] = opts.contentType\n } else if (this.contentType) {\n requestHeaders[`content-type`] = this.contentType\n }\n\n if (opts?.seq) {\n requestHeaders[STREAM_SEQ_HEADER] = opts.seq\n }\n\n const encodedBody = encodeBody(body)\n\n const response = await this.#fetchClient(fetchUrl.toString(), {\n method: `POST`,\n headers: requestHeaders,\n body: encodedBody,\n signal: opts?.signal ?? this.#options.signal,\n })\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new DurableStreamError(\n `Stream not found: ${this.url}`,\n `NOT_FOUND`,\n 404\n )\n }\n if (response.status === 409) {\n throw new DurableStreamError(\n `Sequence conflict: seq is lower than last appended`,\n `CONFLICT_SEQ`,\n 409\n )\n }\n if (response.status === 400) {\n throw new DurableStreamError(\n `Bad request (possibly content-type mismatch)`,\n `BAD_REQUEST`,\n 400\n )\n }\n throw await DurableStreamError.fromResponse(response, this.url)\n }\n }\n\n /**\n * Append a streaming body to the stream.\n *\n * - `source` yields Uint8Array or string chunks.\n * - Strings are encoded as UTF-8; no delimiters are added.\n * - Internally uses chunked transfer or HTTP/2 streaming.\n */\n async appendStream(\n source:\n | ReadableStream<Uint8Array | string>\n | AsyncIterable<Uint8Array | string>,\n opts?: AppendOptions\n ): Promise<void> {\n const { requestHeaders, fetchUrl } = await this.#buildRequest()\n\n if (opts?.contentType) {\n requestHeaders[`content-type`] = opts.contentType\n } else if (this.contentType) {\n requestHeaders[`content-type`] = this.contentType\n }\n\n if (opts?.seq) {\n requestHeaders[STREAM_SEQ_HEADER] = opts.seq\n }\n\n // Convert to ReadableStream if needed\n const body = toReadableStream(source)\n\n const response = await this.#fetchClient(fetchUrl.toString(), {\n method: `POST`,\n headers: requestHeaders,\n body,\n // @ts-expect-error - duplex is needed for streaming but not in types\n duplex: `half`,\n signal: opts?.signal ?? this.#options.signal,\n })\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new DurableStreamError(\n `Stream not found: ${this.url}`,\n `NOT_FOUND`,\n 404\n )\n }\n if (response.status === 409) {\n throw new DurableStreamError(\n `Sequence conflict: seq is lower than last appended`,\n `CONFLICT_SEQ`,\n 409\n )\n }\n throw await DurableStreamError.fromResponse(response, this.url)\n }\n }\n\n /**\n * One-shot read.\n *\n * Performs a single GET from the specified offset/mode and returns a chunk.\n * Caller is responsible for persisting the returned offset if they want to resume.\n */\n async read(opts?: ReadOptions): Promise<ReadResult> {\n const { requestHeaders, fetchUrl } = await this.#buildRequest(opts)\n\n const response = await this.#fetchClient(fetchUrl.toString(), {\n method: `GET`,\n headers: requestHeaders,\n signal: opts?.signal ?? this.#options.signal,\n })\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new DurableStreamError(\n `Stream not found: ${this.url}`,\n `NOT_FOUND`,\n 404\n )\n }\n if (response.status === 204) {\n // Long-poll timeout - no new data\n const offset =\n response.headers.get(STREAM_OFFSET_HEADER) ?? opts?.offset ?? ``\n return {\n data: new Uint8Array(0),\n offset,\n upToDate: true,\n contentType: this.contentType,\n }\n }\n throw await DurableStreamError.fromResponse(response, this.url)\n }\n\n return this.#parseReadResponse(response)\n }\n\n /**\n * Follow the stream as an AsyncIterable of chunks.\n *\n * Default behaviour:\n * - From `offset` (or start if omitted), repeatedly perform catch-up reads\n * until a chunk with upToDate=true.\n * - Then switch to live mode:\n * - SSE if content-type is text/* or application/json;\n * - otherwise long-poll.\n *\n * Explicit live override:\n * - live=\"catchup\": only catch-up, stop at upToDate.\n * - live=\"long-poll\": start long-polling immediately from offset.\n * - live=\"sse\": start SSE immediately (throws if SSE not supported).\n */\n follow(opts?: ReadOptions): AsyncIterable<StreamChunk> {\n const stream = this\n const liveMode = opts?.live\n let currentOffset = opts?.offset\n let currentCursor = opts?.cursor\n let isUpToDate = false\n\n // Create a linked abort controller\n const aborter = new AbortController()\n const { signal, cleanup } = chainAborter(\n aborter,\n opts?.signal ?? stream.#options.signal\n )\n\n // SSE iterator - created once when we enter SSE mode\n let sseIterator: AsyncIterator<StreamChunk> | null = null\n\n return {\n [Symbol.asyncIterator](): AsyncIterator<StreamChunk> {\n return {\n async next(): Promise<IteratorResult<StreamChunk>> {\n try {\n // If we've been aborted, stop\n if (signal.aborted) {\n cleanup()\n return { done: true, value: undefined }\n }\n\n // If we have an SSE iterator, delegate to it\n if (sseIterator) {\n const result = await sseIterator.next()\n if (result.done) {\n // SSE connection closed - cleanup\n cleanup()\n }\n return result\n }\n\n // Determine which mode to use\n if (liveMode === `catchup`) {\n // Only do catch-up reads\n if (isUpToDate) {\n cleanup()\n return { done: true, value: undefined }\n }\n\n const chunk = await stream.read({\n offset: currentOffset,\n cursor: currentCursor,\n signal,\n })\n\n currentOffset = chunk.offset\n currentCursor = chunk.cursor\n isUpToDate = chunk.upToDate\n\n return { done: false, value: chunk }\n }\n\n if (liveMode === `sse`) {\n // SSE mode - create SSE iterator and delegate\n sseIterator = stream.#createSSEIterator(\n currentOffset,\n currentCursor,\n signal\n )\n return sseIterator.next()\n }\n\n if (liveMode === `long-poll`) {\n // Long-poll mode - skip catch-up\n const chunk = await stream.read({\n offset: currentOffset,\n cursor: currentCursor,\n live: `long-poll`,\n signal,\n })\n\n currentOffset = chunk.offset\n currentCursor = chunk.cursor\n\n return { done: false, value: chunk }\n }\n\n // Default mode: catch-up then auto-select live mode\n if (!isUpToDate) {\n // Catch-up phase\n const chunk = await stream.read({\n offset: currentOffset,\n cursor: currentCursor,\n signal,\n })\n\n currentOffset = chunk.offset\n currentCursor = chunk.cursor\n isUpToDate = chunk.upToDate\n\n // Update content type if not set\n if (chunk.contentType && !stream.contentType) {\n stream.contentType = chunk.contentType\n }\n\n return { done: false, value: chunk }\n }\n\n // Live phase - auto-select SSE or long-poll\n if (stream.#isSSECompatible()) {\n // Create SSE iterator and delegate\n sseIterator = stream.#createSSEIterator(\n currentOffset,\n currentCursor,\n signal\n )\n return sseIterator.next()\n } else {\n // Long-poll\n const chunk = await stream.read({\n offset: currentOffset,\n cursor: currentCursor,\n live: `long-poll`,\n signal,\n })\n\n currentOffset = chunk.offset\n currentCursor = chunk.cursor\n\n return { done: false, value: chunk }\n }\n } catch (e) {\n if (e instanceof FetchBackoffAbortError) {\n cleanup()\n return { done: true, value: undefined }\n }\n\n // Handle error with onError callback (following Electric's pattern)\n if (stream.#onError && e instanceof Error) {\n const retryOpts = await stream.#onError(e)\n // Guard against null (typeof null === \"object\" in JavaScript)\n if (retryOpts && typeof retryOpts === `object`) {\n // Update params/headers but don't reset offset\n // We want to continue from where we left off, not refetch everything\n if (retryOpts.params) {\n // Merge new params with existing params to preserve other parameters\n stream.#options.params = {\n ...(stream.#options.params ?? {}),\n ...retryOpts.params,\n }\n }\n\n if (retryOpts.headers) {\n // Merge new headers with existing headers to preserve other headers\n stream.#options.headers = {\n ...(stream.#options.headers ?? {}),\n ...retryOpts.headers,\n }\n }\n\n // Retry without cleanup - keep abort listener chain intact\n return this.next()\n }\n }\n\n // Only cleanup when we're actually terminating\n cleanup()\n throw e\n }\n },\n\n async return(): Promise<IteratorResult<StreamChunk>> {\n // Clean up SSE iterator if it exists\n if (sseIterator?.return) {\n await sseIterator.return()\n }\n cleanup()\n aborter.abort()\n return { done: true, value: undefined }\n },\n }\n },\n }\n }\n\n /**\n * Wrap follow() in a Web ReadableStream for piping.\n *\n * Backpressure:\n * - One chunk is pulled from follow() per pull() call, so standard\n * Web Streams backpressure semantics apply.\n *\n * Cancellation:\n * - rs.cancel() will stop follow() and abort any in-flight request.\n */\n toReadableStream(\n opts?: ReadOptions & { signal?: AbortSignal }\n ): ReadableStream<StreamChunk> {\n const iterator = this.follow(opts)[Symbol.asyncIterator]()\n\n return new ReadableStream<StreamChunk>({\n async pull(controller) {\n try {\n const { done, value } = await iterator.next()\n if (done) {\n controller.close()\n } else {\n controller.enqueue(value)\n }\n } catch (e) {\n controller.error(e)\n }\n },\n\n cancel() {\n iterator.return?.()\n },\n })\n }\n\n /**\n * Wrap follow() in a Web ReadableStream<Uint8Array> for piping raw bytes.\n *\n * This is the native format for many web stream APIs.\n */\n toByteStream(\n opts?: ReadOptions & { signal?: AbortSignal }\n ): ReadableStream<Uint8Array> {\n const iterator = this.follow(opts)[Symbol.asyncIterator]()\n\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const { done, value } = await iterator.next()\n if (done) {\n controller.close()\n } else {\n controller.enqueue(value.data)\n }\n } catch (e) {\n controller.error(e)\n }\n },\n\n cancel() {\n iterator.return?.()\n },\n })\n }\n\n /**\n * Convenience: interpret data as JSON messages.\n * Parses each chunk's data as JSON and yields the parsed values.\n */\n async *json<T = unknown>(opts?: ReadOptions): AsyncIterable<T> {\n const decoder = new TextDecoder()\n\n for await (const chunk of this.follow(opts)) {\n if (chunk.data.length > 0) {\n const text = decoder.decode(chunk.data)\n // Handle potential newline-delimited JSON\n const lines = text.split(`\\n`).filter((l) => l.trim())\n for (const line of lines) {\n yield JSON.parse(line) as T\n }\n }\n }\n }\n\n /**\n * Convenience: interpret data as text (UTF-8).\n */\n async *text(\n opts?: ReadOptions & { decoder?: TextDecoder }\n ): AsyncIterable<string> {\n const decoder = opts?.decoder ?? new TextDecoder()\n\n for await (const chunk of this.follow(opts)) {\n if (chunk.data.length > 0) {\n yield decoder.decode(chunk.data, { stream: true })\n }\n }\n }\n\n // ============================================================================\n // Private methods\n // ============================================================================\n\n /**\n * Build request headers and URL.\n */\n async #buildRequest(\n readOpts?: ReadOptions\n ): Promise<{ requestHeaders: Record<string, string>; fetchUrl: URL }> {\n const requestHeaders = await this.#resolveHeaders()\n const fetchUrl = new URL(this.url)\n\n // Add params\n const params = this.#options.params\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined) {\n const resolved = await resolveValue(value)\n fetchUrl.searchParams.set(key, resolved)\n }\n }\n }\n\n // Add read options to URL\n if (readOpts) {\n if (readOpts.offset) {\n fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, readOpts.offset)\n }\n if (readOpts.live) {\n fetchUrl.searchParams.set(LIVE_QUERY_PARAM, readOpts.live)\n }\n if (readOpts.cursor) {\n fetchUrl.searchParams.set(CURSOR_QUERY_PARAM, readOpts.cursor)\n }\n }\n\n return { requestHeaders, fetchUrl }\n }\n\n /**\n * Resolve headers from auth and headers options.\n */\n async #resolveHeaders(): Promise<Record<string, string>> {\n const headers: Record<string, string> = {}\n\n // Resolve auth\n const auth = this.#options.auth\n if (auth) {\n if (`token` in auth) {\n const headerName = auth.headerName ?? `authorization`\n headers[headerName] = `Bearer ${auth.token}`\n } else if (`headers` in auth) {\n Object.assign(headers, auth.headers)\n } else if (`getHeaders` in auth) {\n const authHeaders = await auth.getHeaders()\n Object.assign(headers, authHeaders)\n }\n }\n\n // Resolve additional headers\n const headersOpt = this.#options.headers\n if (headersOpt) {\n for (const [key, value] of Object.entries(headersOpt)) {\n headers[key] = await resolveValue(value)\n }\n }\n\n return headers\n }\n\n /**\n * Parse a read response into a ReadResult.\n */\n async #parseReadResponse(response: Response): Promise<ReadResult> {\n const data = new Uint8Array(await response.arrayBuffer())\n const offset = response.headers.get(STREAM_OFFSET_HEADER) ?? ``\n const cursor = response.headers.get(STREAM_CURSOR_HEADER) ?? undefined\n const upToDate = response.headers.has(STREAM_UP_TO_DATE_HEADER)\n const etag = response.headers.get(`etag`) ?? undefined\n const contentType = response.headers.get(`content-type`) ?? undefined\n\n // Update instance contentType\n if (contentType && !this.contentType) {\n this.contentType = contentType\n }\n\n return {\n data,\n offset,\n cursor,\n upToDate,\n etag,\n contentType,\n }\n }\n\n /**\n * Check if the stream's content type is compatible with SSE.\n */\n #isSSECompatible(): boolean {\n if (!this.contentType) return false\n\n return SSE_COMPATIBLE_CONTENT_TYPES.some((prefix) =>\n this.contentType!.startsWith(prefix)\n )\n }\n\n /**\n * Create an SSE connection that maintains a persistent connection with an internal queue.\n * Returns an AsyncIterator that yields chunks as they arrive.\n *\n * Follows the Electric client pattern:\n * - Buffer data events until control event (up-to-date)\n * - Flush buffer on control event\n * - Use promise chain for sequential processing\n */\n #createSSEIterator(\n initialOffset: Offset | undefined,\n initialCursor: string | undefined,\n signal: AbortSignal\n ): AsyncIterator<StreamChunk> {\n // Check SSE compatibility\n if (!this.#isSSECompatible()) {\n throw new DurableStreamError(\n `SSE is not supported for content-type: ${this.contentType}`,\n `SSE_NOT_SUPPORTED`,\n 400\n )\n }\n\n // Queue of complete chunks waiting to be consumed\n const chunkQueue: Array<StreamChunk> = []\n\n // Pending resolve for when next() is waiting for data\n let pendingResolve: ((result: IteratorResult<StreamChunk>) => void) | null =\n null\n\n // Track current offset/cursor\n let currentOffset = initialOffset\n let currentCursor = initialCursor\n\n // Connection state\n let connectionClosed = false\n let connectionError: Error | null = null\n\n // Abort controller to close the connection\n const connectionAbort = new AbortController()\n\n // Buffer for accumulating data events before control event\n let dataBuffer: Array<Uint8Array> = []\n\n const stream = this\n\n // Start the SSE connection (following Electric's pattern)\n const startConnection = async (): Promise<void> => {\n const { requestHeaders, fetchUrl } = await stream.#buildRequest({\n offset: currentOffset,\n cursor: currentCursor,\n live: `sse`,\n })\n\n try {\n await fetchEventSource(fetchUrl.toString(), {\n headers: requestHeaders,\n fetch: stream.#sseFetchClient,\n signal: signal.aborted ? signal : connectionAbort.signal,\n\n onopen: async (response: Response) => {\n if (!response.ok) {\n throw await DurableStreamError.fromResponse(response, stream.url)\n }\n\n // Update content type\n const contentType = response.headers.get(`content-type`)\n if (contentType && !stream.contentType) {\n stream.contentType = contentType\n }\n },\n\n onmessage: (event: EventSourceMessage) => {\n if (event.event === `data` && event.data) {\n // Data event - buffer the data (following Electric's buffer pattern)\n const data = stream.#parseSSEData(event.data)\n dataBuffer.push(data)\n } else if (event.event === `control` && event.data) {\n // Control event - flush the buffer (like Electric's up-to-date message)\n try {\n const control = JSON.parse(event.data) as {\n [STREAM_OFFSET_HEADER]?: string\n [STREAM_CURSOR_HEADER]?: string\n }\n\n const newOffset = control[STREAM_OFFSET_HEADER]\n const newCursor = control[STREAM_CURSOR_HEADER]\n\n // Concatenate buffered data\n const totalSize = dataBuffer.reduce(\n (sum, buf) => sum + buf.length,\n 0\n )\n const combinedData = new Uint8Array(totalSize)\n let offset = 0\n for (const buf of dataBuffer) {\n combinedData.set(buf, offset)\n offset += buf.length\n }\n\n // Create complete chunk\n const chunk: StreamChunk = {\n data: combinedData,\n offset: newOffset ?? currentOffset ?? ``,\n cursor: newCursor,\n upToDate: true,\n contentType: stream.contentType,\n }\n\n // Update state\n currentOffset = chunk.offset\n currentCursor = chunk.cursor\n\n // Clear buffer\n dataBuffer = []\n\n // If someone is waiting for data, resolve immediately\n if (pendingResolve) {\n const resolve = pendingResolve\n pendingResolve = null\n resolve({ done: false, value: chunk })\n } else {\n // Otherwise queue it\n chunkQueue.push(chunk)\n }\n } catch {\n // Ignore malformed control messages\n }\n }\n },\n\n onerror: (error: Error) => {\n // Rethrow to close SSE connection (following Electric's pattern)\n throw error\n },\n })\n } catch (error) {\n // Handle abort during SSE parsing (following Electric's pattern)\n if (connectionAbort.signal.aborted || signal.aborted) {\n throw new FetchBackoffAbortError()\n }\n throw error\n }\n }\n\n // Start the connection (don't await - runs in background)\n const connectionPromise = startConnection().catch((e) => {\n if (e instanceof FetchBackoffAbortError) {\n connectionClosed = true\n } else {\n connectionError = e\n connectionClosed = true\n }\n\n // If someone is waiting, signal done or error\n if (pendingResolve) {\n const resolve = pendingResolve\n pendingResolve = null\n resolve({ done: true, value: undefined })\n }\n })\n\n // Also close on external abort\n const abortHandler = (): void => {\n connectionAbort.abort()\n connectionClosed = true\n if (pendingResolve) {\n const resolve = pendingResolve\n pendingResolve = null\n resolve({ done: true, value: undefined })\n }\n }\n signal.addEventListener(`abort`, abortHandler, { once: true })\n\n return {\n async next(): Promise<IteratorResult<StreamChunk>> {\n // If there's queued data, return it immediately\n if (chunkQueue.length > 0) {\n return { done: false, value: chunkQueue.shift()! }\n }\n\n // If connection errored, throw the error\n if (connectionError) {\n throw connectionError\n }\n\n // If connection closed (e.g., aborted), we're done\n if (connectionClosed || signal.aborted) {\n return { done: true, value: undefined }\n }\n\n // Wait for the next chunk\n return new Promise((resolve) => {\n pendingResolve = resolve\n })\n },\n\n async return(): Promise<IteratorResult<StreamChunk>> {\n signal.removeEventListener(`abort`, abortHandler)\n connectionAbort.abort()\n connectionClosed = true\n\n // Wait for connection cleanup\n await connectionPromise.catch(() => {\n // Ignore errors during cleanup\n })\n\n return { done: true, value: undefined }\n },\n }\n }\n\n /**\n * Parse SSE data payload.\n * For application/json, data is wrapped in [ and ], so we unwrap it.\n */\n #parseSSEData(data: string): Uint8Array {\n // SSE data lines are prefixed with \"data: \" and may be wrapped in [ ]\n // for application/json content\n const lines = data.split(`\\n`)\n const content = lines\n .map((line) => {\n // Remove \"data: \" prefix if present\n if (line.startsWith(`data: `)) {\n return line.slice(6)\n }\n return line\n })\n .join(`\\n`)\n\n // For JSON content, unwrap the array wrapper\n let text = content.trim()\n if (\n this.contentType?.includes(`application/json`) &&\n text.startsWith(`[`) &&\n text.endsWith(`]`)\n ) {\n // Remove the wrapper brackets and trailing comma if present\n text = text.slice(1, -1).trim()\n if (text.endsWith(`,`)) {\n text = text.slice(0, -1)\n }\n }\n\n return new TextEncoder().encode(text)\n }\n}\n\n// ============================================================================\n// Utility functions\n// ============================================================================\n\n/**\n * Resolve a value that may be a function.\n */\nasync function resolveValue<T>(value: T | (() => MaybePromise<T>)): Promise<T> {\n if (typeof value === `function`) {\n return (value as () => MaybePromise<T>)()\n }\n return value\n}\n\n/**\n * Encode a body value to the appropriate format.\n * Strings are encoded as UTF-8.\n */\nfunction encodeBody(\n body: BodyInit | Uint8Array | string | undefined\n): BodyInit | undefined {\n if (body === undefined) {\n return undefined\n }\n if (typeof body === `string`) {\n return new TextEncoder().encode(body)\n }\n if (body instanceof Uint8Array) {\n // Cast to ensure compatible BodyInit type\n return body as unknown as BodyInit\n }\n return body\n}\n\n/**\n * Convert an async iterable to a ReadableStream.\n */\nfunction toReadableStream(\n source:\n | ReadableStream<Uint8Array | string>\n | AsyncIterable<Uint8Array | string>\n): ReadableStream<Uint8Array> {\n // If it's already a ReadableStream, transform it\n if (source instanceof ReadableStream) {\n return source.pipeThrough(\n new TransformStream<Uint8Array | string, Uint8Array>({\n transform(chunk, controller) {\n if (typeof chunk === `string`) {\n controller.enqueue(new TextEncoder().encode(chunk))\n } else {\n controller.enqueue(chunk)\n }\n },\n })\n )\n }\n\n // Convert async iterable to ReadableStream\n const encoder = new TextEncoder()\n const iterator = source[Symbol.asyncIterator]()\n\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const { done, value } = await iterator.next()\n if (done) {\n controller.close()\n } else if (typeof value === `string`) {\n controller.enqueue(encoder.encode(value))\n } else {\n controller.enqueue(value)\n }\n } catch (e) {\n controller.error(e)\n }\n },\n\n cancel() {\n iterator.return?.()\n },\n })\n}\n\n/**\n * Validate stream options.\n */\nfunction validateOptions(options: Partial<DurableStreamOptions>): void {\n if (!options.url) {\n throw new MissingStreamUrlError()\n }\n if (options.signal && !(options.signal instanceof AbortSignal)) {\n throw new InvalidSignalError()\n }\n}\n","import type { DurableStreamErrorCode } from \"./types\"\n\n/**\n * Error thrown for transport/network errors.\n * Following the @electric-sql/client FetchError pattern.\n */\nexport class FetchError extends Error {\n status: number\n text?: string\n json?: object\n headers: Record<string, string>\n\n constructor(\n status: number,\n text: string | undefined,\n json: object | undefined,\n headers: Record<string, string>,\n public url: string,\n message?: string\n ) {\n super(\n message ||\n `HTTP Error ${status} at ${url}: ${text ?? JSON.stringify(json)}`\n )\n this.name = `FetchError`\n this.status = status\n this.text = text\n this.json = json\n this.headers = headers\n }\n\n static async fromResponse(\n response: Response,\n url: string\n ): Promise<FetchError> {\n const status = response.status\n const headers = Object.fromEntries([...response.headers.entries()])\n let text: string | undefined = undefined\n let json: object | undefined = undefined\n\n const contentType = response.headers.get(`content-type`)\n if (!response.bodyUsed) {\n if (contentType && contentType.includes(`application/json`)) {\n try {\n json = (await response.json()) as object\n } catch {\n // If JSON parsing fails, fall back to text\n text = await response.text()\n }\n } else {\n text = await response.text()\n }\n }\n\n return new FetchError(status, text, json, headers, url)\n }\n}\n\n/**\n * Error thrown when a fetch operation is aborted during backoff.\n */\nexport class FetchBackoffAbortError extends Error {\n constructor() {\n super(`Fetch with backoff aborted`)\n this.name = `FetchBackoffAbortError`\n }\n}\n\n/**\n * Protocol-level error for Durable Streams operations.\n * Provides structured error handling with error codes.\n */\nexport class DurableStreamError extends Error {\n /**\n * HTTP status code, if applicable.\n */\n status?: number\n\n /**\n * Structured error code for programmatic handling.\n */\n code: DurableStreamErrorCode\n\n /**\n * Additional error details (e.g., raw response body).\n */\n details?: unknown\n\n constructor(\n message: string,\n code: DurableStreamErrorCode,\n status?: number,\n details?: unknown\n ) {\n super(message)\n this.name = `DurableStreamError`\n this.code = code\n this.status = status\n this.details = details\n }\n\n /**\n * Create a DurableStreamError from an HTTP response.\n */\n static async fromResponse(\n response: Response,\n url: string\n ): Promise<DurableStreamError> {\n const status = response.status\n let details: unknown\n\n const contentType = response.headers.get(`content-type`)\n if (!response.bodyUsed) {\n if (contentType && contentType.includes(`application/json`)) {\n try {\n details = await response.json()\n } catch {\n details = await response.text()\n }\n } else {\n details = await response.text()\n }\n }\n\n const code = statusToCode(status)\n const message = `Durable stream error at ${url}: ${response.statusText || status}`\n\n return new DurableStreamError(message, code, status, details)\n }\n\n /**\n * Create a DurableStreamError from a FetchError.\n */\n static fromFetchError(error: FetchError): DurableStreamError {\n const code = statusToCode(error.status)\n return new DurableStreamError(\n error.message,\n code,\n error.status,\n error.json ?? error.text\n )\n }\n}\n\n/**\n * Map HTTP status codes to DurableStreamErrorCode.\n */\nfunction statusToCode(status: number): DurableStreamErrorCode {\n switch (status) {\n case 400:\n return `BAD_REQUEST`\n case 401:\n return `UNAUTHORIZED`\n case 403:\n return `FORBIDDEN`\n case 404:\n return `NOT_FOUND`\n case 409:\n // Could be CONFLICT_SEQ or CONFLICT_EXISTS depending on context\n // Default to CONFLICT_SEQ, caller can override\n return `CONFLICT_SEQ`\n case 429:\n return `RATE_LIMITED`\n case 503:\n return `BUSY`\n default:\n return `UNKNOWN`\n }\n}\n\n/**\n * Error thrown when stream URL is missing.\n */\nexport class MissingStreamUrlError extends Error {\n constructor() {\n super(`Invalid stream options: missing required url parameter`)\n this.name = `MissingStreamUrlError`\n }\n}\n\n/**\n * Error thrown when signal option is invalid.\n */\nexport class InvalidSignalError extends Error {\n constructor() {\n super(`Invalid signal option. It must be an instance of AbortSignal.`)\n this.name = `InvalidSignalError`\n }\n}\n","/**\n * Durable Streams Protocol Constants\n *\n * Header and query parameter names following the Electric Durable Stream Protocol.\n */\n\n// ============================================================================\n// Response Headers\n// ============================================================================\n\n/**\n * Response header containing the next offset to read from.\n * Format: \"<read-seq>_<byte-offset>\"\n */\nexport const STREAM_OFFSET_HEADER = `stream-offset`\n\n/**\n * Response header for cursor (used for CDN collapsing).\n * Echo this value in subsequent long-poll requests.\n */\nexport const STREAM_CURSOR_HEADER = `stream-cursor`\n\n/**\n * Presence header indicating response ends at current end of stream.\n * When present (any value), indicates up-to-date.\n */\nexport const STREAM_UP_TO_DATE_HEADER = `stream-up-to-date`\n\n// ============================================================================\n// Request Headers\n// ============================================================================\n\n/**\n * Request header for writer coordination sequence.\n * Monotonic, lexicographic. If lower than last appended seq -> 409 Conflict.\n */\nexport const STREAM_SEQ_HEADER = `stream-seq`\n\n/**\n * Request header for stream TTL in seconds (on create).\n */\nexport const STREAM_TTL_HEADER = `stream-ttl`\n\n/**\n * Request header for absolute stream expiry time (RFC3339, on create).\n */\nexport const STREAM_EXPIRES_AT_HEADER = `stream-expires-at`\n\n// ============================================================================\n// Query Parameters\n// ============================================================================\n\n/**\n * Query parameter for starting offset.\n */\nexport const OFFSET_QUERY_PARAM = `offset`\n\n/**\n * Query parameter for live mode.\n * Values: \"long-poll\", \"sse\"\n */\nexport const LIVE_QUERY_PARAM = `live`\n\n/**\n * Query parameter for echoing cursor (CDN collapsing).\n */\nexport const CURSOR_QUERY_PARAM = `cursor`\n\n// ============================================================================\n// Internal Constants\n// ============================================================================\n\n/**\n * Content types that support SSE mode.\n * SSE is only valid for text/* or application/json streams.\n */\nexport const SSE_COMPATIBLE_CONTENT_TYPES = [`text/`, `application/json`]\n\n/**\n * Protocol query parameters that should not be set by users.\n */\nexport const DURABLE_STREAM_PROTOCOL_QUERY_PARAMS: Array<string> = [\n OFFSET_QUERY_PARAM,\n LIVE_QUERY_PARAM,\n CURSOR_QUERY_PARAM,\n]\n","/**\n * Fetch utilities with retry and backoff support.\n * Based on @electric-sql/client patterns.\n */\n\nimport { FetchBackoffAbortError, FetchError } from \"./error\"\n\n/**\n * HTTP status codes that should be retried.\n */\nconst HTTP_RETRY_STATUS_CODES = [429, 503]\n\n/**\n * Options for configuring exponential backoff retry behavior.\n */\nexport interface BackoffOptions {\n /**\n * Initial delay before retrying in milliseconds.\n */\n initialDelay: number\n\n /**\n * Maximum retry delay in milliseconds.\n * After reaching this, delay stays constant.\n */\n maxDelay: number\n\n /**\n * Multiplier for exponential backoff.\n */\n multiplier: number\n\n /**\n * Callback invoked on each failed attempt.\n */\n onFailedAttempt?: () => void\n\n /**\n * Enable debug logging.\n */\n debug?: boolean\n\n /**\n * Maximum number of retry attempts before giving up.\n * Set to Infinity for indefinite retries (useful for offline scenarios).\n */\n maxRetries?: number\n}\n\n/**\n * Default backoff options.\n */\nexport const BackoffDefaults: BackoffOptions = {\n initialDelay: 100,\n maxDelay: 60_000, // Cap at 60s\n multiplier: 1.3,\n maxRetries: Infinity, // Retry forever by default\n}\n\n/**\n * Parse Retry-After header value and return delay in milliseconds.\n * Supports both delta-seconds format and HTTP-date format.\n * Returns 0 if header is not present or invalid.\n */\nexport function parseRetryAfterHeader(retryAfter: string | undefined): number {\n if (!retryAfter) return 0\n\n // Try parsing as seconds (delta-seconds format)\n const retryAfterSec = Number(retryAfter)\n if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {\n return retryAfterSec * 1000\n }\n\n // Try parsing as HTTP-date\n const retryDate = Date.parse(retryAfter)\n if (!isNaN(retryDate)) {\n // Handle clock skew: clamp to non-negative, cap at reasonable max\n const deltaMs = retryDate - Date.now()\n return Math.max(0, Math.min(deltaMs, 3600_000)) // Cap at 1 hour\n }\n\n return 0\n}\n\n/**\n * Creates a fetch client that retries failed requests with exponential backoff.\n *\n * @param fetchClient - The base fetch client to wrap\n * @param backoffOptions - Options for retry behavior\n * @returns A fetch function with automatic retry\n */\nexport function createFetchWithBackoff(\n fetchClient: typeof fetch,\n backoffOptions: BackoffOptions = BackoffDefaults\n): typeof fetch {\n const {\n initialDelay,\n maxDelay,\n multiplier,\n debug = false,\n onFailedAttempt,\n maxRetries = Infinity,\n } = backoffOptions\n\n return async (...args: Parameters<typeof fetch>): Promise<Response> => {\n const url = args[0]\n const options = args[1]\n\n let delay = initialDelay\n let attempt = 0\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n try {\n const result = await fetchClient(...args)\n if (result.ok) {\n return result\n }\n\n const err = await FetchError.fromResponse(result, url.toString())\n throw err\n } catch (e) {\n onFailedAttempt?.()\n\n if (options?.signal?.aborted) {\n throw new FetchBackoffAbortError()\n } else if (\n e instanceof FetchError &&\n !HTTP_RETRY_STATUS_CODES.includes(e.status) &&\n e.status >= 400 &&\n e.status < 500\n ) {\n // Client errors (except 429) cannot be backed off on\n throw e\n } else {\n // Check max retries\n attempt++\n if (attempt > maxRetries) {\n if (debug) {\n console.log(\n `Max retries reached (${attempt}/${maxRetries}), giving up`\n )\n }\n throw e\n }\n\n // Calculate wait time honoring server-driven backoff as a floor\n // Parse server-provided Retry-After (if present)\n const serverMinimumMs =\n e instanceof FetchError\n ? parseRetryAfterHeader(e.headers[`retry-after`])\n : 0\n\n // Calculate client backoff with full jitter strategy\n // Full jitter: random_between(0, min(cap, exponential_backoff))\n const jitter = Math.random() * delay\n const clientBackoffMs = Math.min(jitter, maxDelay)\n\n // Server minimum is the floor, client cap is the ceiling\n const waitMs = Math.max(serverMinimumMs, clientBackoffMs)\n\n if (debug) {\n const source = serverMinimumMs > 0 ? `server+client` : `client`\n console.log(\n `Retry attempt #${attempt} after ${waitMs}ms (${source}, serverMin=${serverMinimumMs}ms, clientBackoff=${clientBackoffMs}ms)`\n )\n }\n\n // Wait for the calculated duration\n await new Promise((resolve) => setTimeout(resolve, waitMs))\n\n // Increase the delay for the next attempt (capped at maxDelay)\n delay = Math.min(delay * multiplier, maxDelay)\n }\n }\n }\n }\n}\n\n/**\n * Status codes where we shouldn't try to read the body.\n */\nconst NO_BODY_STATUS_CODES = [201, 204, 205]\n\n/**\n * Creates a fetch client that ensures the response body is fully consumed.\n * This prevents issues with connection pooling when bodies aren't read.\n *\n * Uses arrayBuffer() instead of text() to preserve binary data integrity.\n *\n * @param fetchClient - The base fetch client to wrap\n * @returns A fetch function that consumes response bodies\n */\nexport function createFetchWithConsumedBody(\n fetchClient: typeof fetch\n): typeof fetch {\n return async (...args: Parameters<typeof fetch>): Promise<Response> => {\n const url = args[0]\n const res = await fetchClient(...args)\n\n try {\n if (res.status < 200 || NO_BODY_STATUS_CODES.includes(res.status)) {\n return res\n }\n\n // Read body as arrayBuffer to preserve binary data integrity\n const buf = await res.arrayBuffer()\n return new Response(buf, {\n status: res.status,\n statusText: res.statusText,\n headers: res.headers,\n })\n } catch (err) {\n if (args[1]?.signal?.aborted) {\n throw new FetchBackoffAbortError()\n }\n\n throw new FetchError(\n res.status,\n undefined,\n undefined,\n Object.fromEntries([...res.headers.entries()]),\n url.toString(),\n err instanceof Error\n ? err.message\n : typeof err === `string`\n ? err\n : `failed to read body`\n )\n }\n }\n}\n\n/**\n * Chains an AbortController to an optional source signal.\n * If the source signal is aborted, the provided controller will also abort.\n */\nexport function chainAborter(\n aborter: AbortController,\n sourceSignal?: AbortSignal | null\n): {\n signal: AbortSignal\n cleanup: () => void\n} {\n let cleanup = noop\n if (!sourceSignal) {\n // no-op, nothing to chain to\n } else if (sourceSignal.aborted) {\n // source signal is already aborted, abort immediately\n aborter.abort(sourceSignal.reason)\n } else {\n // chain to source signal abort event\n const abortParent = () => aborter.abort(sourceSignal.reason)\n sourceSignal.addEventListener(`abort`, abortParent, {\n once: true,\n signal: aborter.signal,\n })\n cleanup = () => sourceSignal.removeEventListener(`abort`, abortParent)\n }\n\n return {\n signal: aborter.signal,\n cleanup,\n }\n}\n\nfunction noop() {}\n"],"mappings":"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","names":["_a"]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@durable-streams/client",
3
+ "description": "TypeScript client for the Durable Streams protocol",
4
+ "version": "0.1.0",
5
+ "author": "Durable Stream contributors",
6
+ "license": "Apache-2.0",
7
+ "type": "module",
8
+ "exports": {
9
+ "./package.json": "./package.json",
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src"
24
+ ],
25
+ "main": "./dist/index.cjs",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "sideEffects": false,
29
+ "dependencies": {
30
+ "@microsoft/fetch-event-source": "^2.0.1",
31
+ "fastq": "^1.19.1"
32
+ },
33
+ "devDependencies": {
34
+ "@durable-streams/server": "workspace:*",
35
+ "fast-check": "^4.4.0",
36
+ "tsdown": "^0.9.0"
37
+ },
38
+ "scripts": {
39
+ "build": "tsdown",
40
+ "dev": "tsdown --watch",
41
+ "typecheck": "tsc --noEmit"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ }
46
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Async iterable polyfill for ReadableStream.
3
+ *
4
+ * Safari/iOS may not implement ReadableStream.prototype[Symbol.asyncIterator],
5
+ * preventing `for await...of` consumption. This module provides a soft polyfill
6
+ * that defines [Symbol.asyncIterator] on individual stream instances when missing,
7
+ * without patching the global prototype.
8
+ *
9
+ * The returned stream is still the original ReadableStream instance (not wrapped),
10
+ * so `instanceof ReadableStream` continues to work correctly.
11
+ *
12
+ * **Note on derived streams**: Streams created via `.pipeThrough()` or similar
13
+ * transformations will NOT be automatically patched. Use the exported
14
+ * `asAsyncIterableReadableStream()` helper to patch derived streams:
15
+ *
16
+ * ```typescript
17
+ * import { asAsyncIterableReadableStream } from "@durable-streams/client"
18
+ *
19
+ * const derived = res.bodyStream().pipeThrough(myTransform)
20
+ * const iterable = asAsyncIterableReadableStream(derived)
21
+ * for await (const chunk of iterable) { ... }
22
+ * ```
23
+ */
24
+
25
+ /**
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
+ export type ReadableStreamAsyncIterable<T> = ReadableStream<T> &
32
+ AsyncIterable<T>
33
+
34
+ /**
35
+ * Check if a value has Symbol.asyncIterator defined.
36
+ */
37
+ function hasAsyncIterator(stream: unknown): stream is AsyncIterable<unknown> {
38
+ return (
39
+ typeof Symbol !== `undefined` &&
40
+ typeof (Symbol as unknown as Record<string, unknown>).asyncIterator ===
41
+ `symbol` &&
42
+ typeof (stream as Record<symbol, unknown>)[Symbol.asyncIterator] ===
43
+ `function`
44
+ )
45
+ }
46
+
47
+ /**
48
+ * Define [Symbol.asyncIterator] and .values() on a ReadableStream instance.
49
+ *
50
+ * Uses getReader().read() to implement spec-consistent iteration.
51
+ * On completion or early exit (break/return/throw), releases lock and cancels as appropriate.
52
+ *
53
+ * **Iterator behavior notes:**
54
+ * - `return(value?)` accepts an optional cancellation reason passed to `reader.cancel()`
55
+ * - `return()` always resolves with `{ done: true, value: undefined }` regardless of the
56
+ * input value. This matches `for await...of` semantics where the return value is ignored.
57
+ * Manual iteration users should be aware of this behavior.
58
+ */
59
+ function defineAsyncIterator<T>(stream: ReadableStream<T>): void {
60
+ if (
61
+ typeof Symbol === `undefined` ||
62
+ typeof (Symbol as unknown as Record<string, unknown>).asyncIterator !==
63
+ `symbol`
64
+ ) {
65
+ return
66
+ }
67
+
68
+ if (
69
+ typeof (stream as unknown as Record<symbol, unknown>)[
70
+ Symbol.asyncIterator
71
+ ] === `function`
72
+ ) {
73
+ return
74
+ }
75
+
76
+ // The iterator factory function - shared between [Symbol.asyncIterator] and .values()
77
+ const createIterator = function (
78
+ this: ReadableStream<T>
79
+ ): AsyncIterator<T> & AsyncIterable<T> {
80
+ const reader = this.getReader()
81
+ let finished = false
82
+ // Track pending reads with a counter (not boolean) to handle
83
+ // concurrent next() calls correctly. This is important if someone
84
+ // manually calls next() multiple times without awaiting.
85
+ let pendingReads = 0
86
+
87
+ const iterator: AsyncIterator<T> & AsyncIterable<T> = {
88
+ async next() {
89
+ if (finished) {
90
+ return { done: true, value: undefined as unknown as T }
91
+ }
92
+
93
+ pendingReads++
94
+ try {
95
+ const { value, done } = await reader.read()
96
+
97
+ if (done) {
98
+ finished = true
99
+ reader.releaseLock()
100
+ return { done: true, value: undefined as unknown as T }
101
+ }
102
+
103
+ return { done: false, value: value }
104
+ } catch (err) {
105
+ // On read error, release lock to avoid leaking it
106
+ finished = true
107
+ try {
108
+ reader.releaseLock()
109
+ } catch {
110
+ // Ignore release errors - lock may already be released
111
+ }
112
+ throw err
113
+ } finally {
114
+ pendingReads--
115
+ }
116
+ },
117
+
118
+ /**
119
+ * Called on early exit (break, return, or completion).
120
+ * Accepts an optional cancellation reason passed to reader.cancel().
121
+ *
122
+ * Note: Always returns { done: true, value: undefined } regardless of input,
123
+ * matching for-await-of semantics where return values are ignored.
124
+ */
125
+ async return(value?: unknown) {
126
+ // Per WHATWG Streams spec: reject with TypeError if there are pending reads
127
+ if (pendingReads > 0) {
128
+ throw new TypeError(
129
+ `Cannot close a readable stream reader when it has pending read requests`
130
+ )
131
+ }
132
+
133
+ finished = true
134
+ // Per spec: start cancel with optional reason, release lock, then await cancel
135
+ const cancelPromise = reader.cancel(value)
136
+ reader.releaseLock()
137
+ await cancelPromise
138
+ return { done: true, value: undefined as unknown as T }
139
+ },
140
+
141
+ async throw(err?: unknown) {
142
+ // Per WHATWG Streams spec: reject with TypeError if there are pending reads
143
+ if (pendingReads > 0) {
144
+ throw new TypeError(
145
+ `Cannot close a readable stream reader when it has pending read requests`
146
+ )
147
+ }
148
+
149
+ finished = true
150
+ // Per spec: start cancel with error, release lock, then await cancel
151
+ const cancelPromise = reader.cancel(err)
152
+ reader.releaseLock()
153
+ await cancelPromise
154
+ throw err
155
+ },
156
+
157
+ [Symbol.asyncIterator]() {
158
+ return this
159
+ },
160
+ }
161
+
162
+ return iterator
163
+ }
164
+
165
+ // Define [Symbol.asyncIterator] with defensive try/catch
166
+ // If defineProperty fails (non-extensible object, sandbox, etc.),
167
+ // we gracefully degrade rather than crash
168
+ try {
169
+ Object.defineProperty(stream, Symbol.asyncIterator, {
170
+ configurable: true,
171
+ writable: true,
172
+ value: createIterator,
173
+ })
174
+ } catch {
175
+ // Failed to define - stream remains non-iterable but doesn't crash
176
+ return
177
+ }
178
+
179
+ // Also define .values() for API completeness (mirrors native ReadableStream)
180
+ try {
181
+ Object.defineProperty(stream, `values`, {
182
+ configurable: true,
183
+ writable: true,
184
+ value: createIterator,
185
+ })
186
+ } catch {
187
+ // Failed to define .values() - Symbol.asyncIterator may still work
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Ensure a ReadableStream is async-iterable.
193
+ *
194
+ * If the stream already has [Symbol.asyncIterator] defined (native or polyfilled),
195
+ * it is returned as-is. Otherwise, [Symbol.asyncIterator] is defined on the
196
+ * stream instance (not the prototype).
197
+ *
198
+ * The returned value is the same ReadableStream instance, so:
199
+ * - `stream instanceof ReadableStream` remains true
200
+ * - Any code relying on native branding/internal slots continues to work
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * const stream = someApiReturningReadableStream();
205
+ * const iterableStream = asAsyncIterableReadableStream(stream);
206
+ *
207
+ * // Now works on Safari/iOS:
208
+ * for await (const chunk of iterableStream) {
209
+ * console.log(chunk);
210
+ * }
211
+ * ```
212
+ */
213
+ export function asAsyncIterableReadableStream<T>(
214
+ stream: ReadableStream<T>
215
+ ): ReadableStreamAsyncIterable<T> {
216
+ if (!hasAsyncIterator(stream)) {
217
+ defineAsyncIterator(stream)
218
+ }
219
+ return stream as ReadableStreamAsyncIterable<T>
220
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Durable Streams Protocol Constants
3
+ *
4
+ * Header and query parameter names following the Electric Durable Stream Protocol.
5
+ */
6
+
7
+ // ============================================================================
8
+ // Response Headers
9
+ // ============================================================================
10
+
11
+ /**
12
+ * Response header containing the next offset to read from.
13
+ * Offsets are opaque tokens - clients MUST NOT interpret the format.
14
+ */
15
+ export const STREAM_OFFSET_HEADER = `Stream-Next-Offset`
16
+
17
+ /**
18
+ * Response header for cursor (used for CDN collapsing).
19
+ * Echo this value in subsequent long-poll requests.
20
+ */
21
+ export const STREAM_CURSOR_HEADER = `Stream-Cursor`
22
+
23
+ /**
24
+ * Presence header indicating response ends at current end of stream.
25
+ * When present (any value), indicates up-to-date.
26
+ */
27
+ export const STREAM_UP_TO_DATE_HEADER = `Stream-Up-To-Date`
28
+
29
+ // ============================================================================
30
+ // Request Headers
31
+ // ============================================================================
32
+
33
+ /**
34
+ * Request header for writer coordination sequence.
35
+ * Monotonic, lexicographic. If lower than last appended seq -> 409 Conflict.
36
+ */
37
+ export const STREAM_SEQ_HEADER = `Stream-Seq`
38
+
39
+ /**
40
+ * Request header for stream TTL in seconds (on create).
41
+ */
42
+ export const STREAM_TTL_HEADER = `Stream-TTL`
43
+
44
+ /**
45
+ * Request header for absolute stream expiry time (RFC3339, on create).
46
+ */
47
+ export const STREAM_EXPIRES_AT_HEADER = `Stream-Expires-At`
48
+
49
+ // ============================================================================
50
+ // Query Parameters
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Query parameter for starting offset.
55
+ */
56
+ export const OFFSET_QUERY_PARAM = `offset`
57
+
58
+ /**
59
+ * Query parameter for live mode.
60
+ * Values: "long-poll", "sse"
61
+ */
62
+ export const LIVE_QUERY_PARAM = `live`
63
+
64
+ /**
65
+ * Query parameter for echoing cursor (CDN collapsing).
66
+ */
67
+ export const CURSOR_QUERY_PARAM = `cursor`
68
+
69
+ // ============================================================================
70
+ // SSE Control Event Fields (camelCase per PROTOCOL.md Section 5.7)
71
+ // ============================================================================
72
+
73
+ /**
74
+ * SSE control event field for the next offset.
75
+ * Note: Different from HTTP header name (camelCase vs Header-Case).
76
+ */
77
+ export const SSE_OFFSET_FIELD = `streamNextOffset`
78
+
79
+ /**
80
+ * SSE control event field for cursor.
81
+ * Note: Different from HTTP header name (camelCase vs Header-Case).
82
+ */
83
+ export const SSE_CURSOR_FIELD = `streamCursor`
84
+
85
+ // ============================================================================
86
+ // Internal Constants
87
+ // ============================================================================
88
+
89
+ /**
90
+ * Content types that support SSE mode.
91
+ * SSE is only valid for text/* or application/json streams.
92
+ */
93
+ export const SSE_COMPATIBLE_CONTENT_TYPES: ReadonlyArray<string> = [
94
+ `text/`,
95
+ `application/json`,
96
+ ]
97
+
98
+ /**
99
+ * Protocol query parameters that should not be set by users.
100
+ */
101
+ export const DURABLE_STREAM_PROTOCOL_QUERY_PARAMS: Array<string> = [
102
+ OFFSET_QUERY_PARAM,
103
+ LIVE_QUERY_PARAM,
104
+ CURSOR_QUERY_PARAM,
105
+ ]