dixous 0.1.1 → 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 +196 -64
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +29 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +10 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +82 -113
- package/dist/index.js.map +1 -1
- package/dist/response-methods.d.ts +2 -15
- package/dist/response-methods.d.ts.map +1 -1
- package/dist/response-methods.js +22 -21
- package/dist/response-methods.js.map +1 -1
- package/dist/standard-schema.d.ts +51 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +28 -0
- package/dist/standard-schema.js.map +1 -0
- package/dist/types.d.ts +88 -54
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -5
- package/src/errors.ts +26 -0
- package/src/index.ts +114 -184
- package/src/response-methods.ts +21 -32
- package/src/standard-schema.ts +51 -0
- package/src/types.ts +118 -117
package/src/index.ts
CHANGED
|
@@ -1,132 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ConcurrentNextError, UnexpectedResponseError } from "./errors.ts";
|
|
2
|
+
import { defaultOperationApi } from "./response-methods.ts";
|
|
2
3
|
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Extension,
|
|
8
|
-
ExtensionClientOptions,
|
|
9
|
-
ExtensionDefinition,
|
|
10
|
-
ExtensionMeta,
|
|
11
|
-
ExtensionMethods,
|
|
12
|
-
ExtensionRequestOptions,
|
|
13
|
-
Fetcher,
|
|
14
|
-
FetchResponse,
|
|
15
|
-
Middleware,
|
|
16
|
-
NoClientOptionOverrides,
|
|
17
|
-
NoRequestInitOverrides,
|
|
18
|
-
RequestContext,
|
|
19
|
-
RequestOptions,
|
|
20
|
-
ResponseMethods,
|
|
4
|
+
AnyExtension, ApplyExtensionApi, ApplyExtensionOptions, CoreOptions, CreateOptions,
|
|
5
|
+
DefaultOperationApi, Dixous as DixousClient, Extension,
|
|
6
|
+
ExtensionDefinition, OperationContext, RequestContext, RequestInput,
|
|
7
|
+
MatchedOperation, RequestMiddleware, RequestOptions, ReservedOperationKey,
|
|
21
8
|
} from "./types.ts";
|
|
22
9
|
|
|
23
|
-
export {
|
|
24
|
-
export type {
|
|
25
|
-
|
|
10
|
+
export { ConcurrentNextError, ResponseValidationError, UnexpectedResponseError } from "./errors.ts";
|
|
11
|
+
export type { InferOutput, StandardSchemaIssue, StandardSchemaResult, StandardSchemaV1 } from "./standard-schema.ts";
|
|
26
12
|
export type {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Dixous,
|
|
32
|
-
Extension,
|
|
33
|
-
Fetcher,
|
|
34
|
-
FetchResponse,
|
|
35
|
-
Middleware,
|
|
36
|
-
Next,
|
|
37
|
-
RequestContext,
|
|
38
|
-
RequestOptions,
|
|
13
|
+
AnyExtension, CoreOptions, CreateOptions, DefaultOperationApi, Extension,
|
|
14
|
+
ExtensionDefinition, Match, MatchedOperation, MatchResult, Next, OperationContext,
|
|
15
|
+
RequestContext, RequestInput, RequestMiddleware, RequestOperation, RequestOptions,
|
|
16
|
+
ReservedOperationKey, StatusHandlers,
|
|
39
17
|
} from "./types.ts";
|
|
40
18
|
|
|
41
|
-
export function
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return values.get(key) as T | undefined;
|
|
50
|
-
},
|
|
51
|
-
set<T>(key: ContextKey<T>, value: T) {
|
|
52
|
-
values.set(key, value);
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function defineExtension<const Methods extends ResponseMethods = {}>(
|
|
58
|
-
extension: ExtensionDefinition<{}, {}, Methods>,
|
|
59
|
-
): Extension<{}, {}, Methods>;
|
|
60
|
-
export function defineExtension<
|
|
61
|
-
RequestExtra extends object & NoRequestInitOverrides = {},
|
|
62
|
-
ClientExtra extends object & NoClientOptionOverrides = {},
|
|
63
|
-
>(): <const Methods extends ResponseMethods = {}>(
|
|
64
|
-
extension: ExtensionDefinition<RequestExtra, ClientExtra, Methods>,
|
|
65
|
-
) => Extension<RequestExtra, ClientExtra, Methods>;
|
|
66
|
-
export function defineExtension(extension?: object): unknown {
|
|
67
|
-
// Extension metadata is a type-only brand; composition uses the captured values.
|
|
68
|
-
return extension === undefined ? (definition: object) => definition : extension;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export class HttpError extends Error {
|
|
72
|
-
readonly request: Request;
|
|
73
|
-
readonly response: Response;
|
|
74
|
-
readonly status: number;
|
|
75
|
-
|
|
76
|
-
constructor(request: Request, response: Response) {
|
|
77
|
-
super(`HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`);
|
|
78
|
-
this.name = "HttpError";
|
|
79
|
-
this.request = request;
|
|
80
|
-
this.response = response;
|
|
81
|
-
this.status = response.status;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function snapshotClient(options: ClientOptions = {}): Readonly<ClientOptions> {
|
|
86
|
-
const client = { ...options };
|
|
87
|
-
if (client.baseUrl !== undefined) client.baseUrl = client.baseUrl.toString();
|
|
88
|
-
if (client.headers !== undefined) client.headers = new Headers(client.headers);
|
|
89
|
-
return Object.freeze(client);
|
|
19
|
+
export function defineExtension<const OperationApi extends object = {}>(
|
|
20
|
+
definition: ExtensionDefinition<{}, OperationApi>,
|
|
21
|
+
): Extension<{}, OperationApi>;
|
|
22
|
+
export function defineExtension<Options extends object>(): <const OperationApi extends object = {}>(
|
|
23
|
+
definition: ExtensionDefinition<Options, OperationApi>,
|
|
24
|
+
) => Extension<Options, OperationApi>;
|
|
25
|
+
export function defineExtension(definition?: object): unknown {
|
|
26
|
+
return definition === undefined ? (entry: object) => entry : definition;
|
|
90
27
|
}
|
|
91
28
|
|
|
92
|
-
function
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function createTemplate(
|
|
99
|
-
input: string | URL | Request,
|
|
100
|
-
options: Readonly<RequestOptions>,
|
|
101
|
-
client: Readonly<ClientOptions>,
|
|
102
|
-
): Request {
|
|
103
|
-
const headers = new Headers(client.headers);
|
|
104
|
-
if (input instanceof Request) {
|
|
105
|
-
input.headers.forEach((value, name) => headers.set(name, value));
|
|
29
|
+
function mergeHeaders(...sources: (HeadersInit | undefined)[]): Headers {
|
|
30
|
+
const headers = new Headers();
|
|
31
|
+
for (const source of sources) {
|
|
32
|
+
if (source !== undefined) new Headers(source).forEach((value, name) => headers.set(name, value));
|
|
106
33
|
}
|
|
107
|
-
|
|
108
|
-
new Headers(options.headers).forEach((value, name) => headers.set(name, value));
|
|
109
|
-
}
|
|
110
|
-
const source = !(input instanceof Request) && client.baseUrl !== undefined
|
|
111
|
-
? new URL(input.toString(), client.baseUrl)
|
|
112
|
-
: input;
|
|
113
|
-
return new Request(source, { ...options, headers });
|
|
34
|
+
return headers;
|
|
114
35
|
}
|
|
115
36
|
|
|
116
37
|
function runMiddleware(
|
|
117
|
-
middleware: readonly
|
|
38
|
+
middleware: readonly RequestMiddleware[],
|
|
118
39
|
context: RequestContext,
|
|
119
|
-
|
|
40
|
+
transport: typeof globalThis.fetch,
|
|
120
41
|
): Promise<Response> {
|
|
121
42
|
async function dispatch(index: number): Promise<Response> {
|
|
122
43
|
const current = middleware[index];
|
|
123
|
-
if (current === undefined) return
|
|
124
|
-
|
|
125
|
-
// Each middleware invocation owns its guard. Sequential retries re-enter
|
|
126
|
-
// the downstream chain with the same context and new downstream guards.
|
|
44
|
+
if (current === undefined) return transport(context.request);
|
|
127
45
|
let running = false;
|
|
128
46
|
return current(context, async () => {
|
|
129
|
-
if (running) throw new
|
|
47
|
+
if (running) throw new ConcurrentNextError();
|
|
130
48
|
running = true;
|
|
131
49
|
try {
|
|
132
50
|
return await dispatch(index + 1);
|
|
@@ -138,82 +56,94 @@ function runMiddleware(
|
|
|
138
56
|
return dispatch(0);
|
|
139
57
|
}
|
|
140
58
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
59
|
+
type Configuration = CoreOptions & RequestOptions & { readonly extensions?: readonly AnyExtension[] };
|
|
60
|
+
|
|
61
|
+
const reservedOperationKeys = ["response", "then"] as const satisfies readonly ReservedOperationKey[];
|
|
62
|
+
|
|
63
|
+
function createOperation(
|
|
64
|
+
context: RequestContext,
|
|
65
|
+
execute: () => Promise<Response>,
|
|
66
|
+
extensions: readonly AnyExtension[],
|
|
67
|
+
) {
|
|
68
|
+
const build = (response: () => Promise<Response>): MatchedOperation<{}> => {
|
|
69
|
+
const scoped: OperationContext = Object.create(context, {
|
|
70
|
+
response: { value: response, enumerable: true },
|
|
71
|
+
execute: { value: execute, enumerable: true },
|
|
72
|
+
api: { value: (matched: Response) => build(async () => matched), enumerable: true },
|
|
73
|
+
});
|
|
74
|
+
const operation = Object.assign(Object.create(null), defaultOperationApi(scoped));
|
|
75
|
+
for (const extension of extensions) {
|
|
76
|
+
const contribution = extension.operation?.(scoped);
|
|
77
|
+
if (contribution !== undefined) {
|
|
78
|
+
for (const key of reservedOperationKeys) {
|
|
79
|
+
if (key in contribution) throw new TypeError(`Extension operation cannot replace ${key}`);
|
|
80
|
+
}
|
|
81
|
+
Object.assign(operation, contribution);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return Object.defineProperties(operation, {
|
|
85
|
+
response: { value: execute, enumerable: true },
|
|
86
|
+
then: { value: undefined },
|
|
162
87
|
});
|
|
163
|
-
return execution;
|
|
164
88
|
};
|
|
89
|
+
return build(async () => {
|
|
90
|
+
const response = await execute();
|
|
91
|
+
if (!response.ok) throw new UnexpectedResponseError(context.request, response);
|
|
92
|
+
return response;
|
|
93
|
+
});
|
|
165
94
|
}
|
|
166
95
|
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
96
|
+
function createClient(parent: Configuration = {}, supplied: Configuration = {}): DixousClient {
|
|
97
|
+
const { extensions: inherited = [], ...defaults } = parent;
|
|
98
|
+
const { extensions: appended = [], ...overrides } = supplied;
|
|
99
|
+
const configuration = Object.freeze({
|
|
100
|
+
...defaults,
|
|
101
|
+
...overrides,
|
|
102
|
+
...(overrides.baseUrl !== undefined ? { baseUrl: overrides.baseUrl.toString() } : {}),
|
|
103
|
+
headers: mergeHeaders(defaults.headers, overrides.headers),
|
|
104
|
+
// Capture contributions so later mutations cannot alter an immutable client.
|
|
105
|
+
extensions: Object.freeze([...inherited, ...appended].map(entry => Object.freeze({ ...entry }))),
|
|
106
|
+
});
|
|
107
|
+
const { extensions, ...clientOptions } = configuration;
|
|
108
|
+
const middleware = extensions.flatMap(entry => entry.request ? [entry.request] : []);
|
|
109
|
+
const transport = configuration.fetch ?? globalThis.fetch;
|
|
110
|
+
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
create(options?: Configuration) { return createClient(configuration, options); },
|
|
113
|
+
request(input: RequestInput, suppliedOptions: RequestOptions = {}) {
|
|
114
|
+
const options = Object.freeze({
|
|
115
|
+
...clientOptions,
|
|
116
|
+
...suppliedOptions,
|
|
117
|
+
headers: mergeHeaders(
|
|
118
|
+
configuration.headers,
|
|
119
|
+
input instanceof Request ? input.headers : undefined,
|
|
120
|
+
suppliedOptions.headers,
|
|
121
|
+
),
|
|
122
|
+
});
|
|
123
|
+
const source = !(input instanceof Request) && configuration.baseUrl !== undefined
|
|
124
|
+
? new URL(input.toString(), configuration.baseUrl)
|
|
125
|
+
: input;
|
|
126
|
+
const request = new Request(source, options);
|
|
127
|
+
let execution: Promise<Response> | undefined;
|
|
128
|
+
const execute = () => {
|
|
129
|
+
// Store the promise before middleware can synchronously re-enter execute().
|
|
130
|
+
execution ??= Promise.resolve().then(() => runMiddleware(middleware, context, transport));
|
|
131
|
+
return execution;
|
|
132
|
+
};
|
|
133
|
+
const context: RequestContext = { input, request, options };
|
|
134
|
+
Object.defineProperties(context, { input: { writable: false }, options: { writable: false } });
|
|
135
|
+
return createOperation(context, execute, extensions);
|
|
136
|
+
},
|
|
137
|
+
}) as DixousClient;
|
|
138
|
+
}
|
|
191
139
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
fetch(input, requestOptions) {
|
|
196
|
-
const snapshot = snapshotOptions(requestOptions);
|
|
197
|
-
const template = createTemplate(input, snapshot, client);
|
|
198
|
-
const pending: Record<string, (...args: never[]) => Promise<unknown>> =
|
|
199
|
-
Object.create(null);
|
|
200
|
-
for (const [name, factory] of methods) {
|
|
201
|
-
pending[name] = (...args) => {
|
|
202
|
-
const fetchResponse = createFetchResponse(
|
|
203
|
-
template, snapshot, client, middleware, fetchImpl,
|
|
204
|
-
);
|
|
205
|
-
// Factories and method work are lazy too, and run once per call.
|
|
206
|
-
return factory(fetchResponse)(...args);
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
return Object.freeze(pending);
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
}
|
|
140
|
+
export interface Dixous<Options extends object = {}, OperationApi extends object = DefaultOperationApi>
|
|
141
|
+
extends DixousClient<Options, OperationApi> {}
|
|
213
142
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
143
|
+
export const Dixous: {
|
|
144
|
+
create<const Extensions extends readonly AnyExtension[] = []>(
|
|
145
|
+
options?: CreateOptions<{}, Extensions>,
|
|
146
|
+
): Dixous<ApplyExtensionOptions<{}, Extensions>, ApplyExtensionApi<DefaultOperationApi, Extensions>>;
|
|
147
|
+
} = Object.freeze({
|
|
148
|
+
create: createClient.bind(undefined, {}) as DixousClient["create"],
|
|
149
|
+
});
|
package/src/response-methods.ts
CHANGED
|
@@ -1,36 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { ResponseValidationError, UnexpectedResponseError } from "./errors.ts";
|
|
2
|
+
import type { InferOutput, StandardSchemaV1 } from "./standard-schema.ts";
|
|
3
|
+
import type { DefaultOperationApi, MatchResult, OperationContext, StatusHandlers } from "./types.ts";
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
readonly issues: readonly StandardSchemaV1.Issue[];
|
|
9
|
-
|
|
10
|
-
constructor(issues: readonly StandardSchemaV1.Issue[]) {
|
|
11
|
-
super("Response failed schema validation");
|
|
12
|
-
this.name = "SchemaValidationError";
|
|
13
|
-
this.issues = issues;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Built-ins use the same per-operation factories as extension methods.
|
|
18
|
-
export const defaultResponseMethods = {
|
|
19
|
-
json: (fetchResponse: FetchResponse) =>
|
|
20
|
-
async <Schema extends StandardSchemaV1>(schema: Schema): Promise<InferOutput<Schema>> => {
|
|
21
|
-
const response = await fetchResponse();
|
|
5
|
+
export function defaultOperationApi(operation: OperationContext): DefaultOperationApi {
|
|
6
|
+
return {
|
|
7
|
+
async json<Schema extends StandardSchemaV1>(schema: Schema): Promise<InferOutput<Schema>> {
|
|
8
|
+
const response = await operation.response();
|
|
22
9
|
const result = await schema["~standard"].validate(await response.json());
|
|
23
|
-
if (result.issues
|
|
10
|
+
if (result.issues !== undefined) {
|
|
11
|
+
throw new ResponseValidationError(operation.request, response, result.issues);
|
|
12
|
+
}
|
|
24
13
|
return result.value as InferOutput<Schema>;
|
|
25
14
|
},
|
|
26
|
-
|
|
27
|
-
(await
|
|
28
|
-
|
|
29
|
-
(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
15
|
+
async text() { return (await operation.response()).text(); },
|
|
16
|
+
async blob() { return (await operation.response()).blob(); },
|
|
17
|
+
async arrayBuffer() { return (await operation.response()).arrayBuffer(); },
|
|
18
|
+
async match<Handlers extends StatusHandlers<{}>>(handlers: Handlers): Promise<MatchResult<Handlers>> {
|
|
19
|
+
const response = await operation.execute();
|
|
20
|
+
const handler = handlers[response.status];
|
|
21
|
+
if (handler === undefined) throw new UnexpectedResponseError(operation.request, response);
|
|
22
|
+
return (await handler(operation.api(response))) as MatchResult<Handlers>;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard Schema v1 types, vendored from @standard-schema/spec 1.1.0.
|
|
3
|
+
* https://standardschema.dev/schema
|
|
4
|
+
*
|
|
5
|
+
* MIT License
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2024 Colin McDonnell
|
|
8
|
+
*
|
|
9
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
* in the Software without restriction, including without limitation the rights
|
|
12
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
* furnished to do so, subject to the following conditions:
|
|
15
|
+
*
|
|
16
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
* copies or substantial portions of the Software.
|
|
18
|
+
*
|
|
19
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
* SOFTWARE.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
29
|
+
readonly "~standard": {
|
|
30
|
+
readonly version: 1;
|
|
31
|
+
readonly vendor: string;
|
|
32
|
+
readonly validate: (value: unknown) =>
|
|
33
|
+
StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
|
|
34
|
+
readonly types?: {
|
|
35
|
+
readonly input: Input;
|
|
36
|
+
readonly output: Output;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type StandardSchemaResult<Output> =
|
|
42
|
+
| { readonly value: Output; readonly issues?: undefined }
|
|
43
|
+
| { readonly issues: readonly StandardSchemaIssue[] };
|
|
44
|
+
|
|
45
|
+
export interface StandardSchemaIssue {
|
|
46
|
+
readonly message: string;
|
|
47
|
+
readonly path?: readonly (PropertyKey | { readonly key: PropertyKey })[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type InferOutput<Schema extends StandardSchemaV1> =
|
|
51
|
+
NonNullable<Schema["~standard"]["types"]>["output"];
|