arthron 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +297 -2
- package/dist/index.cjs +665 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +219 -0
- package/dist/index.d.mts +219 -0
- package/dist/index.mjs +656 -0
- package/dist/index.mjs.map +1 -0
- package/llms.txt +99 -0
- package/package.json +59 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/body.ts","../src/error.ts","../src/headers.ts","../src/pipeline.ts","../src/stream.ts","../src/promise.ts","../src/url.ts","../src/client.ts","../src/define.ts","../src/plugins/logging.ts","../src/plugins/retry.ts","../src/plugins/timeout.ts"],"sourcesContent":["type Serialized = {\n body: BodyInit | undefined;\n headers: Record<string, string>;\n};\n\nconst RAW_CTORS = [\n \"FormData\",\n \"Blob\",\n \"ArrayBuffer\",\n \"URLSearchParams\",\n \"ReadableStream\",\n] as const;\n\n/** Bodies fetch already knows how to send. Content-Type is left to the caller / runtime. */\nfunction isRawBody(body: unknown): body is BodyInit {\n if (typeof body === \"string\") return true;\n if (ArrayBuffer.isView(body)) return true;\n return RAW_CTORS.some((name) => {\n const ctor = (globalThis as Record<string, unknown>)[name];\n return typeof ctor === \"function\" && body instanceof ctor;\n });\n}\n\n/**\n * Turn a caller-supplied body into something fetch accepts. Raw bodies pass\n * through untouched; anything else is JSON with `content-type` set — unless\n * the caller already set one (headers arrive lowercased, so a plain lookup is\n * enough).\n */\nexport function serializeBody(\n body: unknown,\n headers: Record<string, string>,\n): Serialized {\n if (body === undefined) return { body: undefined, headers };\n if (isRawBody(body)) return { body, headers };\n const withType =\n \"content-type\" in headers\n ? headers\n : { ...headers, \"content-type\": \"application/json\" };\n return { body: JSON.stringify(body), headers: withType };\n}\n","import type { RequestContext } from \"./types\";\n\nexport type ErrorMessageResolver = (\n data: unknown,\n response: Response,\n) => string | undefined;\n\ntype ArthronErrorInit = {\n message: string;\n status: number;\n statusText: string;\n headers: Headers;\n data: unknown;\n request: RequestContext;\n response?: Response;\n cause?: unknown;\n};\n\n/**\n * The only error arthron itself produces. Two sources: a response with\n * `ok === false`, or fetch rejecting outright (network failure, `status` 0).\n * Everything else — cancellation, interceptor bugs — is rethrown untouched.\n */\nexport class ArthronError extends Error {\n override readonly name = \"ArthronError\";\n /** HTTP status of the failed response; `0` when fetch itself failed (network). */\n readonly status: number;\n readonly statusText: string;\n /** Response headers; empty for network failures. */\n readonly headers: Headers;\n /** Parsed body: JSON if it parses, else the text, else `undefined`. */\n readonly data: unknown;\n /** The context that was sent — url and init after request interceptors. */\n readonly request: RequestContext;\n /** The original Response, still readable (the body was parsed from a clone). Absent for network failures. */\n readonly response: Response | undefined;\n /** The underlying error for network failures. */\n override readonly cause: unknown;\n\n constructor(init: ArthronErrorInit) {\n super(init.message);\n this.status = init.status;\n this.statusText = init.statusText;\n this.headers = init.headers;\n this.data = init.data;\n this.request = init.request;\n this.response = init.response;\n this.cause = init.cause;\n }\n}\n\n/** `true` for errors arthron produced: a `!ok` response or a network failure. Cancellations and plugin bugs are not ArthronErrors. */\nexport function isArthronError(error: unknown): error is ArthronError {\n return error instanceof ArthronError;\n}\n\n/** Cancellation is not an HTTP failure. Matched by name so it works across realms. */\nexport function isCancellation(error: unknown): boolean {\n const name = (error as { name?: unknown } | null | undefined)?.name;\n return name === \"AbortError\" || name === \"TimeoutError\";\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n\n/** `message` → `error` (string) → `error.message` → `detail`. */\nexport function defaultErrorMessage(data: unknown): string | undefined {\n if (!isRecord(data)) return undefined;\n if (typeof data.message === \"string\") return data.message;\n if (typeof data.error === \"string\") return data.error;\n if (isRecord(data.error) && typeof data.error.message === \"string\") {\n return data.error.message;\n }\n if (typeof data.detail === \"string\") return data.detail;\n return undefined;\n}\n\n/** Body as JSON if it parses, else as text, else `undefined` when empty. */\nasync function readBody(response: Response): Promise<unknown> {\n const text = await response.text().catch(() => \"\");\n if (text === \"\") return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nexport async function responseError(\n response: Response,\n request: RequestContext,\n resolve: ErrorMessageResolver = defaultErrorMessage,\n): Promise<ArthronError> {\n // Read from a clone so `error.response` stays usable by the caller.\n const data = await readBody(response.clone());\n return new ArthronError({\n message:\n resolve(data, response) ??\n `Request failed with status ${response.status}`,\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n data,\n request,\n response,\n });\n}\n\nexport function networkError(\n cause: unknown,\n request: RequestContext,\n): ArthronError {\n return new ArthronError({\n message: cause instanceof Error ? cause.message : \"Network error\",\n status: 0,\n statusText: \"\",\n headers: new Headers(),\n data: undefined,\n request,\n cause,\n });\n}\n","/**\n * Normalize any `HeadersInit` (plain object, `Headers`, tuple array) into a\n * plain record. Keys come back lowercased — `Headers` does that for us.\n */\nexport function toHeaderRecord(\n headers: HeadersInit | undefined,\n): Record<string, string> {\n if (!headers) return {};\n return Object.fromEntries(new Headers(headers).entries());\n}\n\n/**\n * Layer `override` on top of `base`. Names are compared case-insensitively and\n * the override wins, so a caller's `content-type` replaces a default\n * `Content-Type` instead of fetch sending both values joined.\n */\nexport function mergeHeaders(\n base: HeadersInit | undefined,\n override: HeadersInit | undefined,\n): Record<string, string> {\n return { ...toHeaderRecord(base), ...toHeaderRecord(override) };\n}\n","import { isArthronError } from \"./error\";\nimport { toHeaderRecord } from \"./headers\";\nimport type {\n ErrorInterceptor,\n Middleware,\n Plugin,\n RequestContext,\n RequestInterceptor,\n ResponseInterceptor,\n} from \"./types\";\n\n/**\n * Interceptors mutate the context in place. A middleware that calls `next`\n * again (retry) must not see the previous attempt's changes, so every attempt\n * starts from a fresh copy.\n */\nfunction cloneContext({ url, init }: RequestContext): RequestContext {\n return { url, init: { ...init, headers: toHeaderRecord(init.headers) } };\n}\n\n/**\n * Owns the order of one request.\n *\n * Middleware wraps the whole call, outermost first in registration order.\n * Each attempt below it runs request interceptors → `send` → response\n * interceptors. Only an `ArthronError` reaches the error interceptors; whatever\n * they return is thrown. Anything else that is thrown — cancellation, a bug\n * in a plugin — propagates untouched.\n */\nexport async function runPipeline(\n plugins: readonly Plugin[],\n ctx: RequestContext,\n send: (ctx: RequestContext) => Promise<Response>,\n): Promise<Response> {\n const middlewares: Middleware[] = [];\n const request: RequestInterceptor[] = [];\n const response: ResponseInterceptor[] = [];\n const error: ErrorInterceptor[] = [];\n for (const plugin of plugins) {\n if (plugin.middleware) middlewares.push(plugin.middleware);\n if (plugin.request) request.push(plugin.request);\n if (plugin.response) response.push(plugin.response);\n if (plugin.error) error.push(plugin.error);\n }\n\n const attempt = async (input: RequestContext): Promise<Response> => {\n let current = cloneContext(input);\n try {\n for (const interceptor of request) current = await interceptor(current);\n let res = await send(current);\n for (const interceptor of response) res = await interceptor(res, current);\n return res;\n } catch (thrown) {\n if (!isArthronError(thrown)) throw thrown;\n let err = thrown;\n for (const interceptor of error) err = await interceptor(err, current);\n throw err;\n }\n };\n\n const dispatch = (\n index: number,\n input: RequestContext,\n ): Promise<Response> => {\n const middleware = middlewares[index];\n if (!middleware) return attempt(input);\n return middleware(input, (next) => dispatch(index + 1, next));\n };\n return dispatch(0, ctx);\n}\n","/** One server-sent event, as described by the WHATWG EventSource spec. */\nexport type SseEvent = {\n /** The `event:` field, when the server sent one. */\n event?: string;\n /** All `data:` lines of the block, joined with `\\n`. */\n data: string;\n /** The `id:` field, when present. */\n id?: string;\n /** The `retry:` field in ms, when present and numeric. */\n retry?: number;\n};\n\n/** Yield the body line by line, reassembling lines split across chunks. */\nasync function* lines(response: Response): AsyncGenerator<string> {\n if (!response.body) return;\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n try {\n while (true) {\n const { value, done } = await reader.read();\n buffer += decoder.decode(value, { stream: !done });\n const parts = buffer.split(/\\r\\n|\\r|\\n/);\n buffer = parts.pop() ?? \"\";\n for (const line of parts) yield line;\n if (done) break;\n }\n if (buffer) yield buffer;\n } finally {\n reader.releaseLock();\n }\n}\n\n/**\n * Parse a `text/event-stream` body into events. Follows the spec: a blank\n * line dispatches, `data:` lines are joined with `\\n`, one leading space\n * after the colon is dropped, lines starting with `:` are comments, and a\n * block without `data` is not dispatched. Provider sentinels like\n * `[DONE]` are passed through untouched — they are not part of the spec.\n */\nexport async function* parseSse(response: Response): AsyncGenerator<SseEvent> {\n let data: string[] = [];\n let event: string | undefined;\n let id: string | undefined;\n let retry: number | undefined;\n\n const flush = (): SseEvent | undefined => {\n if (data.length === 0) {\n event = id = retry = undefined;\n return undefined;\n }\n const out: SseEvent = { data: data.join(\"\\n\") };\n if (event !== undefined) out.event = event;\n if (id !== undefined) out.id = id;\n if (retry !== undefined) out.retry = retry;\n data = [];\n event = id = retry = undefined;\n return out;\n };\n\n for await (const line of lines(response)) {\n if (line === \"\") {\n const out = flush();\n if (out) yield out;\n continue;\n }\n if (line.startsWith(\":\")) continue;\n const colon = line.indexOf(\":\");\n const field = colon === -1 ? line : line.slice(0, colon);\n let value = colon === -1 ? \"\" : line.slice(colon + 1);\n if (value.startsWith(\" \")) value = value.slice(1);\n switch (field) {\n case \"data\":\n data.push(value);\n break;\n case \"event\":\n event = value;\n break;\n case \"id\":\n id = value;\n break;\n case \"retry\": {\n const ms = Number(value);\n if (Number.isInteger(ms) && ms >= 0) retry = ms;\n break;\n }\n default:\n break; // unknown fields are ignored, per spec\n }\n }\n const out = flush();\n if (out) yield out;\n}\n\n/**\n * Parse newline-delimited JSON. Blank lines are skipped; a malformed line\n * rejects the iteration with the `SyntaxError` from `JSON.parse`.\n */\nexport async function* parseNdjson<T = unknown>(\n response: Response,\n): AsyncGenerator<T> {\n for await (const line of lines(response)) {\n if (line.trim() === \"\") continue;\n yield JSON.parse(line) as T;\n }\n}\n","import { parseNdjson, parseSse } from \"./stream\";\nimport type { ArthronPromise } from \"./types\";\n\n/** Defer the Response until iteration starts, so the helper itself never rejects. */\nfunction iterate<T>(\n promise: Promise<Response>,\n parse: (res: Response) => AsyncGenerator<T>,\n): AsyncIterable<T> {\n return {\n async *[Symbol.asyncIterator]() {\n yield* parse(await promise);\n },\n };\n}\n\n/**\n * Sugar over `Promise<Response>`: `.json()` is `(await p).json()`. Awaiting\n * without a helper still yields the untouched `Response`.\n */\nexport function toArthronPromise(promise: Promise<Response>): ArthronPromise {\n return Object.assign(promise, {\n json: <T = unknown>() => promise.then((res) => res.json() as Promise<T>),\n text: () => promise.then((res) => res.text()),\n blob: () => promise.then((res) => res.blob()),\n arrayBuffer: () => promise.then((res) => res.arrayBuffer()),\n formData: () => promise.then((res) => res.formData()),\n sse: () => iterate(promise, parseSse),\n ndjson: <T = unknown>() => iterate(promise, parseNdjson<T>),\n });\n}\n","export type QueryParams = Record<\n string,\n string | number | boolean | null | undefined\n>;\n\nconst ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:\\/\\//i;\n\nexport function isAbsoluteUrl(path: string): boolean {\n return ABSOLUTE_URL.test(path);\n}\n\nfunction toQueryString(params: QueryParams | undefined): string {\n if (!params) return \"\";\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value === null || value === undefined) continue;\n search.append(key, String(value));\n }\n return search.toString();\n}\n\n/**\n * Join `baseURL` and `path` by plain string concatenation so a path prefix in\n * the base (`/v1`) survives a leading slash in the path. WHATWG `new URL()`\n * would drop it. Absolute paths skip the base entirely.\n */\nexport function resolveUrl(\n baseURL: string | undefined,\n path: string,\n params?: QueryParams,\n): string {\n let url: string;\n if (isAbsoluteUrl(path)) {\n url = path;\n } else {\n if (baseURL === undefined) {\n throw new TypeError(\n `arthron: relative path \"${path}\" needs a baseURL — pass baseURL in arthron({ baseURL }) or use an absolute URL`,\n );\n }\n url = `${baseURL.replace(/\\/+$/, \"\")}/${path.replace(/^\\/+/, \"\")}`;\n }\n const query = toQueryString(params);\n if (!query) return url;\n // The query goes before any #fragment, or the server never sees it.\n const hash = url.indexOf(\"#\");\n const [base, fragment] =\n hash === -1 ? [url, \"\"] : [url.slice(0, hash), url.slice(hash)];\n return `${base}${base.includes(\"?\") ? \"&\" : \"?\"}${query}${fragment}`;\n}\n","import { serializeBody } from \"./body\";\nimport { isCancellation, networkError, responseError } from \"./error\";\nimport { mergeHeaders, toHeaderRecord } from \"./headers\";\nimport { runPipeline } from \"./pipeline\";\nimport { toArthronPromise } from \"./promise\";\nimport type {\n Arthron,\n ArthronConfig,\n ArthronPromise,\n Plugin,\n RequestContext,\n RequestInitWithBody,\n RequestOptions,\n} from \"./types\";\nimport { resolveUrl } from \"./url\";\n\nconst BODYLESS_METHODS = new Set([\"GET\", \"HEAD\"]);\n\ntype Resolved = Omit<ArthronConfig, \"headers\" | \"use\"> & {\n headers: Record<string, string>;\n use: readonly Plugin[];\n};\n\n/** Normalize once so `extend` can merge headers and append plugins cheaply. */\nfunction resolveConfig(config: ArthronConfig): Resolved {\n const { headers, use, ...rest } = config;\n return { ...rest, headers: toHeaderRecord(headers), use: use ?? [] };\n}\n\n/**\n * Build a client from config. The result is immutable — `use` and `extend`\n * return new clients and never touch the one they were called on.\n */\nexport function arthron(config: ArthronConfig = {}): Arthron {\n const resolved = resolveConfig(config);\n\n /** The one place a failure becomes an ArthronError. Cancellation passes through. */\n const send = async (ctx: RequestContext): Promise<Response> => {\n // Resolved per call, not per client: a global patched after the client was\n // created (msw's `server.listen()`, for one) must still be honoured.\n const fetchImpl = resolved.fetch ?? globalThis.fetch;\n let res: Response;\n try {\n res = await fetchImpl(ctx.url, ctx.init);\n } catch (thrown) {\n if (isCancellation(thrown)) throw thrown;\n throw networkError(thrown, ctx);\n }\n if (!res.ok) throw await responseError(res, ctx, resolved.errorMessage);\n return res;\n };\n\n const dispatch = (\n path: string,\n method: string,\n rawBody: unknown,\n { params, headers: callHeaders, ...init }: RequestOptions,\n ): ArthronPromise => {\n try {\n if (rawBody !== undefined && BODYLESS_METHODS.has(method.toUpperCase())) {\n // fetch rejects these with a TypeError that would otherwise be mistaken for a network failure and retried.\n throw new TypeError(\n `arthron: ${method.toUpperCase()} requests cannot carry a body — send it as params (query) or switch to POST`,\n );\n }\n const url = resolveUrl(resolved.baseURL, path, params);\n const { body, headers } = serializeBody(\n rawBody,\n mergeHeaders(resolved.headers, callHeaders),\n );\n // Config credentials sit underneath so a per-call value wins, like every other option.\n const requestInit: RequestInit = {\n ...(resolved.credentials !== undefined && {\n credentials: resolved.credentials,\n }),\n ...init,\n method,\n headers,\n };\n if (body !== undefined) requestInit.body = body;\n return toArthronPromise(\n runPipeline(resolved.use, { url, init: requestInit }, send),\n );\n } catch (thrown) {\n // Argument errors (no baseURL, unserializable body) reject like everything else.\n return toArthronPromise(Promise.reject(thrown));\n }\n };\n\n const derive = (next: Partial<ArthronConfig>): Arthron => {\n const merged: ArthronConfig = {\n ...resolved,\n ...next,\n headers: mergeHeaders(resolved.headers, next.headers),\n use: [...resolved.use, ...(next.use ?? [])],\n };\n return arthron(merged);\n };\n\n return {\n get: (path, init = {}) => dispatch(path, \"GET\", undefined, init),\n delete: (path, init = {}) => dispatch(path, \"DELETE\", undefined, init),\n head: (path, init = {}) => dispatch(path, \"HEAD\", undefined, init),\n post: (path, body, init = {}) => dispatch(path, \"POST\", body, init),\n put: (path, body, init = {}) => dispatch(path, \"PUT\", body, init),\n patch: (path, body, init = {}) => dispatch(path, \"PATCH\", body, init),\n request: (path, init: RequestInitWithBody) => {\n const { method, body, ...rest } = init;\n if (!method) {\n return toArthronPromise(\n Promise.reject(\n new TypeError(\n 'arthron: request() needs init.method — e.g. request(path, { method: \"OPTIONS\" }); use get/post/... for the common verbs',\n ),\n ),\n );\n }\n return dispatch(path, method, body, rest);\n },\n use: (...plugins) => derive({ use: plugins }),\n extend: (next) => derive(next),\n };\n}\n","import type {\n ErrorInterceptor,\n Middleware,\n Plugin,\n RequestInterceptor,\n ResponseInterceptor,\n} from \"./types\";\n\n/*\n * Identity helpers. They do nothing at runtime; they exist so a plugin written\n * in its own file gets contextual typing without an explicit annotation.\n */\n\n/** Identity helper: gives a middleware written in its own file the right parameter types. */\nexport const defineMiddleware = (middleware: Middleware): Middleware =>\n middleware;\n\n/** Identity helpers for each interceptor kind — `defineInterceptor.request(fn)` etc. */\nexport const defineInterceptor = {\n request: (interceptor: RequestInterceptor): RequestInterceptor => interceptor,\n response: (interceptor: ResponseInterceptor): ResponseInterceptor =>\n interceptor,\n error: (interceptor: ErrorInterceptor): ErrorInterceptor => interceptor,\n};\n\n/** Identity helper: type-checks a plugin object literal without an annotation. */\nexport const definePlugin = (plugin: Plugin): Plugin => plugin;\n","import { isArthronError, isCancellation } from \"../error\";\nimport type { Plugin } from \"../types\";\n\ntype LogEventBase = { method: string; path: string };\nexport type LogEvent =\n | ({ phase: \"start\" } & LogEventBase)\n | ({ phase: \"success\"; status: number; ms: number } & LogEventBase)\n | ({\n phase: \"error\";\n /** HTTP status for an ArthronError (0 = network); undefined for anything else. */\n status: number | undefined;\n ms: number;\n error: unknown;\n } & LogEventBase);\n\nexport type LoggingOptions = {\n /** Default `true`. The library never reads `NODE_ENV` — pass `isDev` yourself. */\n enabled?: boolean;\n logger?: Pick<Console, \"log\" | \"error\">;\n /** Receive structured events instead of the default console lines. */\n onEvent?: (event: LogEvent) => void;\n /** Clock in ms. Injectable for tests. */\n now?: () => number;\n};\n\n/** Origin + pathname only — the query and userinfo tend to carry secrets. */\nfunction displayPath(url: string): string {\n try {\n const parsed = new URL(url);\n return `${parsed.origin}${parsed.pathname}`;\n } catch {\n return url.split(\"?\")[0] ?? url;\n }\n}\n\nfunction consoleSink(logger: Pick<Console, \"log\" | \"error\">) {\n return (e: LogEvent) => {\n if (e.phase === \"start\") {\n logger.log(`[arthron] → ${e.method} ${e.path}`);\n } else if (e.phase === \"success\") {\n logger.log(`[arthron] ← ${e.method} ${e.path} ${e.status} ${e.ms}ms`);\n } else {\n logger.error(\n `[arthron] ✕ ${e.method} ${e.path} ${e.status ?? \"-\"} ${e.ms}ms`,\n );\n }\n };\n}\n\n/**\n * One event when a request starts, one when it settles, with status and\n * elapsed time. Wraps the whole call, so retries are included in the time.\n * Default output is a console line per event; pass `onEvent` to get the\n * structured event instead. Cancellations are not reported.\n */\nexport function logging(options: LoggingOptions = {}): Plugin {\n const {\n enabled = true,\n logger = console,\n onEvent,\n now = () => performance.now(),\n } = options;\n const emit = onEvent ?? consoleSink(logger);\n return {\n name: \"logging\",\n middleware: async (ctx, next) => {\n if (!enabled) return next(ctx);\n const method = ctx.init.method ?? \"GET\";\n const path = displayPath(ctx.url);\n const started = now();\n const ms = () => Math.round(now() - started);\n\n emit({ phase: \"start\", method, path });\n try {\n const res = await next(ctx);\n emit({ phase: \"success\", method, path, status: res.status, ms: ms() });\n return res;\n } catch (error) {\n if (!isCancellation(error)) {\n const status = isArthronError(error) ? error.status : undefined;\n emit({ phase: \"error\", method, path, status, ms: ms(), error });\n }\n throw error;\n }\n },\n };\n}\n","import { type ArthronError, isArthronError } from \"../error\";\nimport type { Plugin, RequestContext } from \"../types\";\n\nexport type RetryOptions = {\n /** Number of retries after the first attempt. Default 2 (3 requests total). */\n retries?: number;\n /** Statuses worth retrying. Network errors (status 0) always are. */\n retryOn?: readonly number[];\n /** Methods safe to repeat. Compared case-insensitively. */\n methods?: readonly string[];\n /** Backoff in ms for retry number `retry` (0-based). Default 300 · 2^retry. */\n delay?: (retry: number, error: ArthronError) => number;\n /** Injectable for tests. Default `setTimeout`; rejects with the abort reason. */\n sleep?: (ms: number, signal?: AbortSignal | null) => Promise<void>;\n /**\n * Longest `Retry-After` (ms) worth honouring. Beyond it the request is not\n * retried at all. Default 60 000. Also guards `setTimeout`'s 2^31-1 ms cap,\n * past which timers fire immediately.\n */\n maxRetryAfter?: number;\n};\n\nconst DEFAULT_RETRY_ON = [408, 429, 500, 502, 503, 504] as const;\nconst DEFAULT_METHODS = [\"GET\", \"HEAD\", \"OPTIONS\", \"PUT\", \"DELETE\"] as const;\nconst DEFAULT_MAX_RETRY_AFTER = 60_000;\n\nfunction defaultSleep(ms: number, signal?: AbortSignal | null): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) return reject(signal.reason);\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal?.reason);\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n/** `Retry-After` as ms — seconds or an HTTP-date. `undefined` when absent or unparsable. */\nfunction retryAfterMs(error: ArthronError): number | undefined {\n const value = error.headers.get(\"retry-after\")?.trim();\n if (!value) return undefined;\n const seconds = Number(value);\n if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1000);\n const at = Date.parse(value);\n if (Number.isNaN(at)) return undefined;\n return Math.max(0, at - Date.now());\n}\n\nfunction isStreamBody(ctx: RequestContext): boolean {\n return (\n typeof ReadableStream !== \"undefined\" &&\n ctx.init.body instanceof ReadableStream\n );\n}\n\n/**\n * Retry on network errors and the configured statuses by calling `next`\n * again — the whole attempt (request interceptors included) is re-run.\n * Only `ArthronError` is considered; cancellation and plugin bugs pass through.\n */\nexport function retry(options: RetryOptions = {}): Plugin {\n const {\n retries = 2,\n retryOn = DEFAULT_RETRY_ON,\n methods = DEFAULT_METHODS,\n delay = (retry) => 300 * 2 ** retry,\n sleep = defaultSleep,\n maxRetryAfter = DEFAULT_MAX_RETRY_AFTER,\n } = options;\n const allowed = new Set(methods.map((m) => m.toUpperCase()));\n\n return {\n name: \"retry\",\n middleware: async (ctx, next) => {\n const method = (ctx.init.method ?? \"GET\").toUpperCase();\n const eligible = allowed.has(method) && !isStreamBody(ctx);\n\n for (let attempt = 0; ; attempt += 1) {\n try {\n return await next(ctx);\n } catch (thrown) {\n if (!eligible || attempt >= retries) throw thrown;\n if (!isArthronError(thrown)) throw thrown;\n if (thrown.status !== 0 && !retryOn.includes(thrown.status)) {\n throw thrown;\n }\n if (ctx.init.signal?.aborted) throw thrown;\n const retryAfter = retryAfterMs(thrown);\n if (retryAfter !== undefined && retryAfter > maxRetryAfter) {\n throw thrown;\n }\n await sleep(retryAfter ?? delay(attempt, thrown), ctx.init.signal);\n }\n }\n },\n };\n}\n","import type { Plugin } from \"../types\";\n\n/**\n * Wrap `body` so the deadline keeps running until it is fully read, and so an\n * abort while reading surfaces as a stream error instead of a silent stall.\n */\nfunction guardBody(\n body: ReadableStream<Uint8Array>,\n signal: AbortSignal,\n finish: () => void,\n): ReadableStream<Uint8Array> {\n const reader = body.getReader();\n return new ReadableStream<Uint8Array>({\n start(controller) {\n signal.addEventListener(\n \"abort\",\n () => {\n controller.error(signal.reason);\n reader.cancel(signal.reason).catch(() => {});\n },\n { once: true },\n );\n },\n async pull(controller) {\n const { value, done } = await reader.read();\n if (done) {\n finish();\n controller.close();\n return;\n }\n controller.enqueue(value);\n },\n cancel(reason) {\n finish();\n return reader.cancel(reason);\n },\n });\n}\n\n/**\n * Abort the request when it takes longer than `ms` — counted from the start\n * of the attempt until the body has been fully read, so a server that sends\n * headers and then stalls still trips it. Rejects with a `TimeoutError` (a\n * `DOMException`, like `AbortSignal.timeout()` would give); it is not an\n * `ArthronError`, so error interceptors and `retry` leave it alone.\n *\n * Built on `AbortController` + `setTimeout` rather than `AbortSignal.any` /\n * `AbortSignal.timeout` so it runs on Node 18.\n */\nexport function timeout(ms: number): Plugin {\n return {\n name: \"timeout\",\n middleware: async (ctx, next) => {\n const controller = new AbortController();\n const outer = ctx.init.signal;\n const forward = () => controller.abort(outer?.reason);\n if (outer?.aborted) forward();\n else outer?.addEventListener(\"abort\", forward, { once: true });\n\n const timer = setTimeout(\n () =>\n controller.abort(\n new DOMException(\n `arthron: request timed out after ${ms}ms`,\n \"TimeoutError\",\n ),\n ),\n ms,\n );\n const finish = () => {\n clearTimeout(timer);\n outer?.removeEventListener(\"abort\", forward);\n };\n\n let res: Response;\n try {\n res = await next({\n ...ctx,\n init: { ...ctx.init, signal: controller.signal },\n });\n } catch (thrown) {\n finish();\n throw thrown;\n }\n if (!res.body) {\n finish();\n return res;\n }\n return new Response(guardBody(res.body, controller.signal, finish), {\n status: res.status,\n statusText: res.statusText,\n headers: res.headers,\n });\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
//#region src/error.d.ts
|
|
2
|
+
type ErrorMessageResolver = (data: unknown, response: Response) => string | undefined;
|
|
3
|
+
type ArthronErrorInit = {
|
|
4
|
+
message: string;
|
|
5
|
+
status: number;
|
|
6
|
+
statusText: string;
|
|
7
|
+
headers: Headers;
|
|
8
|
+
data: unknown;
|
|
9
|
+
request: RequestContext;
|
|
10
|
+
response?: Response;
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The only error arthron itself produces. Two sources: a response with
|
|
15
|
+
* `ok === false`, or fetch rejecting outright (network failure, `status` 0).
|
|
16
|
+
* Everything else — cancellation, interceptor bugs — is rethrown untouched.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ArthronError extends Error {
|
|
19
|
+
readonly name = "ArthronError";
|
|
20
|
+
/** HTTP status of the failed response; `0` when fetch itself failed (network). */
|
|
21
|
+
readonly status: number;
|
|
22
|
+
readonly statusText: string;
|
|
23
|
+
/** Response headers; empty for network failures. */
|
|
24
|
+
readonly headers: Headers;
|
|
25
|
+
/** Parsed body: JSON if it parses, else the text, else `undefined`. */
|
|
26
|
+
readonly data: unknown;
|
|
27
|
+
/** The context that was sent — url and init after request interceptors. */
|
|
28
|
+
readonly request: RequestContext;
|
|
29
|
+
/** The original Response, still readable (the body was parsed from a clone). Absent for network failures. */
|
|
30
|
+
readonly response: Response | undefined;
|
|
31
|
+
/** The underlying error for network failures. */
|
|
32
|
+
readonly cause: unknown;
|
|
33
|
+
constructor(init: ArthronErrorInit);
|
|
34
|
+
}
|
|
35
|
+
/** `true` for errors arthron produced: a `!ok` response or a network failure. Cancellations and plugin bugs are not ArthronErrors. */
|
|
36
|
+
export declare function isArthronError(error: unknown): error is ArthronError;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/stream.d.ts
|
|
39
|
+
/** One server-sent event, as described by the WHATWG EventSource spec. */
|
|
40
|
+
type SseEvent = {
|
|
41
|
+
/** The `event:` field, when the server sent one. */
|
|
42
|
+
event?: string;
|
|
43
|
+
/** All `data:` lines of the block, joined with `\n`. */
|
|
44
|
+
data: string;
|
|
45
|
+
/** The `id:` field, when present. */
|
|
46
|
+
id?: string;
|
|
47
|
+
/** The `retry:` field in ms, when present and numeric. */
|
|
48
|
+
retry?: number;
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/url.d.ts
|
|
52
|
+
type QueryParams = Record<string, string | number | boolean | null | undefined>;
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/types.d.ts
|
|
55
|
+
type RequestContext = {
|
|
56
|
+
url: string;
|
|
57
|
+
init: RequestInit;
|
|
58
|
+
};
|
|
59
|
+
/** Runs before each attempt. Returns the context to send (may be a new object). */
|
|
60
|
+
type RequestInterceptor = (ctx: RequestContext) => RequestContext | Promise<RequestContext>;
|
|
61
|
+
/** Runs after a successful attempt. The body is single-use — `clone()` to peek. */
|
|
62
|
+
type ResponseInterceptor = (res: Response, ctx: RequestContext) => Response | Promise<Response>;
|
|
63
|
+
/** Runs after a failed attempt. Returns the error to throw; it is always thrown. */
|
|
64
|
+
type ErrorInterceptor = (err: ArthronError, ctx: RequestContext) => ArthronError | Promise<ArthronError>;
|
|
65
|
+
/**
|
|
66
|
+
* Wraps the whole call. `next` runs everything below it (request interceptors →
|
|
67
|
+
* fetch → response/error interceptors) and may be called again to retry.
|
|
68
|
+
* `next` rejects with an `ArthronError`, or with the untouched original when the
|
|
69
|
+
* failure was a cancellation or a bug in a plugin.
|
|
70
|
+
*/
|
|
71
|
+
type Middleware = (ctx: RequestContext, next: (ctx: RequestContext) => Promise<Response>) => Promise<Response>;
|
|
72
|
+
type Plugin = {
|
|
73
|
+
/** For logs and debugging only. */
|
|
74
|
+
name?: string;
|
|
75
|
+
middleware?: Middleware;
|
|
76
|
+
request?: RequestInterceptor;
|
|
77
|
+
response?: ResponseInterceptor;
|
|
78
|
+
error?: ErrorInterceptor;
|
|
79
|
+
};
|
|
80
|
+
type RequestOptions = Omit<RequestInit, "body" | "method"> & {
|
|
81
|
+
params?: QueryParams;
|
|
82
|
+
};
|
|
83
|
+
type RequestInitWithBody = RequestOptions & {
|
|
84
|
+
method: string;
|
|
85
|
+
body?: unknown;
|
|
86
|
+
};
|
|
87
|
+
type ArthronConfig = {
|
|
88
|
+
/** Prefix for relative paths. Without it only absolute URLs are accepted. */
|
|
89
|
+
baseURL?: string;
|
|
90
|
+
/** Default headers. Per-call headers win, compared case-insensitively. */
|
|
91
|
+
headers?: HeadersInit;
|
|
92
|
+
/** Forwarded to fetch as-is. Not set unless you set it. */
|
|
93
|
+
credentials?: RequestCredentials;
|
|
94
|
+
/** Injection point for tests and polyfills. Defaults to `globalThis.fetch`. */
|
|
95
|
+
fetch?: typeof fetch;
|
|
96
|
+
use?: Plugin[];
|
|
97
|
+
/** How to pick a message out of an error body. */
|
|
98
|
+
errorMessage?: ErrorMessageResolver;
|
|
99
|
+
};
|
|
100
|
+
type ArthronPromise = Promise<Response> & {
|
|
101
|
+
json<T = unknown>(): Promise<T>;
|
|
102
|
+
text(): Promise<string>;
|
|
103
|
+
blob(): Promise<Blob>;
|
|
104
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
105
|
+
formData(): Promise<FormData>;
|
|
106
|
+
/** Iterate a `text/event-stream` body as events. */
|
|
107
|
+
sse(): AsyncIterable<SseEvent>;
|
|
108
|
+
/** Iterate a newline-delimited JSON body, one parsed value per line. */
|
|
109
|
+
ndjson<T = unknown>(): AsyncIterable<T>;
|
|
110
|
+
};
|
|
111
|
+
type Arthron = {
|
|
112
|
+
get(path: string, init?: RequestOptions): ArthronPromise;
|
|
113
|
+
delete(path: string, init?: RequestOptions): ArthronPromise;
|
|
114
|
+
head(path: string, init?: RequestOptions): ArthronPromise;
|
|
115
|
+
post(path: string, body?: unknown, init?: RequestOptions): ArthronPromise;
|
|
116
|
+
put(path: string, body?: unknown, init?: RequestOptions): ArthronPromise;
|
|
117
|
+
patch(path: string, body?: unknown, init?: RequestOptions): ArthronPromise;
|
|
118
|
+
request(path: string, init: RequestInitWithBody): ArthronPromise;
|
|
119
|
+
/** New client with these plugins appended. The receiver is untouched. */
|
|
120
|
+
use(...plugins: Plugin[]): Arthron;
|
|
121
|
+
/** New client with config shallow-merged, headers merged, plugins appended. */
|
|
122
|
+
extend(config: Partial<ArthronConfig>): Arthron;
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/client.d.ts
|
|
126
|
+
/**
|
|
127
|
+
* Build a client from config. The result is immutable — `use` and `extend`
|
|
128
|
+
* return new clients and never touch the one they were called on.
|
|
129
|
+
*/
|
|
130
|
+
export declare function arthron(config?: ArthronConfig): Arthron;
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/define.d.ts
|
|
133
|
+
/** Identity helper: gives a middleware written in its own file the right parameter types. */
|
|
134
|
+
export declare const defineMiddleware: (middleware: Middleware) => Middleware;
|
|
135
|
+
/** Identity helpers for each interceptor kind — `defineInterceptor.request(fn)` etc. */
|
|
136
|
+
export declare const defineInterceptor: {
|
|
137
|
+
request: (interceptor: RequestInterceptor) => RequestInterceptor;
|
|
138
|
+
response: (interceptor: ResponseInterceptor) => ResponseInterceptor;
|
|
139
|
+
error: (interceptor: ErrorInterceptor) => ErrorInterceptor;
|
|
140
|
+
};
|
|
141
|
+
/** Identity helper: type-checks a plugin object literal without an annotation. */
|
|
142
|
+
export declare const definePlugin: (plugin: Plugin) => Plugin;
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/plugins/logging.d.ts
|
|
145
|
+
type LogEventBase = {
|
|
146
|
+
method: string;
|
|
147
|
+
path: string;
|
|
148
|
+
};
|
|
149
|
+
type LogEvent = ({
|
|
150
|
+
phase: "start";
|
|
151
|
+
} & LogEventBase) | ({
|
|
152
|
+
phase: "success";
|
|
153
|
+
status: number;
|
|
154
|
+
ms: number;
|
|
155
|
+
} & LogEventBase) | ({
|
|
156
|
+
phase: "error";
|
|
157
|
+
/** HTTP status for an ArthronError (0 = network); undefined for anything else. */
|
|
158
|
+
status: number | undefined;
|
|
159
|
+
ms: number;
|
|
160
|
+
error: unknown;
|
|
161
|
+
} & LogEventBase);
|
|
162
|
+
type LoggingOptions = {
|
|
163
|
+
/** Default `true`. The library never reads `NODE_ENV` — pass `isDev` yourself. */
|
|
164
|
+
enabled?: boolean;
|
|
165
|
+
logger?: Pick<Console, "log" | "error">;
|
|
166
|
+
/** Receive structured events instead of the default console lines. */
|
|
167
|
+
onEvent?: (event: LogEvent) => void;
|
|
168
|
+
/** Clock in ms. Injectable for tests. */
|
|
169
|
+
now?: () => number;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* One event when a request starts, one when it settles, with status and
|
|
173
|
+
* elapsed time. Wraps the whole call, so retries are included in the time.
|
|
174
|
+
* Default output is a console line per event; pass `onEvent` to get the
|
|
175
|
+
* structured event instead. Cancellations are not reported.
|
|
176
|
+
*/
|
|
177
|
+
export declare function logging(options?: LoggingOptions): Plugin;
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/plugins/retry.d.ts
|
|
180
|
+
type RetryOptions = {
|
|
181
|
+
/** Number of retries after the first attempt. Default 2 (3 requests total). */
|
|
182
|
+
retries?: number;
|
|
183
|
+
/** Statuses worth retrying. Network errors (status 0) always are. */
|
|
184
|
+
retryOn?: readonly number[];
|
|
185
|
+
/** Methods safe to repeat. Compared case-insensitively. */
|
|
186
|
+
methods?: readonly string[];
|
|
187
|
+
/** Backoff in ms for retry number `retry` (0-based). Default 300 · 2^retry. */
|
|
188
|
+
delay?: (retry: number, error: ArthronError) => number;
|
|
189
|
+
/** Injectable for tests. Default `setTimeout`; rejects with the abort reason. */
|
|
190
|
+
sleep?: (ms: number, signal?: AbortSignal | null) => Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* Longest `Retry-After` (ms) worth honouring. Beyond it the request is not
|
|
193
|
+
* retried at all. Default 60 000. Also guards `setTimeout`'s 2^31-1 ms cap,
|
|
194
|
+
* past which timers fire immediately.
|
|
195
|
+
*/
|
|
196
|
+
maxRetryAfter?: number;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Retry on network errors and the configured statuses by calling `next`
|
|
200
|
+
* again — the whole attempt (request interceptors included) is re-run.
|
|
201
|
+
* Only `ArthronError` is considered; cancellation and plugin bugs pass through.
|
|
202
|
+
*/
|
|
203
|
+
export declare function retry(options?: RetryOptions): Plugin;
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/plugins/timeout.d.ts
|
|
206
|
+
/**
|
|
207
|
+
* Abort the request when it takes longer than `ms` — counted from the start
|
|
208
|
+
* of the attempt until the body has been fully read, so a server that sends
|
|
209
|
+
* headers and then stalls still trips it. Rejects with a `TimeoutError` (a
|
|
210
|
+
* `DOMException`, like `AbortSignal.timeout()` would give); it is not an
|
|
211
|
+
* `ArthronError`, so error interceptors and `retry` leave it alone.
|
|
212
|
+
*
|
|
213
|
+
* Built on `AbortController` + `setTimeout` rather than `AbortSignal.any` /
|
|
214
|
+
* `AbortSignal.timeout` so it runs on Node 18.
|
|
215
|
+
*/
|
|
216
|
+
export declare function timeout(ms: number): Plugin;
|
|
217
|
+
//#endregion
|
|
218
|
+
export type { Arthron, ArthronConfig, ArthronPromise, ErrorInterceptor, ErrorMessageResolver, LogEvent, LoggingOptions, Middleware, Plugin, QueryParams, RequestContext, RequestInterceptor, RequestOptions, ResponseInterceptor, RetryOptions, SseEvent };
|
|
219
|
+
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
//#region src/error.d.ts
|
|
2
|
+
type ErrorMessageResolver = (data: unknown, response: Response) => string | undefined;
|
|
3
|
+
type ArthronErrorInit = {
|
|
4
|
+
message: string;
|
|
5
|
+
status: number;
|
|
6
|
+
statusText: string;
|
|
7
|
+
headers: Headers;
|
|
8
|
+
data: unknown;
|
|
9
|
+
request: RequestContext;
|
|
10
|
+
response?: Response;
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The only error arthron itself produces. Two sources: a response with
|
|
15
|
+
* `ok === false`, or fetch rejecting outright (network failure, `status` 0).
|
|
16
|
+
* Everything else — cancellation, interceptor bugs — is rethrown untouched.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ArthronError extends Error {
|
|
19
|
+
readonly name = "ArthronError";
|
|
20
|
+
/** HTTP status of the failed response; `0` when fetch itself failed (network). */
|
|
21
|
+
readonly status: number;
|
|
22
|
+
readonly statusText: string;
|
|
23
|
+
/** Response headers; empty for network failures. */
|
|
24
|
+
readonly headers: Headers;
|
|
25
|
+
/** Parsed body: JSON if it parses, else the text, else `undefined`. */
|
|
26
|
+
readonly data: unknown;
|
|
27
|
+
/** The context that was sent — url and init after request interceptors. */
|
|
28
|
+
readonly request: RequestContext;
|
|
29
|
+
/** The original Response, still readable (the body was parsed from a clone). Absent for network failures. */
|
|
30
|
+
readonly response: Response | undefined;
|
|
31
|
+
/** The underlying error for network failures. */
|
|
32
|
+
readonly cause: unknown;
|
|
33
|
+
constructor(init: ArthronErrorInit);
|
|
34
|
+
}
|
|
35
|
+
/** `true` for errors arthron produced: a `!ok` response or a network failure. Cancellations and plugin bugs are not ArthronErrors. */
|
|
36
|
+
export declare function isArthronError(error: unknown): error is ArthronError;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/stream.d.ts
|
|
39
|
+
/** One server-sent event, as described by the WHATWG EventSource spec. */
|
|
40
|
+
type SseEvent = {
|
|
41
|
+
/** The `event:` field, when the server sent one. */
|
|
42
|
+
event?: string;
|
|
43
|
+
/** All `data:` lines of the block, joined with `\n`. */
|
|
44
|
+
data: string;
|
|
45
|
+
/** The `id:` field, when present. */
|
|
46
|
+
id?: string;
|
|
47
|
+
/** The `retry:` field in ms, when present and numeric. */
|
|
48
|
+
retry?: number;
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/url.d.ts
|
|
52
|
+
type QueryParams = Record<string, string | number | boolean | null | undefined>;
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/types.d.ts
|
|
55
|
+
type RequestContext = {
|
|
56
|
+
url: string;
|
|
57
|
+
init: RequestInit;
|
|
58
|
+
};
|
|
59
|
+
/** Runs before each attempt. Returns the context to send (may be a new object). */
|
|
60
|
+
type RequestInterceptor = (ctx: RequestContext) => RequestContext | Promise<RequestContext>;
|
|
61
|
+
/** Runs after a successful attempt. The body is single-use — `clone()` to peek. */
|
|
62
|
+
type ResponseInterceptor = (res: Response, ctx: RequestContext) => Response | Promise<Response>;
|
|
63
|
+
/** Runs after a failed attempt. Returns the error to throw; it is always thrown. */
|
|
64
|
+
type ErrorInterceptor = (err: ArthronError, ctx: RequestContext) => ArthronError | Promise<ArthronError>;
|
|
65
|
+
/**
|
|
66
|
+
* Wraps the whole call. `next` runs everything below it (request interceptors →
|
|
67
|
+
* fetch → response/error interceptors) and may be called again to retry.
|
|
68
|
+
* `next` rejects with an `ArthronError`, or with the untouched original when the
|
|
69
|
+
* failure was a cancellation or a bug in a plugin.
|
|
70
|
+
*/
|
|
71
|
+
type Middleware = (ctx: RequestContext, next: (ctx: RequestContext) => Promise<Response>) => Promise<Response>;
|
|
72
|
+
type Plugin = {
|
|
73
|
+
/** For logs and debugging only. */
|
|
74
|
+
name?: string;
|
|
75
|
+
middleware?: Middleware;
|
|
76
|
+
request?: RequestInterceptor;
|
|
77
|
+
response?: ResponseInterceptor;
|
|
78
|
+
error?: ErrorInterceptor;
|
|
79
|
+
};
|
|
80
|
+
type RequestOptions = Omit<RequestInit, "body" | "method"> & {
|
|
81
|
+
params?: QueryParams;
|
|
82
|
+
};
|
|
83
|
+
type RequestInitWithBody = RequestOptions & {
|
|
84
|
+
method: string;
|
|
85
|
+
body?: unknown;
|
|
86
|
+
};
|
|
87
|
+
type ArthronConfig = {
|
|
88
|
+
/** Prefix for relative paths. Without it only absolute URLs are accepted. */
|
|
89
|
+
baseURL?: string;
|
|
90
|
+
/** Default headers. Per-call headers win, compared case-insensitively. */
|
|
91
|
+
headers?: HeadersInit;
|
|
92
|
+
/** Forwarded to fetch as-is. Not set unless you set it. */
|
|
93
|
+
credentials?: RequestCredentials;
|
|
94
|
+
/** Injection point for tests and polyfills. Defaults to `globalThis.fetch`. */
|
|
95
|
+
fetch?: typeof fetch;
|
|
96
|
+
use?: Plugin[];
|
|
97
|
+
/** How to pick a message out of an error body. */
|
|
98
|
+
errorMessage?: ErrorMessageResolver;
|
|
99
|
+
};
|
|
100
|
+
type ArthronPromise = Promise<Response> & {
|
|
101
|
+
json<T = unknown>(): Promise<T>;
|
|
102
|
+
text(): Promise<string>;
|
|
103
|
+
blob(): Promise<Blob>;
|
|
104
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
105
|
+
formData(): Promise<FormData>;
|
|
106
|
+
/** Iterate a `text/event-stream` body as events. */
|
|
107
|
+
sse(): AsyncIterable<SseEvent>;
|
|
108
|
+
/** Iterate a newline-delimited JSON body, one parsed value per line. */
|
|
109
|
+
ndjson<T = unknown>(): AsyncIterable<T>;
|
|
110
|
+
};
|
|
111
|
+
type Arthron = {
|
|
112
|
+
get(path: string, init?: RequestOptions): ArthronPromise;
|
|
113
|
+
delete(path: string, init?: RequestOptions): ArthronPromise;
|
|
114
|
+
head(path: string, init?: RequestOptions): ArthronPromise;
|
|
115
|
+
post(path: string, body?: unknown, init?: RequestOptions): ArthronPromise;
|
|
116
|
+
put(path: string, body?: unknown, init?: RequestOptions): ArthronPromise;
|
|
117
|
+
patch(path: string, body?: unknown, init?: RequestOptions): ArthronPromise;
|
|
118
|
+
request(path: string, init: RequestInitWithBody): ArthronPromise;
|
|
119
|
+
/** New client with these plugins appended. The receiver is untouched. */
|
|
120
|
+
use(...plugins: Plugin[]): Arthron;
|
|
121
|
+
/** New client with config shallow-merged, headers merged, plugins appended. */
|
|
122
|
+
extend(config: Partial<ArthronConfig>): Arthron;
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/client.d.ts
|
|
126
|
+
/**
|
|
127
|
+
* Build a client from config. The result is immutable — `use` and `extend`
|
|
128
|
+
* return new clients and never touch the one they were called on.
|
|
129
|
+
*/
|
|
130
|
+
export declare function arthron(config?: ArthronConfig): Arthron;
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/define.d.ts
|
|
133
|
+
/** Identity helper: gives a middleware written in its own file the right parameter types. */
|
|
134
|
+
export declare const defineMiddleware: (middleware: Middleware) => Middleware;
|
|
135
|
+
/** Identity helpers for each interceptor kind — `defineInterceptor.request(fn)` etc. */
|
|
136
|
+
export declare const defineInterceptor: {
|
|
137
|
+
request: (interceptor: RequestInterceptor) => RequestInterceptor;
|
|
138
|
+
response: (interceptor: ResponseInterceptor) => ResponseInterceptor;
|
|
139
|
+
error: (interceptor: ErrorInterceptor) => ErrorInterceptor;
|
|
140
|
+
};
|
|
141
|
+
/** Identity helper: type-checks a plugin object literal without an annotation. */
|
|
142
|
+
export declare const definePlugin: (plugin: Plugin) => Plugin;
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/plugins/logging.d.ts
|
|
145
|
+
type LogEventBase = {
|
|
146
|
+
method: string;
|
|
147
|
+
path: string;
|
|
148
|
+
};
|
|
149
|
+
type LogEvent = ({
|
|
150
|
+
phase: "start";
|
|
151
|
+
} & LogEventBase) | ({
|
|
152
|
+
phase: "success";
|
|
153
|
+
status: number;
|
|
154
|
+
ms: number;
|
|
155
|
+
} & LogEventBase) | ({
|
|
156
|
+
phase: "error";
|
|
157
|
+
/** HTTP status for an ArthronError (0 = network); undefined for anything else. */
|
|
158
|
+
status: number | undefined;
|
|
159
|
+
ms: number;
|
|
160
|
+
error: unknown;
|
|
161
|
+
} & LogEventBase);
|
|
162
|
+
type LoggingOptions = {
|
|
163
|
+
/** Default `true`. The library never reads `NODE_ENV` — pass `isDev` yourself. */
|
|
164
|
+
enabled?: boolean;
|
|
165
|
+
logger?: Pick<Console, "log" | "error">;
|
|
166
|
+
/** Receive structured events instead of the default console lines. */
|
|
167
|
+
onEvent?: (event: LogEvent) => void;
|
|
168
|
+
/** Clock in ms. Injectable for tests. */
|
|
169
|
+
now?: () => number;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* One event when a request starts, one when it settles, with status and
|
|
173
|
+
* elapsed time. Wraps the whole call, so retries are included in the time.
|
|
174
|
+
* Default output is a console line per event; pass `onEvent` to get the
|
|
175
|
+
* structured event instead. Cancellations are not reported.
|
|
176
|
+
*/
|
|
177
|
+
export declare function logging(options?: LoggingOptions): Plugin;
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/plugins/retry.d.ts
|
|
180
|
+
type RetryOptions = {
|
|
181
|
+
/** Number of retries after the first attempt. Default 2 (3 requests total). */
|
|
182
|
+
retries?: number;
|
|
183
|
+
/** Statuses worth retrying. Network errors (status 0) always are. */
|
|
184
|
+
retryOn?: readonly number[];
|
|
185
|
+
/** Methods safe to repeat. Compared case-insensitively. */
|
|
186
|
+
methods?: readonly string[];
|
|
187
|
+
/** Backoff in ms for retry number `retry` (0-based). Default 300 · 2^retry. */
|
|
188
|
+
delay?: (retry: number, error: ArthronError) => number;
|
|
189
|
+
/** Injectable for tests. Default `setTimeout`; rejects with the abort reason. */
|
|
190
|
+
sleep?: (ms: number, signal?: AbortSignal | null) => Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* Longest `Retry-After` (ms) worth honouring. Beyond it the request is not
|
|
193
|
+
* retried at all. Default 60 000. Also guards `setTimeout`'s 2^31-1 ms cap,
|
|
194
|
+
* past which timers fire immediately.
|
|
195
|
+
*/
|
|
196
|
+
maxRetryAfter?: number;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Retry on network errors and the configured statuses by calling `next`
|
|
200
|
+
* again — the whole attempt (request interceptors included) is re-run.
|
|
201
|
+
* Only `ArthronError` is considered; cancellation and plugin bugs pass through.
|
|
202
|
+
*/
|
|
203
|
+
export declare function retry(options?: RetryOptions): Plugin;
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/plugins/timeout.d.ts
|
|
206
|
+
/**
|
|
207
|
+
* Abort the request when it takes longer than `ms` — counted from the start
|
|
208
|
+
* of the attempt until the body has been fully read, so a server that sends
|
|
209
|
+
* headers and then stalls still trips it. Rejects with a `TimeoutError` (a
|
|
210
|
+
* `DOMException`, like `AbortSignal.timeout()` would give); it is not an
|
|
211
|
+
* `ArthronError`, so error interceptors and `retry` leave it alone.
|
|
212
|
+
*
|
|
213
|
+
* Built on `AbortController` + `setTimeout` rather than `AbortSignal.any` /
|
|
214
|
+
* `AbortSignal.timeout` so it runs on Node 18.
|
|
215
|
+
*/
|
|
216
|
+
export declare function timeout(ms: number): Plugin;
|
|
217
|
+
//#endregion
|
|
218
|
+
export type { Arthron, ArthronConfig, ArthronPromise, ErrorInterceptor, ErrorMessageResolver, LogEvent, LoggingOptions, Middleware, Plugin, QueryParams, RequestContext, RequestInterceptor, RequestOptions, ResponseInterceptor, RetryOptions, SseEvent };
|
|
219
|
+
//# sourceMappingURL=index.d.mts.map
|