@vielzeug/courier 1.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/README.md +70 -0
- package/dist/_dev.cjs +2 -0
- package/dist/_dev.cjs.map +1 -0
- package/dist/_dev.d.ts +2 -0
- package/dist/_dev.d.ts.map +1 -0
- package/dist/_dev.js +2 -0
- package/dist/_dev.js.map +1 -0
- package/dist/api.cjs +2 -0
- package/dist/api.cjs.map +1 -0
- package/dist/api.d.ts +43 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +2 -0
- package/dist/api.js.map +1 -0
- package/dist/batcher.cjs +2 -0
- package/dist/batcher.cjs.map +1 -0
- package/dist/batcher.d.ts +66 -0
- package/dist/batcher.d.ts.map +1 -0
- package/dist/batcher.js +2 -0
- package/dist/batcher.js.map +1 -0
- package/dist/courier.cjs +7 -0
- package/dist/courier.cjs.map +1 -0
- package/dist/courier.d.ts +154 -0
- package/dist/courier.d.ts.map +1 -0
- package/dist/courier.iife.js +7 -0
- package/dist/courier.iife.js.map +1 -0
- package/dist/courier.js +7 -0
- package/dist/courier.js.map +1 -0
- package/dist/devtools.cjs +2 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.d.ts +31 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +2 -0
- package/dist/devtools.js.map +1 -0
- package/dist/errors.cjs +2 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.ts +105 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -0
- package/dist/focus.cjs +2 -0
- package/dist/focus.cjs.map +1 -0
- package/dist/focus.d.ts +25 -0
- package/dist/focus.d.ts.map +1 -0
- package/dist/focus.js +2 -0
- package/dist/focus.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/interceptors.cjs +2 -0
- package/dist/interceptors.cjs.map +1 -0
- package/dist/interceptors.d.ts +50 -0
- package/dist/interceptors.d.ts.map +1 -0
- package/dist/interceptors.js +2 -0
- package/dist/interceptors.js.map +1 -0
- package/dist/mutation.cjs +2 -0
- package/dist/mutation.cjs.map +1 -0
- package/dist/mutation.d.ts +81 -0
- package/dist/mutation.d.ts.map +1 -0
- package/dist/mutation.js +2 -0
- package/dist/mutation.js.map +1 -0
- package/dist/persist.cjs +2 -0
- package/dist/persist.cjs.map +1 -0
- package/dist/persist.d.ts +86 -0
- package/dist/persist.d.ts.map +1 -0
- package/dist/persist.js +2 -0
- package/dist/persist.js.map +1 -0
- package/dist/query-cache.cjs +2 -0
- package/dist/query-cache.cjs.map +1 -0
- package/dist/query-cache.d.ts +88 -0
- package/dist/query-cache.d.ts.map +1 -0
- package/dist/query-cache.js +2 -0
- package/dist/query-cache.js.map +1 -0
- package/dist/query-observe.cjs +2 -0
- package/dist/query-observe.cjs.map +1 -0
- package/dist/query-observe.d.ts +17 -0
- package/dist/query-observe.d.ts.map +1 -0
- package/dist/query-observe.js +2 -0
- package/dist/query-observe.js.map +1 -0
- package/dist/query.cjs +2 -0
- package/dist/query.cjs.map +1 -0
- package/dist/query.d.ts +110 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +2 -0
- package/dist/query.js.map +1 -0
- package/dist/response.cjs +2 -0
- package/dist/response.cjs.map +1 -0
- package/dist/response.d.ts +4 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +2 -0
- package/dist/response.js.map +1 -0
- package/dist/retry.cjs +2 -0
- package/dist/retry.cjs.map +1 -0
- package/dist/retry.d.ts +34 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +2 -0
- package/dist/retry.js.map +1 -0
- package/dist/serialize.cjs +2 -0
- package/dist/serialize.cjs.map +1 -0
- package/dist/serialize.d.ts +10 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +2 -0
- package/dist/serialize.js.map +1 -0
- package/dist/stream.cjs +7 -0
- package/dist/stream.cjs.map +1 -0
- package/dist/stream.d.ts +113 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +7 -0
- package/dist/stream.js.map +1 -0
- package/dist/sync-store.cjs +2 -0
- package/dist/sync-store.cjs.map +1 -0
- package/dist/sync-store.d.ts +20 -0
- package/dist/sync-store.d.ts.map +1 -0
- package/dist/sync-store.js +2 -0
- package/dist/sync-store.js.map +1 -0
- package/dist/transport.cjs +2 -0
- package/dist/transport.cjs.map +1 -0
- package/dist/transport.d.ts +59 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +2 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/url.cjs +2 -0
- package/dist/url.cjs.map +1 -0
- package/dist/url.d.ts +49 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +2 -0
- package/dist/url.js.map +1 -0
- package/package.json +52 -0
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Params } from './url';
|
|
2
|
+
import { type TransportCore, type TransportOptions } from './transport';
|
|
3
|
+
export type StreamRequestConfig<P extends string = string> = {
|
|
4
|
+
body?: unknown;
|
|
5
|
+
/** Raw fetch options for advanced use (credentials, cache, mode, referrer, etc.). */
|
|
6
|
+
fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
method?: string;
|
|
9
|
+
params?: P extends string ? Record<string, string | number | boolean> : never;
|
|
10
|
+
query?: Params;
|
|
11
|
+
/** External abort signal. Merged with internal cancellation. */
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
/** Request timeout in ms. Overrides client default (which is `Infinity` for streaming). */
|
|
14
|
+
timeout?: number;
|
|
15
|
+
};
|
|
16
|
+
export type ReconnectOptions = {
|
|
17
|
+
/**
|
|
18
|
+
* Delay between reconnect attempts in ms, or a zero-based function where
|
|
19
|
+
* `attempt` is the number of failures so far (0 = waiting before the 2nd try).
|
|
20
|
+
* Defaults to full-jitter exponential backoff: `[0, min(1s × 2ⁿ, 30s)]`.
|
|
21
|
+
*/
|
|
22
|
+
delay?: number | ((attempt: number) => number);
|
|
23
|
+
/** Total reconnect attempts after the first failure. Defaults to `5`. */
|
|
24
|
+
times?: number;
|
|
25
|
+
};
|
|
26
|
+
export type ReadableConfig<P extends string = string> = StreamRequestConfig<P> & {
|
|
27
|
+
/**
|
|
28
|
+
* Called when the reconnect budget is exhausted or a non-retriable error
|
|
29
|
+
* occurs. Not called when the generator is aborted via signal or dispose.
|
|
30
|
+
*/
|
|
31
|
+
onError?: (error: Error) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Response body parsing mode.
|
|
34
|
+
* - `'text'` (default) — yields raw decoded string chunks as they arrive.
|
|
35
|
+
* - `'ndjson'` — splits by newline and JSON-parses each complete line; use `T` to type the values.
|
|
36
|
+
*/
|
|
37
|
+
parse?: 'ndjson' | 'text';
|
|
38
|
+
/**
|
|
39
|
+
* Auto-reconnect on connection loss. Uses the same backoff semantics as
|
|
40
|
+
* `sse()` reconnect. `true` uses defaults (5 attempts). Defaults to `false`.
|
|
41
|
+
*/
|
|
42
|
+
reconnect?: boolean | ReconnectOptions;
|
|
43
|
+
};
|
|
44
|
+
export type SseOptions<P extends string = string> = StreamRequestConfig<P> & {
|
|
45
|
+
/**
|
|
46
|
+
* Called when the connection is permanently lost (reconnect budget exhausted due to error).
|
|
47
|
+
* Not called on `dispose()`.
|
|
48
|
+
*/
|
|
49
|
+
onError?: (error: Error) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Auto-reconnect on connection loss with exponential backoff.
|
|
52
|
+
* `true` uses defaults (5 attempts). Defaults to `false`.
|
|
53
|
+
*/
|
|
54
|
+
reconnect?: boolean | ReconnectOptions;
|
|
55
|
+
};
|
|
56
|
+
/** Connection lifecycle status for an SSE source. */
|
|
57
|
+
export type SseStatus = 'connecting' | 'open' | 'reconnecting' | 'closed';
|
|
58
|
+
/**
|
|
59
|
+
* Typed SSE event source. `TEvents` maps event name → data type.
|
|
60
|
+
* Non-string data values are automatically JSON-parsed from the raw `data:` field.
|
|
61
|
+
* If parsing fails the raw string is delivered instead.
|
|
62
|
+
*/
|
|
63
|
+
export type SseSource<TEvents extends Record<string, unknown> = Record<string, string>> = {
|
|
64
|
+
[Symbol.dispose](): void;
|
|
65
|
+
/** `true` after `dispose()` is called or the reconnect budget is exhausted. */
|
|
66
|
+
readonly closed: boolean;
|
|
67
|
+
/** Permanently closes the SSE connection. No further events will be dispatched. */
|
|
68
|
+
dispose(): void;
|
|
69
|
+
/**
|
|
70
|
+
* Registers a handler for a named SSE event. Returns an unsubscribe function.
|
|
71
|
+
* The special name `'message'` matches events without an explicit `event:` field.
|
|
72
|
+
* Calling `on()` after `dispose()` returns a no-op unsubscribe.
|
|
73
|
+
*/
|
|
74
|
+
on<K extends keyof TEvents & string>(event: K, handler: (data: TEvents[K]) => void): () => void;
|
|
75
|
+
/** Current connection lifecycle status. Useful for displaying a live-connection indicator. */
|
|
76
|
+
readonly status: SseStatus;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Creates a streaming client exposing `sse()` (Server-Sent Events) and `readable()`
|
|
80
|
+
* (`ReadableStream`/NDJSON). Both share the same interceptor pipeline, header management,
|
|
81
|
+
* and `AbortController` lifecycle as `createApi`.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const stream = createStream({ baseUrl: 'https://api.example.com' });
|
|
86
|
+
*
|
|
87
|
+
* const src = stream.sse<{ message: { text: string } }>('/events', { reconnect: true });
|
|
88
|
+
* src.on('message', (data) => console.log(data.text));
|
|
89
|
+
*
|
|
90
|
+
* for await (const chunk of stream.readable('/completions', { body: { prompt } })) {
|
|
91
|
+
* process.stdout.write(chunk);
|
|
92
|
+
* }
|
|
93
|
+
*
|
|
94
|
+
* // later:
|
|
95
|
+
* stream.dispose();
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export declare function createStream(opts?: TransportOptions & {
|
|
99
|
+
transport?: TransportCore;
|
|
100
|
+
}): {
|
|
101
|
+
cancelAll(): void;
|
|
102
|
+
readonly disposalSignal: AbortSignal;
|
|
103
|
+
dispose(): void;
|
|
104
|
+
readonly disposed: boolean;
|
|
105
|
+
getHeaders: () => Readonly<Record<string, string>>;
|
|
106
|
+
headers: (updates: Record<string, string | undefined>) => void;
|
|
107
|
+
readable: <T = string, P extends string = string>(url: P, config?: ReadableConfig<P>) => AsyncGenerator<T>;
|
|
108
|
+
sse: <TEvents extends Record<string, unknown> = Record<string, string>, P extends string = string>(url: P, config?: SseOptions<P>) => SseSource<TEvents>;
|
|
109
|
+
[Symbol.dispose](): void;
|
|
110
|
+
use: (interceptor: import("./transport").Interceptor) => () => void;
|
|
111
|
+
};
|
|
112
|
+
export type StreamClient = ReturnType<typeof createStream>;
|
|
113
|
+
//# sourceMappingURL=stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAKpC,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,gBAAgB,EAEtB,MAAM,aAAa,CAAC;AAGrB,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IAC3D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,qFAAqF;IACrF,SAAS,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC,CAAC;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,KAAK,CAAC;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,2FAA2F;IAC3F,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;IAC/C,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI,mBAAmB,CAAC,CAAC,CAAC,GAAG;IAC/E;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC;;;;OAIG;IACH,KAAK,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC1B;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI,mBAAmB,CAAC,CAAC,CAAC,GAAG;IAC3E;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;CACxC,CAAC;AAEF,qDAAqD;AACrD,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;AAE1E;;;;GAIG;AACH,MAAM,MAAM,SAAS,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI;IACxF,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;IACzB,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,mFAAmF;IACnF,OAAO,IAAI,IAAI,CAAC;IAChB;;;;OAIG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,OAAO,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAChG,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;CAC5B,CAAC;AAmEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,IAAI,CAAC,EAAE,gBAAgB,GAAG;IAAE,SAAS,CAAC,EAAE,aAAa,CAAA;CAAE;iBAqbnE,IAAI;;eAMN,IAAI;;;;eAxKQ,CAAC,WAAW,CAAC,SAAS,MAAM,gBAC9C,CAAC,WACE,cAAc,CAAC,CAAC,CAAC,KACxB,cAAc,CAAC,CAAC,CAAC;UA5PP,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,2BAA2B,CAAC,SAAS,MAAM,gBACxF,CAAC,WACE,UAAU,CAAC,CAAC,CAAC,KACpB,SAAS,CAAC,OAAO,CAAC;wBAwaC,IAAI;;EAK3B;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC"}
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import{CourierDisposedError as e,CourierHttpError as t,CourierParseError as n,classifyRequestError as r}from"./errors.js";import{buildRequestInit as i}from"./serialize.js";import{anySignal as a,buildTimeoutSignal as o,createTransportCore as s,validateTimeout as c}from"./transport.js";import{buildUrl as l}from"./url.js";import{fullJitterDelay as u}from"./retry.js";import{sleep as d}from"@vielzeug/arsenal";function f(e){try{return JSON.parse(e)}catch{return e}}async function p(e,s,c,u,d){let f=u.method.toUpperCase(),p=l(e.baseUrl,s,u.params,u.query),m=a(u.extSignal,c.signal)??c.signal,h=o(u.timeout??1/0,m),{headers:g,..._}=i(f,e.mergeHeaders(u.headers,d),u.body,h,u.fetchInit??{}),v;try{v=await e.dispatch({headers:g,init:_,url:p})}catch(e){throw r(e,f,p,h??m)}if(!v.ok){let e=await v.text().catch(()=>``);throw t.fromResponse(v,e,f,p)}if(!v.body)throw new n(`Response has no body`);return v}function m(t){let{transport:r,...i}=t??{},a=!r,o=r??s(i);function l(t,n={}){if(o.disposed)throw new e(`StreamClient`);let{body:r,fetchInit:i,headers:a,method:s=r===void 0?`GET`:`POST`,onError:l,params:m,query:h,reconnect:g=!1,signal:_,timeout:v}=n;v!==void 0&&c(v);let y=new Map,b=new AbortController,x=o.track(b),S=!1,C=`connecting`,w=``,T=g===!0?{}:g===!1?{times:0}:g,E=T.times??(g?5:0),D=T.delay??u;function O(e,t){let n=y.get(e);if(!n?.size)return;let r=f(t);for(let e of n)e(r)}async function k(){let e=(await p(o,t,b,{body:r,extSignal:_,fetchInit:i,headers:a,method:s,params:m,query:h,timeout:v},{accept:`text/event-stream`,"cache-control":`no-cache`,...w?{"last-event-id":w}:{}})).body.getReader(),n=new TextDecoder,c=``,l=`message`,u=``,d=``;try{for(;;){let{done:t,value:r}=await e.read();if(t)break;c+=n.decode(r,{stream:!0}).replace(/\r\n/g,`
|
|
2
|
+
`).replace(/\r/g,`
|
|
3
|
+
`);let i;for(;(i=c.indexOf(`
|
|
4
|
+
`))!==-1;){let e=c.slice(0,i);if(c=c.slice(i+1),e===``)u!==``&&(d&&(w=d),O(l,u.replace(/\n$/,``))),l=`message`,u=``,d=``;else if(!e.startsWith(`:`)){let t=e.indexOf(`:`),n=t===-1?e:e.slice(0,t),r=t===-1?``:e.slice(t+1).replace(/^ /,``);if(n===`event`)l=r;else if(n===`data`)u+=(u?`
|
|
5
|
+
`:``)+r;else if(n===`id`&&!r.includes(`\0`))d=r;else if(n===`retry`){let e=parseInt(r,10);Number.isNaN(e)||(D=e)}}}}}finally{e.releaseLock()}}async function A(){let e=0;try{for(;!S&&!o.disposed&&!b.signal.aborted;){let t;C=`connecting`;try{C=`open`,await k()}catch(e){if(S||o.disposed||b.signal.aborted)break;t=e instanceof Error?e:Error(String(e))}if(S||o.disposed||b.signal.aborted)break;if(E===0){t&&l?.(t);break}if(e>=E){l?.(t??Error(`[courier] SSE reconnect budget exhausted`));break}C=`reconnecting`;let n=typeof D==`function`?D(e):D;e++,await d(n,b.signal).catch(()=>{})}}finally{C=`closed`,x()}}return A().catch(e=>{!S&&!o.disposed&&l?.(e instanceof Error?e:Error(String(e)))}),{get closed(){return S},dispose(){S=!0,b.abort(),y.clear()},on(e,t){if(S)return()=>{};let n=y.get(e);return n||(n=new Set,y.set(e,n)),n.add(t),()=>n.delete(t)},get status(){return C},[Symbol.dispose](){this.dispose()}}}async function*m(t,r={}){if(o.disposed)throw new e(`StreamClient`);let{body:i,fetchInit:a,headers:s,method:l=i===void 0?`GET`:`POST`,onError:f,params:m,parse:h=`text`,query:g,reconnect:_=!1,signal:v,timeout:y}=r;y!==void 0&&c(y);let b=_===!0?{}:_===!1?{times:0}:_,x=b.times??(_?5:0),S=b.delay??u,C=new AbortController,w=o.track(C),T=0,E=!1;try{for(;!o.disposed&&!C.signal.aborted;){let e,r=!1;try{let e=(await p(o,t,C,{body:i,extSignal:v,fetchInit:a,headers:s,method:l,params:m,query:g,timeout:y})).body.getReader(),c=new TextDecoder;try{if(h===`ndjson`){let t=``;for(;;){let{done:i,value:a}=await e.read();if(i){let e=t.trim();if(e)try{yield JSON.parse(e)}catch{throw new n(`NDJSON: failed to parse line: ${e.slice(0,200)}`)}r=!0;break}t+=c.decode(a,{stream:!0});let o;for(;(o=t.indexOf(`
|
|
6
|
+
`))!==-1;){let e=t.slice(0,o).trim();if(t=t.slice(o+1),e)try{yield JSON.parse(e)}catch{throw new n(`NDJSON: failed to parse line: ${e.slice(0,200)}`)}}}}else for(;;){let{done:t,value:n}=await e.read();if(t){r=!0;break}yield c.decode(n,{stream:!0})}}finally{e.releaseLock()}}catch(t){if(o.disposed||C.signal.aborted)break;e=t instanceof Error?t:Error(String(t))}if(o.disposed||C.signal.aborted)break;if(x===0){if(e)if(f)f(e);else throw e;else E=r;break}if(T>=x){let t=e??Error(`[courier] readable() reconnect budget exhausted`);if(f)f(t);else if(e)throw e;break}if(r&&!e){E=!0;break}let c=typeof S==`function`?S(T):S;T++,await d(c,C.signal).catch(()=>{})}}finally{E||C.abort(),w()}}return{cancelAll(){o.cancelAll()},get disposalSignal(){return o.disposalSignal},dispose(){a&&o.dispose()},get disposed(){return o.disposed},getHeaders:o.getHeaders,headers:o.headers,readable:m,sse:l,[Symbol.dispose](){this.dispose()},use:o.use}}export{m as createStream};
|
|
7
|
+
//# sourceMappingURL=stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.js","names":[],"sources":["../src/stream.ts"],"sourcesContent":["import { sleep } from '@vielzeug/arsenal';\n\nimport type { Params } from './url';\n\nimport { classifyRequestError, CourierDisposedError, CourierHttpError, CourierParseError } from './errors';\nimport { fullJitterDelay } from './retry';\nimport { buildRequestInit } from './serialize';\nimport {\n anySignal,\n buildTimeoutSignal,\n createTransportCore,\n type TransportCore,\n type TransportOptions,\n validateTimeout,\n} from './transport';\nimport { buildUrl } from './url';\n\nexport type StreamRequestConfig<P extends string = string> = {\n body?: unknown;\n /** Raw fetch options for advanced use (credentials, cache, mode, referrer, etc.). */\n fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;\n headers?: Record<string, string>;\n method?: string;\n params?: P extends string ? Record<string, string | number | boolean> : never;\n query?: Params;\n /** External abort signal. Merged with internal cancellation. */\n signal?: AbortSignal;\n /** Request timeout in ms. Overrides client default (which is `Infinity` for streaming). */\n timeout?: number;\n};\n\nexport type ReconnectOptions = {\n /**\n * Delay between reconnect attempts in ms, or a zero-based function where\n * `attempt` is the number of failures so far (0 = waiting before the 2nd try).\n * Defaults to full-jitter exponential backoff: `[0, min(1s × 2ⁿ, 30s)]`.\n */\n delay?: number | ((attempt: number) => number);\n /** Total reconnect attempts after the first failure. Defaults to `5`. */\n times?: number;\n};\n\nexport type ReadableConfig<P extends string = string> = StreamRequestConfig<P> & {\n /**\n * Called when the reconnect budget is exhausted or a non-retriable error\n * occurs. Not called when the generator is aborted via signal or dispose.\n */\n onError?: (error: Error) => void;\n /**\n * Response body parsing mode.\n * - `'text'` (default) — yields raw decoded string chunks as they arrive.\n * - `'ndjson'` — splits by newline and JSON-parses each complete line; use `T` to type the values.\n */\n parse?: 'ndjson' | 'text';\n /**\n * Auto-reconnect on connection loss. Uses the same backoff semantics as\n * `sse()` reconnect. `true` uses defaults (5 attempts). Defaults to `false`.\n */\n reconnect?: boolean | ReconnectOptions;\n};\n\nexport type SseOptions<P extends string = string> = StreamRequestConfig<P> & {\n /**\n * Called when the connection is permanently lost (reconnect budget exhausted due to error).\n * Not called on `dispose()`.\n */\n onError?: (error: Error) => void;\n /**\n * Auto-reconnect on connection loss with exponential backoff.\n * `true` uses defaults (5 attempts). Defaults to `false`.\n */\n reconnect?: boolean | ReconnectOptions;\n};\n\n/** Connection lifecycle status for an SSE source. */\nexport type SseStatus = 'connecting' | 'open' | 'reconnecting' | 'closed';\n\n/**\n * Typed SSE event source. `TEvents` maps event name → data type.\n * Non-string data values are automatically JSON-parsed from the raw `data:` field.\n * If parsing fails the raw string is delivered instead.\n */\nexport type SseSource<TEvents extends Record<string, unknown> = Record<string, string>> = {\n [Symbol.dispose](): void;\n /** `true` after `dispose()` is called or the reconnect budget is exhausted. */\n readonly closed: boolean;\n /** Permanently closes the SSE connection. No further events will be dispatched. */\n dispose(): void;\n /**\n * Registers a handler for a named SSE event. Returns an unsubscribe function.\n * The special name `'message'` matches events without an explicit `event:` field.\n * Calling `on()` after `dispose()` returns a no-op unsubscribe.\n */\n on<K extends keyof TEvents & string>(event: K, handler: (data: TEvents[K]) => void): () => void;\n /** Current connection lifecycle status. Useful for displaying a live-connection indicator. */\n readonly status: SseStatus;\n};\n\n/** Parse a raw SSE data string: try JSON, fall back to raw string. */\nfunction parseEventData(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Shared stream setup: build URL, combine signals, merge headers, dispatch through\n * transport. Returns the Response so both sse() and readable() can focus on parsing.\n * The caller owns the AbortController lifecycle (track + untrack).\n */\nasync function openStreamWith(\n transport: TransportCore,\n url: string,\n ac: AbortController,\n config: {\n body?: unknown;\n extSignal?: AbortSignal;\n fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;\n headers?: Record<string, string>;\n method: string;\n params?: Params;\n query?: Params;\n timeout?: number;\n },\n extraHeaders?: Record<string, string>,\n): Promise<Response> {\n // Normalize once — error paths and RequestInit both receive the canonical form.\n const method = config.method.toUpperCase();\n const full = buildUrl(transport.baseUrl, url, config.params, config.query);\n const combined = anySignal(config.extSignal, ac.signal) ?? ac.signal;\n // Streaming connections are long-lived — default to Infinity so the transport's\n // REST default (30s) does not cut off streams prematurely.\n const signal = buildTimeoutSignal(config.timeout ?? Number.POSITIVE_INFINITY, combined);\n const requestHeaders = transport.mergeHeaders(config.headers, extraHeaders);\n const { headers: initHeaders, ...restInit } = buildRequestInit(\n method,\n requestHeaders,\n config.body,\n signal,\n config.fetchInit ?? {},\n );\n\n let res: Response;\n\n try {\n res = await transport.dispatch({ headers: initHeaders as Record<string, string>, init: restInit, url: full });\n } catch (err) {\n throw classifyRequestError(err, method, full, signal ?? combined);\n }\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n\n throw CourierHttpError.fromResponse(res, text, method, full);\n }\n\n if (!res.body) throw new CourierParseError('Response has no body');\n\n return res;\n}\n\n/**\n * Creates a streaming client exposing `sse()` (Server-Sent Events) and `readable()`\n * (`ReadableStream`/NDJSON). Both share the same interceptor pipeline, header management,\n * and `AbortController` lifecycle as `createApi`.\n *\n * @example\n * ```ts\n * const stream = createStream({ baseUrl: 'https://api.example.com' });\n *\n * const src = stream.sse<{ message: { text: string } }>('/events', { reconnect: true });\n * src.on('message', (data) => console.log(data.text));\n *\n * for await (const chunk of stream.readable('/completions', { body: { prompt } })) {\n * process.stdout.write(chunk);\n * }\n *\n * // later:\n * stream.dispose();\n * ```\n */\nexport function createStream(opts?: TransportOptions & { transport?: TransportCore }) {\n const { transport: sharedTransport, ...transportOpts } = opts ?? {};\n const ownTransport = !sharedTransport;\n const transport = sharedTransport ?? createTransportCore(transportOpts);\n\n /**\n * Opens a Server-Sent Events connection and returns a typed source.\n *\n * - Intercepts request through the same pipeline as `createApi` (auth, logging, etc.)\n * - Auto-reconnects with exponential backoff when `reconnect` is set\n * - Sends `Last-Event-ID` header on reconnect to resume from last received event\n * - Non-string event data is automatically JSON-parsed\n *\n * @example\n * ```ts\n * const src = stream.sse<{ message: { text: string }; ping: null }>('/events', {\n * reconnect: true,\n * onError: (err) => console.error(err),\n * });\n *\n * src.on('message', (data) => console.log(data.text));\n * src.on('ping', () => {});\n * // later:\n * src.dispose();\n * ```\n */\n function sse<TEvents extends Record<string, unknown> = Record<string, string>, P extends string = string>(\n url: P,\n config: SseOptions<P> = {} as SseOptions<P>,\n ): SseSource<TEvents> {\n if (transport.disposed) throw new CourierDisposedError('StreamClient');\n\n const {\n body,\n fetchInit,\n headers: perRequestHeaders,\n method = body !== undefined ? 'POST' : 'GET',\n onError,\n params,\n query,\n reconnect = false,\n signal: extSignal,\n timeout: cfgTimeout,\n } = config as SseOptions;\n\n if (cfgTimeout !== undefined) validateTimeout(cfgTimeout);\n\n const listeners = new Map<string, Set<(data: unknown) => void>>();\n const ac = new AbortController();\n // Track `ac` once for the entire SSE lifetime — covers active connections AND\n // reconnect sleep windows, so cancelAll()/dispose() always reach it.\n const untrack = transport.track(ac);\n let closed = false;\n let sseStatus: SseStatus = 'connecting';\n let lastEventId = '';\n\n const reconnectOpts: ReconnectOptions = reconnect === true ? {} : reconnect === false ? { times: 0 } : reconnect;\n const maxReconnects = reconnectOpts.times ?? (reconnect ? 5 : 0);\n // `activeReconnectDelay` is mutable so the server's `retry:` field can update\n // the reconnection interval without the caller needing to handle it manually.\n let activeReconnectDelay: number | ((attempt: number) => number) = reconnectOpts.delay ?? fullJitterDelay;\n\n function dispatchEvent(event: string, rawData: string): void {\n const handlers = listeners.get(event);\n\n if (!handlers?.size) return;\n\n const data = parseEventData(rawData);\n\n for (const handler of handlers) handler(data);\n }\n\n async function connect(): Promise<void> {\n const res = await openStreamWith(\n transport,\n url,\n ac,\n {\n body,\n extSignal,\n fetchInit,\n headers: perRequestHeaders as Record<string, string> | undefined,\n method,\n params: params as Params | undefined,\n query,\n timeout: cfgTimeout,\n },\n {\n accept: 'text/event-stream',\n 'cache-control': 'no-cache',\n ...(lastEventId ? { 'last-event-id': lastEventId } : {}),\n },\n );\n\n const reader = res.body!.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentEvent = 'message';\n let currentData = '';\n let currentId = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) break;\n\n // Normalise \\r\\n and \\r to \\n before processing\n buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n\n let nlIdx: number;\n\n while ((nlIdx = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, nlIdx);\n\n buffer = buffer.slice(nlIdx + 1);\n\n if (line === '') {\n // Empty line → dispatch event\n if (currentData !== '') {\n if (currentId) lastEventId = currentId;\n\n dispatchEvent(currentEvent, currentData.replace(/\\n$/, ''));\n }\n\n currentEvent = 'message';\n currentData = '';\n currentId = '';\n } else if (line.startsWith(':')) {\n // Comment — ignore\n } else {\n const colonIdx = line.indexOf(':');\n const field = colonIdx === -1 ? line : line.slice(0, colonIdx);\n const value = colonIdx === -1 ? '' : line.slice(colonIdx + 1).replace(/^ /, '');\n\n if (field === 'event') currentEvent = value;\n else if (field === 'data') currentData += (currentData ? '\\n' : '') + value;\n else if (field === 'id' && !value.includes('\\0')) currentId = value;\n else if (field === 'retry') {\n // Honor the server's advised reconnect interval (SSE spec §9.2.6).\n const ms = parseInt(value, 10);\n\n if (!Number.isNaN(ms)) activeReconnectDelay = ms;\n }\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n async function run(): Promise<void> {\n let attempt = 0;\n\n try {\n while (!closed && !transport.disposed && !ac.signal.aborted) {\n let connectError: Error | undefined;\n\n sseStatus = 'connecting';\n\n try {\n sseStatus = 'open';\n await connect();\n // Clean server-side close — falls through to reconnect logic below.\n // The attempt counter is intentionally NOT reset here: `times`\n // bounds total reconnects regardless of whether the close was clean or an\n // error, preventing infinite reconnection against servers that always close\n // after a response window (e.g. SSE keepalive patterns).\n } catch (err) {\n if (closed || transport.disposed || ac.signal.aborted) break;\n\n connectError = err instanceof Error ? err : new Error(String(err));\n }\n\n if (closed || transport.disposed || ac.signal.aborted) break;\n\n // No reconnect budget → fire onError for errors, stop either way\n if (maxReconnects === 0) {\n if (connectError) onError?.(connectError);\n\n break;\n }\n\n // Budget exhausted → always signal via onError so the caller knows\n // the reconnect cycle has ended (whether the close was clean or not).\n if (attempt >= maxReconnects) {\n onError?.(connectError ?? new Error('[courier] SSE reconnect budget exhausted'));\n\n break;\n }\n\n // Budget available → sleep and retry\n sseStatus = 'reconnecting';\n\n const delay =\n typeof activeReconnectDelay === 'function' ? activeReconnectDelay(attempt) : activeReconnectDelay;\n\n attempt++;\n await sleep(delay, ac.signal).catch(() => {});\n }\n } finally {\n sseStatus = 'closed';\n untrack();\n }\n }\n\n // Surface unexpected programming errors (not network failures) via onError\n run().catch((err) => {\n if (!closed && !transport.disposed) {\n onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n return {\n get closed(): boolean {\n return closed;\n },\n\n dispose(): void {\n closed = true;\n ac.abort();\n listeners.clear();\n },\n\n on<K extends keyof TEvents & string>(event: K, handler: (data: TEvents[K]) => void): () => void {\n // After dispose(), return a no-op to avoid silent dead-listener bugs\n if (closed) return () => {};\n\n let set = listeners.get(event);\n\n if (!set) {\n set = new Set<(data: unknown) => void>();\n listeners.set(event, set);\n }\n\n set.add(handler as (data: unknown) => void);\n\n return () => set.delete(handler as (data: unknown) => void);\n },\n\n get status(): SseStatus {\n return sseStatus;\n },\n\n [Symbol.dispose](): void {\n this.dispose();\n },\n };\n }\n\n /**\n * Opens an HTTP streaming connection and returns an async generator yielding\n * response body chunks as they arrive.\n *\n * - `parse: 'text'` (default) — yields raw decoded string chunks\n * - `parse: 'ndjson'` — splits by newline and JSON-parses each line; use `T` to\n * type the parsed values\n *\n * @example\n * ```ts\n * // Raw text stream\n * for await (const chunk of stream.readable('/completions', { body: { prompt } })) {\n * process.stdout.write(chunk);\n * }\n *\n * // NDJSON (e.g. AI token stream, log stream)\n * for await (const msg of stream.readable<ChatMessage>('/chat', {\n * body: { prompt },\n * parse: 'ndjson',\n * })) {\n * console.log(msg.content);\n * }\n * ```\n */\n async function* readable<T = string, P extends string = string>(\n url: P,\n config: ReadableConfig<P> = {} as ReadableConfig<P>,\n ): AsyncGenerator<T> {\n if (transport.disposed) throw new CourierDisposedError('StreamClient');\n\n const {\n body,\n fetchInit,\n headers: perRequestHeaders,\n method = body !== undefined ? 'POST' : 'GET',\n onError,\n params,\n parse = 'text',\n query,\n reconnect = false,\n signal: extSignal,\n timeout: cfgTimeout,\n } = config as ReadableConfig;\n\n if (cfgTimeout !== undefined) validateTimeout(cfgTimeout);\n\n const reconnectOpts: ReconnectOptions = reconnect === true ? {} : reconnect === false ? { times: 0 } : reconnect;\n const maxReconnects = reconnectOpts.times ?? (reconnect ? 5 : 0);\n const reconnectDelay: number | ((attempt: number) => number) = reconnectOpts.delay ?? fullJitterDelay;\n\n const ac = new AbortController();\n const untrack = transport.track(ac);\n let attempt = 0;\n let overallNaturalEnd = false;\n\n try {\n while (!transport.disposed && !ac.signal.aborted) {\n let connectError: Error | undefined;\n let connectionNaturalEnd = false;\n\n try {\n const res = await openStreamWith(transport, url, ac, {\n body,\n extSignal,\n fetchInit,\n headers: perRequestHeaders as Record<string, string> | undefined,\n method,\n params: params as Params | undefined,\n query,\n timeout: cfgTimeout,\n });\n\n const reader = res.body!.getReader();\n const decoder = new TextDecoder();\n\n try {\n if (parse === 'ndjson') {\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n const remaining = buffer.trim();\n\n if (remaining) {\n try {\n yield JSON.parse(remaining) as T;\n } catch {\n throw new CourierParseError(`NDJSON: failed to parse line: ${remaining.slice(0, 200)}`);\n }\n }\n\n connectionNaturalEnd = true;\n break;\n }\n\n buffer += decoder.decode(value, { stream: true });\n\n let nlIdx: number;\n\n while ((nlIdx = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, nlIdx).trim();\n\n buffer = buffer.slice(nlIdx + 1);\n\n if (line) {\n try {\n yield JSON.parse(line) as T;\n } catch {\n throw new CourierParseError(`NDJSON: failed to parse line: ${line.slice(0, 200)}`);\n }\n }\n }\n }\n } else {\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n connectionNaturalEnd = true;\n break;\n }\n\n yield decoder.decode(value, { stream: true }) as unknown as T;\n }\n }\n } finally {\n reader.releaseLock();\n }\n } catch (err) {\n if (transport.disposed || ac.signal.aborted) break;\n\n connectError = err instanceof Error ? err : new Error(String(err));\n }\n\n if (transport.disposed || ac.signal.aborted) break;\n\n // No reconnect budget — throw or call onError on errors, stop either way\n if (maxReconnects === 0) {\n if (connectError) {\n if (onError) {\n onError(connectError);\n } else {\n throw connectError;\n }\n } else {\n overallNaturalEnd = connectionNaturalEnd;\n }\n\n break;\n }\n\n // Budget exhausted\n if (attempt >= maxReconnects) {\n const budgetError = connectError ?? new Error('[courier] readable() reconnect budget exhausted');\n\n if (onError) {\n onError(budgetError);\n } else if (connectError) {\n throw connectError;\n }\n\n break;\n }\n\n // Clean server close with no reconnect error — done\n if (connectionNaturalEnd && !connectError) {\n overallNaturalEnd = true;\n break;\n }\n\n // Sleep before reconnecting\n const delay = typeof reconnectDelay === 'function' ? reconnectDelay(attempt) : reconnectDelay;\n\n attempt++;\n await sleep(delay, ac.signal).catch(() => {});\n }\n } finally {\n if (!overallNaturalEnd) ac.abort();\n\n untrack();\n }\n }\n\n return {\n cancelAll(): void {\n transport.cancelAll();\n },\n get disposalSignal() {\n return transport.disposalSignal;\n },\n dispose(): void {\n if (ownTransport) transport.dispose();\n },\n get disposed() {\n return transport.disposed;\n },\n getHeaders: transport.getHeaders,\n headers: transport.headers,\n readable,\n sse,\n [Symbol.dispose](): void {\n this.dispose();\n },\n use: transport.use,\n };\n}\n\nexport type StreamClient = ReturnType<typeof createStream>;\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-store.cjs","names":[],"sources":["../src/sync-store.ts"],"sourcesContent":["import type { SyncStore, Unsubscribe } from './types';\n\n/**\n * Converts any object with `peek()` and `subscribe()` methods into a plain\n * `SyncStore<T>` suitable for framework integration adapters.\n *\n * @example\n * ```ts\n * // React\n * const store = toSyncStore(mutation);\n * const state = useSyncExternalStore(store.subscribe, store.peek);\n *\n * // Svelte\n * const readable = toSyncStore(mutation);\n * ```\n */\nexport function toSyncStore<T>(source: { peek(): T; subscribe(cb: () => void): Unsubscribe }): SyncStore<T> {\n return {\n peek: () => source.peek(),\n subscribe: (cb) => source.subscribe(cb),\n };\n}\n"],"mappings":"AAgBA,SAAgB,EAAe,EAA6E,CAC1G,MAAO,CACL,SAAY,EAAO,KAAK,EACxB,UAAY,GAAO,EAAO,UAAU,CAAE,CACxC,CACF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SyncStore, Unsubscribe } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Converts any object with `peek()` and `subscribe()` methods into a plain
|
|
4
|
+
* `SyncStore<T>` suitable for framework integration adapters.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* // React
|
|
9
|
+
* const store = toSyncStore(mutation);
|
|
10
|
+
* const state = useSyncExternalStore(store.subscribe, store.peek);
|
|
11
|
+
*
|
|
12
|
+
* // Svelte
|
|
13
|
+
* const readable = toSyncStore(mutation);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function toSyncStore<T>(source: {
|
|
17
|
+
peek(): T;
|
|
18
|
+
subscribe(cb: () => void): Unsubscribe;
|
|
19
|
+
}): SyncStore<T>;
|
|
20
|
+
//# sourceMappingURL=sync-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-store.d.ts","sourceRoot":"","sources":["../src/sync-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE;IAAE,IAAI,IAAI,CAAC,CAAC;IAAC,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,WAAW,CAAA;CAAE,GAAG,SAAS,CAAC,CAAC,CAAC,CAK1G"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-store.js","names":[],"sources":["../src/sync-store.ts"],"sourcesContent":["import type { SyncStore, Unsubscribe } from './types';\n\n/**\n * Converts any object with `peek()` and `subscribe()` methods into a plain\n * `SyncStore<T>` suitable for framework integration adapters.\n *\n * @example\n * ```ts\n * // React\n * const store = toSyncStore(mutation);\n * const state = useSyncExternalStore(store.subscribe, store.peek);\n *\n * // Svelte\n * const readable = toSyncStore(mutation);\n * ```\n */\nexport function toSyncStore<T>(source: { peek(): T; subscribe(cb: () => void): Unsubscribe }): SyncStore<T> {\n return {\n peek: () => source.peek(),\n subscribe: (cb) => source.subscribe(cb),\n };\n}\n"],"mappings":"AAgBA,SAAgB,EAAe,EAA6E,CAC1G,MAAO,CACL,SAAY,EAAO,KAAK,EACxB,UAAY,GAAO,EAAO,UAAU,CAAE,CACxC,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./errors.cjs");function t(e,n,r){return{headers:n,init:r,url:e,withHeaders(i){let a={...n};for(let[e,t]of Object.entries(i))a[e.toLowerCase()]=t;return t(e,a,r)}}}function n(...e){let t=e.filter(e=>e!=null);if(t.length!==0)return t.length===1?t[0]:AbortSignal.any(t)}var r=3e4;function i(t){if((t<=0||!Number.isFinite(t))&&t!==1/0)throw new e.CourierError(`[courier] timeout must be a positive number or Infinity`)}function a(e,t){return e===1/0?t??void 0:n(AbortSignal.timeout(e),t)}function o(e={}){let{baseUrl:n=``,fetch:a=globalThis.fetch,headers:o={},timeout:s=r}=e;i(s);let c=Object.fromEntries(Object.entries(o).map(([e,t])=>[e.toLowerCase(),t])),l=new Set,u=new AbortController,d=[],f=null,p=!1;function m(){if(f)return f;let e=e=>a(e.url,{...e.init,headers:e.headers});return f=d.length===0?e:d.reduceRight((e,t)=>n=>t(n,e),e),f}function h(e){return d.push(e),f=null,()=>{let t=d.indexOf(e);t!==-1&&(d.splice(t,1),f=null)}}function g(e){for(let[t,n]of Object.entries(e)){let e=t.toLowerCase();n===void 0?delete c[e]:c[e]=n}}function _(){return{...c}}function v(e,t){let n=e?Object.fromEntries(Object.entries(e).map(([e,t])=>[e.toLowerCase(),t])):void 0;return{...c,...n,...t}}function y(e){return l.add(e),()=>l.delete(e)}function b(e){return m()(t(e.url,e.headers,e.init))}function x(){for(let e of[...l])e.abort();l.clear()}function S(){if(!p){p=!0,u.abort();for(let e of l)e.abort();l.clear(),d.length=0,f=null}}return{baseUrl:n,cancelAll:x,dispatch:b,get disposalSignal(){return u.signal},dispose:S,get disposed(){return p},getHeaders:_,getTimeout(){return s},headers:g,mergeHeaders:v,track:y,use:h}}exports.DEFAULT_TIMEOUT=r,exports.anySignal=n,exports.buildTimeoutSignal=a,exports.createTransportCore=o,exports.validateTimeout=i;
|
|
2
|
+
//# sourceMappingURL=transport.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.cjs","names":[],"sources":["../src/transport.ts"],"sourcesContent":["import { CourierError } from './errors';\n\n/**\n * Immutable request context passed through the interceptor pipeline.\n *\n * **Never mutate `init` or `headers` directly.** Use `ctx.withHeaders(updates)` to\n * produce a new context with merged headers — this is the safe, idiomatic pattern\n * for interceptors that need to add or override headers.\n *\n * @example\n * ```ts\n * // Correct — returns a new context\n * return next(ctx.withHeaders({ authorization: `Bearer ${token}` }));\n *\n * // Wrong — mutates shared state, risks stomping other interceptors\n * ctx.init.headers = { ...ctx.init.headers, authorization: `Bearer ${token}` };\n * ```\n */\nexport type FetchContext = {\n readonly headers: Readonly<Record<string, string>>;\n readonly init: Readonly<Omit<RequestInit, 'headers'>>;\n readonly url: string;\n /** Returns a new `FetchContext` with the given header overrides merged in (lowercase keys). */\n withHeaders(updates: Record<string, string>): FetchContext;\n};\n\nfunction makeFetchContext(\n url: string,\n headers: Record<string, string>,\n init: Omit<RequestInit, 'headers'>,\n): FetchContext {\n const ctx: FetchContext = {\n headers,\n init,\n url,\n withHeaders(updates: Record<string, string>): FetchContext {\n const merged = { ...headers };\n\n for (const [k, v] of Object.entries(updates)) merged[k.toLowerCase()] = v;\n\n return makeFetchContext(url, merged, init);\n },\n };\n\n return ctx;\n}\n\nexport function anySignal(...signals: ReadonlyArray<AbortSignal | null | undefined>): AbortSignal | undefined {\n const active = signals.filter((s): s is AbortSignal => s != null);\n\n if (active.length === 0) return undefined;\n\n if (active.length === 1) return active[0];\n\n return AbortSignal.any(active);\n}\nexport type Interceptor = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;\n\nexport type TransportOptions = {\n baseUrl?: string;\n fetch?: typeof globalThis.fetch;\n headers?: Record<string, string>;\n timeout?: number;\n};\n\nexport const DEFAULT_TIMEOUT = 30_000;\n\nexport function validateTimeout(timeoutMs: number): void {\n if ((timeoutMs <= 0 || !Number.isFinite(timeoutMs)) && timeoutMs !== Number.POSITIVE_INFINITY) {\n throw new CourierError('[courier] timeout must be a positive number or Infinity');\n }\n}\n\nexport function buildTimeoutSignal(timeoutMs: number, external?: AbortSignal | null): AbortSignal | undefined {\n if (timeoutMs === Number.POSITIVE_INFINITY) {\n return external ?? undefined;\n }\n\n return anySignal(AbortSignal.timeout(timeoutMs), external);\n}\n\n/**\n * Shared transport core: interceptor pipeline, header management, AbortController lifecycle.\n * Both `createApi` and `createStream` build on this to avoid duplicating infrastructure.\n */\nexport function createTransportCore(opts: TransportOptions = {}) {\n const {\n baseUrl = '',\n fetch: fetchFn = globalThis.fetch,\n headers: initialHeaders = {},\n timeout = DEFAULT_TIMEOUT,\n } = opts;\n\n validateTimeout(timeout);\n\n const globalHeaders: Record<string, string> = Object.fromEntries(\n Object.entries(initialHeaders).map(([k, v]) => [k.toLowerCase(), v]),\n );\n const activeControllers = new Set<AbortController>();\n const disposeController = new AbortController();\n const interceptors: Interceptor[] = [];\n let cachedPipeline: ((ctx: FetchContext) => Promise<Response>) | null = null;\n let disposed = false;\n\n function getPipeline(): (ctx: FetchContext) => Promise<Response> {\n if (cachedPipeline) return cachedPipeline;\n\n const base: (ctx: FetchContext) => Promise<Response> = (ctx) =>\n fetchFn(ctx.url, { ...ctx.init, headers: ctx.headers });\n\n cachedPipeline =\n interceptors.length === 0\n ? base\n : interceptors.reduceRight<(ctx: FetchContext) => Promise<Response>>(\n (next, interceptor) => (ctx) => interceptor(ctx, next),\n base,\n );\n\n return cachedPipeline;\n }\n\n function use(interceptor: Interceptor): () => void {\n interceptors.push(interceptor);\n cachedPipeline = null;\n\n return () => {\n const i = interceptors.indexOf(interceptor);\n\n if (i !== -1) {\n interceptors.splice(i, 1);\n cachedPipeline = null;\n }\n };\n }\n\n function headers(updates: Record<string, string | undefined>): void {\n for (const [key, value] of Object.entries(updates)) {\n const k = key.toLowerCase();\n\n if (value === undefined) delete globalHeaders[k];\n else globalHeaders[k] = value;\n }\n }\n\n /** Returns a read-only snapshot of the current global headers. */\n function getHeaders(): Readonly<Record<string, string>> {\n return { ...globalHeaders };\n }\n\n /**\n * Merge global headers with optional per-request and extra headers.\n * Per-request keys are normalised to lowercase.\n */\n function mergeHeaders(perRequest?: Record<string, string>, extra?: Record<string, string>): Record<string, string> {\n const normalized = perRequest\n ? Object.fromEntries(Object.entries(perRequest).map(([k, v]) => [k.toLowerCase(), v]))\n : undefined;\n\n return { ...globalHeaders, ...normalized, ...extra };\n }\n\n /** Register an AbortController for lifecycle tracking (cancelAll / dispose). Returns an untrack fn. */\n function track(ac: AbortController): () => void {\n activeControllers.add(ac);\n\n return () => activeControllers.delete(ac);\n }\n\n /**\n * Dispatch a request through the interceptor pipeline.\n * Accepts a plain `{ url, headers, init }` object and promotes it to a\n * full `FetchContext` with `withHeaders()` before entering the pipeline.\n */\n function dispatch(raw: {\n headers: Record<string, string>;\n init: Omit<RequestInit, 'headers'>;\n url: string;\n }): Promise<Response> {\n return getPipeline()(makeFetchContext(raw.url, raw.headers, raw.init));\n }\n\n function cancelAll(): void {\n for (const ac of [...activeControllers]) ac.abort();\n activeControllers.clear();\n }\n\n function dispose(): void {\n if (disposed) return;\n\n disposed = true;\n disposeController.abort();\n\n for (const ac of activeControllers) ac.abort();\n\n activeControllers.clear();\n interceptors.length = 0;\n cachedPipeline = null;\n }\n\n return {\n baseUrl,\n cancelAll,\n dispatch,\n get disposalSignal() {\n return disposeController.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n getHeaders,\n /** Returns the configured request timeout in ms. */\n getTimeout(): number {\n return timeout;\n },\n headers,\n mergeHeaders,\n track,\n use,\n };\n}\n\nexport type TransportCore = ReturnType<typeof createTransportCore>;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable request context passed through the interceptor pipeline.
|
|
3
|
+
*
|
|
4
|
+
* **Never mutate `init` or `headers` directly.** Use `ctx.withHeaders(updates)` to
|
|
5
|
+
* produce a new context with merged headers — this is the safe, idiomatic pattern
|
|
6
|
+
* for interceptors that need to add or override headers.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* // Correct — returns a new context
|
|
11
|
+
* return next(ctx.withHeaders({ authorization: `Bearer ${token}` }));
|
|
12
|
+
*
|
|
13
|
+
* // Wrong — mutates shared state, risks stomping other interceptors
|
|
14
|
+
* ctx.init.headers = { ...ctx.init.headers, authorization: `Bearer ${token}` };
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export type FetchContext = {
|
|
18
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
19
|
+
readonly init: Readonly<Omit<RequestInit, 'headers'>>;
|
|
20
|
+
readonly url: string;
|
|
21
|
+
/** Returns a new `FetchContext` with the given header overrides merged in (lowercase keys). */
|
|
22
|
+
withHeaders(updates: Record<string, string>): FetchContext;
|
|
23
|
+
};
|
|
24
|
+
export declare function anySignal(...signals: ReadonlyArray<AbortSignal | null | undefined>): AbortSignal | undefined;
|
|
25
|
+
export type Interceptor = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;
|
|
26
|
+
export type TransportOptions = {
|
|
27
|
+
baseUrl?: string;
|
|
28
|
+
fetch?: typeof globalThis.fetch;
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
timeout?: number;
|
|
31
|
+
};
|
|
32
|
+
export declare const DEFAULT_TIMEOUT = 30000;
|
|
33
|
+
export declare function validateTimeout(timeoutMs: number): void;
|
|
34
|
+
export declare function buildTimeoutSignal(timeoutMs: number, external?: AbortSignal | null): AbortSignal | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Shared transport core: interceptor pipeline, header management, AbortController lifecycle.
|
|
37
|
+
* Both `createApi` and `createStream` build on this to avoid duplicating infrastructure.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createTransportCore(opts?: TransportOptions): {
|
|
40
|
+
baseUrl: string;
|
|
41
|
+
cancelAll: () => void;
|
|
42
|
+
dispatch: (raw: {
|
|
43
|
+
headers: Record<string, string>;
|
|
44
|
+
init: Omit<RequestInit, "headers">;
|
|
45
|
+
url: string;
|
|
46
|
+
}) => Promise<Response>;
|
|
47
|
+
readonly disposalSignal: AbortSignal;
|
|
48
|
+
dispose: () => void;
|
|
49
|
+
readonly disposed: boolean;
|
|
50
|
+
getHeaders: () => Readonly<Record<string, string>>;
|
|
51
|
+
/** Returns the configured request timeout in ms. */
|
|
52
|
+
getTimeout(): number;
|
|
53
|
+
headers: (updates: Record<string, string | undefined>) => void;
|
|
54
|
+
mergeHeaders: (perRequest?: Record<string, string>, extra?: Record<string, string>) => Record<string, string>;
|
|
55
|
+
track: (ac: AbortController) => () => void;
|
|
56
|
+
use: (interceptor: Interceptor) => () => void;
|
|
57
|
+
};
|
|
58
|
+
export type TransportCore = ReturnType<typeof createTransportCore>;
|
|
59
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,+FAA+F;IAC/F,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC;CAC5D,CAAC;AAuBF,wBAAgB,SAAS,CAAC,GAAG,OAAO,EAAE,aAAa,CAAC,WAAW,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,WAAW,GAAG,SAAS,CAQ5G;AACD,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEnH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,eAAe,QAAS,CAAC;AAEtC,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAIvD;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,WAAW,GAAG,IAAI,GAAG,WAAW,GAAG,SAAS,CAM5G;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,gBAAqB;;qBAgGvC,IAAI;oBARH;QACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;QACnC,GAAG,EAAE,MAAM,CAAC;KACb,KAAG,OAAO,CAAC,QAAQ,CAAC;;mBASD,IAAI;;sBAzCD,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAkErD,oDAAoD;kBACtC,MAAM;uBA7EI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KAAG,IAAI;gCAkBhC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;gBAS/F,eAAe,KAAG,MAAM,IAAI;uBAzCrB,WAAW,KAAG,MAAM,IAAI;EAmGnD;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{CourierError as e}from"./errors.js";function t(e,n,r){return{headers:n,init:r,url:e,withHeaders(i){let a={...n};for(let[e,t]of Object.entries(i))a[e.toLowerCase()]=t;return t(e,a,r)}}}function n(...e){let t=e.filter(e=>e!=null);if(t.length!==0)return t.length===1?t[0]:AbortSignal.any(t)}var r=3e4;function i(t){if((t<=0||!Number.isFinite(t))&&t!==1/0)throw new e(`[courier] timeout must be a positive number or Infinity`)}function a(e,t){return e===1/0?t??void 0:n(AbortSignal.timeout(e),t)}function o(e={}){let{baseUrl:n=``,fetch:a=globalThis.fetch,headers:o={},timeout:s=r}=e;i(s);let c=Object.fromEntries(Object.entries(o).map(([e,t])=>[e.toLowerCase(),t])),l=new Set,u=new AbortController,d=[],f=null,p=!1;function m(){if(f)return f;let e=e=>a(e.url,{...e.init,headers:e.headers});return f=d.length===0?e:d.reduceRight((e,t)=>n=>t(n,e),e),f}function h(e){return d.push(e),f=null,()=>{let t=d.indexOf(e);t!==-1&&(d.splice(t,1),f=null)}}function g(e){for(let[t,n]of Object.entries(e)){let e=t.toLowerCase();n===void 0?delete c[e]:c[e]=n}}function _(){return{...c}}function v(e,t){let n=e?Object.fromEntries(Object.entries(e).map(([e,t])=>[e.toLowerCase(),t])):void 0;return{...c,...n,...t}}function y(e){return l.add(e),()=>l.delete(e)}function b(e){return m()(t(e.url,e.headers,e.init))}function x(){for(let e of[...l])e.abort();l.clear()}function S(){if(!p){p=!0,u.abort();for(let e of l)e.abort();l.clear(),d.length=0,f=null}}return{baseUrl:n,cancelAll:x,dispatch:b,get disposalSignal(){return u.signal},dispose:S,get disposed(){return p},getHeaders:_,getTimeout(){return s},headers:g,mergeHeaders:v,track:y,use:h}}export{r as DEFAULT_TIMEOUT,n as anySignal,a as buildTimeoutSignal,o as createTransportCore,i as validateTimeout};
|
|
2
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","names":[],"sources":["../src/transport.ts"],"sourcesContent":["import { CourierError } from './errors';\n\n/**\n * Immutable request context passed through the interceptor pipeline.\n *\n * **Never mutate `init` or `headers` directly.** Use `ctx.withHeaders(updates)` to\n * produce a new context with merged headers — this is the safe, idiomatic pattern\n * for interceptors that need to add or override headers.\n *\n * @example\n * ```ts\n * // Correct — returns a new context\n * return next(ctx.withHeaders({ authorization: `Bearer ${token}` }));\n *\n * // Wrong — mutates shared state, risks stomping other interceptors\n * ctx.init.headers = { ...ctx.init.headers, authorization: `Bearer ${token}` };\n * ```\n */\nexport type FetchContext = {\n readonly headers: Readonly<Record<string, string>>;\n readonly init: Readonly<Omit<RequestInit, 'headers'>>;\n readonly url: string;\n /** Returns a new `FetchContext` with the given header overrides merged in (lowercase keys). */\n withHeaders(updates: Record<string, string>): FetchContext;\n};\n\nfunction makeFetchContext(\n url: string,\n headers: Record<string, string>,\n init: Omit<RequestInit, 'headers'>,\n): FetchContext {\n const ctx: FetchContext = {\n headers,\n init,\n url,\n withHeaders(updates: Record<string, string>): FetchContext {\n const merged = { ...headers };\n\n for (const [k, v] of Object.entries(updates)) merged[k.toLowerCase()] = v;\n\n return makeFetchContext(url, merged, init);\n },\n };\n\n return ctx;\n}\n\nexport function anySignal(...signals: ReadonlyArray<AbortSignal | null | undefined>): AbortSignal | undefined {\n const active = signals.filter((s): s is AbortSignal => s != null);\n\n if (active.length === 0) return undefined;\n\n if (active.length === 1) return active[0];\n\n return AbortSignal.any(active);\n}\nexport type Interceptor = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;\n\nexport type TransportOptions = {\n baseUrl?: string;\n fetch?: typeof globalThis.fetch;\n headers?: Record<string, string>;\n timeout?: number;\n};\n\nexport const DEFAULT_TIMEOUT = 30_000;\n\nexport function validateTimeout(timeoutMs: number): void {\n if ((timeoutMs <= 0 || !Number.isFinite(timeoutMs)) && timeoutMs !== Number.POSITIVE_INFINITY) {\n throw new CourierError('[courier] timeout must be a positive number or Infinity');\n }\n}\n\nexport function buildTimeoutSignal(timeoutMs: number, external?: AbortSignal | null): AbortSignal | undefined {\n if (timeoutMs === Number.POSITIVE_INFINITY) {\n return external ?? undefined;\n }\n\n return anySignal(AbortSignal.timeout(timeoutMs), external);\n}\n\n/**\n * Shared transport core: interceptor pipeline, header management, AbortController lifecycle.\n * Both `createApi` and `createStream` build on this to avoid duplicating infrastructure.\n */\nexport function createTransportCore(opts: TransportOptions = {}) {\n const {\n baseUrl = '',\n fetch: fetchFn = globalThis.fetch,\n headers: initialHeaders = {},\n timeout = DEFAULT_TIMEOUT,\n } = opts;\n\n validateTimeout(timeout);\n\n const globalHeaders: Record<string, string> = Object.fromEntries(\n Object.entries(initialHeaders).map(([k, v]) => [k.toLowerCase(), v]),\n );\n const activeControllers = new Set<AbortController>();\n const disposeController = new AbortController();\n const interceptors: Interceptor[] = [];\n let cachedPipeline: ((ctx: FetchContext) => Promise<Response>) | null = null;\n let disposed = false;\n\n function getPipeline(): (ctx: FetchContext) => Promise<Response> {\n if (cachedPipeline) return cachedPipeline;\n\n const base: (ctx: FetchContext) => Promise<Response> = (ctx) =>\n fetchFn(ctx.url, { ...ctx.init, headers: ctx.headers });\n\n cachedPipeline =\n interceptors.length === 0\n ? base\n : interceptors.reduceRight<(ctx: FetchContext) => Promise<Response>>(\n (next, interceptor) => (ctx) => interceptor(ctx, next),\n base,\n );\n\n return cachedPipeline;\n }\n\n function use(interceptor: Interceptor): () => void {\n interceptors.push(interceptor);\n cachedPipeline = null;\n\n return () => {\n const i = interceptors.indexOf(interceptor);\n\n if (i !== -1) {\n interceptors.splice(i, 1);\n cachedPipeline = null;\n }\n };\n }\n\n function headers(updates: Record<string, string | undefined>): void {\n for (const [key, value] of Object.entries(updates)) {\n const k = key.toLowerCase();\n\n if (value === undefined) delete globalHeaders[k];\n else globalHeaders[k] = value;\n }\n }\n\n /** Returns a read-only snapshot of the current global headers. */\n function getHeaders(): Readonly<Record<string, string>> {\n return { ...globalHeaders };\n }\n\n /**\n * Merge global headers with optional per-request and extra headers.\n * Per-request keys are normalised to lowercase.\n */\n function mergeHeaders(perRequest?: Record<string, string>, extra?: Record<string, string>): Record<string, string> {\n const normalized = perRequest\n ? Object.fromEntries(Object.entries(perRequest).map(([k, v]) => [k.toLowerCase(), v]))\n : undefined;\n\n return { ...globalHeaders, ...normalized, ...extra };\n }\n\n /** Register an AbortController for lifecycle tracking (cancelAll / dispose). Returns an untrack fn. */\n function track(ac: AbortController): () => void {\n activeControllers.add(ac);\n\n return () => activeControllers.delete(ac);\n }\n\n /**\n * Dispatch a request through the interceptor pipeline.\n * Accepts a plain `{ url, headers, init }` object and promotes it to a\n * full `FetchContext` with `withHeaders()` before entering the pipeline.\n */\n function dispatch(raw: {\n headers: Record<string, string>;\n init: Omit<RequestInit, 'headers'>;\n url: string;\n }): Promise<Response> {\n return getPipeline()(makeFetchContext(raw.url, raw.headers, raw.init));\n }\n\n function cancelAll(): void {\n for (const ac of [...activeControllers]) ac.abort();\n activeControllers.clear();\n }\n\n function dispose(): void {\n if (disposed) return;\n\n disposed = true;\n disposeController.abort();\n\n for (const ac of activeControllers) ac.abort();\n\n activeControllers.clear();\n interceptors.length = 0;\n cachedPipeline = null;\n }\n\n return {\n baseUrl,\n cancelAll,\n dispatch,\n get disposalSignal() {\n return disposeController.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n getHeaders,\n /** Returns the configured request timeout in ms. */\n getTimeout(): number {\n return timeout;\n },\n headers,\n mergeHeaders,\n track,\n use,\n };\n}\n\nexport type TransportCore = ReturnType<typeof createTransportCore>;\n"],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single segment in a query key — JSON-safe primitives or a plain object of primitives.
|
|
3
|
+
* No `Date`, `bigint`, `Map`, `Set`, or nested arrays — these are not reliably serialisable.
|
|
4
|
+
*/
|
|
5
|
+
export type QueryKeyAtom = string | number | boolean | null | {
|
|
6
|
+
readonly [k: string]: string | number | boolean | null;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Identifies a cached query. Must be a non-empty flat array of `QueryKeyAtom` values.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* ['users', userId]
|
|
14
|
+
* ['users', userId, 'posts', { page: 1, limit: 10 }]
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export type QueryKey = readonly [QueryKeyAtom, ...QueryKeyAtom[]];
|
|
18
|
+
export type Unsubscribe = () => void;
|
|
19
|
+
/**
|
|
20
|
+
* Minimal external-store contract for framework integrations.
|
|
21
|
+
*
|
|
22
|
+
* - React: useSyncExternalStore(store.subscribe, store.peek)
|
|
23
|
+
* - Vue: assign `store.peek()` into a shallowRef inside `store.subscribe`
|
|
24
|
+
* - Svelte: adapt to `readable` by forwarding `subscribe`
|
|
25
|
+
*/
|
|
26
|
+
export interface SyncStore<T> {
|
|
27
|
+
peek(): T;
|
|
28
|
+
subscribe(onStoreChange: () => void): Unsubscribe;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Lifecycle status for an async operation.
|
|
32
|
+
*
|
|
33
|
+
* - `'loading'` — no data yet; a fetch may or may not be in-flight.
|
|
34
|
+
* - `'success'` — data is available; `isFetching` may be `true` during background revalidation.
|
|
35
|
+
* - `'error'` — the last operation failed; stale `data` from a prior success may still be present.
|
|
36
|
+
*/
|
|
37
|
+
export type AsyncStatus = 'loading' | 'success' | 'error';
|
|
38
|
+
export type AsyncState<T = unknown> = {
|
|
39
|
+
/**
|
|
40
|
+
* `true` while a fetch is in-flight (including background revalidation).
|
|
41
|
+
* Orthogonal to `status` — a `'success'` entry can have `isFetching: true`.
|
|
42
|
+
*/
|
|
43
|
+
readonly isFetching: boolean;
|
|
44
|
+
/** Shorthand for `status === 'loading'`. Useful as a loading-spinner predicate. */
|
|
45
|
+
readonly isLoading: boolean;
|
|
46
|
+
} & ({
|
|
47
|
+
readonly data: undefined;
|
|
48
|
+
readonly error: null;
|
|
49
|
+
readonly status: 'loading';
|
|
50
|
+
readonly updatedAt: undefined;
|
|
51
|
+
} | {
|
|
52
|
+
readonly data: T;
|
|
53
|
+
readonly error: null;
|
|
54
|
+
readonly status: 'success';
|
|
55
|
+
readonly updatedAt: number;
|
|
56
|
+
} | {
|
|
57
|
+
readonly data: T | undefined;
|
|
58
|
+
readonly error: Error;
|
|
59
|
+
readonly status: 'error';
|
|
60
|
+
/** Timestamp (ms since epoch) of when the error was committed. May coexist with stale `data` from a prior successful fetch. */
|
|
61
|
+
readonly updatedAt: number;
|
|
62
|
+
});
|
|
63
|
+
export type QueryState<T = unknown> = AsyncState<T>;
|
|
64
|
+
export type MutationState<TData = unknown> = AsyncState<TData>;
|
|
65
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,YAAY,GACtB,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG;IAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;CAAE,CAAC;AAEhG;;;;;;;;GAQG;AACH,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC,YAAY,EAAE,GAAG,YAAY,EAAE,CAAC,CAAC;AAElE,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,IAAI,IAAI,CAAC,CAAC;IACV,SAAS,CAAC,aAAa,EAAE,MAAM,IAAI,GAAG,WAAW,CAAC;CACnD;AAED;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE1D,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,OAAO,IAAI;IACpC;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,mFAAmF;IACnF,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B,GAAG,CACA;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;CAC/B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,SAAS,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,+HAA+H;IAC/H,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CACJ,CAAC;AAEF,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,OAAO,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;AACpD,MAAM,MAAM,aAAa,CAAC,KAAK,GAAG,OAAO,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC"}
|
package/dist/url.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./errors.cjs");function t(t,n,r,i){let a=t.replace(/\/+$/,``),o=n.replace(/^\/+/,``);o=o.replace(/\{([^}]+)\}/g,(t,i)=>{let a=r?.[i];if(a==null)throw new e.CourierParseError(`unresolved path param {${i}} in '${n}'`);return encodeURIComponent(String(a))});let s=a&&o?`${a}/${o}`:a||o;if(!i)return s;let c=new URLSearchParams;for(let[e,t]of Object.entries(i))if(t!==void 0){if(Array.isArray(t)){for(let n of t)n!==void 0&&c.append(e,n===null?``:String(n));continue}c.append(e,t===null?``:String(t))}let l=c.toString();return l?`${s}${s.includes(`?`)?`&`:`?`}${l}`:s}exports.buildUrl=t;
|
|
2
|
+
//# sourceMappingURL=url.cjs.map
|
package/dist/url.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.cjs","names":[],"sources":["../src/url.ts"],"sourcesContent":["import type { ResponseType } from './response';\n\nimport { CourierParseError } from './errors';\n\ntype QueryScalar = string | number | boolean | null;\n\nexport type ParamValue = QueryScalar | readonly QueryScalar[] | undefined;\nexport type Params = Record<string, ParamValue>;\n\n// Type-safe path params: extracts {param} placeholders from a path string\ntype ExtractPathParams<P extends string> = P extends `${string}{${infer K}}${infer R}`\n ? K | ExtractPathParams<R>\n : never;\n\nexport type PathConfig<P extends string> = [ExtractPathParams<P>] extends [never]\n ? { params?: never }\n : { params: Record<ExtractPathParams<P>, string | number | boolean> };\n\nexport type CourierRequestConfig<P extends string = string, T = unknown> = PathConfig<P> & {\n /** Request body. Plain objects → JSON. BodyInit values passed as-is. */\n body?: unknown;\n /**\n * Set to `false` to bypass in-flight deduplication for this request.\n * Safe + idempotent methods (GET, HEAD, OPTIONS) are deduplicated by default.\n * DELETE is idempotent but has side-effects, so it does not auto-dedupe;\n * pass an explicit `dedupeKey` to opt in.\n */\n dedupe?: boolean;\n /** Explicit deduplication key for non-idempotent requests. */\n dedupeKey?: unknown;\n /** Query string parameters. */\n query?: Params;\n /** Response parsing strategy. */\n responseType?: ResponseType;\n /**\n * Optional schema for response validation. Any object with a `parse(data)` method\n * works — e.g. a `@vielzeug/spell` schema or a plain validator function wrapper.\n * Called after the response body is parsed; throws if validation fails.\n * The schema's return type must be assignable to `T`.\n */\n schema?: { parse(data: unknown): T };\n /** Request timeout in ms. Overrides client default. */\n timeout?: number;\n};\n\nexport type HttpRequestConfig<P extends string = string, T = unknown> = CourierRequestConfig<P, T> & {\n /** Raw fetch options for advanced use (credentials, cache, mode, referrer, etc.). */\n fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;\n /** Per-request headers merged with (and overriding) global client headers. */\n headers?: Record<string, string>;\n /** External abort signal merged with internal cancellation. */\n signal?: AbortSignal;\n};\n\nexport function buildUrl(base: string, path: string, params?: Params, query?: Params): string {\n const baseClean = base.replace(/\\/+$/, '');\n let pathClean = path.replace(/^\\/+/, '');\n\n pathClean = pathClean.replace(/\\{([^}]+)\\}/g, (_, key: string) => {\n const val = params?.[key];\n\n if (val === undefined || val === null) throw new CourierParseError(`unresolved path param {${key}} in '${path}'`);\n\n return encodeURIComponent(String(val));\n });\n\n const url = baseClean && pathClean ? `${baseClean}/${pathClean}` : baseClean || pathClean;\n\n if (!query) return url;\n\n const qs = new URLSearchParams();\n\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) {\n continue;\n }\n\n if (Array.isArray(value)) {\n for (const entry of value) {\n if (entry !== undefined) {\n qs.append(key, entry === null ? '' : String(entry));\n }\n }\n\n continue;\n }\n\n qs.append(key, value === null ? '' : String(value));\n }\n\n const queryString = qs.toString();\n\n return queryString ? `${url}${url.includes('?') ? '&' : '?'}${queryString}` : url;\n}\n"],"mappings":"gCAsDA,SAAgB,EAAS,EAAc,EAAc,EAAiB,EAAwB,CAC5F,IAAM,EAAY,EAAK,QAAQ,OAAQ,EAAE,EACrC,EAAY,EAAK,QAAQ,OAAQ,EAAE,EAEvC,EAAY,EAAU,QAAQ,gBAAiB,EAAG,IAAgB,CAChE,IAAM,EAAM,IAAS,GAErB,GAAI,GAA6B,KAAM,MAAM,IAAI,EAAA,kBAAkB,0BAA0B,EAAI,QAAQ,EAAK,EAAE,EAEhH,OAAO,mBAAmB,OAAO,CAAG,CAAC,CACvC,CAAC,EAED,IAAM,EAAM,GAAa,EAAY,GAAG,EAAU,GAAG,IAAc,GAAa,EAEhF,GAAI,CAAC,EAAO,OAAO,EAEnB,IAAM,EAAK,IAAI,gBAEf,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,CAAK,EACzC,OAAU,IAAA,GAId,IAAI,MAAM,QAAQ,CAAK,EAAG,CACxB,IAAK,IAAM,KAAS,EACd,IAAU,IAAA,IACZ,EAAG,OAAO,EAAK,IAAU,KAAO,GAAK,OAAO,CAAK,CAAC,EAItD,QACF,CAEA,EAAG,OAAO,EAAK,IAAU,KAAO,GAAK,OAAO,CAAK,CAAC,CAFlD,CAKF,IAAM,EAAc,EAAG,SAAS,EAEhC,OAAO,EAAc,GAAG,IAAM,EAAI,SAAS,GAAG,EAAI,IAAM,MAAM,IAAgB,CAChF"}
|
package/dist/url.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ResponseType } from './response';
|
|
2
|
+
type QueryScalar = string | number | boolean | null;
|
|
3
|
+
export type ParamValue = QueryScalar | readonly QueryScalar[] | undefined;
|
|
4
|
+
export type Params = Record<string, ParamValue>;
|
|
5
|
+
type ExtractPathParams<P extends string> = P extends `${string}{${infer K}}${infer R}` ? K | ExtractPathParams<R> : never;
|
|
6
|
+
export type PathConfig<P extends string> = [ExtractPathParams<P>] extends [never] ? {
|
|
7
|
+
params?: never;
|
|
8
|
+
} : {
|
|
9
|
+
params: Record<ExtractPathParams<P>, string | number | boolean>;
|
|
10
|
+
};
|
|
11
|
+
export type CourierRequestConfig<P extends string = string, T = unknown> = PathConfig<P> & {
|
|
12
|
+
/** Request body. Plain objects → JSON. BodyInit values passed as-is. */
|
|
13
|
+
body?: unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Set to `false` to bypass in-flight deduplication for this request.
|
|
16
|
+
* Safe + idempotent methods (GET, HEAD, OPTIONS) are deduplicated by default.
|
|
17
|
+
* DELETE is idempotent but has side-effects, so it does not auto-dedupe;
|
|
18
|
+
* pass an explicit `dedupeKey` to opt in.
|
|
19
|
+
*/
|
|
20
|
+
dedupe?: boolean;
|
|
21
|
+
/** Explicit deduplication key for non-idempotent requests. */
|
|
22
|
+
dedupeKey?: unknown;
|
|
23
|
+
/** Query string parameters. */
|
|
24
|
+
query?: Params;
|
|
25
|
+
/** Response parsing strategy. */
|
|
26
|
+
responseType?: ResponseType;
|
|
27
|
+
/**
|
|
28
|
+
* Optional schema for response validation. Any object with a `parse(data)` method
|
|
29
|
+
* works — e.g. a `@vielzeug/spell` schema or a plain validator function wrapper.
|
|
30
|
+
* Called after the response body is parsed; throws if validation fails.
|
|
31
|
+
* The schema's return type must be assignable to `T`.
|
|
32
|
+
*/
|
|
33
|
+
schema?: {
|
|
34
|
+
parse(data: unknown): T;
|
|
35
|
+
};
|
|
36
|
+
/** Request timeout in ms. Overrides client default. */
|
|
37
|
+
timeout?: number;
|
|
38
|
+
};
|
|
39
|
+
export type HttpRequestConfig<P extends string = string, T = unknown> = CourierRequestConfig<P, T> & {
|
|
40
|
+
/** Raw fetch options for advanced use (credentials, cache, mode, referrer, etc.). */
|
|
41
|
+
fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;
|
|
42
|
+
/** Per-request headers merged with (and overriding) global client headers. */
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
/** External abort signal merged with internal cancellation. */
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
};
|
|
47
|
+
export declare function buildUrl(base: string, path: string, params?: Params, query?: Params): string;
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI/C,KAAK,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAEpD,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,SAAS,WAAW,EAAE,GAAG,SAAS,CAAC;AAC1E,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAGhD,KAAK,iBAAiB,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,GAAG,MAAM,IAAI,MAAM,CAAC,IAAI,MAAM,CAAC,EAAE,GAClF,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,GACxB,KAAK,CAAC;AAEV,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAC7E;IAAE,MAAM,CAAC,EAAE,KAAK,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,OAAO,IAAI,UAAU,CAAC,CAAC,CAAC,GAAG;IACzF,wEAAwE;IACxE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,OAAO,GAAG,CAAC,CAAA;KAAE,CAAC;IACrC,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,OAAO,IAAI,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IACnG,qFAAqF;IACrF,SAAS,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC,CAAC;IACxE,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAuC5F"}
|
package/dist/url.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{CourierParseError as e}from"./errors.js";function t(t,n,r,i){let a=t.replace(/\/+$/,``),o=n.replace(/^\/+/,``);o=o.replace(/\{([^}]+)\}/g,(t,i)=>{let a=r?.[i];if(a==null)throw new e(`unresolved path param {${i}} in '${n}'`);return encodeURIComponent(String(a))});let s=a&&o?`${a}/${o}`:a||o;if(!i)return s;let c=new URLSearchParams;for(let[e,t]of Object.entries(i))if(t!==void 0){if(Array.isArray(t)){for(let n of t)n!==void 0&&c.append(e,n===null?``:String(n));continue}c.append(e,t===null?``:String(t))}let l=c.toString();return l?`${s}${s.includes(`?`)?`&`:`?`}${l}`:s}export{t as buildUrl};
|
|
2
|
+
//# sourceMappingURL=url.js.map
|
package/dist/url.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.js","names":[],"sources":["../src/url.ts"],"sourcesContent":["import type { ResponseType } from './response';\n\nimport { CourierParseError } from './errors';\n\ntype QueryScalar = string | number | boolean | null;\n\nexport type ParamValue = QueryScalar | readonly QueryScalar[] | undefined;\nexport type Params = Record<string, ParamValue>;\n\n// Type-safe path params: extracts {param} placeholders from a path string\ntype ExtractPathParams<P extends string> = P extends `${string}{${infer K}}${infer R}`\n ? K | ExtractPathParams<R>\n : never;\n\nexport type PathConfig<P extends string> = [ExtractPathParams<P>] extends [never]\n ? { params?: never }\n : { params: Record<ExtractPathParams<P>, string | number | boolean> };\n\nexport type CourierRequestConfig<P extends string = string, T = unknown> = PathConfig<P> & {\n /** Request body. Plain objects → JSON. BodyInit values passed as-is. */\n body?: unknown;\n /**\n * Set to `false` to bypass in-flight deduplication for this request.\n * Safe + idempotent methods (GET, HEAD, OPTIONS) are deduplicated by default.\n * DELETE is idempotent but has side-effects, so it does not auto-dedupe;\n * pass an explicit `dedupeKey` to opt in.\n */\n dedupe?: boolean;\n /** Explicit deduplication key for non-idempotent requests. */\n dedupeKey?: unknown;\n /** Query string parameters. */\n query?: Params;\n /** Response parsing strategy. */\n responseType?: ResponseType;\n /**\n * Optional schema for response validation. Any object with a `parse(data)` method\n * works — e.g. a `@vielzeug/spell` schema or a plain validator function wrapper.\n * Called after the response body is parsed; throws if validation fails.\n * The schema's return type must be assignable to `T`.\n */\n schema?: { parse(data: unknown): T };\n /** Request timeout in ms. Overrides client default. */\n timeout?: number;\n};\n\nexport type HttpRequestConfig<P extends string = string, T = unknown> = CourierRequestConfig<P, T> & {\n /** Raw fetch options for advanced use (credentials, cache, mode, referrer, etc.). */\n fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;\n /** Per-request headers merged with (and overriding) global client headers. */\n headers?: Record<string, string>;\n /** External abort signal merged with internal cancellation. */\n signal?: AbortSignal;\n};\n\nexport function buildUrl(base: string, path: string, params?: Params, query?: Params): string {\n const baseClean = base.replace(/\\/+$/, '');\n let pathClean = path.replace(/^\\/+/, '');\n\n pathClean = pathClean.replace(/\\{([^}]+)\\}/g, (_, key: string) => {\n const val = params?.[key];\n\n if (val === undefined || val === null) throw new CourierParseError(`unresolved path param {${key}} in '${path}'`);\n\n return encodeURIComponent(String(val));\n });\n\n const url = baseClean && pathClean ? `${baseClean}/${pathClean}` : baseClean || pathClean;\n\n if (!query) return url;\n\n const qs = new URLSearchParams();\n\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) {\n continue;\n }\n\n if (Array.isArray(value)) {\n for (const entry of value) {\n if (entry !== undefined) {\n qs.append(key, entry === null ? '' : String(entry));\n }\n }\n\n continue;\n }\n\n qs.append(key, value === null ? '' : String(value));\n }\n\n const queryString = qs.toString();\n\n return queryString ? `${url}${url.includes('?') ? '&' : '?'}${queryString}` : url;\n}\n"],"mappings":"gDAsDA,SAAgB,EAAS,EAAc,EAAc,EAAiB,EAAwB,CAC5F,IAAM,EAAY,EAAK,QAAQ,OAAQ,EAAE,EACrC,EAAY,EAAK,QAAQ,OAAQ,EAAE,EAEvC,EAAY,EAAU,QAAQ,gBAAiB,EAAG,IAAgB,CAChE,IAAM,EAAM,IAAS,GAErB,GAAI,GAA6B,KAAM,MAAM,IAAI,EAAkB,0BAA0B,EAAI,QAAQ,EAAK,EAAE,EAEhH,OAAO,mBAAmB,OAAO,CAAG,CAAC,CACvC,CAAC,EAED,IAAM,EAAM,GAAa,EAAY,GAAG,EAAU,GAAG,IAAc,GAAa,EAEhF,GAAI,CAAC,EAAO,OAAO,EAEnB,IAAM,EAAK,IAAI,gBAEf,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,CAAK,EACzC,OAAU,IAAA,GAId,IAAI,MAAM,QAAQ,CAAK,EAAG,CACxB,IAAK,IAAM,KAAS,EACd,IAAU,IAAA,IACZ,EAAG,OAAO,EAAK,IAAU,KAAO,GAAK,OAAO,CAAK,CAAC,EAItD,QACF,CAEA,EAAG,OAAO,EAAK,IAAU,KAAO,GAAK,OAAO,CAAK,CAAC,CAFlD,CAKF,IAAM,EAAc,EAAG,SAAS,EAEhC,OAAO,EAAc,GAAG,IAAM,EAAI,SAAS,GAAG,EAAI,IAAM,MAAM,IAAgB,CAChF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vielzeug/courier",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "HTTP client with caching, request deduplication, typed mutations, SSE streaming, and interceptors",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
"./devtools": {
|
|
14
|
+
"source": "./src/devtools.ts",
|
|
15
|
+
"types": "./dist/devtools.d.ts",
|
|
16
|
+
"import": "./dist/devtools.js",
|
|
17
|
+
"require": "./dist/devtools.cjs"
|
|
18
|
+
},
|
|
19
|
+
".": {
|
|
20
|
+
"source": "./src/index.ts",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"require": "./dist/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "vite build && pnpm run build:bundle && pnpm run build:types",
|
|
28
|
+
"build:types": "tsc -p tsconfig.declarations.json",
|
|
29
|
+
"fix": "eslint --fix src",
|
|
30
|
+
"lint": "eslint src",
|
|
31
|
+
"prepublishOnly": "pnpm run build",
|
|
32
|
+
"preview": "vite preview",
|
|
33
|
+
"test": "vitest",
|
|
34
|
+
"build:bundle": "vite build --config vite.bundle.config.ts"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"registry": "https://registry.npmjs.org/"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20.3.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@vielzeug/arsenal": "workspace:*"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^26.1.0",
|
|
48
|
+
"typescript": "~6.0.3",
|
|
49
|
+
"vite": "^8.1.3",
|
|
50
|
+
"vitest": "^4.1.9"
|
|
51
|
+
}
|
|
52
|
+
}
|