arthron 1.1.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/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_plugins = require("./plugins-CVrzb8x6.cjs");
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,32 +24,43 @@ function toHeaderRecord(headers) {
24
24
  }
25
25
  //#endregion
26
26
  //#region src/pipeline/pipeline.ts
27
- /** The mutable body kinds an interceptor can `append` to; anything else is shared as is. */
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
- if (typeof FormData !== "undefined" && body instanceof FormData) {
30
- const copy = new FormData();
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)
46
55
  };
47
56
  if (init.body) copy.body = cloneBody(init.body);
48
- return {
57
+ const out = {
49
58
  url,
50
59
  init: copy,
51
60
  meta: { ...meta }
52
61
  };
62
+ if (fetchFn) out.fetchFn = fetchFn;
63
+ return out;
53
64
  }
54
65
  /**
55
66
  * Put headers back into a plain lowercase record. A plugin written in JS (or
@@ -57,7 +68,7 @@ function cloneContext({ url, init, meta }) {
57
68
  * `{ ...ctx.init.headers }` would otherwise drop every header.
58
69
  */
59
70
  function normalize(ctx, previous) {
60
- return {
71
+ const out = {
61
72
  ...ctx,
62
73
  init: {
63
74
  ...ctx.init,
@@ -65,6 +76,18 @@ function normalize(ctx, previous) {
65
76
  },
66
77
  meta: ctx.meta ?? previous.meta
67
78
  };
79
+ const fetchFn = ctx.fetchFn ?? previous.fetchFn;
80
+ if (fetchFn) out.fetchFn = fetchFn;
81
+ return out;
82
+ }
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 }`);
68
91
  }
69
92
  /**
70
93
  * A plugin written in JS (or with a cast) that forgets to return would hand
@@ -76,7 +99,9 @@ function returned(value, who, what, cause, verb = "returned") {
76
99
  throw new TypeError(`arthron: ${who} ${verb} ${value} — return ${what}`, cause === void 0 ? void 0 : { cause });
77
100
  }
78
101
  /**
79
- * Owns the order of one request.
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.
80
105
  *
81
106
  * Middleware wraps the whole call, outermost first in registration order.
82
107
  * Each attempt below it runs request interceptors → `send` → response
@@ -84,51 +109,61 @@ function returned(value, who, what, cause, verb = "returned") {
84
109
  * they return is thrown. Anything else that is thrown — cancellation, a bug
85
110
  * in a plugin — propagates untouched.
86
111
  */
87
- async function runPipeline(plugins, ctx, send) {
112
+ function createPipeline(plugins) {
88
113
  const middlewares = [];
89
- const request = [];
90
- const response = [];
91
- const error = [];
114
+ const requestInterceptors = [];
115
+ const responseInterceptors = [];
116
+ const errorInterceptors = [];
92
117
  for (const plugin of plugins) {
93
118
  if (plugin.middleware) middlewares.push(plugin.middleware);
94
- if (plugin.request) request.push(plugin.request);
95
- if (plugin.response) response.push(plugin.response);
96
- if (plugin.error) error.push(plugin.error);
119
+ if (plugin.requestInterceptor) requestInterceptors.push(plugin.requestInterceptor);
120
+ if (plugin.responseInterceptor) responseInterceptors.push(plugin.responseInterceptor);
121
+ if (plugin.errorInterceptor) errorInterceptors.push(plugin.errorInterceptor);
97
122
  }
98
- const attempt = async (input) => {
123
+ const attempt = async (input, send) => {
99
124
  let current = cloneContext(input);
100
- for (const interceptor of request) current = normalize(returned(await interceptor(current), "a request interceptor", "the context (ctx or a changed copy)"), current);
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
+ }
101
130
  let res;
102
131
  try {
103
132
  res = await send(current);
104
133
  } catch (thrown) {
105
- if (!require_plugins.isArthronError(thrown)) throw thrown;
134
+ if (!require_create_error.isArthronError(thrown)) throw thrown;
106
135
  let err = thrown;
107
- for (const interceptor of error) err = returned(await interceptor(err, current), "an error interceptor", "the error (err or a changed one)", thrown);
136
+ for (const interceptor of errorInterceptors) err = returned(await interceptor(err, current), "an error interceptor", "the error (error or a changed one)", thrown);
108
137
  throw err;
109
138
  }
110
- for (const interceptor of response) res = returned(await interceptor(res, current), "a response interceptor", "the Response (res or a new one)");
139
+ for (const interceptor of responseInterceptors) res = returned(await interceptor(res, current), "a response interceptor", "the Response (response or a new one)");
111
140
  return res;
112
141
  };
113
- const dispatch = async (index, input) => {
142
+ const runFrom = async (index, input, send) => {
114
143
  const middleware = middlewares[index];
115
- if (!middleware) return attempt(input);
116
- return returned(await middleware(input, (next) => dispatch(index + 1, {
117
- ...next,
118
- meta: next.meta ?? input.meta
119
- })), "a middleware", "the Response, usually `return next(ctx)`", void 0, "resolved with");
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");
147
+ const ctx = {
148
+ ...passed,
149
+ meta: passed.meta ?? input.meta
150
+ };
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");
120
155
  };
121
- return dispatch(0, ctx);
156
+ return (ctx, send) => runFrom(0, ctx, send);
122
157
  }
123
158
  //#endregion
124
159
  //#region src/response/schema-validation.ts
125
160
  const pathOf = (issue) => (issue.path ?? []).map((part) => String(typeof part === "object" && part !== null ? part.key : part)).join(".");
126
161
  /**
127
- * `.json(schema)` got a body that does not match. Not an ArthronError: the
128
- * 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.
129
164
  */
130
- var SchemaError = class extends Error {
131
- name = "SchemaError";
165
+ var SchemaMismatchError = class extends Error {
166
+ name = "SchemaMismatchError";
132
167
  /** Every problem the validator reported, with the path to it. */
133
168
  issues;
134
169
  constructor(issues) {
@@ -139,10 +174,10 @@ var SchemaError = class extends Error {
139
174
  this.issues = issues;
140
175
  }
141
176
  };
142
- /** Validate `data`; resolve with the schema's output or reject with a SchemaError. */
143
- async function validate(schema, data) {
144
- const result = await schema["~standard"].validate(data);
145
- if (result.issues) throw new SchemaError(result.issues);
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);
146
181
  return result.value;
147
182
  }
148
183
  //#endregion
@@ -197,24 +232,24 @@ async function* lines(response, maxLineLength) {
197
232
  * Provider sentinels like
198
233
  * `[DONE]` are passed through untouched — they are not part of the spec.
199
234
  */
200
- async function* parseSse(response, { maxLineLength = DEFAULT_LIMIT, maxEventSize = DEFAULT_LIMIT } = {}) {
235
+ async function* parseSse(response, { maxLineLength = DEFAULT_LIMIT, maxDataLength = DEFAULT_LIMIT } = {}) {
201
236
  let data = [];
202
237
  let size = 0;
203
- let event;
238
+ let type;
204
239
  let id;
205
240
  let lastEventId = "";
206
- let retry;
241
+ let reconnectionTime;
207
242
  const flush = () => {
208
243
  const out = data.length === 0 ? void 0 : {
244
+ type: type || "message",
209
245
  data: data.join("\n"),
210
246
  lastEventId
211
247
  };
212
- if (out && event !== void 0) out.event = event;
213
248
  if (out && id !== void 0) out.id = id;
214
- if (out && retry !== void 0) out.retry = retry;
249
+ if (out && reconnectionTime !== void 0) out.reconnectionTime = reconnectionTime;
215
250
  data = [];
216
251
  size = 0;
217
- event = id = void 0;
252
+ type = id = void 0;
218
253
  return out;
219
254
  };
220
255
  for await (const line of lines(response, maxLineLength)) {
@@ -231,16 +266,16 @@ async function* parseSse(response, { maxLineLength = DEFAULT_LIMIT, maxEventSize
231
266
  switch (field) {
232
267
  case "data":
233
268
  size += value.length + 1;
234
- if (size > maxEventSize + 1) throw new RangeError(`arthron: an SSE event's data is longer than ${maxEventSize} characters — pass a larger maxEventSize to .sse() if that is expected`);
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`);
235
270
  data.push(value);
236
271
  break;
237
272
  case "event":
238
- event = value;
273
+ type = value;
239
274
  break;
240
275
  case "id":
241
276
  if (!value.includes("\0")) id = lastEventId = value;
242
277
  break;
243
- case "retry": if (/^\d+$/.test(value)) retry = Number(value);
278
+ case "retry": if (/^\d+$/.test(value)) reconnectionTime = Number(value);
244
279
  }
245
280
  }
246
281
  }
@@ -255,7 +290,7 @@ async function* parseNdjson(response, { maxLineLength = DEFAULT_LIMIT } = {}) {
255
290
  }
256
291
  }
257
292
  //#endregion
258
- //#region src/response/arthron-promise.ts
293
+ //#region src/response/response-promise.ts
259
294
  /** Defer the Response until iteration starts, so the helper itself never rejects. */
260
295
  function iterate(promise, parse, signal) {
261
296
  promise.catch(() => {});
@@ -263,7 +298,7 @@ function iterate(promise, parse, signal) {
263
298
  try {
264
299
  yield* parse(await promise);
265
300
  } catch (error) {
266
- throw require_plugins.cancellationError(error, signal);
301
+ throw require_create_error.cancellationError(error, signal);
267
302
  }
268
303
  } };
269
304
  }
@@ -271,14 +306,14 @@ function iterate(promise, parse, signal) {
271
306
  * Sugar over `Promise<Response>`: `.json()` is `(await p).json()`. Awaiting
272
307
  * without a helper still yields the untouched `Response`.
273
308
  */
274
- function toArthronPromise(promise, signal) {
275
- const read = (body) => promise.then(body).catch((error) => {
276
- throw require_plugins.cancellationError(error, signal);
309
+ function toResponsePromise(promise, signal) {
310
+ const read = (readAs) => promise.then(readAs).catch((error) => {
311
+ throw require_create_error.cancellationError(error, signal);
277
312
  });
278
313
  return Object.assign(promise, {
279
314
  json: ((schema) => read(async (res) => {
280
- const data = await res.json();
281
- return schema ? validate(schema, data) : data;
315
+ const body = await res.json();
316
+ return schema ? validate(schema, body) : body;
282
317
  })),
283
318
  text: () => read((res) => res.text()),
284
319
  blob: () => read((res) => res.blob()),
@@ -289,19 +324,75 @@ function toArthronPromise(promise, signal) {
289
324
  });
290
325
  }
291
326
  //#endregion
292
- //#region src/client/resolve-config.ts
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
+ };
293
341
  /**
294
- * Config keys that configure arthron itself; every other key is a fetch
295
- * default. Typed so that a new config key fails to compile until it is
296
- * listed here — otherwise it would be sent to fetch.
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
383
+ /**
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.
297
388
  */
298
389
  const OWN_KEYS = {
299
390
  baseURL: true,
300
- allowAbsoluteUrls: true,
391
+ allowedOrigins: true,
301
392
  headers: true,
302
- fetch: true,
303
- use: true,
304
- errorDetail: true,
393
+ fetchFn: true,
394
+ plugins: true,
395
+ extractDescription: true,
305
396
  throwOn: true
306
397
  };
307
398
  /** Fields that belong to one request, refused as client defaults. */
@@ -311,15 +402,27 @@ const PER_CALL_ONLY = [
311
402
  "method"
312
403
  ];
313
404
  /**
314
- * Add plugins to a list. A named plugin takes the place of the one already
315
- * there with that name — same position, so a replaced `logging()` stays
316
- * outermost and a second `retry()` does not multiply attempts. Unnamed
317
- * plugins append.
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.
318
420
  */
319
421
  function addPlugins(base, added) {
320
422
  const out = [...base];
321
423
  for (const plugin of added) {
322
- const at = plugin.name === void 0 ? -1 : out.findIndex((p) => p.name === plugin.name);
424
+ checkPlugin(plugin);
425
+ const at = out.findIndex((p) => p.pluginName === plugin.pluginName);
323
426
  if (at === -1) out.push(plugin);
324
427
  else out[at] = plugin;
325
428
  }
@@ -330,31 +433,57 @@ function fetchDefaults(config) {
330
433
  return Object.fromEntries(Object.entries(config).filter(([key, value]) => value !== void 0 && !(key in OWN_KEYS)));
331
434
  }
332
435
  /** Normalize once so `extend` can merge headers and add plugins cheaply. */
333
- function resolveConfig(config) {
436
+ function resolveClientOptions(config) {
334
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 }))`);
335
- const { headers, use, ...rest } = config;
336
- return {
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 = {
337
446
  ...rest,
338
- headers: toHeaderRecord(headers),
339
- use: addPlugins([], use ?? [])
447
+ headers: record,
448
+ plugins: addPlugins([], plugins ?? [])
340
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?)`);
341
468
  }
342
469
  //#endregion
343
470
  //#region src/client/send.ts
344
471
  /**
345
472
  * The global fetch rejects a malformed URL or method with a TypeError that
346
- * looks just like a network failure (status 0, retried). A bodiless `Request`
347
- * runs the same checks first, so those become argument errors instead. Only
348
- * for the global fetch: an injected one owns its URL rules (SvelteKit's
349
- * server-side `fetch` accepts relative URLs, for one).
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).
350
478
  */
351
479
  function assertSendable(url, method) {
352
480
  if (typeof Request !== "function") return;
353
481
  try {
354
482
  new Request(url, { method });
355
483
  } catch (cause) {
356
- const reason = require_plugins.redactUrlIn(cause instanceof Error ? cause.message : String(cause), url);
357
- throw new TypeError(`arthron: cannot send ${method} ${require_plugins.redactUrl(url)} (${reason}) — use an absolute URL or a baseURL with a scheme (https://…), and a valid HTTP method`, { cause });
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 });
358
487
  }
359
488
  }
360
489
  /**
@@ -364,8 +493,8 @@ function assertSendable(url, method) {
364
493
  * assertSendable.
365
494
  */
366
495
  function assertStreamable(url, method, body) {
367
- if (!require_plugins.isReadableStream(body) || require_plugins.runtimeStreamsRequestBody()) return;
368
- throw new TypeError(`arthron: cannot send ${method} ${require_plugins.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`);
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`);
369
498
  }
370
499
  /**
371
500
  * Non-ok responses the caller asked for are not failures: an opaque one
@@ -377,23 +506,23 @@ function failedByDefault(res, init) {
377
506
  }
378
507
  /** The one place a failure becomes an ArthronError. Cancellation passes through. */
379
508
  async function send(resolved, ctx, throwOn) {
380
- const fetchImpl = resolved.fetch ?? globalThis.fetch;
381
- if (resolved.fetch === void 0) {
382
- const method = ctx.init.method ?? "GET";
383
- assertSendable(ctx.url, method);
384
- assertStreamable(ctx.url, method, ctx.init.body);
385
- }
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);
386
514
  const { signal } = ctx.init;
387
515
  let res;
388
516
  try {
389
- res = await fetchImpl(ctx.url, ctx.init);
517
+ res = await fetchFn(ctx.url, ctx.init);
390
518
  } catch (thrown) {
391
- if (require_plugins.isCancellation(thrown, signal)) throw require_plugins.cancellationError(thrown, signal);
392
- throw require_plugins.networkError(thrown, ctx);
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);
393
522
  }
394
523
  if (res.ok) return res;
395
524
  if (!(throwOn ? throwOn(res) : failedByDefault(res, ctx.init))) return res;
396
- const error = await require_plugins.responseError(res, ctx, resolved.errorDetail);
525
+ const error = await require_create_error.responseError(res, ctx, resolved.extractDescription);
397
526
  if (signal?.aborted) throw signal.reason;
398
527
  throw error;
399
528
  }
@@ -412,21 +541,32 @@ const STANDARD_METHODS = /* @__PURE__ */ new Set([
412
541
  "QUERY"
413
542
  ]);
414
543
  /**
415
- * Build a client from config. The result is immutable — `use` and `extend`
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`
416
553
  * return new clients and never touch the one they were called on.
417
554
  */
418
- function arthron(config = {}) {
419
- const resolved = resolveConfig(config);
555
+ function arthron(clientOptions = {}) {
556
+ const resolved = resolveClientOptions(clientOptions);
420
557
  const initDefaults = fetchDefaults(resolved);
421
- const dispatch = (path, method, rawBody, { params, headers: callHeaders, meta, throwOn = resolved.throwOn, path: pathValues, ...init }) => {
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 }) => {
422
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}: { … } }`);
423
563
  const upper = method.toUpperCase();
424
564
  const verb = STANDARD_METHODS.has(upper) ? upper : method;
425
- if (rawBody !== void 0 && BODYLESS_METHODS.has(upper)) throw new TypeError(`arthron: ${upper} requests cannot carry a body — send it as params (query) or switch to POST`);
426
- if (pathValues) path = require_plugins.fillPath(path, pathValues);
427
- const url = require_plugins.resolveUrl(resolved.baseURL, path, params, resolved.allowAbsoluteUrls);
428
- const defaults = require_plugins.leavesBase(resolved.baseURL, path) ? void 0 : resolved.headers;
429
- const { body, headers } = require_plugins.serializeBody(rawBody, mergeHeaders(defaults, callHeaders));
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));
430
570
  const requestInit = {
431
571
  ...initDefaults,
432
572
  ...init,
@@ -434,75 +574,53 @@ function arthron(config = {}) {
434
574
  headers
435
575
  };
436
576
  if (body !== void 0) requestInit.body = body;
437
- if (require_plugins.isReadableStream(body)) Object.assign(requestInit, { duplex: "half" });
438
- return toArthronPromise(runPipeline(resolved.use, {
577
+ if (require_create_error.isReadableStream(body)) Object.assign(requestInit, { duplex: "half" });
578
+ return toResponsePromise(pipeline({
439
579
  url,
440
580
  init: requestInit,
441
- meta: { ...meta }
581
+ meta: { ...meta },
582
+ fetchFn: resolved.fetchFn ?? globalThis.fetch
442
583
  }, (ctx) => send(resolved, ctx, throwOn)), init.signal);
443
584
  } catch (thrown) {
444
- return toArthronPromise(Promise.reject(thrown));
585
+ return toResponsePromise(Promise.reject(thrown));
445
586
  }
446
587
  };
447
- const derive = (next) => {
588
+ const derive = (changes) => {
448
589
  return arthron({
449
590
  ...resolved,
450
- ...next,
451
- headers: mergeHeaders(resolved.headers, next.headers),
452
- use: addPlugins(resolved.use, next.use ?? [])
591
+ ...changes,
592
+ headers: mergeHeaders(resolved.headers, changes.headers),
593
+ plugins: addPlugins(resolved.plugins, changes.plugins ?? [])
453
594
  });
454
595
  };
455
596
  return {
456
- get: (path, init = {}) => dispatch(path, "GET", void 0, init),
457
- delete: (path, init = {}) => dispatch(path, "DELETE", void 0, init),
458
- head: (path, init = {}) => dispatch(path, "HEAD", void 0, init),
459
- post: (path, body, init = {}) => dispatch(path, "POST", body, init),
460
- put: (path, body, init = {}) => dispatch(path, "PUT", body, init),
461
- patch: (path, body, init = {}) => dispatch(path, "PATCH", body, init),
462
- query: (path, body, init = {}) => dispatch(path, "QUERY", body, init),
463
- request: (path, init) => {
464
- const { method, body, ...rest } = init;
465
- if (!method) return toArthronPromise(Promise.reject(/* @__PURE__ */ new TypeError("arthron: request() needs init.method — e.g. request(path, { method: \"OPTIONS\" }); use get/post/... for the common verbs")));
466
- return dispatch(path, method, body, rest);
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);
467
608
  },
468
- use: (...plugins) => derive({ use: plugins }),
469
- extend: (next) => derive(next),
470
- without: (...names) => arthron({
609
+ use: (...plugins) => derive({ plugins }),
610
+ extend: (changes) => derive(changes),
611
+ without: (...pluginNames) => arthron({
471
612
  ...resolved,
472
- use: resolved.use.filter((p) => p.name === void 0 || !names.includes(p.name))
613
+ plugins: resolved.plugins.filter((p) => !pluginNames.includes(p.pluginName))
473
614
  })
474
615
  };
475
616
  }
476
617
  //#endregion
477
- //#region src/pipeline/define-plugin.ts
478
- /** Identity helper: gives a middleware written in its own file the right parameter types. */
479
- const defineMiddleware = (middleware) => middleware;
480
- /** Identity helpers for each interceptor kind — `defineInterceptor.request(fn)` etc. */
481
- const defineInterceptor = {
482
- request: (interceptor) => interceptor,
483
- response: (interceptor) => interceptor,
484
- error: (interceptor) => interceptor
485
- };
486
- /** Identity helper: type-checks a plugin object literal without an annotation. */
487
- const definePlugin = (plugin) => plugin;
488
- //#endregion
489
- //#region src/index.ts
490
- /** @deprecated Import from "arthron/plugins"; this export goes in the next major. */
491
- const logging = require_plugins.logging;
492
- /** @deprecated Import from "arthron/plugins"; this export goes in the next major. */
493
- const retry = require_plugins.retry;
494
- /** @deprecated Import from "arthron/plugins"; this export goes in the next major. */
495
- const timeout = require_plugins.timeout;
496
- //#endregion
497
- exports.ArthronError = require_plugins.ArthronError;
498
- exports.SchemaError = SchemaError;
618
+ exports.ArthronError = require_create_error.ArthronError;
619
+ exports.SchemaMismatchError = SchemaMismatchError;
499
620
  exports.arthron = arthron;
500
- exports.defineInterceptor = defineInterceptor;
501
- exports.defineMiddleware = defineMiddleware;
502
- exports.definePlugin = definePlugin;
503
- exports.isArthronError = require_plugins.isArthronError;
504
- exports.logging = logging;
505
- exports.retry = retry;
506
- exports.timeout = timeout;
507
-
508
- //# 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;