UNPKG

@durable-streams/client

Version:

TypeScript client for the Durable Streams protocol

210 lines (188 loc) 5.15 kB
import type { DurableStreamErrorCode } from "./types" /** * Error thrown for transport/network errors. * Following the @electric-sql/client FetchError pattern. */ export class FetchError extends Error { status: number text?: string json?: object headers: Record<string, string> constructor( status: number, text: string | undefined, json: object | undefined, headers: Record<string, string>, public url: string, message?: string ) { super( message || `HTTP Error ${status} at ${url}: ${text ?? JSON.stringify(json)}` ) this.name = `FetchError` this.status = status this.text = text this.json = json this.headers = headers } static async fromResponse( response: Response, url: string ): Promise<FetchError> { const status = response.status const headers = Object.fromEntries([...response.headers.entries()]) let text: string | undefined = undefined let json: object | undefined = undefined const contentType = response.headers.get(`content-type`) if (!response.bodyUsed && response.body !== null) { if (contentType && contentType.includes(`application/json`)) { try { json = (await response.json()) as object } catch { // If JSON parsing fails, fall back to text text = await response.text() } } else { text = await response.text() } } return new FetchError(status, text, json, headers, url) } } /** * Error thrown when a fetch operation is aborted during backoff. */ export class FetchBackoffAbortError extends Error { constructor() { super(`Fetch with backoff aborted`) this.name = `FetchBackoffAbortError` } } /** * Protocol-level error for Durable Streams operations. * Provides structured error handling with error codes. */ export class DurableStreamError extends Error { /** * HTTP status code, if applicable. */ status?: number /** * Structured error code for programmatic handling. */ code: DurableStreamErrorCode /** * Additional error details (e.g., raw response body). */ details?: unknown constructor( message: string, code: DurableStreamErrorCode, status?: number, details?: unknown ) { super(message) this.name = `DurableStreamError` this.code = code this.status = status this.details = details } /** * Create a DurableStreamError from an HTTP response. */ static async fromResponse( response: Response, url: string ): Promise<DurableStreamError> { const status = response.status let details: unknown const contentType = response.headers.get(`content-type`) if (!response.bodyUsed && response.body !== null) { if (contentType && contentType.includes(`application/json`)) { try { details = await response.json() } catch { details = await response.text() } } else { details = await response.text() } } const code = statusToCode(status) const message = `Durable stream error at ${url}: ${response.statusText || status}` return new DurableStreamError(message, code, status, details) } /** * Create a DurableStreamError from a FetchError. */ static fromFetchError(error: FetchError): DurableStreamError { const code = statusToCode(error.status) return new DurableStreamError( error.message, code, error.status, error.json ?? error.text ) } } /** * Map HTTP status codes to DurableStreamErrorCode. */ function statusToCode(status: number): DurableStreamErrorCode { switch (status) { case 400: return `BAD_REQUEST` case 401: return `UNAUTHORIZED` case 403: return `FORBIDDEN` case 404: return `NOT_FOUND` case 409: // Could be CONFLICT_SEQ or CONFLICT_EXISTS depending on context // Default to CONFLICT_SEQ, caller can override return `CONFLICT_SEQ` case 429: return `RATE_LIMITED` case 503: return `BUSY` default: return `UNKNOWN` } } /** * Error thrown when stream URL is missing. */ export class MissingStreamUrlError extends Error { constructor() { super(`Invalid stream options: missing required url parameter`) this.name = `MissingStreamUrlError` } } /** * Error thrown when attempting to append to a closed stream. */ export class StreamClosedError extends DurableStreamError { readonly code = `STREAM_CLOSED` as const readonly status = 409 readonly streamClosed = true /** * The final offset of the stream, if available from the response. */ readonly finalOffset?: string constructor(url?: string, finalOffset?: string) { super(`Cannot append to closed stream`, `STREAM_CLOSED`, 409, url) this.name = `StreamClosedError` this.finalOffset = finalOffset } } /** * Error thrown when signal option is invalid. */ export class InvalidSignalError extends Error { constructor() { super(`Invalid signal option. It must be an instance of AbortSignal.`) this.name = `InvalidSignalError` } }