arthron 1.2.0 → 2.0.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/README.md +309 -185
- package/dist/index.cjs +263 -156
- package/dist/index.d.cts +158 -115
- package/dist/index.d.mts +158 -115
- package/dist/index.mjs +244 -136
- package/dist/plugins.cjs +846 -6
- package/dist/plugins.d.cts +129 -2
- package/dist/plugins.d.mts +129 -2
- package/dist/plugins.mjs +842 -1
- package/dist/shared.cjs +635 -0
- package/dist/shared.d.cts +239 -0
- package/dist/shared.d.mts +239 -0
- package/dist/shared.mjs +498 -0
- package/llms.txt +45 -42
- package/package.json +5 -3
- package/dist/index-C3lylSNd.d.cts +0 -333
- package/dist/index-C3lylSNd.d.mts +0 -333
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/plugins-CHCMxtAE.mjs +0 -1166
- package/dist/plugins-CHCMxtAE.mjs.map +0 -1
- package/dist/plugins-Cigqz2SS.cjs +0 -1279
- package/dist/plugins-Cigqz2SS.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
2
|
+
const require_create_error = require("./shared.cjs");
|
|
3
3
|
//#region src/request/headers.ts
|
|
4
4
|
const isPlainRecord = (headers) => !Array.isArray(headers) && Object.prototype.toString.call(headers) !== "[object Headers]";
|
|
5
5
|
/**
|
|
@@ -24,22 +24,31 @@ function toHeaderRecord(headers) {
|
|
|
24
24
|
}
|
|
25
25
|
//#endregion
|
|
26
26
|
//#region src/pipeline/pipeline.ts
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* The mutable body kinds an interceptor can `append` to; anything else is
|
|
29
|
+
* shared as is. Known by type tag, as serializeBody knows them, and copied
|
|
30
|
+
* into their own class — a polyfill's (formdata-node) or another realm's
|
|
31
|
+
* holds values the global one may not take.
|
|
32
|
+
*/
|
|
28
33
|
function cloneBody(body) {
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
const tag = require_create_error.tagOf(body);
|
|
35
|
+
if (tag !== "FormData" && tag !== "URLSearchParams") return body;
|
|
36
|
+
const Own = body.constructor;
|
|
37
|
+
try {
|
|
38
|
+
if (tag === "URLSearchParams") return new Own(String(body));
|
|
39
|
+
const copy = new Own();
|
|
31
40
|
for (const [name, value] of body) copy.append(name, value);
|
|
32
41
|
return copy;
|
|
42
|
+
} catch {
|
|
43
|
+
return body;
|
|
33
44
|
}
|
|
34
|
-
if (body instanceof URLSearchParams) return new URLSearchParams(body);
|
|
35
|
-
return body;
|
|
36
45
|
}
|
|
37
46
|
/**
|
|
38
47
|
* Interceptors mutate the context in place. A middleware that calls `next`
|
|
39
48
|
* again (retry) must not see the previous attempt's changes, so every attempt
|
|
40
49
|
* starts from a fresh copy of the headers and of a FormData/URLSearchParams body.
|
|
41
50
|
*/
|
|
42
|
-
function cloneContext({ url, init, meta,
|
|
51
|
+
function cloneContext({ url, init, meta, fetchFn }) {
|
|
43
52
|
const copy = {
|
|
44
53
|
...init,
|
|
45
54
|
headers: toHeaderRecord(init.headers)
|
|
@@ -50,7 +59,7 @@ function cloneContext({ url, init, meta, fetch }) {
|
|
|
50
59
|
init: copy,
|
|
51
60
|
meta: { ...meta }
|
|
52
61
|
};
|
|
53
|
-
if (
|
|
62
|
+
if (fetchFn) out.fetchFn = fetchFn;
|
|
54
63
|
return out;
|
|
55
64
|
}
|
|
56
65
|
/**
|
|
@@ -67,11 +76,20 @@ function normalize(ctx, previous) {
|
|
|
67
76
|
},
|
|
68
77
|
meta: ctx.meta ?? previous.meta
|
|
69
78
|
};
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
79
|
+
const fetchFn = ctx.fetchFn ?? previous.fetchFn;
|
|
80
|
+
if (fetchFn) out.fetchFn = fetchFn;
|
|
72
81
|
return out;
|
|
73
82
|
}
|
|
74
83
|
/**
|
|
84
|
+
* The 1.x name of `fetchFn`. TypeScript does not flag it on a spread
|
|
85
|
+
* (`{ ...ctx, fetch }`), and ignored, a test's mock would be passed over for
|
|
86
|
+
* the real network.
|
|
87
|
+
*/
|
|
88
|
+
function refuseFetchField(ctx, problem, fix) {
|
|
89
|
+
if (ctx.fetch === void 0) return;
|
|
90
|
+
throw new TypeError(`arthron: ${problem}, which is requestContext.fetchFn since 2.0 — ${fix} { ...requestContext, fetchFn }`);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
75
93
|
* A plugin written in JS (or with a cast) that forgets to return would hand
|
|
76
94
|
* `undefined` on — and an error interceptor's would be what the caller
|
|
77
95
|
* catches. Name the plugin slot instead.
|
|
@@ -81,7 +99,9 @@ function returned(value, who, what, cause, verb = "returned") {
|
|
|
81
99
|
throw new TypeError(`arthron: ${who} ${verb} ${value} — return ${what}`, cause === void 0 ? void 0 : { cause });
|
|
82
100
|
}
|
|
83
101
|
/**
|
|
84
|
-
* Owns the order of
|
|
102
|
+
* Owns the order of every request a client sends. The plugins are sorted
|
|
103
|
+
* into their slots once, when the client is created — a client's plugins
|
|
104
|
+
* never change.
|
|
85
105
|
*
|
|
86
106
|
* Middleware wraps the whole call, outermost first in registration order.
|
|
87
107
|
* Each attempt below it runs request interceptors → `send` → response
|
|
@@ -89,56 +109,61 @@ function returned(value, who, what, cause, verb = "returned") {
|
|
|
89
109
|
* they return is thrown. Anything else that is thrown — cancellation, a bug
|
|
90
110
|
* in a plugin — propagates untouched.
|
|
91
111
|
*/
|
|
92
|
-
|
|
112
|
+
function createPipeline(plugins) {
|
|
93
113
|
const middlewares = [];
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const
|
|
114
|
+
const requestInterceptors = [];
|
|
115
|
+
const responseInterceptors = [];
|
|
116
|
+
const errorInterceptors = [];
|
|
97
117
|
for (const plugin of plugins) {
|
|
98
118
|
if (plugin.middleware) middlewares.push(plugin.middleware);
|
|
99
|
-
if (plugin.
|
|
100
|
-
if (plugin.
|
|
101
|
-
if (plugin.
|
|
119
|
+
if (plugin.requestInterceptor) requestInterceptors.push(plugin.requestInterceptor);
|
|
120
|
+
if (plugin.responseInterceptor) responseInterceptors.push(plugin.responseInterceptor);
|
|
121
|
+
if (plugin.errorInterceptor) errorInterceptors.push(plugin.errorInterceptor);
|
|
102
122
|
}
|
|
103
|
-
const attempt = async (input) => {
|
|
123
|
+
const attempt = async (input, send) => {
|
|
104
124
|
let current = cloneContext(input);
|
|
105
|
-
for (const interceptor of
|
|
125
|
+
for (const interceptor of requestInterceptors) {
|
|
126
|
+
const changed = returned(await interceptor(current), "a request interceptor", "the context (requestContext or a changed copy)");
|
|
127
|
+
refuseFetchField(changed, "a request interceptor set requestContext.fetch", "return");
|
|
128
|
+
current = normalize(changed, current);
|
|
129
|
+
}
|
|
106
130
|
let res;
|
|
107
131
|
try {
|
|
108
132
|
res = await send(current);
|
|
109
133
|
} catch (thrown) {
|
|
110
|
-
if (!
|
|
134
|
+
if (!require_create_error.isArthronError(thrown)) throw thrown;
|
|
111
135
|
let err = thrown;
|
|
112
|
-
for (const interceptor of
|
|
136
|
+
for (const interceptor of errorInterceptors) err = returned(await interceptor(err, current), "an error interceptor", "the error (error or a changed one)", thrown);
|
|
113
137
|
throw err;
|
|
114
138
|
}
|
|
115
|
-
for (const interceptor of
|
|
139
|
+
for (const interceptor of responseInterceptors) res = returned(await interceptor(res, current), "a response interceptor", "the Response (response or a new one)");
|
|
116
140
|
return res;
|
|
117
141
|
};
|
|
118
|
-
const
|
|
142
|
+
const runFrom = async (index, input, send) => {
|
|
119
143
|
const middleware = middlewares[index];
|
|
120
|
-
if (!middleware) return attempt(input);
|
|
121
|
-
return returned(await middleware(input, (
|
|
144
|
+
if (!middleware) return attempt(input, send);
|
|
145
|
+
return returned(await middleware(input, (passed) => {
|
|
146
|
+
refuseFetchField(passed, "a middleware passed requestContext.fetch to next", "pass");
|
|
122
147
|
const ctx = {
|
|
123
|
-
...
|
|
124
|
-
meta:
|
|
148
|
+
...passed,
|
|
149
|
+
meta: passed.meta ?? input.meta
|
|
125
150
|
};
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
return
|
|
129
|
-
}), "a middleware", "the Response, usually `return next(
|
|
151
|
+
const fetchFn = passed.fetchFn ?? input.fetchFn;
|
|
152
|
+
if (fetchFn) ctx.fetchFn = fetchFn;
|
|
153
|
+
return runFrom(index + 1, ctx, send);
|
|
154
|
+
}), "a middleware", "the Response, usually `return next(requestContext)`", void 0, "resolved with");
|
|
130
155
|
};
|
|
131
|
-
return
|
|
156
|
+
return (ctx, send) => runFrom(0, ctx, send);
|
|
132
157
|
}
|
|
133
158
|
//#endregion
|
|
134
159
|
//#region src/response/schema-validation.ts
|
|
135
160
|
const pathOf = (issue) => (issue.path ?? []).map((part) => String(typeof part === "object" && part !== null ? part.key : part)).join(".");
|
|
136
161
|
/**
|
|
137
|
-
* `.json(schema)` got a body that does not match. Not an
|
|
138
|
-
* request succeeded — the data is what is wrong.
|
|
162
|
+
* `.json(schema)` got a body that does not match the schema. Not an
|
|
163
|
+
* ArthronError: the request succeeded — the data is what is wrong.
|
|
139
164
|
*/
|
|
140
|
-
var
|
|
141
|
-
name = "
|
|
165
|
+
var SchemaMismatchError = class extends Error {
|
|
166
|
+
name = "SchemaMismatchError";
|
|
142
167
|
/** Every problem the validator reported, with the path to it. */
|
|
143
168
|
issues;
|
|
144
169
|
constructor(issues) {
|
|
@@ -149,10 +174,10 @@ var SchemaError = class extends Error {
|
|
|
149
174
|
this.issues = issues;
|
|
150
175
|
}
|
|
151
176
|
};
|
|
152
|
-
/** Validate `
|
|
153
|
-
async function validate(schema,
|
|
154
|
-
const result = await schema["~standard"].validate(
|
|
155
|
-
if (result.issues) throw new
|
|
177
|
+
/** Validate `body`; resolve with the schema's output or reject with a SchemaMismatchError. */
|
|
178
|
+
async function validate(schema, body) {
|
|
179
|
+
const result = await schema["~standard"].validate(body);
|
|
180
|
+
if (result.issues) throw new SchemaMismatchError(result.issues);
|
|
156
181
|
return result.value;
|
|
157
182
|
}
|
|
158
183
|
//#endregion
|
|
@@ -207,24 +232,24 @@ async function* lines(response, maxLineLength) {
|
|
|
207
232
|
* Provider sentinels like
|
|
208
233
|
* `[DONE]` are passed through untouched — they are not part of the spec.
|
|
209
234
|
*/
|
|
210
|
-
async function* parseSse(response, { maxLineLength = DEFAULT_LIMIT,
|
|
235
|
+
async function* parseSse(response, { maxLineLength = DEFAULT_LIMIT, maxDataLength = DEFAULT_LIMIT } = {}) {
|
|
211
236
|
let data = [];
|
|
212
237
|
let size = 0;
|
|
213
|
-
let
|
|
238
|
+
let type;
|
|
214
239
|
let id;
|
|
215
240
|
let lastEventId = "";
|
|
216
|
-
let
|
|
241
|
+
let reconnectionTime;
|
|
217
242
|
const flush = () => {
|
|
218
243
|
const out = data.length === 0 ? void 0 : {
|
|
244
|
+
type: type || "message",
|
|
219
245
|
data: data.join("\n"),
|
|
220
246
|
lastEventId
|
|
221
247
|
};
|
|
222
|
-
if (out && event !== void 0) out.event = event;
|
|
223
248
|
if (out && id !== void 0) out.id = id;
|
|
224
|
-
if (out &&
|
|
249
|
+
if (out && reconnectionTime !== void 0) out.reconnectionTime = reconnectionTime;
|
|
225
250
|
data = [];
|
|
226
251
|
size = 0;
|
|
227
|
-
|
|
252
|
+
type = id = void 0;
|
|
228
253
|
return out;
|
|
229
254
|
};
|
|
230
255
|
for await (const line of lines(response, maxLineLength)) {
|
|
@@ -241,16 +266,16 @@ async function* parseSse(response, { maxLineLength = DEFAULT_LIMIT, maxEventSize
|
|
|
241
266
|
switch (field) {
|
|
242
267
|
case "data":
|
|
243
268
|
size += value.length + 1;
|
|
244
|
-
if (size >
|
|
269
|
+
if (size > maxDataLength + 1) throw new RangeError(`arthron: an SSE event's data is longer than ${maxDataLength} characters — pass a larger maxDataLength to .sse() if that is expected`);
|
|
245
270
|
data.push(value);
|
|
246
271
|
break;
|
|
247
272
|
case "event":
|
|
248
|
-
|
|
273
|
+
type = value;
|
|
249
274
|
break;
|
|
250
275
|
case "id":
|
|
251
276
|
if (!value.includes("\0")) id = lastEventId = value;
|
|
252
277
|
break;
|
|
253
|
-
case "retry": if (/^\d+$/.test(value))
|
|
278
|
+
case "retry": if (/^\d+$/.test(value)) reconnectionTime = Number(value);
|
|
254
279
|
}
|
|
255
280
|
}
|
|
256
281
|
}
|
|
@@ -265,7 +290,7 @@ async function* parseNdjson(response, { maxLineLength = DEFAULT_LIMIT } = {}) {
|
|
|
265
290
|
}
|
|
266
291
|
}
|
|
267
292
|
//#endregion
|
|
268
|
-
//#region src/response/
|
|
293
|
+
//#region src/response/response-promise.ts
|
|
269
294
|
/** Defer the Response until iteration starts, so the helper itself never rejects. */
|
|
270
295
|
function iterate(promise, parse, signal) {
|
|
271
296
|
promise.catch(() => {});
|
|
@@ -273,7 +298,7 @@ function iterate(promise, parse, signal) {
|
|
|
273
298
|
try {
|
|
274
299
|
yield* parse(await promise);
|
|
275
300
|
} catch (error) {
|
|
276
|
-
throw
|
|
301
|
+
throw require_create_error.cancellationError(error, signal);
|
|
277
302
|
}
|
|
278
303
|
} };
|
|
279
304
|
}
|
|
@@ -281,14 +306,14 @@ function iterate(promise, parse, signal) {
|
|
|
281
306
|
* Sugar over `Promise<Response>`: `.json()` is `(await p).json()`. Awaiting
|
|
282
307
|
* without a helper still yields the untouched `Response`.
|
|
283
308
|
*/
|
|
284
|
-
function
|
|
285
|
-
const read = (
|
|
286
|
-
throw
|
|
309
|
+
function toResponsePromise(promise, signal) {
|
|
310
|
+
const read = (readAs) => promise.then(readAs).catch((error) => {
|
|
311
|
+
throw require_create_error.cancellationError(error, signal);
|
|
287
312
|
});
|
|
288
313
|
return Object.assign(promise, {
|
|
289
314
|
json: ((schema) => read(async (res) => {
|
|
290
|
-
const
|
|
291
|
-
return schema ? validate(schema,
|
|
315
|
+
const body = await res.json();
|
|
316
|
+
return schema ? validate(schema, body) : body;
|
|
292
317
|
})),
|
|
293
318
|
text: () => read((res) => res.text()),
|
|
294
319
|
blob: () => read((res) => res.blob()),
|
|
@@ -299,19 +324,75 @@ function toArthronPromise(promise, signal) {
|
|
|
299
324
|
});
|
|
300
325
|
}
|
|
301
326
|
//#endregion
|
|
302
|
-
//#region src/
|
|
327
|
+
//#region src/pipeline/create-plugin.ts
|
|
328
|
+
/** The slots a plugin may fill. */
|
|
329
|
+
const SLOTS = [
|
|
330
|
+
"middleware",
|
|
331
|
+
"requestInterceptor",
|
|
332
|
+
"responseInterceptor",
|
|
333
|
+
"errorInterceptor"
|
|
334
|
+
];
|
|
335
|
+
/** 1.x slot names: a plugin that still uses one would have it ignored. */
|
|
336
|
+
const REPLACED_SLOTS = {
|
|
337
|
+
request: "requestInterceptor",
|
|
338
|
+
response: "responseInterceptor",
|
|
339
|
+
error: "errorInterceptor"
|
|
340
|
+
};
|
|
341
|
+
/**
|
|
342
|
+
* Throws a TypeError for what a plugin can get wrong where TypeScript does
|
|
343
|
+
* not look — JS, a cast, an object built at runtime: not an object, no
|
|
344
|
+
* pluginName, a key it does not take (a typo, a 1.x name), a slot that is not
|
|
345
|
+
* a function, or nothing to run. `createPlugin` runs it where the plugin is
|
|
346
|
+
* written; `use` and `plugins` run it again on whatever they are given.
|
|
347
|
+
*/
|
|
348
|
+
function checkPlugin(plugin) {
|
|
349
|
+
if (typeof plugin !== "object" || plugin === null) throw new TypeError(`arthron: a plugin is an object, got ${plugin === null ? "null" : `a ${typeof plugin}`} — e.g. createPlugin({ pluginName: "trace", middleware: fn })`);
|
|
350
|
+
const fields = plugin;
|
|
351
|
+
const own = (key) => Object.hasOwn(fields, key) ? fields[key] : void 0;
|
|
352
|
+
const given = fields.pluginName ?? own("name");
|
|
353
|
+
const label = typeof given === "string" && given !== "" ? `plugin "${given}"` : "a plugin";
|
|
354
|
+
if (own("name") !== void 0) throw new TypeError(`arthron: ${label}: the name field is called pluginName since 2.0 — rename it in the plugin`);
|
|
355
|
+
for (const [old, now] of Object.entries(REPLACED_SLOTS)) if (own(old) !== void 0) throw new TypeError(`arthron: ${label}: the ${old} interceptor is called ${now} since 2.0 — rename it in the plugin`);
|
|
356
|
+
if (typeof fields.pluginName !== "string" || fields.pluginName === "") throw new TypeError("arthron: a plugin needs a pluginName — e.g. createPlugin({ pluginName: \"trace\", … })");
|
|
357
|
+
for (const key of Object.keys(fields)) if (key !== "pluginName" && !SLOTS.includes(key)) throw new TypeError(`arthron: ${label} has an unknown key "${key}" — use pluginName, middleware, requestInterceptor, responseInterceptor or errorInterceptor; keep state in a closure, in a function that returns createPlugin({ … })`);
|
|
358
|
+
for (const slot of SLOTS) if (fields[slot] !== void 0 && typeof fields[slot] !== "function") throw new TypeError(`arthron: ${label}: ${slot} must be a function`);
|
|
359
|
+
if (SLOTS.every((slot) => fields[slot] === void 0)) throw new TypeError(`arthron: ${label} has nothing to run — add middleware, requestInterceptor, responseInterceptor or errorInterceptor`);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* A plugin, checked where it is written and typed without annotations:
|
|
363
|
+
* `createPlugin({ pluginName: "trace", requestInterceptor: (requestContext) => … })`.
|
|
364
|
+
*/
|
|
365
|
+
function createPlugin(plugin) {
|
|
366
|
+
checkPlugin(plugin);
|
|
367
|
+
return plugin;
|
|
368
|
+
}
|
|
369
|
+
function aFunction(value, helper, example) {
|
|
370
|
+
if (typeof value !== "function") throw new TypeError(`arthron: ${helper} needs a function — e.g. ${example}`);
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
/** A middleware written on its own, typed without annotations. */
|
|
374
|
+
const createMiddleware = (middleware) => aFunction(middleware, "createMiddleware", "createMiddleware((requestContext, next) => next(requestContext))");
|
|
375
|
+
/** A request interceptor written on its own, typed without annotations. */
|
|
376
|
+
const createRequestInterceptor = (interceptor) => aFunction(interceptor, "createRequestInterceptor", "createRequestInterceptor((requestContext) => requestContext)");
|
|
377
|
+
/** A response interceptor written on its own, typed without annotations. */
|
|
378
|
+
const createResponseInterceptor = (interceptor) => aFunction(interceptor, "createResponseInterceptor", "createResponseInterceptor((response) => response)");
|
|
379
|
+
/** An error interceptor written on its own, typed without annotations. */
|
|
380
|
+
const createErrorInterceptor = (interceptor) => aFunction(interceptor, "createErrorInterceptor", "createErrorInterceptor((error) => error)");
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/client/resolve-options.ts
|
|
303
383
|
/**
|
|
304
|
-
*
|
|
305
|
-
* default. Typed so that a new
|
|
306
|
-
* listed here — otherwise it would be sent to fetch.
|
|
384
|
+
* Option keys that configure arthron itself; every other key is a fetch
|
|
385
|
+
* default. Typed so that a new option key fails to compile until it is
|
|
386
|
+
* listed here — otherwise it would be sent to fetch. `headers` is a fetch
|
|
387
|
+
* field too, but one with arthron's own merge rules.
|
|
307
388
|
*/
|
|
308
389
|
const OWN_KEYS = {
|
|
309
390
|
baseURL: true,
|
|
310
|
-
|
|
391
|
+
allowedOrigins: true,
|
|
311
392
|
headers: true,
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
393
|
+
fetchFn: true,
|
|
394
|
+
plugins: true,
|
|
395
|
+
extractDescription: true,
|
|
315
396
|
throwOn: true
|
|
316
397
|
};
|
|
317
398
|
/** Fields that belong to one request, refused as client defaults. */
|
|
@@ -321,15 +402,27 @@ const PER_CALL_ONLY = [
|
|
|
321
402
|
"method"
|
|
322
403
|
];
|
|
323
404
|
/**
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
|
|
405
|
+
* 1.x config keys and what replaced them. Left alone, each would become an
|
|
406
|
+
* unknown key, then a fetch default, and be ignored: plugins not applied, a
|
|
407
|
+
* test's fake fetch passed over for the real network.
|
|
408
|
+
*/
|
|
409
|
+
const REPLACED = {
|
|
410
|
+
fetch: "fetchFn",
|
|
411
|
+
use: "plugins",
|
|
412
|
+
errorDetail: "extractDescription",
|
|
413
|
+
allowAbsoluteUrls: "allowedOrigins"
|
|
414
|
+
};
|
|
415
|
+
/**
|
|
416
|
+
* Add plugins to a list, each checked as `createPlugin` checks it. A plugin
|
|
417
|
+
* takes the place of the one already there with its pluginName — same position,
|
|
418
|
+
* so a replaced `logging()` stays outermost and a second `retry()` does not
|
|
419
|
+
* multiply attempts. Other names append.
|
|
328
420
|
*/
|
|
329
421
|
function addPlugins(base, added) {
|
|
330
422
|
const out = [...base];
|
|
331
423
|
for (const plugin of added) {
|
|
332
|
-
|
|
424
|
+
checkPlugin(plugin);
|
|
425
|
+
const at = out.findIndex((p) => p.pluginName === plugin.pluginName);
|
|
333
426
|
if (at === -1) out.push(plugin);
|
|
334
427
|
else out[at] = plugin;
|
|
335
428
|
}
|
|
@@ -340,31 +433,57 @@ function fetchDefaults(config) {
|
|
|
340
433
|
return Object.fromEntries(Object.entries(config).filter(([key, value]) => value !== void 0 && !(key in OWN_KEYS)));
|
|
341
434
|
}
|
|
342
435
|
/** Normalize once so `extend` can merge headers and add plugins cheaply. */
|
|
343
|
-
function
|
|
436
|
+
function resolveClientOptions(config) {
|
|
344
437
|
for (const key of PER_CALL_ONLY) if (config[key] !== void 0) throw new TypeError(`arthron: ${key} cannot be a client default, it belongs to one request — pass it per call (api.get(path, { signal }), api.post(path, body), api.request(path, { method }))`);
|
|
345
|
-
const {
|
|
346
|
-
|
|
438
|
+
for (const [old, now] of Object.entries(REPLACED)) if (config[old] !== void 0) throw new TypeError(`arthron: the ${old} option was replaced by ${now} in 2.0 — pass arthron({ ${now} })`);
|
|
439
|
+
const { headers, plugins, allowedOrigins, ...rest } = config;
|
|
440
|
+
if (rest.fetchFn !== void 0 && typeof rest.fetchFn !== "function") throw new TypeError(`arthron: fetchFn must be a function with fetch's signature, got ${kindOf(rest.fetchFn)} — e.g. arthron({ fetchFn: fetch })`);
|
|
441
|
+
if (rest.baseURL !== void 0) checkBaseURL(rest.baseURL);
|
|
442
|
+
if (allowedOrigins !== void 0 && !Array.isArray(allowedOrigins)) throw new TypeError(`arthron: allowedOrigins takes a list of URLs, got ${kindOf(allowedOrigins)} — e.g. allowedOrigins: ["https://cdn.example.com"]`);
|
|
443
|
+
const record = toHeaderRecord(headers);
|
|
444
|
+
if (rest.baseURL === void 0 && allowedOrigins !== void 0 && allowedOrigins.length > 0 && Object.keys(record).length > 0) throw new TypeError("arthron: config headers go to baseURL's origin only, and this client has allowedOrigins but no baseURL — set baseURL, or pass these headers per call");
|
|
445
|
+
const resolved = {
|
|
347
446
|
...rest,
|
|
348
|
-
headers:
|
|
349
|
-
|
|
447
|
+
headers: record,
|
|
448
|
+
plugins: addPlugins([], plugins ?? [])
|
|
350
449
|
};
|
|
450
|
+
if (allowedOrigins !== void 0) resolved.allowedOrigins = [...allowedOrigins];
|
|
451
|
+
return resolved;
|
|
452
|
+
}
|
|
453
|
+
/** "a string", "an object", "null" — for messages about a value of the wrong kind. */
|
|
454
|
+
function kindOf(value) {
|
|
455
|
+
if (value === null) return "null";
|
|
456
|
+
const kind = typeof value;
|
|
457
|
+
return /^[aeiou]/.test(kind) ? `an ${kind}` : `a ${kind}`;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* An http(s) baseURL must name a host. `https://${HOST}/api` with HOST unset
|
|
461
|
+
* is "https:///api", which a URL parser reads as host "api" — and a relative
|
|
462
|
+
* path appended to "https://" chooses the host itself.
|
|
463
|
+
*/
|
|
464
|
+
function checkBaseURL(baseURL) {
|
|
465
|
+
if (!/^https?:\/\//i.test(baseURL)) return;
|
|
466
|
+
if (!/^https?:\/\/(?:[/\\?#]|$)/i.test(baseURL) && require_create_error.httpOrigin(baseURL)) return;
|
|
467
|
+
throw new TypeError(`arthron: baseURL "${require_create_error.withoutSecrets(baseURL)}" has no host — pass a full URL such as "https://api.example.com" (an unset environment variable?)`);
|
|
351
468
|
}
|
|
352
469
|
//#endregion
|
|
353
470
|
//#region src/client/send.ts
|
|
354
471
|
/**
|
|
355
472
|
* The global fetch rejects a malformed URL or method with a TypeError that
|
|
356
|
-
* looks just like a network failure (status 0, retried).
|
|
357
|
-
* runs the same checks
|
|
358
|
-
*
|
|
359
|
-
*
|
|
473
|
+
* looks just like a network failure (status 0, retried). Once fetch has
|
|
474
|
+
* failed, a bodiless `Request` runs the same checks, so those become argument
|
|
475
|
+
* errors instead — at no cost to a call that goes through. Only for the
|
|
476
|
+
* global fetch: an injected one owns its URL rules (SvelteKit's server-side
|
|
477
|
+
* `fetch` accepts relative URLs, for one).
|
|
360
478
|
*/
|
|
361
479
|
function assertSendable(url, method) {
|
|
362
480
|
if (typeof Request !== "function") return;
|
|
363
481
|
try {
|
|
364
482
|
new Request(url, { method });
|
|
365
483
|
} catch (cause) {
|
|
366
|
-
|
|
367
|
-
|
|
484
|
+
require_create_error.redactErrorUrl(cause, url);
|
|
485
|
+
const reason = require_create_error.redactUrlIn(cause instanceof Error ? cause.message : String(cause), url);
|
|
486
|
+
throw new TypeError(`arthron: cannot send ${method} ${require_create_error.redactUrl(url)} (${reason}) — use an absolute URL or a baseURL with a scheme (https://…), and a valid HTTP method`, { cause });
|
|
368
487
|
}
|
|
369
488
|
}
|
|
370
489
|
/**
|
|
@@ -374,8 +493,8 @@ function assertSendable(url, method) {
|
|
|
374
493
|
* assertSendable.
|
|
375
494
|
*/
|
|
376
495
|
function assertStreamable(url, method, body) {
|
|
377
|
-
if (!
|
|
378
|
-
throw new TypeError(`arthron: cannot send ${method} ${
|
|
496
|
+
if (!require_create_error.isReadableStream(body) || require_create_error.runtimeStreamsRequestBody()) return;
|
|
497
|
+
throw new TypeError(`arthron: cannot send ${method} ${require_create_error.redactUrl(url)} with a ReadableStream body — this browser cannot stream a request body and would send it garbled or not at all; pass a Blob or ArrayBuffer instead`);
|
|
379
498
|
}
|
|
380
499
|
/**
|
|
381
500
|
* Non-ok responses the caller asked for are not failures: an opaque one
|
|
@@ -387,23 +506,23 @@ function failedByDefault(res, init) {
|
|
|
387
506
|
}
|
|
388
507
|
/** The one place a failure becomes an ArthronError. Cancellation passes through. */
|
|
389
508
|
async function send(resolved, ctx, throwOn) {
|
|
390
|
-
const
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
509
|
+
const fetchFn = ctx.fetchFn ?? resolved.fetchFn ?? globalThis.fetch;
|
|
510
|
+
if (typeof fetchFn !== "function") throw new TypeError(`arthron: requestContext.fetchFn must be a function with fetch's signature, got ${fetchFn === null ? "null" : `a ${typeof fetchFn}`} — a plugin set it; return { ...requestContext, fetchFn: fn }`);
|
|
511
|
+
const runtimeFetch = resolved.fetchFn === void 0;
|
|
512
|
+
const method = ctx.init.method ?? "GET";
|
|
513
|
+
if (runtimeFetch) assertStreamable(ctx.url, method, ctx.init.body);
|
|
396
514
|
const { signal } = ctx.init;
|
|
397
515
|
let res;
|
|
398
516
|
try {
|
|
399
|
-
res = await
|
|
517
|
+
res = await fetchFn(ctx.url, ctx.init);
|
|
400
518
|
} catch (thrown) {
|
|
401
|
-
if (
|
|
402
|
-
|
|
519
|
+
if (require_create_error.isCancellation(thrown, signal)) throw require_create_error.cancellationError(thrown, signal);
|
|
520
|
+
if (runtimeFetch) assertSendable(ctx.url, method);
|
|
521
|
+
throw require_create_error.networkError(thrown, ctx);
|
|
403
522
|
}
|
|
404
523
|
if (res.ok) return res;
|
|
405
524
|
if (!(throwOn ? throwOn(res) : failedByDefault(res, ctx.init))) return res;
|
|
406
|
-
const error = await
|
|
525
|
+
const error = await require_create_error.responseError(res, ctx, resolved.extractDescription);
|
|
407
526
|
if (signal?.aborted) throw signal.reason;
|
|
408
527
|
throw error;
|
|
409
528
|
}
|
|
@@ -422,21 +541,32 @@ const STANDARD_METHODS = /* @__PURE__ */ new Set([
|
|
|
422
541
|
"QUERY"
|
|
423
542
|
]);
|
|
424
543
|
/**
|
|
425
|
-
*
|
|
544
|
+
* 1.x per-call option names and what replaced them. Left in init they would
|
|
545
|
+
* reach fetch and be ignored: {id} sent unfilled, the query left off.
|
|
546
|
+
*/
|
|
547
|
+
const REPLACED_PER_CALL = {
|
|
548
|
+
path: "pathParams",
|
|
549
|
+
params: "queryParams"
|
|
550
|
+
};
|
|
551
|
+
/**
|
|
552
|
+
* Build a client from its options. The result is immutable — `use` and `extend`
|
|
426
553
|
* return new clients and never touch the one they were called on.
|
|
427
554
|
*/
|
|
428
|
-
function arthron(
|
|
429
|
-
const resolved =
|
|
555
|
+
function arthron(clientOptions = {}) {
|
|
556
|
+
const resolved = resolveClientOptions(clientOptions);
|
|
430
557
|
const initDefaults = fetchDefaults(resolved);
|
|
431
|
-
const
|
|
558
|
+
const allowed = resolved.allowedOrigins && require_create_error.originSet(resolved.allowedOrigins);
|
|
559
|
+
const pipeline = createPipeline(resolved.plugins);
|
|
560
|
+
const call = (path, method, rawBody, { queryParams, headers: callHeaders, meta, throwOn = resolved.throwOn, pathParams, ...init }) => {
|
|
432
561
|
try {
|
|
562
|
+
for (const [old, now] of Object.entries(REPLACED_PER_CALL)) if (init[old] !== void 0) throw new TypeError(`arthron: the ${old} option was replaced by ${now} in 2.0 — pass { ${now}: { … } }`);
|
|
433
563
|
const upper = method.toUpperCase();
|
|
434
564
|
const verb = STANDARD_METHODS.has(upper) ? upper : method;
|
|
435
|
-
if (rawBody !== void 0 && BODYLESS_METHODS.has(upper)) throw new TypeError(`arthron: ${upper} requests cannot carry a body — send it as
|
|
436
|
-
if (
|
|
437
|
-
const url =
|
|
438
|
-
const defaults =
|
|
439
|
-
const { body, headers } =
|
|
565
|
+
if (rawBody !== void 0 && BODYLESS_METHODS.has(upper)) throw new TypeError(`arthron: ${upper} requests cannot carry a body — send it as queryParams or switch to POST`);
|
|
566
|
+
if (pathParams) path = require_create_error.fillPath(path, pathParams);
|
|
567
|
+
const url = require_create_error.resolveUrl(resolved.baseURL, path, queryParams, allowed);
|
|
568
|
+
const defaults = require_create_error.leavesBase(resolved.baseURL, path) ? void 0 : resolved.headers;
|
|
569
|
+
const { body, headers } = require_create_error.serializeBody(rawBody, mergeHeaders(defaults, callHeaders));
|
|
440
570
|
const requestInit = {
|
|
441
571
|
...initDefaults,
|
|
442
572
|
...init,
|
|
@@ -444,76 +574,53 @@ function arthron(config = {}) {
|
|
|
444
574
|
headers
|
|
445
575
|
};
|
|
446
576
|
if (body !== void 0) requestInit.body = body;
|
|
447
|
-
if (
|
|
448
|
-
return
|
|
577
|
+
if (require_create_error.isReadableStream(body)) Object.assign(requestInit, { duplex: "half" });
|
|
578
|
+
return toResponsePromise(pipeline({
|
|
449
579
|
url,
|
|
450
580
|
init: requestInit,
|
|
451
581
|
meta: { ...meta },
|
|
452
|
-
|
|
582
|
+
fetchFn: resolved.fetchFn ?? globalThis.fetch
|
|
453
583
|
}, (ctx) => send(resolved, ctx, throwOn)), init.signal);
|
|
454
584
|
} catch (thrown) {
|
|
455
|
-
return
|
|
585
|
+
return toResponsePromise(Promise.reject(thrown));
|
|
456
586
|
}
|
|
457
587
|
};
|
|
458
|
-
const derive = (
|
|
588
|
+
const derive = (changes) => {
|
|
459
589
|
return arthron({
|
|
460
590
|
...resolved,
|
|
461
|
-
...
|
|
462
|
-
headers: mergeHeaders(resolved.headers,
|
|
463
|
-
|
|
591
|
+
...changes,
|
|
592
|
+
headers: mergeHeaders(resolved.headers, changes.headers),
|
|
593
|
+
plugins: addPlugins(resolved.plugins, changes.plugins ?? [])
|
|
464
594
|
});
|
|
465
595
|
};
|
|
466
596
|
return {
|
|
467
|
-
get: (path,
|
|
468
|
-
delete: (path,
|
|
469
|
-
head: (path,
|
|
470
|
-
post: (path, body,
|
|
471
|
-
put: (path, body,
|
|
472
|
-
patch: (path, body,
|
|
473
|
-
query: (path, body,
|
|
474
|
-
request: (path,
|
|
475
|
-
const { method, body, ...rest } =
|
|
476
|
-
if (!method) return
|
|
477
|
-
return
|
|
597
|
+
get: (path, requestOptions = {}) => call(path, "GET", void 0, requestOptions),
|
|
598
|
+
delete: (path, requestOptions = {}) => call(path, "DELETE", void 0, requestOptions),
|
|
599
|
+
head: (path, requestOptions = {}) => call(path, "HEAD", void 0, requestOptions),
|
|
600
|
+
post: (path, body, requestOptions = {}) => call(path, "POST", body, requestOptions),
|
|
601
|
+
put: (path, body, requestOptions = {}) => call(path, "PUT", body, requestOptions),
|
|
602
|
+
patch: (path, body, requestOptions = {}) => call(path, "PATCH", body, requestOptions),
|
|
603
|
+
query: (path, body, requestOptions = {}) => call(path, "QUERY", body, requestOptions),
|
|
604
|
+
request: (path, requestOptions) => {
|
|
605
|
+
const { method, body, ...rest } = requestOptions;
|
|
606
|
+
if (!method) return toResponsePromise(Promise.reject(/* @__PURE__ */ new TypeError("arthron: request() needs a method — e.g. request(path, { method: \"OPTIONS\" }); use get/post/... for the common verbs")));
|
|
607
|
+
return call(path, method, body, rest);
|
|
478
608
|
},
|
|
479
|
-
use: (...plugins) => derive({
|
|
480
|
-
extend: (
|
|
481
|
-
without: (...
|
|
609
|
+
use: (...plugins) => derive({ plugins }),
|
|
610
|
+
extend: (changes) => derive(changes),
|
|
611
|
+
without: (...pluginNames) => arthron({
|
|
482
612
|
...resolved,
|
|
483
|
-
|
|
613
|
+
plugins: resolved.plugins.filter((p) => !pluginNames.includes(p.pluginName))
|
|
484
614
|
})
|
|
485
615
|
};
|
|
486
616
|
}
|
|
487
617
|
//#endregion
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const defineMiddleware = (middleware) => middleware;
|
|
491
|
-
/** Identity helpers for each interceptor kind — `defineInterceptor.request(fn)` etc. */
|
|
492
|
-
const defineInterceptor = {
|
|
493
|
-
request: (interceptor) => interceptor,
|
|
494
|
-
response: (interceptor) => interceptor,
|
|
495
|
-
error: (interceptor) => interceptor
|
|
496
|
-
};
|
|
497
|
-
/** Identity helper: type-checks a plugin object literal without an annotation. */
|
|
498
|
-
const definePlugin = (plugin) => plugin;
|
|
499
|
-
//#endregion
|
|
500
|
-
//#region src/index.ts
|
|
501
|
-
/** @deprecated Import from "arthron/plugins"; this export goes in the next major. */
|
|
502
|
-
const logging = require_plugins.logging;
|
|
503
|
-
/** @deprecated Import from "arthron/plugins"; this export goes in the next major. */
|
|
504
|
-
const retry = require_plugins.retry;
|
|
505
|
-
/** @deprecated Import from "arthron/plugins"; this export goes in the next major. */
|
|
506
|
-
const timeout = require_plugins.timeout;
|
|
507
|
-
//#endregion
|
|
508
|
-
exports.ArthronError = require_plugins.ArthronError;
|
|
509
|
-
exports.SchemaError = SchemaError;
|
|
618
|
+
exports.ArthronError = require_create_error.ArthronError;
|
|
619
|
+
exports.SchemaMismatchError = SchemaMismatchError;
|
|
510
620
|
exports.arthron = arthron;
|
|
511
|
-
exports.
|
|
512
|
-
exports.
|
|
513
|
-
exports.
|
|
514
|
-
exports.
|
|
515
|
-
exports.
|
|
516
|
-
exports.
|
|
517
|
-
exports.timeout = timeout;
|
|
518
|
-
|
|
519
|
-
//# sourceMappingURL=index.cjs.map
|
|
621
|
+
exports.createErrorInterceptor = createErrorInterceptor;
|
|
622
|
+
exports.createMiddleware = createMiddleware;
|
|
623
|
+
exports.createPlugin = createPlugin;
|
|
624
|
+
exports.createRequestInterceptor = createRequestInterceptor;
|
|
625
|
+
exports.createResponseInterceptor = createResponseInterceptor;
|
|
626
|
+
exports.isArthronError = require_create_error.isArthronError;
|