arthron 1.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_plugins = require("./plugins-Cigqz2SS.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,22 +24,31 @@ 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, fetch }) {
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 (fetch) out.fetch = fetch;
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 fetch = ctx.fetch ?? previous.fetch;
71
- if (fetch) out.fetch = fetch;
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 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.
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
- async function runPipeline(plugins, ctx, send) {
112
+ function createPipeline(plugins) {
93
113
  const middlewares = [];
94
- const request = [];
95
- const response = [];
96
- const error = [];
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.request) request.push(plugin.request);
100
- if (plugin.response) response.push(plugin.response);
101
- 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);
102
122
  }
103
- const attempt = async (input) => {
123
+ const attempt = async (input, send) => {
104
124
  let current = cloneContext(input);
105
- 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
+ }
106
130
  let res;
107
131
  try {
108
132
  res = await send(current);
109
133
  } catch (thrown) {
110
- if (!require_plugins.isArthronError(thrown)) throw thrown;
134
+ if (!require_create_error.isArthronError(thrown)) throw thrown;
111
135
  let err = thrown;
112
- 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);
113
137
  throw err;
114
138
  }
115
- 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)");
116
140
  return res;
117
141
  };
118
- const dispatch = async (index, input) => {
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, (next) => {
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
- ...next,
124
- meta: next.meta ?? input.meta
148
+ ...passed,
149
+ meta: passed.meta ?? input.meta
125
150
  };
126
- const fetch = next.fetch ?? input.fetch;
127
- if (fetch) ctx.fetch = fetch;
128
- return dispatch(index + 1, ctx);
129
- }), "a middleware", "the Response, usually `return next(ctx)`", void 0, "resolved with");
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 dispatch(0, ctx);
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 ArthronError: the
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 SchemaError = class extends Error {
141
- name = "SchemaError";
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,15 +174,16 @@ var SchemaError = class extends Error {
149
174
  this.issues = issues;
150
175
  }
151
176
  };
152
- /** Validate `data`; resolve with the schema's output or reject with a SchemaError. */
153
- async function validate(schema, data) {
154
- const result = await schema["~standard"].validate(data);
155
- 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);
156
181
  return result.value;
157
182
  }
158
183
  //#endregion
159
184
  //#region src/response/stream-parser.ts
160
185
  const DEFAULT_LIMIT = 8388608;
186
+ const DECODE_CHUNK_BYTES = 16384;
161
187
  /**
162
188
  * Yield the body line by line, reassembling lines split across chunks. Only
163
189
  * the newly arrived text is scanned, so a long line costs linear time; a line
@@ -174,28 +200,37 @@ async function* lines(response, maxLineLength) {
174
200
  const breaks = /\r\n|\r|\n/g;
175
201
  let pending = "";
176
202
  let afterCR = false;
203
+ const scan = function* (decoded) {
204
+ let text = decoded;
205
+ if (afterCR && text.startsWith("\n")) {
206
+ text = text.slice(1);
207
+ afterCR = false;
208
+ }
209
+ if (text) afterCR = text.endsWith("\r");
210
+ let start = 0;
211
+ breaks.lastIndex = 0;
212
+ for (let m = breaks.exec(text); m; m = breaks.exec(text)) {
213
+ yield checked(pending + text.slice(start, m.index));
214
+ pending = "";
215
+ start = breaks.lastIndex;
216
+ }
217
+ pending = checked(pending + text.slice(start));
218
+ };
177
219
  try {
178
220
  while (true) {
179
221
  const { value, done } = await reader.read();
180
- let text = decoder.decode(value, { stream: !done });
181
- if (afterCR && text.startsWith("\n")) {
182
- text = text.slice(1);
183
- afterCR = false;
222
+ if (value) for (let offset = 0; offset < value.byteLength; offset += DECODE_CHUNK_BYTES) {
223
+ const part = value.subarray(offset, offset + DECODE_CHUNK_BYTES);
224
+ yield* scan(decoder.decode(part, { stream: true }));
184
225
  }
185
- if (text) afterCR = text.endsWith("\r");
186
- let start = 0;
187
- breaks.lastIndex = 0;
188
- for (let m = breaks.exec(text); m; m = breaks.exec(text)) {
189
- yield checked(pending + text.slice(start, m.index));
190
- pending = "";
191
- start = breaks.lastIndex;
226
+ if (done) {
227
+ yield* scan(decoder.decode());
228
+ break;
192
229
  }
193
- pending = checked(pending + text.slice(start));
194
- if (done) break;
195
230
  }
196
231
  if (pending) yield pending;
197
232
  } finally {
198
- await reader.cancel().catch(() => {});
233
+ reader.cancel().catch(() => {});
199
234
  reader.releaseLock();
200
235
  }
201
236
  }
@@ -207,24 +242,24 @@ async function* lines(response, maxLineLength) {
207
242
  * Provider sentinels like
208
243
  * `[DONE]` are passed through untouched — they are not part of the spec.
209
244
  */
210
- async function* parseSse(response, { maxLineLength = DEFAULT_LIMIT, maxEventSize = DEFAULT_LIMIT } = {}) {
245
+ async function* parseSse(response, { maxLineLength = DEFAULT_LIMIT, maxDataLength = DEFAULT_LIMIT } = {}) {
211
246
  let data = [];
212
247
  let size = 0;
213
- let event;
248
+ let type;
214
249
  let id;
215
250
  let lastEventId = "";
216
- let retry;
251
+ let reconnectionTime;
217
252
  const flush = () => {
218
253
  const out = data.length === 0 ? void 0 : {
254
+ type: type || "message",
219
255
  data: data.join("\n"),
220
256
  lastEventId
221
257
  };
222
- if (out && event !== void 0) out.event = event;
223
258
  if (out && id !== void 0) out.id = id;
224
- if (out && retry !== void 0) out.retry = retry;
259
+ if (out && reconnectionTime !== void 0) out.reconnectionTime = reconnectionTime;
225
260
  data = [];
226
261
  size = 0;
227
- event = id = void 0;
262
+ type = id = void 0;
228
263
  return out;
229
264
  };
230
265
  for await (const line of lines(response, maxLineLength)) {
@@ -241,16 +276,16 @@ async function* parseSse(response, { maxLineLength = DEFAULT_LIMIT, maxEventSize
241
276
  switch (field) {
242
277
  case "data":
243
278
  size += value.length + 1;
244
- 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`);
279
+ 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
280
  data.push(value);
246
281
  break;
247
282
  case "event":
248
- event = value;
283
+ type = value;
249
284
  break;
250
285
  case "id":
251
286
  if (!value.includes("\0")) id = lastEventId = value;
252
287
  break;
253
- case "retry": if (/^\d+$/.test(value)) retry = Number(value);
288
+ case "retry": if (/^\d+$/.test(value)) reconnectionTime = Number(value);
254
289
  }
255
290
  }
256
291
  }
@@ -265,7 +300,7 @@ async function* parseNdjson(response, { maxLineLength = DEFAULT_LIMIT } = {}) {
265
300
  }
266
301
  }
267
302
  //#endregion
268
- //#region src/response/arthron-promise.ts
303
+ //#region src/response/response-promise.ts
269
304
  /** Defer the Response until iteration starts, so the helper itself never rejects. */
270
305
  function iterate(promise, parse, signal) {
271
306
  promise.catch(() => {});
@@ -273,7 +308,7 @@ function iterate(promise, parse, signal) {
273
308
  try {
274
309
  yield* parse(await promise);
275
310
  } catch (error) {
276
- throw require_plugins.cancellationError(error, signal);
311
+ throw require_create_error.cancellationError(error, signal);
277
312
  }
278
313
  } };
279
314
  }
@@ -281,14 +316,14 @@ function iterate(promise, parse, signal) {
281
316
  * Sugar over `Promise<Response>`: `.json()` is `(await p).json()`. Awaiting
282
317
  * without a helper still yields the untouched `Response`.
283
318
  */
284
- function toArthronPromise(promise, signal) {
285
- const read = (body) => promise.then(body).catch((error) => {
286
- throw require_plugins.cancellationError(error, signal);
319
+ function toResponsePromise(promise, signal) {
320
+ const read = (readAs) => promise.then(readAs).catch((error) => {
321
+ throw require_create_error.cancellationError(error, signal);
287
322
  });
288
323
  return Object.assign(promise, {
289
324
  json: ((schema) => read(async (res) => {
290
- const data = await res.json();
291
- return schema ? validate(schema, data) : data;
325
+ const body = await res.json();
326
+ return schema ? validate(schema, body) : body;
292
327
  })),
293
328
  text: () => read((res) => res.text()),
294
329
  blob: () => read((res) => res.blob()),
@@ -299,19 +334,75 @@ function toArthronPromise(promise, signal) {
299
334
  });
300
335
  }
301
336
  //#endregion
302
- //#region src/client/resolve-config.ts
337
+ //#region src/pipeline/create-plugin.ts
338
+ /** The slots a plugin may fill. */
339
+ const SLOTS = [
340
+ "middleware",
341
+ "requestInterceptor",
342
+ "responseInterceptor",
343
+ "errorInterceptor"
344
+ ];
345
+ /** 1.x slot names: a plugin that still uses one would have it ignored. */
346
+ const REPLACED_SLOTS = {
347
+ request: "requestInterceptor",
348
+ response: "responseInterceptor",
349
+ error: "errorInterceptor"
350
+ };
351
+ /**
352
+ * Throws a TypeError for what a plugin can get wrong where TypeScript does
353
+ * not look — JS, a cast, an object built at runtime: not an object, no
354
+ * pluginName, a key it does not take (a typo, a 1.x name), a slot that is not
355
+ * a function, or nothing to run. `createPlugin` runs it where the plugin is
356
+ * written; `use` and `plugins` run it again on whatever they are given.
357
+ */
358
+ function checkPlugin(plugin) {
359
+ 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 })`);
360
+ const fields = plugin;
361
+ const own = (key) => Object.hasOwn(fields, key) ? fields[key] : void 0;
362
+ const given = fields.pluginName ?? own("name");
363
+ const label = typeof given === "string" && given !== "" ? `plugin "${given}"` : "a plugin";
364
+ if (own("name") !== void 0) throw new TypeError(`arthron: ${label}: the name field is called pluginName since 2.0 — rename it in the plugin`);
365
+ 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`);
366
+ if (typeof fields.pluginName !== "string" || fields.pluginName === "") throw new TypeError("arthron: a plugin needs a pluginName — e.g. createPlugin({ pluginName: \"trace\", … })");
367
+ 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({ … })`);
368
+ for (const slot of SLOTS) if (fields[slot] !== void 0 && typeof fields[slot] !== "function") throw new TypeError(`arthron: ${label}: ${slot} must be a function`);
369
+ if (SLOTS.every((slot) => fields[slot] === void 0)) throw new TypeError(`arthron: ${label} has nothing to run — add middleware, requestInterceptor, responseInterceptor or errorInterceptor`);
370
+ }
371
+ /**
372
+ * A plugin, checked where it is written and typed without annotations:
373
+ * `createPlugin({ pluginName: "trace", requestInterceptor: (requestContext) => … })`.
374
+ */
375
+ function createPlugin(plugin) {
376
+ checkPlugin(plugin);
377
+ return plugin;
378
+ }
379
+ function aFunction(value, helper, example) {
380
+ if (typeof value !== "function") throw new TypeError(`arthron: ${helper} needs a function — e.g. ${example}`);
381
+ return value;
382
+ }
383
+ /** A middleware written on its own, typed without annotations. */
384
+ const createMiddleware = (middleware) => aFunction(middleware, "createMiddleware", "createMiddleware((requestContext, next) => next(requestContext))");
385
+ /** A request interceptor written on its own, typed without annotations. */
386
+ const createRequestInterceptor = (interceptor) => aFunction(interceptor, "createRequestInterceptor", "createRequestInterceptor((requestContext) => requestContext)");
387
+ /** A response interceptor written on its own, typed without annotations. */
388
+ const createResponseInterceptor = (interceptor) => aFunction(interceptor, "createResponseInterceptor", "createResponseInterceptor((response) => response)");
389
+ /** An error interceptor written on its own, typed without annotations. */
390
+ const createErrorInterceptor = (interceptor) => aFunction(interceptor, "createErrorInterceptor", "createErrorInterceptor((error) => error)");
391
+ //#endregion
392
+ //#region src/client/resolve-options.ts
303
393
  /**
304
- * Config keys that configure arthron itself; every other key is a fetch
305
- * default. Typed so that a new config key fails to compile until it is
306
- * listed here — otherwise it would be sent to fetch.
394
+ * Option keys that configure arthron itself; every other key is a fetch
395
+ * default. Typed so that a new option key fails to compile until it is
396
+ * listed here — otherwise it would be sent to fetch. `headers` is a fetch
397
+ * field too, but one with arthron's own merge rules.
307
398
  */
308
399
  const OWN_KEYS = {
309
400
  baseURL: true,
310
- allowAbsoluteUrls: true,
401
+ allowedOrigins: true,
311
402
  headers: true,
312
- fetch: true,
313
- use: true,
314
- errorDetail: true,
403
+ fetchFn: true,
404
+ plugins: true,
405
+ extractDescription: true,
315
406
  throwOn: true
316
407
  };
317
408
  /** Fields that belong to one request, refused as client defaults. */
@@ -321,15 +412,27 @@ const PER_CALL_ONLY = [
321
412
  "method"
322
413
  ];
323
414
  /**
324
- * Add plugins to a list. A named plugin takes the place of the one already
325
- * there with that name — same position, so a replaced `logging()` stays
326
- * outermost and a second `retry()` does not multiply attempts. Unnamed
327
- * plugins append.
415
+ * 1.x config keys and what replaced them. Left alone, each would become an
416
+ * unknown key, then a fetch default, and be ignored: plugins not applied, a
417
+ * test's fake fetch passed over for the real network.
418
+ */
419
+ const REPLACED = {
420
+ fetch: "fetchFn",
421
+ use: "plugins",
422
+ errorDetail: "extractDescription",
423
+ allowAbsoluteUrls: "allowedOrigins"
424
+ };
425
+ /**
426
+ * Add plugins to a list, each checked as `createPlugin` checks it. A plugin
427
+ * takes the place of the one already there with its pluginName — same position,
428
+ * so a replaced `logging()` stays outermost and a second `retry()` does not
429
+ * multiply attempts. Other names append.
328
430
  */
329
431
  function addPlugins(base, added) {
330
432
  const out = [...base];
331
433
  for (const plugin of added) {
332
- const at = plugin.name === void 0 ? -1 : out.findIndex((p) => p.name === plugin.name);
434
+ checkPlugin(plugin);
435
+ const at = out.findIndex((p) => p.pluginName === plugin.pluginName);
333
436
  if (at === -1) out.push(plugin);
334
437
  else out[at] = plugin;
335
438
  }
@@ -340,31 +443,57 @@ function fetchDefaults(config) {
340
443
  return Object.fromEntries(Object.entries(config).filter(([key, value]) => value !== void 0 && !(key in OWN_KEYS)));
341
444
  }
342
445
  /** Normalize once so `extend` can merge headers and add plugins cheaply. */
343
- function resolveConfig(config) {
446
+ function resolveClientOptions(config) {
344
447
  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 { headers, use, ...rest } = config;
346
- return {
448
+ 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} })`);
449
+ const { headers, plugins, allowedOrigins, ...rest } = config;
450
+ 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 })`);
451
+ if (rest.baseURL !== void 0) checkBaseURL(rest.baseURL);
452
+ 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"]`);
453
+ const record = toHeaderRecord(headers);
454
+ 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");
455
+ const resolved = {
347
456
  ...rest,
348
- headers: toHeaderRecord(headers),
349
- use: addPlugins([], use ?? [])
457
+ headers: record,
458
+ plugins: addPlugins([], plugins ?? [])
350
459
  };
460
+ if (allowedOrigins !== void 0) resolved.allowedOrigins = [...allowedOrigins];
461
+ return resolved;
462
+ }
463
+ /** "a string", "an object", "null" — for messages about a value of the wrong kind. */
464
+ function kindOf(value) {
465
+ if (value === null) return "null";
466
+ const kind = typeof value;
467
+ return /^[aeiou]/.test(kind) ? `an ${kind}` : `a ${kind}`;
468
+ }
469
+ /**
470
+ * An http(s) baseURL must name a host. `https://${HOST}/api` with HOST unset
471
+ * is "https:///api", which a URL parser reads as host "api" — and a relative
472
+ * path appended to "https://" chooses the host itself.
473
+ */
474
+ function checkBaseURL(baseURL) {
475
+ if (!/^https?:\/\//i.test(baseURL)) return;
476
+ if (!/^https?:\/\/(?:[/\\?#]|$)/i.test(baseURL) && require_create_error.httpOrigin(baseURL)) return;
477
+ 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
478
  }
352
479
  //#endregion
353
480
  //#region src/client/send.ts
354
481
  /**
355
482
  * The global fetch rejects a malformed URL or method with a TypeError that
356
- * looks just like a network failure (status 0, retried). A bodiless `Request`
357
- * runs the same checks first, so those become argument errors instead. Only
358
- * for the global fetch: an injected one owns its URL rules (SvelteKit's
359
- * server-side `fetch` accepts relative URLs, for one).
483
+ * looks just like a network failure (status 0, retried). Once fetch has
484
+ * failed, a bodiless `Request` runs the same checks, so those become argument
485
+ * errors instead — at no cost to a call that goes through. Only for the
486
+ * global fetch: an injected one owns its URL rules (SvelteKit's server-side
487
+ * `fetch` accepts relative URLs, for one).
360
488
  */
361
489
  function assertSendable(url, method) {
362
490
  if (typeof Request !== "function") return;
363
491
  try {
364
492
  new Request(url, { method });
365
493
  } catch (cause) {
366
- const reason = require_plugins.redactUrlIn(cause instanceof Error ? cause.message : String(cause), url);
367
- 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 });
494
+ const safeCause = require_create_error.redactErrorUrl(cause, url);
495
+ const reason = require_create_error.redactUrlIn(cause instanceof Error ? cause.message : String(cause), url);
496
+ 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: safeCause });
368
497
  }
369
498
  }
370
499
  /**
@@ -374,8 +503,8 @@ function assertSendable(url, method) {
374
503
  * assertSendable.
375
504
  */
376
505
  function assertStreamable(url, method, body) {
377
- if (!require_plugins.isReadableStream(body) || require_plugins.runtimeStreamsRequestBody()) return;
378
- 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`);
506
+ if (!require_create_error.isReadableStream(body) || require_create_error.runtimeStreamsRequestBody()) return;
507
+ 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
508
  }
380
509
  /**
381
510
  * Non-ok responses the caller asked for are not failures: an opaque one
@@ -387,23 +516,23 @@ function failedByDefault(res, init) {
387
516
  }
388
517
  /** The one place a failure becomes an ArthronError. Cancellation passes through. */
389
518
  async function send(resolved, ctx, throwOn) {
390
- const fetchImpl = ctx.fetch ?? resolved.fetch ?? globalThis.fetch;
391
- if (resolved.fetch === void 0) {
392
- const method = ctx.init.method ?? "GET";
393
- assertSendable(ctx.url, method);
394
- assertStreamable(ctx.url, method, ctx.init.body);
395
- }
519
+ const fetchFn = ctx.fetchFn ?? resolved.fetchFn ?? globalThis.fetch;
520
+ 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 }`);
521
+ const runtimeFetch = resolved.fetchFn === void 0;
522
+ const method = ctx.init.method ?? "GET";
523
+ if (runtimeFetch) assertStreamable(ctx.url, method, ctx.init.body);
396
524
  const { signal } = ctx.init;
397
525
  let res;
398
526
  try {
399
- res = await fetchImpl(ctx.url, ctx.init);
527
+ res = await fetchFn(ctx.url, ctx.init);
400
528
  } catch (thrown) {
401
- if (require_plugins.isCancellation(thrown, signal)) throw require_plugins.cancellationError(thrown, signal);
402
- throw require_plugins.networkError(thrown, ctx);
529
+ if (require_create_error.isCancellation(thrown, signal)) throw require_create_error.cancellationError(thrown, signal);
530
+ if (runtimeFetch) assertSendable(ctx.url, method);
531
+ throw require_create_error.networkError(thrown, ctx);
403
532
  }
404
533
  if (res.ok) return res;
405
534
  if (!(throwOn ? throwOn(res) : failedByDefault(res, ctx.init))) return res;
406
- const error = await require_plugins.responseError(res, ctx, resolved.errorDetail);
535
+ const error = await require_create_error.responseError(res, ctx, resolved.extractDescription);
407
536
  if (signal?.aborted) throw signal.reason;
408
537
  throw error;
409
538
  }
@@ -422,21 +551,32 @@ const STANDARD_METHODS = /* @__PURE__ */ new Set([
422
551
  "QUERY"
423
552
  ]);
424
553
  /**
425
- * Build a client from config. The result is immutable — `use` and `extend`
554
+ * 1.x per-call option names and what replaced them. Left in init they would
555
+ * reach fetch and be ignored: {id} sent unfilled, the query left off.
556
+ */
557
+ const REPLACED_PER_CALL = {
558
+ path: "pathParams",
559
+ params: "queryParams"
560
+ };
561
+ /**
562
+ * Build a client from its options. The result is immutable — `use` and `extend`
426
563
  * return new clients and never touch the one they were called on.
427
564
  */
428
- function arthron(config = {}) {
429
- const resolved = resolveConfig(config);
565
+ function arthron(clientOptions = {}) {
566
+ const resolved = resolveClientOptions(clientOptions);
430
567
  const initDefaults = fetchDefaults(resolved);
431
- const dispatch = (path, method, rawBody, { params, headers: callHeaders, meta, throwOn = resolved.throwOn, path: pathValues, ...init }) => {
568
+ const allowed = resolved.allowedOrigins && require_create_error.originSet(resolved.allowedOrigins);
569
+ const pipeline = createPipeline(resolved.plugins);
570
+ const call = (path, method, rawBody, { queryParams, headers: callHeaders, meta, throwOn = resolved.throwOn, pathParams, ...init }) => {
432
571
  try {
572
+ 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
573
  const upper = method.toUpperCase();
434
574
  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 params (query) or switch to POST`);
436
- if (pathValues) path = require_plugins.fillPath(path, pathValues);
437
- const url = require_plugins.resolveUrl(resolved.baseURL, path, params, resolved.allowAbsoluteUrls);
438
- const defaults = require_plugins.leavesBase(resolved.baseURL, path) ? void 0 : resolved.headers;
439
- const { body, headers } = require_plugins.serializeBody(rawBody, mergeHeaders(defaults, callHeaders));
575
+ 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`);
576
+ if (pathParams) path = require_create_error.fillPath(path, pathParams);
577
+ const url = require_create_error.resolveUrl(resolved.baseURL, path, queryParams, allowed);
578
+ const defaults = require_create_error.leavesBase(resolved.baseURL, path) ? void 0 : resolved.headers;
579
+ const { body, headers } = require_create_error.serializeBody(rawBody, mergeHeaders(defaults, callHeaders));
440
580
  const requestInit = {
441
581
  ...initDefaults,
442
582
  ...init,
@@ -444,76 +584,53 @@ function arthron(config = {}) {
444
584
  headers
445
585
  };
446
586
  if (body !== void 0) requestInit.body = body;
447
- if (require_plugins.isReadableStream(body)) Object.assign(requestInit, { duplex: "half" });
448
- return toArthronPromise(runPipeline(resolved.use, {
587
+ if (require_create_error.isReadableStream(body)) Object.assign(requestInit, { duplex: "half" });
588
+ return toResponsePromise(pipeline({
449
589
  url,
450
590
  init: requestInit,
451
591
  meta: { ...meta },
452
- fetch: resolved.fetch ?? globalThis.fetch
592
+ fetchFn: resolved.fetchFn ?? globalThis.fetch
453
593
  }, (ctx) => send(resolved, ctx, throwOn)), init.signal);
454
594
  } catch (thrown) {
455
- return toArthronPromise(Promise.reject(thrown));
595
+ return toResponsePromise(Promise.reject(thrown));
456
596
  }
457
597
  };
458
- const derive = (next) => {
598
+ const derive = (changes) => {
459
599
  return arthron({
460
600
  ...resolved,
461
- ...next,
462
- headers: mergeHeaders(resolved.headers, next.headers),
463
- use: addPlugins(resolved.use, next.use ?? [])
601
+ ...changes,
602
+ headers: mergeHeaders(resolved.headers, changes.headers),
603
+ plugins: addPlugins(resolved.plugins, changes.plugins ?? [])
464
604
  });
465
605
  };
466
606
  return {
467
- get: (path, init = {}) => dispatch(path, "GET", void 0, init),
468
- delete: (path, init = {}) => dispatch(path, "DELETE", void 0, init),
469
- head: (path, init = {}) => dispatch(path, "HEAD", void 0, init),
470
- post: (path, body, init = {}) => dispatch(path, "POST", body, init),
471
- put: (path, body, init = {}) => dispatch(path, "PUT", body, init),
472
- patch: (path, body, init = {}) => dispatch(path, "PATCH", body, init),
473
- query: (path, body, init = {}) => dispatch(path, "QUERY", body, init),
474
- request: (path, init) => {
475
- const { method, body, ...rest } = init;
476
- 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")));
477
- return dispatch(path, method, body, rest);
607
+ get: (path, requestOptions = {}) => call(path, "GET", void 0, requestOptions),
608
+ delete: (path, requestOptions = {}) => call(path, "DELETE", void 0, requestOptions),
609
+ head: (path, requestOptions = {}) => call(path, "HEAD", void 0, requestOptions),
610
+ post: (path, body, requestOptions = {}) => call(path, "POST", body, requestOptions),
611
+ put: (path, body, requestOptions = {}) => call(path, "PUT", body, requestOptions),
612
+ patch: (path, body, requestOptions = {}) => call(path, "PATCH", body, requestOptions),
613
+ query: (path, body, requestOptions = {}) => call(path, "QUERY", body, requestOptions),
614
+ request: (path, requestOptions) => {
615
+ const { method, body, ...rest } = requestOptions;
616
+ 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")));
617
+ return call(path, method, body, rest);
478
618
  },
479
- use: (...plugins) => derive({ use: plugins }),
480
- extend: (next) => derive(next),
481
- without: (...names) => arthron({
619
+ use: (...plugins) => derive({ plugins }),
620
+ extend: (changes) => derive(changes),
621
+ without: (...pluginNames) => arthron({
482
622
  ...resolved,
483
- use: resolved.use.filter((p) => p.name === void 0 || !names.includes(p.name))
623
+ plugins: resolved.plugins.filter((p) => !pluginNames.includes(p.pluginName))
484
624
  })
485
625
  };
486
626
  }
487
627
  //#endregion
488
- //#region src/pipeline/define-plugin.ts
489
- /** Identity helper: gives a middleware written in its own file the right parameter types. */
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;
628
+ exports.ArthronError = require_create_error.ArthronError;
629
+ exports.SchemaMismatchError = SchemaMismatchError;
510
630
  exports.arthron = arthron;
511
- exports.defineInterceptor = defineInterceptor;
512
- exports.defineMiddleware = defineMiddleware;
513
- exports.definePlugin = definePlugin;
514
- exports.isArthronError = require_plugins.isArthronError;
515
- exports.logging = logging;
516
- exports.retry = retry;
517
- exports.timeout = timeout;
518
-
519
- //# sourceMappingURL=index.cjs.map
631
+ exports.createErrorInterceptor = createErrorInterceptor;
632
+ exports.createMiddleware = createMiddleware;
633
+ exports.createPlugin = createPlugin;
634
+ exports.createRequestInterceptor = createRequestInterceptor;
635
+ exports.createResponseInterceptor = createResponseInterceptor;
636
+ exports.isArthronError = require_create_error.isArthronError;