@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.
@@ -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
@@ -0,0 +1,7 @@
1
+ //#region src/Plugin.ts
2
+ function plugin(definition) {
3
+ return definition;
4
+ }
5
+
6
+ //#endregion
7
+ export { plugin };
@@ -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,5 @@
1
+ import { DecodeError, HttpError, NetworkError, isFetchError } from "./Errors.js";
2
+ import { make } from "./Client.js";
3
+ import { plugin } from "./Plugin.js";
4
+
5
+ export { DecodeError, HttpError, NetworkError, isFetchError, make, plugin };
@@ -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 };
@@ -0,0 +1,6 @@
1
+ //#region src/internal/object.ts
2
+ /** `true` if `key` is an own property of `object`, never an inherited one. */
3
+ const hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
4
+
5
+ //#endregion
6
+ export { hasOwn };