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/src/types.ts CHANGED
@@ -1,141 +1,142 @@
1
- declare const contextKeyType: unique symbol;
1
+ import type { InferOutput, StandardSchemaV1 } from "./standard-schema.ts";
2
2
 
3
- export type ContextKey<T> = symbol & {
4
- readonly [contextKeyType]: (value: T) => T;
5
- };
6
-
7
- export interface Context {
8
- get<T>(key: ContextKey<T>): T | undefined;
9
- set<T>(key: ContextKey<T>, value: T): void;
10
- }
11
-
12
- export type RequestOptions<Extra extends object = {}> = RequestInit & Extra;
13
-
14
- export interface BaseClientOptions {
15
- baseUrl?: string | URL;
16
- headers?: HeadersInit;
17
- }
3
+ export type RequestInput = string | URL | Request;
4
+ export type RequestOptions<Options extends object = {}> = RequestInit & Options;
18
5
 
19
- export type ClientOptions<Extra extends object = {}> = BaseClientOptions & Extra;
20
-
21
- export interface RequestContext<
22
- RequestExtra extends object = {},
23
- ClientExtra extends object = {},
24
- > {
6
+ export interface RequestContext<Options extends object = {}> {
7
+ /** Exact original input, even when middleware replaces request. */
8
+ readonly input: RequestInput;
9
+ /** Request used by the next execution step; middleware may replace it. */
25
10
  request: Request;
26
- readonly options: Readonly<RequestOptions<RequestExtra>>;
27
- readonly client: Readonly<ClientOptions<ClientExtra>>;
28
- readonly state: Context;
11
+ /** Shallow readonly snapshot of effective client and request options. */
12
+ readonly options: Readonly<RequestOptions<Options>>;
29
13
  }
30
14
 
15
+ /**
16
+ * Reruns the remaining chain. Sequential calls are allowed; concurrent calls
17
+ * reject. Dixous does not clone or buffer bodies: callers must ensure replayability.
18
+ */
31
19
  export type Next = () => Promise<Response>;
32
-
33
- export type Middleware<
34
- RequestExtra extends object = {},
35
- ClientExtra extends object = {},
36
- > = (
37
- context: RequestContext<RequestExtra, ClientExtra>,
38
- next: Next,
20
+ export type RequestMiddleware<Options extends object = {}> = (
21
+ context: RequestContext<Options>, next: Next,
39
22
  ) => Promise<Response>;
40
23
 
41
- export type FetchResponse = () => Promise<Response>;
24
+ export interface OperationContext<Options extends object = {}> extends RequestContext<Options> {
25
+ /** Operation methods observe the request; only middleware replaces it. */
26
+ readonly request: Request;
27
+ /** The response this operation reads: rejects non-ok unless built by api(). The body is one-shot. */
28
+ response(): Promise<Response>;
29
+ /** Memoized raw execution without a status policy. */
30
+ execute(): Promise<Response>;
31
+ /** Rebuilds every contribution over a specific response. Contributions are pure factories. */
32
+ api(response: Response): MatchedOperation<{}>;
33
+ }
42
34
 
43
- export type ResponseMethod = (
44
- fetchResponse: FetchResponse,
45
- ) => (...args: never[]) => Promise<unknown>;
35
+ /** Default body readers require response.ok; schema/platform errors are preserved. */
36
+ export interface DefaultOperationApi {
37
+ json<Schema extends StandardSchemaV1>(schema: Schema): Promise<InferOutput<Schema>>;
38
+ text(): Promise<string>;
39
+ blob(): Promise<Blob>;
40
+ arrayBuffer(): Promise<ArrayBuffer>;
41
+ /** Branches on the exact status; unmatched statuses reject with UnexpectedResponseError. */
42
+ match: Match<{}>;
43
+ }
46
44
 
47
- export type ResponseMethods = Record<string, ResponseMethod>;
45
+ export type ReservedOperationKey = "response" | "then";
48
46
 
49
- type InstantiateMethods<Methods extends ResponseMethods> = {
50
- readonly [K in keyof Methods]: ReturnType<Methods[K]>;
47
+ export type StatusHandlers<OperationApi extends object> = {
48
+ readonly [status: number]: (operation: MatchedOperation<OperationApi>) => unknown;
51
49
  };
50
+ export type MatchResult<Handlers extends StatusHandlers<any>> =
51
+ Awaited<ReturnType<Handlers[keyof Handlers & number]>>;
52
+ export type Match<OperationApi extends object> =
53
+ <Handlers extends StatusHandlers<OperationApi>>(handlers: Handlers) => Promise<MatchResult<Handlers>>;
54
+
55
+ interface CoreOperation {
56
+ /** Returns the native response without applying a status policy. */
57
+ readonly response: () => Promise<Response>;
58
+ readonly then?: never;
59
+ }
60
+
61
+ export type MatchedOperation<OperationApi extends object = DefaultOperationApi> =
62
+ Omit<OperationApi, "match"> & CoreOperation;
63
+
64
+ // The default match is declared over {} and bound here, where the final API is known.
65
+ export type RequestOperation<OperationApi extends object = DefaultOperationApi> = {
66
+ [Key in keyof OperationApi]: Key extends "match"
67
+ ? OperationApi[Key] extends Match<{}> ? Match<OperationApi> : OperationApi[Key]
68
+ : OperationApi[Key];
69
+ } & CoreOperation;
52
70
 
53
71
  declare const extensionType: unique symbol;
54
72
 
55
- export interface ExtensionMeta {
56
- readonly [extensionType]: {
57
- request: object;
58
- client: object;
59
- methods: ResponseMethods;
60
- };
73
+ export interface ExtensionDefinition<Options extends object, OperationApi extends object> {
74
+ /** Runs in extension order with onion semantics. */
75
+ readonly request?: RequestMiddleware<Options>;
76
+ /**
77
+ * Pure API factory: may run more than once for one logical request, once per
78
+ * api() call. Later contributions replace earlier methods.
79
+ */
80
+ readonly operation?: (
81
+ operation: OperationContext<Options>,
82
+ ) => OperationApiContribution<OperationApi>;
61
83
  }
62
84
 
63
- export interface Extension<
64
- RequestExtra extends object = {},
65
- ClientExtra extends object = {},
66
- Methods extends ResponseMethods = {},
67
- > {
68
- readonly [extensionType]: {
69
- request: RequestExtra;
70
- client: ClientExtra;
71
- methods: Methods;
85
+ export type Extension<Options extends object = {}, OperationApi extends object = {}> =
86
+ ExtensionDefinition<Options, OperationApi> & {
87
+ readonly [extensionType]?: {
88
+ readonly options: Options;
89
+ readonly operationApi: OperationApi;
90
+ };
72
91
  };
73
- request?: Middleware<RequestExtra, ClientExtra>;
74
- methods?: Methods;
92
+
93
+ export interface CoreOptions {
94
+ /** Resolves string and URL inputs using native WHATWG URL semantics. */
95
+ readonly baseUrl?: string | URL;
96
+ /** Defaults merged by header name in derived clients and requests. */
97
+ readonly headers?: HeadersInit;
98
+ /** Defaults to globalThis.fetch. */
99
+ readonly fetch?: typeof globalThis.fetch;
75
100
  }
76
101
 
77
- export type ExtensionDefinition<
78
- RequestExtra extends object,
79
- ClientExtra extends object,
80
- Methods extends ResponseMethods,
81
- > = Pick<Extension<RequestExtra, ClientExtra, Methods>, "request"> & {
82
- // Contextually type factory parameters while preserving each method's signature.
83
- methods?: Methods & Record<string, (fetchResponse: FetchResponse) => unknown>;
84
- };
102
+ export type CreateOptions<CurrentOptions extends object, Extensions extends readonly AnyExtension[]> =
103
+ { readonly extensions?: Extensions & NoInfer<{
104
+ // Validate inline contributions too; erased APIs retain runtime validation.
105
+ [Index in keyof Extensions]:
106
+ [OperationApiContribution<ReturnType<NonNullable<Extensions[Index]["operation"]>>>] extends [never]
107
+ ? never : Extensions[Index];
108
+ }> } &
109
+ CoreOptions & {
110
+ // Materialize the fold so inline extension arrays retain tuple inference.
111
+ [Key in keyof ApplyExtensionOptions<CurrentOptions, Extensions>]:
112
+ ApplyExtensionOptions<CurrentOptions, Extensions>[Key];
113
+ };
85
114
 
86
- export type NoRequestInitOverrides = {
87
- [K in keyof RequestInit]?: never;
88
- };
115
+ export interface Dixous<Options extends object = {}, OperationApi extends object = DefaultOperationApi> {
116
+ /** Constructs a Request immediately; execution waits until response() is called. */
117
+ request(input: RequestInput, options?: RequestOptions<Options>): RequestOperation<OperationApi>;
118
+ /** Inherits configuration, merges headers, appends extensions; never mutates the parent. */
119
+ create<const Extensions extends readonly AnyExtension[] = []>(
120
+ options?: CreateOptions<Options, Extensions>,
121
+ ): Dixous<ApplyExtensionOptions<Options, Extensions>, ApplyExtensionApi<OperationApi, Extensions>>;
122
+ readonly then?: never;
123
+ }
89
124
 
90
- export type NoClientOptionOverrides = {
91
- [K in keyof BaseClientOptions]?: never;
92
- };
125
+ export type AnyExtension = Extension<any, any>;
93
126
 
94
- type UnionToIntersection<T> = [T] extends [never]
95
- ? {}
96
- : (T extends unknown ? (value: T) => void : never) extends (
97
- value: infer Result,
98
- ) => void
99
- ? Result
100
- : never;
101
-
102
- export type ExtensionRequestOptions<
103
- Extensions extends readonly ExtensionMeta[],
104
- > = UnionToIntersection<
105
- Extensions[number][typeof extensionType]["request"]
106
- > extends infer Result extends object
107
- ? Result
108
- : never;
109
-
110
- export type ExtensionClientOptions<
111
- Extensions extends readonly ExtensionMeta[],
112
- > = UnionToIntersection<
113
- Extensions[number][typeof extensionType]["client"]
114
- > extends infer Result extends object
115
- ? Result
116
- : never;
117
-
118
- export type ExtensionMethods<Extensions extends readonly ExtensionMeta[]> =
119
- UnionToIntersection<
120
- Extensions[number][typeof extensionType]["methods"]
121
- > extends infer Result extends ResponseMethods
122
- ? Result
123
- : never;
124
-
125
- export interface Fetcher<
126
- RequestExtra extends object,
127
- Methods extends ResponseMethods,
128
- > {
129
- fetch(
130
- input: string | URL | Request,
131
- options?: RequestOptions<RequestExtra>,
132
- ): InstantiateMethods<Methods>;
133
- }
127
+ // AnyExtension erases the API for composition; concrete APIs still reserve core keys.
128
+ type OperationApiContribution<Api extends object> =
129
+ unknown extends Api ? Api : [Extract<keyof Api, ReservedOperationKey>] extends [never] ? Api : never;
130
+ type ExtensionOptions<E extends AnyExtension> = E extends Extension<infer Options, any> ? Options : {};
131
+ type ExtensionOperationApi<E extends AnyExtension> = E extends Extension<any, infer Api> ? Api : {};
132
+ type Merge<Left extends object, Right extends object> = Omit<Left, keyof Right> & Right;
134
133
 
135
- export interface Dixous<
136
- RequestExtra extends object,
137
- ClientExtra extends object,
138
- Methods extends ResponseMethods,
139
- > extends Fetcher<RequestExtra, Methods> {
140
- (options?: ClientOptions<ClientExtra>): Fetcher<RequestExtra, Methods>;
141
- }
134
+ export type ApplyExtensionOptions<Current extends object, Extensions extends readonly AnyExtension[]> =
135
+ Extensions extends readonly [infer Head extends AnyExtension, ...infer Tail extends readonly AnyExtension[]]
136
+ ? ApplyExtensionOptions<Merge<Current, ExtensionOptions<Head>>, Tail>
137
+ : Current;
138
+
139
+ export type ApplyExtensionApi<Current extends object, Extensions extends readonly AnyExtension[]> =
140
+ Extensions extends readonly [infer Head extends AnyExtension, ...infer Tail extends readonly AnyExtension[]]
141
+ ? ApplyExtensionApi<Merge<Current, ExtensionOperationApi<Head>>, Tail>
142
+ : Current;