dixous 0.1.0 → 0.2.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # Dixous
2
2
 
3
- A minimal, fully typed, extendable fetch client.
3
+ A small, fully typed HTTP client built on Fetch.
4
4
 
5
- Start with a simple request, then add validation, middleware, and custom response handlers as you need them.
5
+ Dixous gives you runtime-validated responses, composable clients, and an extension system that can change how requests execute and what APIs are available on them.
6
6
 
7
7
  ## Installation
8
8
 
@@ -10,105 +10,233 @@ Start with a simple request, then add validation, middleware, and custom respons
10
10
  npm install dixous
11
11
  ```
12
12
 
13
- Or from JSR: `deno add jsr:@asguho/dixous` / `npx jsr add @asguho/dixous`.
14
- Requires a runtime with native Fetch (Node.js 22+, Deno, or a modern browser).
15
-
16
- ## Usage
13
+ ## Quick start
17
14
 
18
15
  ```ts
19
- import { createDixous } from "dixous";
20
- const dixous = createDixous();
21
- import { z } from "zod";
16
+ import { Dixous } from "dixous"
17
+ import { z } from "zod"
22
18
 
23
- const image = await dixous.fetch("https://example.com/image.png").blob();
19
+ const api = Dixous.create({
20
+ baseUrl: "https://api.example.com/",
21
+ headers: {
22
+ Authorization: "Bearer YOUR_API_TOKEN",
23
+ },
24
+ })
24
25
 
25
26
  const User = z.object({
26
27
  id: z.number(),
27
28
  name: z.string(),
28
- });
29
+ })
29
30
 
30
- const api = dixous({
31
- baseUrl: "https://api.example.com/",
32
- headers: { Authorization: "Bearer YOUR_API_TOKEN" },
33
- });
34
- const user = await api.fetch("users/1").json(User);
31
+ const user = await api
32
+ .request("users/1")
33
+ .json(User)
35
34
 
36
- console.log(user.name); // string, validated at runtime
35
+ console.log(user.name)
37
36
  ```
38
37
 
39
- Requests run when you call `.json(schema)`, `.text()`, `.blob()`, `.arrayBuffer()`, or `.response()`. JSON accepts any Standard Schema v1 validator.
38
+ The response is validated at runtime and inferred automatically from the schema.
39
+
40
+ Dixous works with any [Standard Schema](https://standardschema.dev/) validator.
41
+
42
+ ## Why Dixous?
43
+
44
+ Dixous tries to stay small without becoming limiting.
45
+
46
+ * Built around native `Request` and `Response`
47
+ * Runtime validation with full TypeScript inference
48
+ * Immutable clients that compose and specialize naturally
49
+ * Extensions can add options, wrap request execution, and add request APIs
50
+ * Features such as retries, caching, logging, authentication, and custom formats stay outside the core
51
+ * Drop down to the native `Response` whenever you need to
40
52
 
41
- ## Fully extensible
53
+ ## Extend the request API
42
54
 
43
- Define your client once. Add response methods, middleware, and typed options with `defineExtension`.
55
+ Extensions can add entirely new request methods.
56
+
57
+ For example, [Schema XML](https://github.com/Asguho/schema-xml) can make XML feel like a native Dixous response format:
44
58
 
45
59
  ```ts
46
- // lib/dixous.ts
47
- import { createDixous, defineExtension } from "dixous";
48
- import { parseXml } from "schema-xml";
49
- import { z } from "zod";
60
+ import {
61
+ Dixous,
62
+ defineExtension,
63
+ } from "dixous"
64
+ import { parseXml } from "schema-xml"
65
+ import { z } from "zod"
50
66
 
51
- // Parse and validate XML with Schema XML.
52
67
  const xml = defineExtension({
53
- methods: {
54
- xml: (fetchResponse) =>
55
- async <S extends z.ZodType>(schema: S): Promise<z.output<S>> =>
56
- parseXml(await (await fetchResponse()).text(), schema),
57
- },
58
- });
59
-
60
- // Retry GET requests up to three times in total on a 503 response.
61
- const retry = defineExtension({
62
- async request({ request }, next) {
63
- for (let attempt = 1; ; attempt++) {
64
- request.signal.throwIfAborted();
65
- const response = await next();
66
- if (request.method !== "GET" || response.status !== 503 || attempt === 3) {
67
- return response;
68
- }
69
- await response.body?.cancel();
68
+ operation(operation) {
69
+ return {
70
+ async xml<Schema extends z.ZodType>(
71
+ schema: Schema,
72
+ ): Promise<z.output<Schema>> {
73
+ const response = await operation.response()
74
+
75
+ return parseXml(
76
+ await response.text(),
77
+ schema,
78
+ )
79
+ },
70
80
  }
71
81
  },
72
- });
82
+ })
83
+
84
+ const api = Dixous.create({
85
+ extensions: [xml],
86
+ })
87
+
88
+ const Catalog = z.object({
89
+ catalog: z.object({
90
+ book: z.array(
91
+ z.object({
92
+ title: z.string(),
93
+ }),
94
+ ),
95
+ }),
96
+ })
97
+
98
+ const catalog = await api
99
+ .request("https://example.com/catalog.xml")
100
+ .xml(Catalog)
101
+
102
+ console.log(catalog.catalog.book)
103
+ // { title: string }[]
104
+ ```
105
+
106
+ Dixous itself knows nothing about XML. Installing the extension adds `.xml(schema)` directly to the request type with full inference.
107
+
108
+ Inside an extension, `operation.response()` is the response the operation reads. Dixous rejects non-OK responses before the extension sees them, so extensions only decode bodies.
109
+
110
+ ## Branch on status
111
+
112
+ Use `.match()` when different statuses carry different bodies. Each handler receives the full operation API, including extension methods, bound to the matched response:
113
+
114
+ ```ts
115
+ const result = await api
116
+ .request("users/1")
117
+ .match({
118
+ 200: operation => operation.json(User),
119
+ 404: operation => operation.text(),
120
+ 422: operation => operation.json(ValidationError),
121
+ })
122
+ // User | string | ValidationError
123
+ ```
124
+
125
+ Statuses match exactly. An unmatched status rejects with `UnexpectedResponseError`.
126
+
127
+ `match` is a default method like `json`. An extension can replace it, using `operation.execute()` for the raw response and `operation.api(response)` to rebuild the operation API over it.
128
+
129
+ ## Composable by design
130
+
131
+ Create one shared Dixous client for your application, then import and specialize it where needed.
132
+
133
+ ```ts
134
+ // lib/dixous.ts
135
+
136
+ import { Dixous } from "dixous"
137
+
138
+ export const dixous = Dixous.create({
139
+ baseUrl: "https://api.example.com/",
140
+ extensions: [
141
+ retry(),
142
+ query(),
143
+ ],
144
+ })
145
+ ```
146
+
147
+ Elsewhere:
148
+
149
+ ```ts
150
+ import { dixous } from "./lib/dixous"
151
+
152
+ const github = dixous.create({
153
+ baseUrl: "https://api.github.com/",
154
+ headers: {
155
+ Authorization: `Bearer ${token}`,
156
+ },
157
+ retryAttempts: 5,
158
+ })
159
+
160
+ const users = await github
161
+ .request("users", {
162
+ query: {
163
+ since: "100",
164
+ },
165
+ })
166
+ .json(Users)
167
+ ```
168
+
169
+ Derived clients inherit their parent's configuration, extensions, and types, while more specific configuration overrides shared defaults. The parent client is never changed.
170
+
171
+ ## Extend request behavior
73
172
 
74
- // Add a typed query option.
75
- const query = defineExtension<{ query?: Record<string, string> }>()({
173
+ Extensions can also change how requests execute and contribute their own typed options.
174
+
175
+ ```ts
176
+ const query = defineExtension<{
177
+ query?: Record<string, string>
178
+ }>()({
76
179
  async request(context, next) {
77
- const url = new URL(context.request.url);
180
+ const url = new URL(context.request.url)
181
+
78
182
  for (const [key, value] of Object.entries(context.options.query ?? {})) {
79
- url.searchParams.append(key, value);
183
+ url.searchParams.append(key, value)
80
184
  }
81
- context.request = new Request(url, context.request);
82
- return next();
185
+
186
+ context.request = new Request(
187
+ url,
188
+ context.request,
189
+ )
190
+
191
+ return next()
83
192
  },
84
- });
193
+ })
194
+ ```
195
+
196
+ Install it:
85
197
 
86
- export const dixous = createDixous({ extensions: [xml, query, retry] });
198
+ ```ts
199
+ const api = Dixous.create({
200
+ extensions: [query],
201
+ })
87
202
  ```
88
- Use the extended client anywhere:
203
+
204
+ And the option becomes part of the client:
89
205
 
90
206
  ```ts
91
- import { dixous } from "./lib/dixous";
92
- import { z } from "zod";
207
+ const books = await api
208
+ .request("books", {
209
+ query: {
210
+ author: "Ursula K. Le Guin",
211
+ },
212
+ })
213
+ .json(Books)
214
+ ```
93
215
 
94
- const Catalog = z.object({
95
- catalog: z.object({ book: z.array(z.object({ title: z.string() })) }),
96
- });
216
+ Without the extension, `query` is not part of the request options.
97
217
 
98
- const result = await dixous.fetch("https://example.com/catalog", {
99
- query: { author: "Ursula K. Le Guin" },
100
- }).xml(Catalog);
218
+ The same mechanism can power retries, authentication, caching, logging, tracing, rate limiting, and more.
101
219
 
102
- console.log(result.catalog.book); // { title: string }[]
103
- ```
220
+ ## Native when you need it
104
221
 
105
- The XML example uses [Schema XML](https://github.com/Asguho/schema-xml) and Zod (`npm install schema-xml zod`).
222
+ Use `.response()` whenever the HTTP response itself is part of your application logic:
106
223
 
107
- ## Development
224
+ ```ts
225
+ const response = await api
226
+ .request("users/1")
227
+ .response()
108
228
 
109
- ```sh
110
- npm ci
111
- npm test
229
+ if (response.status === 404) {
230
+ // Handle an expected missing user.
231
+ }
232
+
233
+ if (response.ok) {
234
+ const body = await response.json()
235
+ }
112
236
  ```
113
237
 
114
- See [RELEASING.md](./RELEASING.md) for npm and JSR publishing.
238
+ It returns the final native `Response` without applying a status policy.
239
+
240
+ Requests are lazy and memoized per `request()` call: `.response()`, `.match()`, and the body readers share one execution, while response bodies keep their normal native consumption semantics.
241
+
242
+ See [Extensions](./docs/extensions.md) for middleware ordering, retries, caching, logging, custom formats, extension state, and advanced composition.
@@ -0,0 +1,16 @@
1
+ import type { StandardSchemaIssue } from "./standard-schema.ts";
2
+ export declare class UnexpectedResponseError extends Error {
3
+ readonly request: Request;
4
+ readonly response: Response;
5
+ constructor(request: Request, response: Response);
6
+ }
7
+ export declare class ResponseValidationError extends Error {
8
+ readonly request: Request;
9
+ readonly response: Response;
10
+ readonly issues: readonly StandardSchemaIssue[];
11
+ constructor(request: Request, response: Response, issues: readonly StandardSchemaIssue[]);
12
+ }
13
+ export declare class ConcurrentNextError extends Error {
14
+ constructor();
15
+ }
16
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,qBAAa,uBAAwB,SAAQ,KAAK;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO;IAAE,QAAQ,CAAC,QAAQ,EAAE,QAAQ;IAAlE,YAAqB,OAAO,EAAE,OAAO,EAAW,QAAQ,EAAE,QAAQ,EAGjE;CACF;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IAE9C,QAAQ,CAAC,OAAO,EAAE,OAAO;IACzB,QAAQ,CAAC,QAAQ,EAAE,QAAQ;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS,mBAAmB,EAAE;IAHjD,YACW,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,SAAS,mBAAmB,EAAE,EAIhD;CACF;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,cAGC;CACF"}
package/dist/errors.js ADDED
@@ -0,0 +1,29 @@
1
+ export class UnexpectedResponseError extends Error {
2
+ request;
3
+ response;
4
+ constructor(request, response) {
5
+ super(`HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`);
6
+ this.request = request;
7
+ this.response = response;
8
+ this.name = "UnexpectedResponseError";
9
+ }
10
+ }
11
+ export class ResponseValidationError extends Error {
12
+ request;
13
+ response;
14
+ issues;
15
+ constructor(request, response, issues) {
16
+ super("Response failed schema validation");
17
+ this.request = request;
18
+ this.response = response;
19
+ this.issues = issues;
20
+ this.name = "ResponseValidationError";
21
+ }
22
+ }
23
+ export class ConcurrentNextError extends Error {
24
+ constructor() {
25
+ super("Concurrent calls to the same next() are not allowed");
26
+ this.name = "ConcurrentNextError";
27
+ }
28
+ }
29
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAC3B,OAAO;IAAoB,QAAQ;IAAxD,YAAqB,OAAgB,EAAW,QAAkB;QAChE,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;uBADrE,OAAO;wBAAoB,QAAQ;QAEtD,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAErC,OAAO;IACP,QAAQ;IACR,MAAM;IAHjB,YACW,OAAgB,EAChB,QAAkB,EAClB,MAAsC;QAE/C,KAAK,CAAC,mCAAmC,CAAC,CAAC;uBAJlC,OAAO;wBACP,QAAQ;sBACR,MAAM;QAGf,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C;QACE,KAAK,CAAC,qDAAqD,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF"}
package/dist/index.d.ts CHANGED
@@ -1,19 +1,12 @@
1
- import { type DefaultResponseMethods } from "./response-methods.ts";
2
- import type { ContextKey, Dixous, Extension, ExtensionClientOptions, ExtensionDefinition, ExtensionMeta, ExtensionMethods, ExtensionRequestOptions, NoClientOptionOverrides, NoRequestInitOverrides, ResponseMethods } from "./types.ts";
3
- export { SchemaValidationError } from "./response-methods.ts";
4
- export type { DefaultResponseMethods, InferOutput } from "./response-methods.ts";
5
- export type { BaseClientOptions, ClientOptions, Context, ContextKey, Dixous, Extension, Fetcher, FetchResponse, Middleware, Next, RequestContext, RequestOptions, } from "./types.ts";
6
- export declare function createContextKey<T>(): ContextKey<T>;
7
- export declare function defineExtension<const Methods extends ResponseMethods = {}>(extension: ExtensionDefinition<{}, {}, Methods>): Extension<{}, {}, Methods>;
8
- export declare function defineExtension<RequestExtra extends object & NoRequestInitOverrides = {}, ClientExtra extends object & NoClientOptionOverrides = {}>(): <const Methods extends ResponseMethods = {}>(extension: ExtensionDefinition<RequestExtra, ClientExtra, Methods>) => Extension<RequestExtra, ClientExtra, Methods>;
9
- export declare class HttpError extends Error {
10
- readonly request: Request;
11
- readonly response: Response;
12
- readonly status: number;
13
- constructor(request: Request, response: Response);
1
+ import type { AnyExtension, ApplyExtensionApi, ApplyExtensionOptions, CreateOptions, DefaultOperationApi, Dixous as DixousClient, Extension, ExtensionDefinition } from "./types.ts";
2
+ export { ConcurrentNextError, ResponseValidationError, UnexpectedResponseError } from "./errors.ts";
3
+ export type { InferOutput, StandardSchemaIssue, StandardSchemaResult, StandardSchemaV1 } from "./standard-schema.ts";
4
+ export type { AnyExtension, CoreOptions, CreateOptions, DefaultOperationApi, Extension, ExtensionDefinition, Match, MatchedOperation, MatchResult, Next, OperationContext, RequestContext, RequestInput, RequestMiddleware, RequestOperation, RequestOptions, ReservedOperationKey, StatusHandlers, } from "./types.ts";
5
+ export declare function defineExtension<const OperationApi extends object = {}>(definition: ExtensionDefinition<{}, OperationApi>): Extension<{}, OperationApi>;
6
+ export declare function defineExtension<Options extends object>(): <const OperationApi extends object = {}>(definition: ExtensionDefinition<Options, OperationApi>) => Extension<Options, OperationApi>;
7
+ export interface Dixous<Options extends object = {}, OperationApi extends object = DefaultOperationApi> extends DixousClient<Options, OperationApi> {
14
8
  }
15
- export declare function createDixous<const Extensions extends readonly ExtensionMeta[] = []>(options?: {
16
- extensions?: Extensions;
17
- fetch?: typeof globalThis.fetch;
18
- }): Dixous<ExtensionRequestOptions<Extensions>, ExtensionClientOptions<Extensions>, DefaultResponseMethods & ExtensionMethods<Extensions>>;
9
+ export declare const Dixous: {
10
+ create<const Extensions extends readonly AnyExtension[] = []>(options?: CreateOptions<{}, Extensions>): Dixous<ApplyExtensionOptions<{}, Extensions>, ApplyExtensionApi<DefaultOperationApi, Extensions>>;
11
+ };
19
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC5F,OAAO,KAAK,EAGV,UAAU,EACV,MAAM,EACN,SAAS,EACT,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,uBAAuB,EAIvB,uBAAuB,EACvB,sBAAsB,EAGtB,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,YAAY,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEjF,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,OAAO,EACP,UAAU,EACV,MAAM,EACN,SAAS,EACT,OAAO,EACP,aAAa,EACb,UAAU,EACV,IAAI,EACJ,cAAc,EACd,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,wBAAgB,gBAAgB,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,CAEnD;AAcD,wBAAgB,eAAe,CAAC,KAAK,CAAC,OAAO,SAAS,eAAe,GAAG,EAAE,EACxE,SAAS,EAAE,mBAAmB,CAAC,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,GAC9C,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;AAC9B,wBAAgB,eAAe,CAC7B,YAAY,SAAS,MAAM,GAAG,sBAAsB,GAAG,EAAE,EACzD,WAAW,SAAS,MAAM,GAAG,uBAAuB,GAAG,EAAE,KACtD,CAAC,KAAK,CAAC,OAAO,SAAS,eAAe,GAAG,EAAE,EAC9C,SAAS,EAAE,mBAAmB,CAAC,YAAY,EAAE,WAAW,EAAE,OAAO,CAAC,KAC/D,SAAS,CAAC,YAAY,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;AAMnD,qBAAa,SAAU,SAAQ,KAAK;IAClC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,YAAY,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAM/C;CACF;AAoFD,wBAAgB,YAAY,CAC1B,KAAK,CAAC,UAAU,SAAS,SAAS,aAAa,EAAE,GAAG,EAAE,EACtD,OAAO,CAAC,EAAE;IACV,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC,GAAG,MAAM,CACR,uBAAuB,CAAC,UAAU,CAAC,EACnC,sBAAsB,CAAC,UAAU,CAAC,EAClC,sBAAsB,GAAG,gBAAgB,CAAC,UAAU,CAAC,CACtD,CA2CA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAAE,iBAAiB,EAAE,qBAAqB,EAAe,aAAa,EAClF,mBAAmB,EAAE,MAAM,IAAI,YAAY,EAAE,SAAS,EACtD,mBAAmB,EAEpB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACpG,YAAY,EAAE,WAAW,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACrH,YAAY,EACV,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,mBAAmB,EAAE,SAAS,EACxE,mBAAmB,EAAE,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,EACjF,cAAc,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,cAAc,EACjF,oBAAoB,EAAE,cAAc,GACrC,MAAM,YAAY,CAAC;AAEpB,wBAAgB,eAAe,CAAC,KAAK,CAAC,YAAY,SAAS,MAAM,GAAG,EAAE,EACpE,UAAU,EAAE,mBAAmB,CAAC,EAAE,EAAE,YAAY,CAAC,GAChD,SAAS,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;AAC/B,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,KAAK,CAAC,KAAK,CAAC,YAAY,SAAS,MAAM,GAAG,EAAE,EAChG,UAAU,EAAE,mBAAmB,CAAC,OAAO,EAAE,YAAY,CAAC,KACnD,SAAS,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;AAoHtC,MAAM,WAAW,MAAM,CAAC,OAAO,SAAS,MAAM,GAAG,EAAE,EAAE,YAAY,SAAS,MAAM,GAAG,mBAAmB,CACpG,SAAQ,YAAY,CAAC,OAAO,EAAE,YAAY,CAAC;CAAG;AAEhD,eAAO,MAAM,MAAM,EAAE;IACnB,MAAM,CAAC,KAAK,CAAC,UAAU,SAAS,SAAS,YAAY,EAAE,GAAG,EAAE,EAC1D,OAAO,CAAC,EAAE,aAAa,CAAC,EAAE,EAAE,UAAU,CAAC,GACtC,MAAM,CAAC,qBAAqB,CAAC,EAAE,EAAE,UAAU,CAAC,EAAE,iBAAiB,CAAC,mBAAmB,EAAE,UAAU,CAAC,CAAC,CAAC;CAGrG,CAAC"}
package/dist/index.js CHANGED
@@ -1,73 +1,26 @@
1
- import { defaultResponseMethods } from "./response-methods.js";
2
- export { SchemaValidationError } from "./response-methods.js";
3
- export function createContextKey() {
4
- return Symbol();
1
+ import { ConcurrentNextError, UnexpectedResponseError } from "./errors.js";
2
+ import { defaultOperationApi } from "./response-methods.js";
3
+ export { ConcurrentNextError, ResponseValidationError, UnexpectedResponseError } from "./errors.js";
4
+ export function defineExtension(definition) {
5
+ return definition === undefined ? (entry) => entry : definition;
5
6
  }
6
- function createContext() {
7
- const values = new Map();
8
- return {
9
- get(key) {
10
- return values.get(key);
11
- },
12
- set(key, value) {
13
- values.set(key, value);
14
- },
15
- };
16
- }
17
- export function defineExtension(extension) {
18
- // Extension metadata is a type-only brand; composition uses the captured values.
19
- return extension === undefined ? (definition) => definition : extension;
20
- }
21
- export class HttpError extends Error {
22
- request;
23
- response;
24
- status;
25
- constructor(request, response) {
26
- super(`HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`);
27
- this.name = "HttpError";
28
- this.request = request;
29
- this.response = response;
30
- this.status = response.status;
7
+ function mergeHeaders(...sources) {
8
+ const headers = new Headers();
9
+ for (const source of sources) {
10
+ if (source !== undefined)
11
+ new Headers(source).forEach((value, name) => headers.set(name, value));
31
12
  }
13
+ return headers;
32
14
  }
33
- function snapshotClient(options = {}) {
34
- const client = { ...options };
35
- if (client.baseUrl !== undefined)
36
- client.baseUrl = client.baseUrl.toString();
37
- if (client.headers !== undefined)
38
- client.headers = new Headers(client.headers);
39
- return Object.freeze(client);
40
- }
41
- function snapshotOptions(options = {}) {
42
- const snapshot = { ...options };
43
- if (snapshot.headers !== undefined)
44
- snapshot.headers = new Headers(snapshot.headers);
45
- return Object.freeze(snapshot);
46
- }
47
- function createTemplate(input, options, client) {
48
- const headers = new Headers(client.headers);
49
- if (input instanceof Request) {
50
- input.headers.forEach((value, name) => headers.set(name, value));
51
- }
52
- if (options.headers !== undefined) {
53
- new Headers(options.headers).forEach((value, name) => headers.set(name, value));
54
- }
55
- const source = !(input instanceof Request) && client.baseUrl !== undefined
56
- ? new URL(input.toString(), client.baseUrl)
57
- : input;
58
- return new Request(source, { ...options, headers });
59
- }
60
- function runMiddleware(middleware, context, fetchImpl) {
15
+ function runMiddleware(middleware, context, transport) {
61
16
  async function dispatch(index) {
62
17
  const current = middleware[index];
63
18
  if (current === undefined)
64
- return fetchImpl(context.request.clone());
65
- // Each middleware invocation owns its guard. Sequential retries re-enter
66
- // the downstream chain with the same context and new downstream guards.
19
+ return transport(context.request);
67
20
  let running = false;
68
21
  return current(context, async () => {
69
22
  if (running)
70
- throw new Error("Overlapping next() calls are not allowed");
23
+ throw new ConcurrentNextError();
71
24
  running = true;
72
25
  try {
73
26
  return await dispatch(index + 1);
@@ -79,60 +32,76 @@ function runMiddleware(middleware, context, fetchImpl) {
79
32
  }
80
33
  return dispatch(0);
81
34
  }
82
- function createFetchResponse(template, options, client, middleware, fetchImpl) {
83
- let execution;
84
- return () => {
85
- // Defer execution until after storing the promise, including when a
86
- // synchronous middleware re-enters its operation's FetchResponse.
87
- execution ??= Promise.resolve().then(async () => {
88
- const context = {
89
- request: template.clone(),
90
- options,
91
- client,
92
- state: createContext(),
93
- };
94
- const response = await runMiddleware(middleware, context, fetchImpl);
95
- if (!response.ok)
96
- throw new HttpError(context.request, response);
97
- return response;
35
+ const reservedOperationKeys = ["response", "then"];
36
+ function createOperation(context, execute, extensions) {
37
+ const build = (response) => {
38
+ const scoped = Object.create(context, {
39
+ response: { value: response, enumerable: true },
40
+ execute: { value: execute, enumerable: true },
41
+ api: { value: (matched) => build(async () => matched), enumerable: true },
42
+ });
43
+ const operation = Object.assign(Object.create(null), defaultOperationApi(scoped));
44
+ for (const extension of extensions) {
45
+ const contribution = extension.operation?.(scoped);
46
+ if (contribution !== undefined) {
47
+ for (const key of reservedOperationKeys) {
48
+ if (key in contribution)
49
+ throw new TypeError(`Extension operation cannot replace ${key}`);
50
+ }
51
+ Object.assign(operation, contribution);
52
+ }
53
+ }
54
+ return Object.defineProperties(operation, {
55
+ response: { value: execute, enumerable: true },
56
+ then: { value: undefined },
98
57
  });
99
- return execution;
100
58
  };
59
+ return build(async () => {
60
+ const response = await execute();
61
+ if (!response.ok)
62
+ throw new UnexpectedResponseError(context.request, response);
63
+ return response;
64
+ });
101
65
  }
102
- export function createDixous(options) {
103
- const fetchImpl = options?.fetch ?? globalThis.fetch;
104
- const middleware = [];
105
- const methods = new Map(Object.entries(defaultResponseMethods));
106
- for (const entry of options?.extensions ?? []) {
107
- // Contributions are erased only inside the kernel; the public signature
108
- // intersects their exact types when constructing the resulting client.
109
- const extension = entry;
110
- if (extension.request !== undefined)
111
- middleware.push(extension.request);
112
- for (const [name, factory] of Object.entries(extension.methods ?? {})) {
113
- if (methods.has(name))
114
- throw new Error(`Duplicate response method: ${name}`);
115
- methods.set(name, factory);
116
- }
117
- }
118
- function configured(clientOptions) {
119
- const client = snapshotClient(clientOptions);
120
- return {
121
- fetch(input, requestOptions) {
122
- const snapshot = snapshotOptions(requestOptions);
123
- const template = createTemplate(input, snapshot, client);
124
- const pending = Object.create(null);
125
- for (const [name, factory] of methods) {
126
- pending[name] = (...args) => {
127
- const fetchResponse = createFetchResponse(template, snapshot, client, middleware, fetchImpl);
128
- // Factories and method work are lazy too, and run once per call.
129
- return factory(fetchResponse)(...args);
130
- };
131
- }
132
- return Object.freeze(pending);
133
- },
134
- };
135
- }
136
- return Object.assign(configured, { fetch: configured().fetch });
66
+ function createClient(parent = {}, supplied = {}) {
67
+ const { extensions: inherited = [], ...defaults } = parent;
68
+ const { extensions: appended = [], ...overrides } = supplied;
69
+ const configuration = Object.freeze({
70
+ ...defaults,
71
+ ...overrides,
72
+ ...(overrides.baseUrl !== undefined ? { baseUrl: overrides.baseUrl.toString() } : {}),
73
+ headers: mergeHeaders(defaults.headers, overrides.headers),
74
+ // Capture contributions so later mutations cannot alter an immutable client.
75
+ extensions: Object.freeze([...inherited, ...appended].map(entry => Object.freeze({ ...entry }))),
76
+ });
77
+ const { extensions, ...clientOptions } = configuration;
78
+ const middleware = extensions.flatMap(entry => entry.request ? [entry.request] : []);
79
+ const transport = configuration.fetch ?? globalThis.fetch;
80
+ return Object.freeze({
81
+ create(options) { return createClient(configuration, options); },
82
+ request(input, suppliedOptions = {}) {
83
+ const options = Object.freeze({
84
+ ...clientOptions,
85
+ ...suppliedOptions,
86
+ headers: mergeHeaders(configuration.headers, input instanceof Request ? input.headers : undefined, suppliedOptions.headers),
87
+ });
88
+ const source = !(input instanceof Request) && configuration.baseUrl !== undefined
89
+ ? new URL(input.toString(), configuration.baseUrl)
90
+ : input;
91
+ const request = new Request(source, options);
92
+ let execution;
93
+ const execute = () => {
94
+ // Store the promise before middleware can synchronously re-enter execute().
95
+ execution ??= Promise.resolve().then(() => runMiddleware(middleware, context, transport));
96
+ return execution;
97
+ };
98
+ const context = { input, request, options };
99
+ Object.defineProperties(context, { input: { writable: false }, options: { writable: false } });
100
+ return createOperation(context, execute, extensions);
101
+ },
102
+ });
137
103
  }
104
+ export const Dixous = Object.freeze({
105
+ create: createClient.bind(undefined, {}),
106
+ });
138
107
  //# sourceMappingURL=index.js.map