@monarcode/effetch 0.0.0-stage → 0.1.0-alpha.1
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/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +613 -2
- package/dist/Client.d.ts +367 -0
- package/dist/Client.js +110 -0
- package/dist/Errors.d.ts +224 -0
- package/dist/Errors.js +151 -0
- package/dist/Options.d.ts +462 -0
- package/dist/Path.d.ts +72 -0
- package/dist/Plugin.d.ts +553 -0
- package/dist/Plugin.js +7 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +5 -0
- package/dist/internal/body.js +70 -0
- package/dist/internal/object.js +6 -0
- package/dist/internal/path.js +23 -0
- package/dist/internal/pipeline.js +29 -0
- package/dist/internal/plugins.js +92 -0
- package/dist/internal/query.js +22 -0
- package/dist/internal/request.js +33 -0
- package/dist/internal/resolve.js +23 -0
- package/dist/internal/response.js +73 -0
- package/dist/internal/url.js +24 -0
- package/dist/plugins/bearer.d.ts +152 -0
- package/dist/plugins/bearer.js +63 -0
- package/dist/plugins/index.d.ts +32 -0
- package/dist/plugins/index.js +6 -0
- package/dist/plugins/logger.d.ts +66 -0
- package/dist/plugins/logger.js +67 -0
- package/dist/plugins/retry.d.ts +94 -0
- package/dist/plugins/retry.js +74 -0
- package/dist/plugins/timeout.d.ts +108 -0
- package/dist/plugins/timeout.js +84 -0
- package/package.json +85 -3
package/dist/Plugin.d.ts
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
3
|
+
import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
4
|
+
import * as HttpClientResponse from "effect/http/HttpClientResponse";
|
|
5
|
+
//#region src/Plugin.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* `unknown` if `T` is `any`, else `T`, so erased plugin types don't spread
|
|
8
|
+
* `any`.
|
|
9
|
+
*/
|
|
10
|
+
type AnyToUnknown<T> = 0 extends 1 & T ? unknown : T;
|
|
11
|
+
/**
|
|
12
|
+
* `T` itself, once known. As a return type it stops TypeScript inferring
|
|
13
|
+
* `T` from where the result goes: inside a `plugins` list, that would be
|
|
14
|
+
* `Plugin.Any`, and the plugin's errors and services would become `any`.
|
|
15
|
+
*/
|
|
16
|
+
type Settled<T> = [T] extends [infer U] ? U : never;
|
|
17
|
+
/** The error (`E`), services (`R`) and options (`Opts`) types of plugin `P`. */
|
|
18
|
+
type Parts<P> =
|
|
19
|
+
P extends Plugin<string, infer E, infer R, infer Opts>
|
|
20
|
+
? {
|
|
21
|
+
readonly E: AnyToUnknown<E>;
|
|
22
|
+
readonly R: AnyToUnknown<R>;
|
|
23
|
+
readonly Opts: Opts;
|
|
24
|
+
}
|
|
25
|
+
: never;
|
|
26
|
+
/**
|
|
27
|
+
* An optional override per plugin name in `Ps`, leaving out non-literal
|
|
28
|
+
* names.
|
|
29
|
+
*/
|
|
30
|
+
type OverrideMap<Ps extends ReadonlyArray<Plugin.Any>> = {
|
|
31
|
+
readonly [P in Ps[number] as string extends P["name"] ? never : P["name"]]?:
|
|
32
|
+
| Plugin.Override<P>
|
|
33
|
+
| undefined;
|
|
34
|
+
};
|
|
35
|
+
/** The brand that makes {@link Plugin.UpstreamError} opaque. */
|
|
36
|
+
declare const UpstreamErrorTypeId: unique symbol;
|
|
37
|
+
/** The brand that makes {@link Plugin.UpstreamServices} opaque. */
|
|
38
|
+
declare const UpstreamServicesTypeId: unique symbol;
|
|
39
|
+
/**
|
|
40
|
+
* What a plugin hook receives besides the request or response.
|
|
41
|
+
*
|
|
42
|
+
* - `request`: in `onRequest`, the request it receives. In `onResponse`,
|
|
43
|
+
* the request this plugin passed on (plugins after it may have changed it
|
|
44
|
+
* further; `response.request` is the one that was sent). In `onError`,
|
|
45
|
+
* the request as the call built it, before any plugin ran.
|
|
46
|
+
* - `options`: the plugin's `defaults` merged with this call's override,
|
|
47
|
+
* `{ ...defaults, ...override }`, where `undefined` values in the
|
|
48
|
+
* override keep the default. `undefined` for a plugin without
|
|
49
|
+
* `defaults`.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import * as Effect from "effect/Effect";
|
|
54
|
+
* import * as Fetch from "@monarcode/effetch";
|
|
55
|
+
*
|
|
56
|
+
* const tracing = Fetch.plugin({
|
|
57
|
+
* name: "tracing",
|
|
58
|
+
* defaults: { header: "x-request-id" },
|
|
59
|
+
* onResponse: (response, ctx: Fetch.PluginContext<{ header: string }>) =>
|
|
60
|
+
* Effect.as(
|
|
61
|
+
* Effect.logDebug(ctx.request.url, response.headers[ctx.options.header]),
|
|
62
|
+
* response,
|
|
63
|
+
* ),
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @since 0.1.0
|
|
68
|
+
*/
|
|
69
|
+
export interface PluginContext<Opts> {
|
|
70
|
+
/** The request this hook is about (see above for which one). */
|
|
71
|
+
readonly request: HttpClientRequest.HttpClientRequest;
|
|
72
|
+
/** The plugin's `defaults` merged with this call's override. */
|
|
73
|
+
readonly options: Opts;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A plugin hooks into every call a client makes.
|
|
77
|
+
*
|
|
78
|
+
* - `onRequest` can change the request, or fail. Plugins run it in array
|
|
79
|
+
* order. It sees the full URL and the client's default headers.
|
|
80
|
+
* - `onResponse` can change the response, or fail. It runs in reverse order
|
|
81
|
+
* and sees every response, non-2xx included: the library checks the status
|
|
82
|
+
* and decodes `output` only after the whole plugin chain.
|
|
83
|
+
* - `onError` observes any failure of the call (after status check and
|
|
84
|
+
* decoding) and can't change the outcome.
|
|
85
|
+
* - `wrap` wraps the rest of the chain, for retries, timeouts and the like.
|
|
86
|
+
* It can add errors (`E`) but can't remove the ones from further in.
|
|
87
|
+
*
|
|
88
|
+
* `plugins[0]` is the outermost layer. A plugin's `wrap` encloses its own
|
|
89
|
+
* `onRequest` and `onResponse` and every plugin after it, so a retrying
|
|
90
|
+
* `wrap` runs them again on each attempt. Transport failures reach plugins
|
|
91
|
+
* as `NetworkError`s, never as Effect's `HttpClientError`.
|
|
92
|
+
*
|
|
93
|
+
* The services a plugin needs (`R`) are required by `make`, once; calls on
|
|
94
|
+
* the client don't need them.
|
|
95
|
+
*
|
|
96
|
+
* `Opts` is the type of the options calls can override, and `defaults`
|
|
97
|
+
* holds their values. Calls override them with
|
|
98
|
+
* `plugins: { [name]: { ...partial } }`, and any plugin can be skipped for
|
|
99
|
+
* one call with `plugins: { [name]: false }`. A plugin with options must
|
|
100
|
+
* have `defaults`; {@link plugin} enforces this.
|
|
101
|
+
*
|
|
102
|
+
* Build plugins with {@link plugin}, which infers the type parameters.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* import * as Effect from "effect/Effect";
|
|
107
|
+
* import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
108
|
+
* import * as Fetch from "@monarcode/effetch";
|
|
109
|
+
*
|
|
110
|
+
* const apiKey: Fetch.Plugin<"apiKey"> = Fetch.plugin({
|
|
111
|
+
* name: "apiKey",
|
|
112
|
+
* onRequest: (request) =>
|
|
113
|
+
* Effect.succeed(
|
|
114
|
+
* HttpClientRequest.setHeader(request, "x-api-key", "secret"),
|
|
115
|
+
* ),
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* @since 0.1.0
|
|
120
|
+
*/
|
|
121
|
+
export interface Plugin<
|
|
122
|
+
Name extends string,
|
|
123
|
+
E = never,
|
|
124
|
+
R = never,
|
|
125
|
+
Opts = undefined,
|
|
126
|
+
> {
|
|
127
|
+
/** Unique within a client. Used as the key for per-request overrides. */
|
|
128
|
+
readonly name: Name;
|
|
129
|
+
/** Default options. Enables per-request `Partial<Opts>` overrides. */
|
|
130
|
+
readonly defaults?: Opts | undefined;
|
|
131
|
+
/** Runs before the request is sent, in array order. */
|
|
132
|
+
readonly onRequest?: Plugin.OnRequest<Opts, E, R> | undefined;
|
|
133
|
+
/** Runs on every response, non-2xx included, in reverse array order. */
|
|
134
|
+
readonly onResponse?: Plugin.OnResponse<Opts, E, R> | undefined;
|
|
135
|
+
/** Observes any failure of the call. Can't change the outcome. */
|
|
136
|
+
readonly onError?: Plugin.OnError<Opts, R> | undefined;
|
|
137
|
+
/** Wraps the rest of the chain. */
|
|
138
|
+
readonly wrap?: Plugin.Wrap<Opts, E, R> | undefined;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Type-level helpers for {@link Plugin}.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
146
|
+
*
|
|
147
|
+
* type Plugins = [Fetch.Plugin<"bearer">];
|
|
148
|
+
* type Overrides = Fetch.Plugin.Overrides<Plugins>;
|
|
149
|
+
* // ^? { readonly bearer?: false | undefined }
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @since 0.1.0
|
|
153
|
+
*/
|
|
154
|
+
export declare namespace Plugin {
|
|
155
|
+
/**
|
|
156
|
+
* The `onRequest` hook: returns the request to send, possibly changed.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* import * as Effect from "effect/Effect";
|
|
161
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
162
|
+
*
|
|
163
|
+
* const passThrough: Fetch.Plugin.OnRequest<undefined, never, never> = (
|
|
164
|
+
* request,
|
|
165
|
+
* ) => Effect.succeed(request);
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* @since 0.1.0
|
|
169
|
+
*/
|
|
170
|
+
type OnRequest<Opts, E, R> = (
|
|
171
|
+
request: HttpClientRequest.HttpClientRequest,
|
|
172
|
+
ctx: PluginContext<Opts>,
|
|
173
|
+
) => Effect.Effect<HttpClientRequest.HttpClientRequest, E, R>;
|
|
174
|
+
/**
|
|
175
|
+
* The `onResponse` hook: returns the response, possibly changed, or
|
|
176
|
+
* fails. Sees non-2xx responses too.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* import * as Effect from "effect/Effect";
|
|
181
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
182
|
+
*
|
|
183
|
+
* const passThrough: Fetch.Plugin.OnResponse<undefined, never, never> = (
|
|
184
|
+
* response,
|
|
185
|
+
* ) => Effect.succeed(response);
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* @since 0.1.0
|
|
189
|
+
*/
|
|
190
|
+
type OnResponse<Opts, E, R> = (
|
|
191
|
+
response: HttpClientResponse.HttpClientResponse,
|
|
192
|
+
ctx: PluginContext<Opts>,
|
|
193
|
+
) => Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>;
|
|
194
|
+
/**
|
|
195
|
+
* The `onError` hook. `error` is a `FetchError`, another plugin's error,
|
|
196
|
+
* or a defect's cause. It can't fail or change the call's outcome: if it
|
|
197
|
+
* dies, the defect is logged at Debug level and ignored.
|
|
198
|
+
*
|
|
199
|
+
* Hooks run in array order, after the status check and decoding. They
|
|
200
|
+
* don't run when the call succeeds or is interrupted, when it dies
|
|
201
|
+
* before its request is built (a missing path param, a body that can't
|
|
202
|
+
* be encoded), or for a plugin the call skips.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```ts
|
|
206
|
+
* import * as Effect from "effect/Effect";
|
|
207
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
208
|
+
*
|
|
209
|
+
* const report: Fetch.Plugin.OnError<undefined, never> = (error, ctx) =>
|
|
210
|
+
* Effect.logWarning(ctx.request.url, error);
|
|
211
|
+
* ```
|
|
212
|
+
*
|
|
213
|
+
* @since 0.1.0
|
|
214
|
+
*/
|
|
215
|
+
type OnError<Opts, R> = (
|
|
216
|
+
error: unknown,
|
|
217
|
+
ctx: PluginContext<Opts>,
|
|
218
|
+
) => Effect.Effect<void, never, R>;
|
|
219
|
+
/**
|
|
220
|
+
* The `wrap` hook: wraps the rest of the chain (`client`). Its error and
|
|
221
|
+
* service types are opaque placeholders, so a `wrap` can add errors and
|
|
222
|
+
* services but can't drop the ones from further in. `options` reads
|
|
223
|
+
* this call's merged options.
|
|
224
|
+
*
|
|
225
|
+
* Write the body with `Effect.gen` rather than a ternary that returns
|
|
226
|
+
* two different Effects: TypeScript can't union their error types.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* import * as HttpClient from "effect/http/HttpClient";
|
|
231
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
232
|
+
*
|
|
233
|
+
* const retryTwice: Fetch.Plugin.Wrap<undefined, never, never> = (client) =>
|
|
234
|
+
* HttpClient.retry(client, { times: 2 });
|
|
235
|
+
* ```
|
|
236
|
+
*
|
|
237
|
+
* @since 0.1.0
|
|
238
|
+
*/
|
|
239
|
+
type Wrap<Opts, E, R> = (
|
|
240
|
+
client: Upstream,
|
|
241
|
+
options: Effect.Effect<Opts>,
|
|
242
|
+
) => HttpClient.HttpClient.With<UpstreamError | E, UpstreamServices | R>;
|
|
243
|
+
/**
|
|
244
|
+
* Any plugin. Use it to constrain a generic plugin-list parameter
|
|
245
|
+
* (`Ps extends ReadonlyArray<Plugin.Any>`).
|
|
246
|
+
*
|
|
247
|
+
* Don't annotate a plugin list with it: that erases the plugins' types,
|
|
248
|
+
* so a client's errors and services become `unknown` and per-request
|
|
249
|
+
* overrides accept no names. Pass the list inline or let TypeScript
|
|
250
|
+
* infer it.
|
|
251
|
+
*
|
|
252
|
+
* It's structural rather than `Plugin<string, any, any, any>`: the
|
|
253
|
+
* `HttpClient` returned by `wrap` is invariant in its error and service
|
|
254
|
+
* types, and `any` isn't assignable to `never`, so plugins without
|
|
255
|
+
* services wouldn't fit that form.
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```ts
|
|
259
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
260
|
+
*
|
|
261
|
+
* type Names<Ps extends ReadonlyArray<Fetch.Plugin.Any>> =
|
|
262
|
+
* Ps[number]["name"];
|
|
263
|
+
* ```
|
|
264
|
+
*
|
|
265
|
+
* @since 0.1.0
|
|
266
|
+
*/
|
|
267
|
+
interface Any {
|
|
268
|
+
/** The plugin's name. */
|
|
269
|
+
readonly name: string;
|
|
270
|
+
/** The plugin's default options, if any. */
|
|
271
|
+
readonly defaults?: any;
|
|
272
|
+
/** The plugin's `onRequest` hook, if any. */
|
|
273
|
+
readonly onRequest?: OnRequest<any, any, any> | undefined;
|
|
274
|
+
/** The plugin's `onResponse` hook, if any. */
|
|
275
|
+
readonly onResponse?: OnResponse<any, any, any> | undefined;
|
|
276
|
+
/** The plugin's `onError` hook, if any. */
|
|
277
|
+
readonly onError?: OnError<any, any> | undefined;
|
|
278
|
+
/** The plugin's `wrap` hook, if any. */
|
|
279
|
+
readonly wrap?:
|
|
280
|
+
| ((
|
|
281
|
+
client: Upstream,
|
|
282
|
+
options: Effect.Effect<any>,
|
|
283
|
+
) => HttpClient.HttpClient.With<any, any>)
|
|
284
|
+
| undefined;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Stands for the errors of everything inside a plugin's `wrap`: the
|
|
288
|
+
* network, the library's other errors and inner plugins' errors. It's
|
|
289
|
+
* opaque on purpose; a `wrap` can only pass it through.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```ts
|
|
293
|
+
* import type * as HttpClient from "effect/http/HttpClient";
|
|
294
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
295
|
+
*
|
|
296
|
+
* type Wrapped = HttpClient.HttpClient.With<
|
|
297
|
+
* Fetch.Plugin.UpstreamError | Error,
|
|
298
|
+
* Fetch.Plugin.UpstreamServices
|
|
299
|
+
* >;
|
|
300
|
+
* ```
|
|
301
|
+
*
|
|
302
|
+
* @since 0.1.0
|
|
303
|
+
*/
|
|
304
|
+
interface UpstreamError {
|
|
305
|
+
/** Makes the type opaque: no real error has this property. */
|
|
306
|
+
readonly [UpstreamErrorTypeId]: typeof UpstreamErrorTypeId;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Stands for the services required by everything inside a plugin's
|
|
310
|
+
* `wrap`. Opaque, like {@link UpstreamError}.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```ts
|
|
314
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
315
|
+
*
|
|
316
|
+
* declare const services: Fetch.Plugin.UpstreamServices;
|
|
317
|
+
* ```
|
|
318
|
+
*
|
|
319
|
+
* @since 0.1.0
|
|
320
|
+
*/
|
|
321
|
+
interface UpstreamServices {
|
|
322
|
+
/** Makes the type opaque: no real service has this property. */
|
|
323
|
+
readonly [UpstreamServicesTypeId]: typeof UpstreamServicesTypeId;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The client a plugin's `wrap` receives: the rest of the chain.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```ts
|
|
330
|
+
* import * as HttpClient from "effect/http/HttpClient";
|
|
331
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
332
|
+
*
|
|
333
|
+
* const withRetries = (client: Fetch.Plugin.Upstream) =>
|
|
334
|
+
* HttpClient.retry(client, { times: 2 });
|
|
335
|
+
* ```
|
|
336
|
+
*
|
|
337
|
+
* @since 0.1.0
|
|
338
|
+
*/
|
|
339
|
+
interface Upstream extends HttpClient.HttpClient.With<
|
|
340
|
+
UpstreamError,
|
|
341
|
+
UpstreamServices
|
|
342
|
+
> {}
|
|
343
|
+
/**
|
|
344
|
+
* What {@link plugin} accepts. Like {@link Plugin}, but each hook has its
|
|
345
|
+
* own error and service type parameters, so hooks that fail with
|
|
346
|
+
* different errors infer their union instead of conflicting.
|
|
347
|
+
*
|
|
348
|
+
* `Opts` is inferred from `defaults` only, never from a hook's `ctx`
|
|
349
|
+
* annotation, so `ctx.options` is typed as present only when `defaults`
|
|
350
|
+
* exist.
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```ts
|
|
354
|
+
* import * as Effect from "effect/Effect";
|
|
355
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
356
|
+
*
|
|
357
|
+
* const definition: Fetch.Plugin.Definition<
|
|
358
|
+
* "noop", never, never, never, never, never, never, never, undefined
|
|
359
|
+
* > = { name: "noop", onRequest: Effect.succeed };
|
|
360
|
+
* ```
|
|
361
|
+
*
|
|
362
|
+
* @since 0.1.0
|
|
363
|
+
*/
|
|
364
|
+
interface Definition<
|
|
365
|
+
Name extends string,
|
|
366
|
+
ERequest,
|
|
367
|
+
EResponse,
|
|
368
|
+
EWrap,
|
|
369
|
+
RRequest,
|
|
370
|
+
RResponse,
|
|
371
|
+
RError,
|
|
372
|
+
RWrap,
|
|
373
|
+
Opts,
|
|
374
|
+
> {
|
|
375
|
+
/** Unique within a client. Used as the key for per-request overrides. */
|
|
376
|
+
readonly name: Name;
|
|
377
|
+
/** Default options. Enables per-request `Partial<Opts>` overrides. */
|
|
378
|
+
readonly defaults?: Opts | undefined;
|
|
379
|
+
/** Runs before the request is sent, in array order. */
|
|
380
|
+
readonly onRequest?:
|
|
381
|
+
| OnRequest<NoInfer<Opts>, ERequest, RRequest>
|
|
382
|
+
| undefined;
|
|
383
|
+
/** Runs on every response, non-2xx included, in reverse array order. */
|
|
384
|
+
readonly onResponse?:
|
|
385
|
+
| OnResponse<NoInfer<Opts>, EResponse, RResponse>
|
|
386
|
+
| undefined;
|
|
387
|
+
/** Observes any failure of the call. Can't change the outcome. */
|
|
388
|
+
readonly onError?: OnError<NoInfer<Opts>, RError> | undefined;
|
|
389
|
+
/** Wraps the rest of the chain. */
|
|
390
|
+
readonly wrap?: Wrap<NoInfer<Opts>, EWrap, RWrap> | undefined;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* The union of errors the plugins in `Ps` can add to a call. `unknown`
|
|
394
|
+
* if a plugin's types were erased (for example by {@link Any}).
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* ```ts
|
|
398
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
399
|
+
*
|
|
400
|
+
* class TimeoutError extends Error {
|
|
401
|
+
* readonly _tag = "TimeoutError";
|
|
402
|
+
* }
|
|
403
|
+
*
|
|
404
|
+
* type E = Fetch.Plugin.Errors<[Fetch.Plugin<"timeout", TimeoutError>]>;
|
|
405
|
+
* // ^? TimeoutError
|
|
406
|
+
* ```
|
|
407
|
+
*
|
|
408
|
+
* @since 0.1.0
|
|
409
|
+
*/
|
|
410
|
+
type Errors<Ps extends ReadonlyArray<Any>> = Parts<Ps[number]>["E"];
|
|
411
|
+
/**
|
|
412
|
+
* The union of services the plugins in `Ps` require. `make` requires
|
|
413
|
+
* them once; calls on the client don't. `unknown` if a plugin's types
|
|
414
|
+
* were erased.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* ```ts
|
|
418
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
419
|
+
*
|
|
420
|
+
* declare const plugins: [Fetch.Plugin<"auth", never, "Session">];
|
|
421
|
+
*
|
|
422
|
+
* type R = Fetch.Plugin.Services<typeof plugins>;
|
|
423
|
+
* // ^? "Session"
|
|
424
|
+
* ```
|
|
425
|
+
*
|
|
426
|
+
* @since 0.1.0
|
|
427
|
+
*/
|
|
428
|
+
type Services<Ps extends ReadonlyArray<Any>> = Parts<Ps[number]>["R"];
|
|
429
|
+
/**
|
|
430
|
+
* The options type of plugin `P`.
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* ```ts
|
|
434
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
435
|
+
*
|
|
436
|
+
* type Opts = Fetch.Plugin.Options<
|
|
437
|
+
* Fetch.Plugin<"retry", never, never, { times: number }>
|
|
438
|
+
* >;
|
|
439
|
+
* // ^? { times: number }
|
|
440
|
+
* ```
|
|
441
|
+
*
|
|
442
|
+
* @since 0.1.0
|
|
443
|
+
*/
|
|
444
|
+
type Options<P extends Any> = Parts<P>["Opts"];
|
|
445
|
+
/**
|
|
446
|
+
* What one plugin accepts as a per-request override: `false` to skip
|
|
447
|
+
* it, or a partial of its options. Plugins without options accept only
|
|
448
|
+
* `false`. Keys set to `undefined` keep their default.
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```ts
|
|
452
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
453
|
+
*
|
|
454
|
+
* type RetryOverride = Fetch.Plugin.Override<
|
|
455
|
+
* Fetch.Plugin<"retry", never, never, { times: number }>
|
|
456
|
+
* >;
|
|
457
|
+
* // ^? false | Partial<{ times: number }>
|
|
458
|
+
* ```
|
|
459
|
+
*
|
|
460
|
+
* @since 0.1.0
|
|
461
|
+
*/
|
|
462
|
+
type Override<P extends Any> = [Options<P>] extends [undefined]
|
|
463
|
+
? false
|
|
464
|
+
: false | Partial<Options<P>>;
|
|
465
|
+
/**
|
|
466
|
+
* The `plugins` option of a call: an override per plugin name in `Ps`.
|
|
467
|
+
* Unknown names are compile errors, including when the client has no
|
|
468
|
+
* plugins. Plugins whose `name` isn't a string literal can't be
|
|
469
|
+
* overridden (they'd make every name valid).
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* ```ts
|
|
473
|
+
* import type * as Fetch from "@monarcode/effetch";
|
|
474
|
+
*
|
|
475
|
+
* type Overrides = Fetch.Plugin.Overrides<
|
|
476
|
+
* [
|
|
477
|
+
* Fetch.Plugin<"bearer">,
|
|
478
|
+
* Fetch.Plugin<"retry", never, never, { times: number }>,
|
|
479
|
+
* ]
|
|
480
|
+
* >;
|
|
481
|
+
*
|
|
482
|
+
* const overrides: Overrides = { bearer: false, retry: { times: 0 } };
|
|
483
|
+
* ```
|
|
484
|
+
*
|
|
485
|
+
* @since 0.1.0
|
|
486
|
+
*/
|
|
487
|
+
type Overrides<Ps extends ReadonlyArray<Any>> = [
|
|
488
|
+
keyof OverrideMap<Ps>,
|
|
489
|
+
] extends [never]
|
|
490
|
+
? {
|
|
491
|
+
readonly [name: string]: never;
|
|
492
|
+
}
|
|
493
|
+
: OverrideMap<Ps>;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Defines a plugin. Returns its argument unchanged; it exists to infer
|
|
497
|
+
* `Name`, the errors, the services and the options type from the hooks.
|
|
498
|
+
*
|
|
499
|
+
* The options type comes from `defaults` alone, so a hook can only read
|
|
500
|
+
* `ctx.options` when `defaults` are given.
|
|
501
|
+
*
|
|
502
|
+
* @example
|
|
503
|
+
* ```ts
|
|
504
|
+
* import * as Effect from "effect/Effect";
|
|
505
|
+
* import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
506
|
+
* import * as Fetch from "@monarcode/effetch";
|
|
507
|
+
*
|
|
508
|
+
* const userAgent = Fetch.plugin({
|
|
509
|
+
* name: "userAgent",
|
|
510
|
+
* defaults: { value: "my-app/1.0" },
|
|
511
|
+
* onRequest: (request, ctx) =>
|
|
512
|
+
* Effect.succeed(
|
|
513
|
+
* HttpClientRequest.setHeader(request, "user-agent", ctx.options.value),
|
|
514
|
+
* ),
|
|
515
|
+
* });
|
|
516
|
+
*
|
|
517
|
+
* // Per call: api.get("/x", { plugins: { userAgent: { value: "other" } } })
|
|
518
|
+
* ```
|
|
519
|
+
*
|
|
520
|
+
* @since 0.1.0
|
|
521
|
+
*/
|
|
522
|
+
export declare function plugin<
|
|
523
|
+
const Name extends string,
|
|
524
|
+
ERequest = never,
|
|
525
|
+
EResponse = never,
|
|
526
|
+
EWrap = never,
|
|
527
|
+
RRequest = never,
|
|
528
|
+
RResponse = never,
|
|
529
|
+
RError = never,
|
|
530
|
+
RWrap = never,
|
|
531
|
+
Opts = undefined,
|
|
532
|
+
>(
|
|
533
|
+
definition: Plugin.Definition<
|
|
534
|
+
Name,
|
|
535
|
+
ERequest,
|
|
536
|
+
EResponse,
|
|
537
|
+
EWrap,
|
|
538
|
+
RRequest,
|
|
539
|
+
RResponse,
|
|
540
|
+
RError,
|
|
541
|
+
RWrap,
|
|
542
|
+
Opts
|
|
543
|
+
>,
|
|
544
|
+
): Plugin<
|
|
545
|
+
Name,
|
|
546
|
+
Settled<ERequest | EResponse | Exclude<EWrap, Plugin.UpstreamError>>,
|
|
547
|
+
Settled<
|
|
548
|
+
RRequest | RResponse | RError | Exclude<RWrap, Plugin.UpstreamServices>
|
|
549
|
+
>,
|
|
550
|
+
Settled<Opts>
|
|
551
|
+
>;
|
|
552
|
+
//#endregion
|
|
553
|
+
export {};
|
package/dist/Plugin.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Client, ClientConfig, make } from "./Client.js";
|
|
2
|
+
import {
|
|
3
|
+
DecodeError,
|
|
4
|
+
FetchError,
|
|
5
|
+
HttpError,
|
|
6
|
+
NetworkError,
|
|
7
|
+
isFetchError,
|
|
8
|
+
} from "./Errors.js";
|
|
9
|
+
import {
|
|
10
|
+
AnySchema,
|
|
11
|
+
Args,
|
|
12
|
+
BaseOptions,
|
|
13
|
+
BodyMethod,
|
|
14
|
+
BodyOptions,
|
|
15
|
+
CallEffect,
|
|
16
|
+
CallError,
|
|
17
|
+
CallServices,
|
|
18
|
+
NoBodyMethod,
|
|
19
|
+
NoBodyRequestOptions,
|
|
20
|
+
ParamsOption,
|
|
21
|
+
QueryInput,
|
|
22
|
+
QueryValue,
|
|
23
|
+
RequestMethod,
|
|
24
|
+
RequestOptions,
|
|
25
|
+
Success,
|
|
26
|
+
} from "./Options.js";
|
|
27
|
+
import { PathParamNames, PathParamValue, PathParams } from "./Path.js";
|
|
28
|
+
import { Plugin, PluginContext, plugin } from "./Plugin.js";
|
|
29
|
+
export {
|
|
30
|
+
type AnySchema,
|
|
31
|
+
type Args,
|
|
32
|
+
type BaseOptions,
|
|
33
|
+
type BodyMethod,
|
|
34
|
+
type BodyOptions,
|
|
35
|
+
type CallEffect,
|
|
36
|
+
type CallError,
|
|
37
|
+
type CallServices,
|
|
38
|
+
type Client,
|
|
39
|
+
type ClientConfig,
|
|
40
|
+
DecodeError,
|
|
41
|
+
type FetchError,
|
|
42
|
+
HttpError,
|
|
43
|
+
NetworkError,
|
|
44
|
+
type NoBodyMethod,
|
|
45
|
+
type NoBodyRequestOptions,
|
|
46
|
+
type ParamsOption,
|
|
47
|
+
type PathParamNames,
|
|
48
|
+
type PathParamValue,
|
|
49
|
+
type PathParams,
|
|
50
|
+
type Plugin,
|
|
51
|
+
type PluginContext,
|
|
52
|
+
type QueryInput,
|
|
53
|
+
type QueryValue,
|
|
54
|
+
type RequestMethod,
|
|
55
|
+
type RequestOptions,
|
|
56
|
+
type Success,
|
|
57
|
+
isFetchError,
|
|
58
|
+
make,
|
|
59
|
+
plugin,
|
|
60
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as HttpBody from "effect/http/HttpBody";
|
|
3
|
+
import * as SchemaIssue from "effect/SchemaIssue";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/body.ts
|
|
6
|
+
let formatIssue;
|
|
7
|
+
const describeFailure = (error) => {
|
|
8
|
+
const { reason, cause } = error;
|
|
9
|
+
if (reason._tag === "SchemaError") {
|
|
10
|
+
formatIssue ?? (formatIssue = SchemaIssue.makeFormatterDefault());
|
|
11
|
+
return `it doesn't match the input schema: ${formatIssue(reason.issue)}`;
|
|
12
|
+
}
|
|
13
|
+
return `it can't be serialised as JSON: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
14
|
+
};
|
|
15
|
+
const invalidBody = (message, cause) => Effect.die(new Error(`Invalid request body: ${message}`, { cause }));
|
|
16
|
+
const orDieInvalid = (self) => self.pipe(Effect.catch((error) => invalidBody(describeFailure(error), error)));
|
|
17
|
+
/**
|
|
18
|
+
* True when `value` is an instance of the global constructor `name`.
|
|
19
|
+
*
|
|
20
|
+
* Falls back to the `Object.prototype.toString` tag so values from another
|
|
21
|
+
* realm (an iframe, a `vm` context, a second undici copy) still match.
|
|
22
|
+
* React Native's polyfills have no tag, but they are the global
|
|
23
|
+
* constructors, so `instanceof` covers them.
|
|
24
|
+
*/
|
|
25
|
+
const isGlobalInstance = (value, name) => {
|
|
26
|
+
const constructor = globalThis[name];
|
|
27
|
+
return typeof constructor === "function" && value instanceof constructor || Object.prototype.toString.call(value) === `[object ${name}]`;
|
|
28
|
+
};
|
|
29
|
+
const isBlob = (value) => isGlobalInstance(value, "Blob") || isGlobalInstance(value, "File");
|
|
30
|
+
const toBytes = (value) => ArrayBuffer.isView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : new Uint8Array(value);
|
|
31
|
+
const encodeJson = (body) => Effect.flatMap(orDieInvalid(HttpBody.json(body)), (encoded) => encoded.contentLength === 0 ? invalidBody(`it can't be serialised as JSON: ${typeof body} has no JSON representation`) : Effect.succeed(encoded));
|
|
32
|
+
const encodePlain = (body) => {
|
|
33
|
+
if (HttpBody.isHttpBody(body)) return Effect.succeed(body);
|
|
34
|
+
if (typeof body === "string") return Effect.succeed(HttpBody.text(body));
|
|
35
|
+
if (ArrayBuffer.isView(body) || isGlobalInstance(body, "ArrayBuffer")) return Effect.succeed(HttpBody.uint8Array(toBytes(body)));
|
|
36
|
+
if (isBlob(body)) return Effect.succeed(HttpBody.raw(body, {
|
|
37
|
+
contentType: body.type === "" ? void 0 : body.type,
|
|
38
|
+
contentLength: body.size
|
|
39
|
+
}));
|
|
40
|
+
if (isGlobalInstance(body, "FormData")) return Effect.succeed(HttpBody.formData(body));
|
|
41
|
+
if (isGlobalInstance(body, "URLSearchParams")) return Effect.succeed(HttpBody.urlParams(body));
|
|
42
|
+
return encodeJson(body);
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Turns the `body` option into an `HttpBody`, or `undefined` for no body.
|
|
46
|
+
*
|
|
47
|
+
* With an `input` schema the body is always encoded with it and sent as JSON.
|
|
48
|
+
* Without one it is sent by runtime type, checked in this order:
|
|
49
|
+
*
|
|
50
|
+
* - `HttpBody`: as-is.
|
|
51
|
+
* - `string`: `text/plain`.
|
|
52
|
+
* - `ArrayBuffer` and any view (`Uint8Array`, `DataView`, …):
|
|
53
|
+
* `application/octet-stream`, only the viewed bytes.
|
|
54
|
+
* - `Blob` / `File`: as-is, with the blob's own type and size.
|
|
55
|
+
* - `FormData`: as-is, so the platform sets the multipart boundary.
|
|
56
|
+
* - `URLSearchParams`: `application/x-www-form-urlencoded`.
|
|
57
|
+
* - anything else: JSON.
|
|
58
|
+
*
|
|
59
|
+
* Dies (it does not fail) when the body doesn't match `input` or can't be
|
|
60
|
+
* serialised as JSON. The defect is an `Error` whose message starts with
|
|
61
|
+
* `Invalid request body:` and whose `cause` is the underlying error.
|
|
62
|
+
*/
|
|
63
|
+
const encodeBody = (body, input) => {
|
|
64
|
+
if (body === void 0) return Effect.succeed(void 0);
|
|
65
|
+
if (input !== void 0) return orDieInvalid(HttpBody.jsonSchema(input)(body));
|
|
66
|
+
return encodePlain(body);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
export { encodeBody };
|