@optionfactory/fml 9.0.0-rc3 → 9.0.0-rc31

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.
Files changed (44) hide show
  1. package/README.md +16 -88
  2. package/dist/client-errors.iife.js +40 -49
  3. package/dist/client-errors.iife.js.map +1 -1
  4. package/dist/client-errors.iife.min.js +1 -1
  5. package/dist/client-errors.iife.min.js.map +1 -1
  6. package/dist/custom-elements.json +491 -51
  7. package/dist/fml.css +25 -20
  8. package/dist/fml.css.map +1 -1
  9. package/dist/fml.iife.js +5866 -2563
  10. package/dist/fml.iife.js.map +1 -1
  11. package/dist/fml.iife.min.js +1 -1
  12. package/dist/fml.iife.min.js.map +1 -1
  13. package/dist/ftl.d.mts +425 -231
  14. package/dist/ftl.iife.js +774 -1044
  15. package/dist/ftl.iife.js.map +1 -1
  16. package/dist/ftl.iife.min.js +1 -1
  17. package/dist/ftl.iife.min.js.map +1 -1
  18. package/dist/ftl.min.mjs +1 -1
  19. package/dist/ftl.min.mjs.map +1 -1
  20. package/dist/ftl.mjs +774 -1044
  21. package/dist/ftl.mjs.map +1 -1
  22. package/dist/ful.css +25 -20
  23. package/dist/ful.css.map +1 -1
  24. package/dist/ful.d.mts +3048 -556
  25. package/dist/ful.iife.js +4977 -1395
  26. package/dist/ful.iife.js.map +1 -1
  27. package/dist/ful.iife.min.js +1 -1
  28. package/dist/ful.iife.min.js.map +1 -1
  29. package/dist/ful.min.mjs +1 -1
  30. package/dist/ful.min.mjs.map +1 -1
  31. package/dist/ful.mjs +4975 -1396
  32. package/dist/ful.mjs.map +1 -1
  33. package/dist/httpc.d.mts +143 -123
  34. package/dist/httpc.iife.js +163 -175
  35. package/dist/httpc.iife.js.map +1 -1
  36. package/dist/httpc.iife.min.js +1 -1
  37. package/dist/httpc.iife.min.js.map +1 -1
  38. package/dist/httpc.min.mjs +1 -1
  39. package/dist/httpc.min.mjs.map +1 -1
  40. package/dist/httpc.mjs +163 -175
  41. package/dist/httpc.mjs.map +1 -1
  42. package/dist/vscode.html-custom-data.json +239 -39
  43. package/dist/web-types.json +498 -51
  44. package/package.json +2 -2
@@ -2,6 +2,9 @@ var httpc = (function (exports) {
2
2
  'use strict';
3
3
 
4
4
  /**
5
+ * One problem of a failure: its `type` (a code such as `FIELD_ERROR` or
6
+ * `GENERIC_PROBLEM`), the `context` naming the field it belongs to or null, the
7
+ * human `reason`, and optional `details`.
5
8
  * @typedef {{ type: string; context: string?; reason: string; details: any?; }} Problem
6
9
  */
7
10
  /**
@@ -11,10 +14,9 @@ var httpc = (function (exports) {
11
14
  */
12
15
  class Failure extends Error {
13
16
  /**
14
- *
15
17
  * @param {string} message
16
18
  * @param {Problem[]} problems
17
- * @param {*} cause
19
+ * @param {*} [cause]
18
20
  */
19
21
  constructor(message, problems, cause) {
20
22
  super(message, { cause });
@@ -31,6 +33,7 @@ var httpc = (function (exports) {
31
33
  return new Failure(this.message, Failure.dropProblemsContext(this.problems, prefix), this);
32
34
  }
33
35
  /**
36
+ * Copies of the problems, each context losing the prefix where it starts with it.
34
37
  * @param {Problem[]} problems
35
38
  * @param {string} prefix
36
39
  * @returns {Problem[]}
@@ -41,21 +44,6 @@ var httpc = (function (exports) {
41
44
  return { type, context: nctx, reason, details };
42
45
  });
43
46
  }
44
- /**
45
- * The one reading of a failure: its problems' reasons, one per line, or
46
- * the fallback when the value carries none. An empty problems array
47
- * carries nothing: the failure's own message reads instead.
48
- *
49
- * @param {any} cause
50
- * @param {string|null} [fallback]
51
- * @returns {string}
52
- */
53
- static problemsText(cause, fallback = null) {
54
- if (cause?.problems?.length) {
55
- return cause.problems.map((p) => `${p.reason}`).join('\n');
56
- }
57
- return fallback ?? `${cause?.message ?? cause}`;
58
- }
59
47
  }
60
48
 
61
49
  /**
@@ -65,6 +53,10 @@ var httpc = (function (exports) {
65
53
  * of corrupting silently.
66
54
  */
67
55
  class Base64 {
56
+ /** The RFC 4648 alphabet, with `+` and `/`. */
57
+ static STANDARD = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
58
+ /** The RFC 4648 url and filename safe alphabet, with `-` and `_`. */
59
+ static URL_SAFE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
68
60
  /**
69
61
  * @param {ArrayBuffer} arrayBuffer
70
62
  * @param {string} [dialect] one of Base64.STANDARD or Base64.URL_SAFE, URL_SAFE by default
@@ -90,14 +82,14 @@ var httpc = (function (exports) {
90
82
  return res;
91
83
  }
92
84
  /**
93
- * @param {string} str
85
+ * @param {string} str padded or unpadded
94
86
  * @param {string} [dialect] one of Base64.STANDARD or Base64.URL_SAFE, URL_SAFE by default
95
87
  * @returns {ArrayBuffer}
88
+ * @throws on padding anywhere but the tail or longer than two, on an
89
+ * impossible length, and on a character outside the dialect
96
90
  */
97
91
  static decode(str, dialect) {
98
92
  const d = dialect || Base64.URL_SAFE;
99
- //padding belongs at the tail only, two at most, and nothing outside the
100
- //dialect decodes: reject instead of corrupting silently
101
93
  let end = str.length;
102
94
  while (end > 0 && str.charAt(end - 1) === '=') {
103
95
  --end;
@@ -119,9 +111,6 @@ var httpc = (function (exports) {
119
111
 
120
112
  let vi = 0;
121
113
  let si = 0;
122
- //every group yields three bytes, but a padded input stops short of the last
123
- //one or two: writing them anyway would rely on typed arrays dropping writes
124
- //past their length
125
114
  while (vi < nbytes) {
126
115
  const v1 = d.indexOf(unpadded.charAt(si++));
127
116
  const v2 = d.indexOf(unpadded.charAt(si++));
@@ -142,16 +131,14 @@ var httpc = (function (exports) {
142
131
  }
143
132
  }
144
133
 
145
- Base64.STANDARD = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
146
- Base64.URL_SAFE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
147
-
148
134
  /**
149
135
  * Hex encoding and decoding over byte sequences, lowercase by default.
150
136
  */
151
137
  class Hex {
152
138
  /**
153
- * @param {string} hex
139
+ * @param {string} hex either case
154
140
  * @returns {Uint8Array}
141
+ * @throws on an odd length or a character that is not a hex digit
155
142
  */
156
143
  static decode(hex) {
157
144
  if (hex.length % 2 !== 0) {
@@ -168,12 +155,20 @@ var httpc = (function (exports) {
168
155
  });
169
156
  }
170
157
  /**
171
- * @param {Iterable<number>} bytes
172
- * @param {boolean} [upper]
173
- * @returns {string}
158
+ * @param {Iterable<number>|ArrayBuffer|ArrayBufferView} bytes octets, or
159
+ * a buffer or view read as its bytes
160
+ * @param {boolean} [upper] uppercase digits instead of lowercase
161
+ * @returns {string} two digits per byte
174
162
  */
175
163
  static encode(bytes, upper) {
176
- return Array.from(bytes)
164
+ let octets = /** @type {Iterable<number>} */ (bytes);
165
+ if (bytes instanceof ArrayBuffer) {
166
+ octets = new Uint8Array(bytes);
167
+ }
168
+ if (ArrayBuffer.isView(bytes)) {
169
+ octets = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
170
+ }
171
+ return Array.from(octets)
177
172
  .map((b) => b.toString(16))
178
173
  .map((b) => (upper ? b.toUpperCase() : b))
179
174
  .map((o) => o.padStart(2, '0'))
@@ -189,19 +184,22 @@ var httpc = (function (exports) {
189
184
  this.#type = type;
190
185
  this.#subtype = subtype;
191
186
  }
187
+ /** @returns {string} `type/subtype`, lowercase */
192
188
  get normalized() {
193
189
  return `${this.#type}/${this.#subtype}`;
194
190
  }
191
+ /** @returns {string} */
195
192
  get type() {
196
193
  return this.#type;
197
194
  }
195
+ /** @returns {string} */
198
196
  get subtype() {
199
197
  return this.#subtype;
200
198
  }
201
199
  /**
202
200
  * Parses a Content-Type header value into its type/subtype pair, dropping any parameter.
203
201
  * @param {string|null|undefined} v
204
- * @returns
202
+ * @returns {MediaType} `unknown/unknown` for a missing or malformed value
205
203
  */
206
204
  static parse(v) {
207
205
  if (!v) {
@@ -251,28 +249,36 @@ var httpc = (function (exports) {
251
249
  return new HttpClientError(this.message, this.status, Failure.dropProblemsContext(this.problems, prefix), this);
252
250
  }
253
251
  /**
254
- * One problem of the client's own making: the four the client mints are the
255
- * same shape, and the server's arrive already shaped from the wire.
252
+ * A problem with no context and no details.
256
253
  * @param {string} type
257
254
  * @param {string} reason
255
+ * @returns {{ type: string; context: null; reason: string; details: null; }}
258
256
  */
259
257
  static problem(type, reason) {
260
258
  return { type, context: null, reason, details: null };
261
259
  }
262
260
  /**
263
- * Creates a client failure carrying no status, wrapping the cause and its message.
261
+ * A failure with status 0 and one problem of the given type, whose reason is
262
+ * the cause's message.
264
263
  * @param {string} type
265
264
  * @param {any} cause
266
- * @returns
265
+ * @returns {HttpClientError}
267
266
  */
268
267
  static of(type, cause) {
269
268
  const reason = String(cause?.message ?? cause ?? 'unknown failure');
270
269
  return new HttpClientError(reason, 0, [HttpClientError.problem(type, reason)], cause);
271
270
  }
272
271
  /**
273
- * Creates an HttpClientError from a Response.
274
- * @param {Response} response
275
- * @returns an HttpClientError
272
+ * The failure an error response stands for, keeping its status. An
273
+ * `application/failures+json` body is the problems list; an
274
+ * `application/problem+json` body contributes its non-empty `problems`
275
+ * array, or one GENERIC_PROBLEM made of whichever of its title and detail
276
+ * it has. Any other body is one GENERIC_PROBLEM carrying the body text. A
277
+ * json body that does not decode, or decodes to the wrong shape or to an
278
+ * empty failures array, and a body that cannot be read at all, each
279
+ * become one GENERIC_PROBLEM saying so.
280
+ * @param {Response} response consumed
281
+ * @returns {Promise<HttpClientError>}
276
282
  */
277
283
  static async fromResponse(response) {
278
284
  switch (MediaType.parse(response.headers.get('Content-Type')).normalized) {
@@ -282,7 +288,10 @@ var httpc = (function (exports) {
282
288
  return HttpClientError.#undecodable(response);
283
289
  }
284
290
  if (!Array.isArray(data)) {
285
- return HttpClientError.#undecodable(response, 'as a failures array');
291
+ return HttpClientError.#undecodable(response, 'does not decode as a failures array');
292
+ }
293
+ if (data.length === 0) {
294
+ return HttpClientError.#undecodable(response, 'carries no failures');
286
295
  }
287
296
  const message = `${response.status} ${response.statusText}: ${data.length} failures`;
288
297
  return new HttpClientError(message, response.status, data);
@@ -293,13 +302,18 @@ var httpc = (function (exports) {
293
302
  return HttpClientError.#undecodable(response);
294
303
  }
295
304
  if (typeof data !== 'object' || data === null || Array.isArray(data)) {
296
- return HttpClientError.#undecodable(response, 'as a problem object');
305
+ return HttpClientError.#undecodable(response, 'does not decode as a problem object');
297
306
  }
298
- const message = `${response.status} ${response.statusText}: ${data.title} ${data.detail}`;
307
+ const text = [data.title, data.detail].filter((part) => part !== undefined && part !== null).join(' ');
308
+ const message = text
309
+ ? `${response.status} ${response.statusText}: ${text}`
310
+ : `${response.status} ${response.statusText}`;
299
311
  return new HttpClientError(
300
312
  message,
301
313
  response.status,
302
- data.problems || [HttpClientError.problem('GENERIC_PROBLEM', message)],
314
+ Array.isArray(data.problems) && data.problems.length > 0
315
+ ? data.problems
316
+ : [HttpClientError.problem('GENERIC_PROBLEM', message)],
303
317
  );
304
318
  }
305
319
  default: {
@@ -307,24 +321,17 @@ var httpc = (function (exports) {
307
321
  }
308
322
  }
309
323
  }
310
- /** marks a body whose json() rejected, telling it apart from a body decoding to json null */
311
324
  static #unreadable = Symbol('unreadable body');
312
325
  /**
313
- * A json body that failed to decode, or that decoded to something other than
314
- * the declared contract, has consumed its stream: there is no text left to
315
- * embed, the status, the declared media type and the shape are the report.
316
326
  * @param {Response} response
317
- * @param {string} [as] - what the body does not decode as
327
+ * @param {string} [flaw] what is wrong with the body
318
328
  */
319
- static #undecodable(response, as = 'as json') {
329
+ static #undecodable(response, flaw = 'does not decode as json') {
320
330
  const mediaType = MediaType.parse(response.headers.get('Content-Type')).normalized;
321
- const message = `${response.status} ${response.statusText}: the ${mediaType} body does not decode ${as}`;
331
+ const message = `${response.status} ${response.statusText}: the ${mediaType} body ${flaw}`;
322
332
  return new HttpClientError(message, response.status, [HttpClientError.problem('GENERIC_PROBLEM', message)]);
323
333
  }
324
334
  static async #generic(response) {
325
- //a body that cannot be read (the connection cut mid-body) must not
326
- //masquerade as a connection problem: the response was served, its
327
- //status is the report
328
335
  const text = await response.text().catch(() => null);
329
336
  const message =
330
337
  text === null
@@ -337,19 +344,14 @@ var httpc = (function (exports) {
337
344
  const metaContent = (name) =>
338
345
  globalThis.document?.querySelector(`meta[name="${name}"]`)?.getAttribute('content') ?? undefined;
339
346
 
340
- /**
341
- * @implements {HttpInterceptor}
342
- */
343
347
  /**
344
348
  * Sends the csrf header named by the page's `_csrf_header` meta, with the token
345
349
  * from `_csrf`. Both are read per request, so metas replaced after the client
346
350
  * was built are honoured, and the header is sent to the page's own origin only.
351
+ * @implements {HttpInterceptor}
347
352
  */
348
353
  class CsrfTokenInterceptor {
349
354
  async intercept(url, request, chain) {
350
- //the token is the page's own: it travels to the page's origin only, and it
351
- //is read at request time, so metas landed after the client was built (a
352
- //login flow) are honored without a rebuild
353
355
  if (url.origin !== (globalThis.window?.location?.origin ?? url.origin)) {
354
356
  return await chain.proceed(url, request);
355
357
  }
@@ -361,13 +363,12 @@ var httpc = (function (exports) {
361
363
  return await chain.proceed(url, request);
362
364
  }
363
365
  }
364
- /**
365
- * @implements {HttpInterceptor}
366
- */
367
366
  /**
368
367
  * Navigates to a login url when a response comes back 401. The promise it
369
368
  * returns never settles, so callers keep waiting while the page unloads
370
- * instead of showing a failure nobody will be present to read.
369
+ * instead of showing a failure; where the navigation is blocked, as by a
370
+ * beforeunload prompt, they stay pending until the page leaves.
371
+ * @implements {HttpInterceptor}
371
372
  */
372
373
  class RedirectOnUnauthorizedInterceptor {
373
374
  #redirectUri;
@@ -381,10 +382,6 @@ var httpc = (function (exports) {
381
382
  const response = await chain.proceed(url, request);
382
383
  if (response.status === 401) {
383
384
  window.location.href = this.#redirectUri;
384
- //the page is navigating away: a promise that never settles keeps the
385
- //callers' spinners up instead of flashing a failure nobody will read.
386
- //Where the navigation is blocked (a beforeunload gate), they stay
387
- //pending until the page actually leaves
388
385
  return new Promise(() => {});
389
386
  }
390
387
  return response;
@@ -400,38 +397,48 @@ var httpc = (function (exports) {
400
397
  constructor() {
401
398
  this.#interceptors = [];
402
399
  }
400
+ /**
401
+ * Adds the CsrfTokenInterceptor.
402
+ * @returns {HttpClientBuilder} this builder
403
+ */
403
404
  withCsrfToken() {
404
405
  this.#interceptors.push(new CsrfTokenInterceptor());
405
406
  return this;
406
407
  }
408
+ /**
409
+ * Adds the RedirectOnUnauthorizedInterceptor.
410
+ * @param {string} redirectUri where a 401 navigates to
411
+ * @returns {HttpClientBuilder} this builder
412
+ */
407
413
  withRedirectOnUnauthorized(redirectUri) {
408
414
  this.#interceptors.push(new RedirectOnUnauthorizedInterceptor(redirectUri));
409
415
  return this;
410
416
  }
411
417
  /**
418
+ * Adds interceptors, run in the order given after the ones added before.
412
419
  * @param {...HttpInterceptor} interceptors
420
+ * @returns {HttpClientBuilder} this builder
413
421
  */
414
422
  withInterceptors(...interceptors) {
415
423
  this.#interceptors.push(...interceptors);
416
424
  return this;
417
425
  }
426
+ /** @returns {HttpClient} a client running the interceptors added so far */
418
427
  build() {
419
- return new HttpClient(this.#interceptors);
428
+ return new HttpClient([...this.#interceptors]);
420
429
  }
421
430
  }
422
431
 
423
432
  /**
433
+ * The last interceptor in every chain, performing the request with fetch. A
434
+ * fetch rejection becomes an HttpClientError with a CONNECTION_PROBLEM.
424
435
  * @implements {HttpInterceptor}
425
436
  */
426
- /** The last interceptor in every chain: the one that performs the request. */
427
437
  class HttpCall {
428
438
  async intercept(url, request, chain) {
429
439
  try {
430
440
  return await fetch(url, request);
431
441
  } catch (ex) {
432
- //the one place a connection problem is a connection problem: the
433
- //transport itself refused to deliver. Everything above this is code,
434
- //and code that throws has a different story to tell
435
442
  throw HttpClientError.of('CONNECTION_PROBLEM', ex);
436
443
  }
437
444
  }
@@ -442,19 +449,18 @@ var httpc = (function (exports) {
442
449
  #interceptors;
443
450
  #current;
444
451
  /**
445
- *
446
452
  * @param {HttpInterceptor[]} interceptors
447
- * @param {number} current
453
+ * @param {number} current the index of the interceptor `proceed` runs
448
454
  */
449
455
  constructor(interceptors, current) {
450
456
  this.#interceptors = interceptors;
451
457
  this.#current = current;
452
458
  }
453
459
  /**
454
- *
460
+ * Runs the next interceptor.
455
461
  * @param {URL} url
456
462
  * @param {RequestInit} request
457
- * @returns {Promise<Response>} the response
463
+ * @returns {Promise<Response>}
458
464
  */
459
465
  async proceed(url, request) {
460
466
  const interceptor = this.#interceptors[this.#current];
@@ -488,12 +494,14 @@ var httpc = (function (exports) {
488
494
  this.#interceptors = interceptors || [];
489
495
  }
490
496
  /**
491
- * Performs an HTTP exchange.
492
- * @async
493
- * @param {string} uri - the (possibly relative) request url
494
- * @param {RequestInit|undefined} options - fetch options
495
- * @param {HttpInterceptor[]|undefined} interceptors - the HttpInterceptors to be registered for this exchange.
496
- * @returns {Promise<Response>} the response
497
+ * Performs an HTTP exchange through the client's interceptors, then the
498
+ * given ones.
499
+ * @param {string} uri the request url, relative to the page's
500
+ * @param {RequestInit|undefined} options fetch options
501
+ * @param {HttpInterceptor[]|undefined} interceptors run for this exchange only
502
+ * @returns {Promise<Response>} the response, whatever its status; rejecting
503
+ * with an HttpClientError CONNECTION_PROBLEM when the transport fails, or
504
+ * with what an interceptor throws
497
505
  */
498
506
  async exchange(uri, options, interceptors) {
499
507
  const is = [...this.#interceptors, ...(interceptors || []), new HttpCall()];
@@ -503,58 +511,58 @@ var httpc = (function (exports) {
503
511
  return await chain.proceed(url, request);
504
512
  }
505
513
  /**
506
- * Creates a request builder.
507
- * @param {string} method - the HTTP method to be used
508
- * @param {string} uri - the (possibly relative) request url
509
- * @returns {HttpRequestBuilder} the request builder
514
+ * Starts a request with any method.
515
+ * @param {string} method
516
+ * @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
517
+ * @returns {HttpRequestBuilder}
510
518
  */
511
519
  request(method, uri) {
512
520
  return HttpRequestBuilder.create(this, method, uri);
513
521
  }
514
522
  /**
515
- * Creates a request builder.
516
- * @param {string} uri - the (possibly relative) request url
517
- * @returns {HttpRequestBuilder} the request builder
523
+ * Starts a GET request.
524
+ * @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
525
+ * @returns {HttpRequestBuilder}
518
526
  */
519
527
  get(uri) {
520
528
  return HttpRequestBuilder.create(this, 'GET', uri);
521
529
  }
522
530
  /**
523
- * Creates a request builder.
524
- * @param {string} uri - the (possibly relative) request url
525
- * @returns {HttpRequestBuilder} the request builder
531
+ * Starts a HEAD request.
532
+ * @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
533
+ * @returns {HttpRequestBuilder}
526
534
  */
527
535
  head(uri) {
528
536
  return HttpRequestBuilder.create(this, 'HEAD', uri);
529
537
  }
530
538
  /**
531
- * Creates a request builder.
532
- * @param {string} uri - the (possibly relative) request url
533
- * @returns {HttpRequestBuilder} the request builder
539
+ * Starts a POST request.
540
+ * @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
541
+ * @returns {HttpRequestBuilder}
534
542
  */
535
543
  post(uri) {
536
544
  return HttpRequestBuilder.create(this, 'POST', uri);
537
545
  }
538
546
  /**
539
- * Creates a request builder.
540
- * @param {string} uri - the (possibly relative) request url
541
- * @returns {HttpRequestBuilder} the request builder
547
+ * Starts a PUT request.
548
+ * @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
549
+ * @returns {HttpRequestBuilder}
542
550
  */
543
551
  put(uri) {
544
552
  return HttpRequestBuilder.create(this, 'PUT', uri);
545
553
  }
546
554
  /**
547
- * Creates a request builder.
548
- * @param {string} uri - the (possibly relative) request url
549
- * @returns {HttpRequestBuilder} the request builder
555
+ * Starts a PATCH request.
556
+ * @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
557
+ * @returns {HttpRequestBuilder}
550
558
  */
551
559
  patch(uri) {
552
560
  return HttpRequestBuilder.create(this, 'PATCH', uri);
553
561
  }
554
562
  /**
555
- * Creates a request builder.
556
- * @param {string} uri - the (possibly relative) request url
557
- * @returns {HttpRequestBuilder} the request builder
563
+ * Starts a DELETE request.
564
+ * @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
565
+ * @returns {HttpRequestBuilder}
558
566
  */
559
567
  delete(uri) {
560
568
  return HttpRequestBuilder.create(this, 'DELETE', uri);
@@ -565,7 +573,7 @@ var httpc = (function (exports) {
565
573
  * Reads the response body as the given type, wrapping a failed read as an UNMARSHALING_PROBLEM.
566
574
  * @param {Response} response
567
575
  * @param {'text'|'json'|'blob'|'arrayBuffer'} type
568
- * @returns
576
+ * @returns {Promise<any>}
569
577
  */
570
578
  const unmarshal = async (response, type) => {
571
579
  try {
@@ -576,9 +584,7 @@ var httpc = (function (exports) {
576
584
  };
577
585
 
578
586
  /**
579
- * Yields the entries of a headers or params initializer preserving nullish values:
580
- * normalizing through `Headers`/`URLSearchParams` first would stringify them to
581
- * "null"/"undefined" instead of removing the key.
587
+ * The entries of a headers or params initializer with nullish values kept as they are.
582
588
  * @param {any} source
583
589
  * @returns {Iterable<[string, any]>}
584
590
  */
@@ -612,15 +618,14 @@ var httpc = (function (exports) {
612
618
  #interceptors;
613
619
  #fragment;
614
620
  /**
615
- * Creates an HttpRequestBuilder.
621
+ * A builder for the url: its query becomes the initial params, split at the
622
+ * first `?`, and its fragment, from the first `#`, is kept for the final url.
616
623
  * @param {HttpClient} client
617
- * @param {string} method - the HTTP method to be used
618
- * @param {string} uri - the (possibly relative) request url
619
- * @returns {HttpRequestBuilder} the builder
624
+ * @param {string} method
625
+ * @param {string} uri
626
+ * @returns {HttpRequestBuilder}
620
627
  */
621
628
  static create(client, method, uri) {
622
- //'/a#frag?p=1' parses as hash '#frag?p=1' with an empty query, and a '?'
623
- //may appear in a query itself: only the first of each splits
624
629
  const hashIndex = uri.indexOf('#');
625
630
  const fragment = hashIndex === -1 ? '' : uri.slice(hashIndex);
626
631
  const withoutFragment = hashIndex === -1 ? uri : uri.slice(0, hashIndex);
@@ -638,10 +643,9 @@ var httpc = (function (exports) {
638
643
  );
639
644
  }
640
645
  /**
641
- * Creates an HttpRequestBuilder.
642
646
  * @param {HttpClient} client
643
- * @param {string} method - the HTTP method to be used
644
- * @param {string} uri - the (possibly relative) request url
647
+ * @param {string} method
648
+ * @param {string} uri the url without query and fragment
645
649
  * @param {URLSearchParams} params
646
650
  * @param {Headers} headers
647
651
  * @param {any} body
@@ -661,24 +665,20 @@ var httpc = (function (exports) {
661
665
  this.#fragment = fragment;
662
666
  }
663
667
  /**
664
- * Add all passed headers to the request, overriding existing ones if that key already exists. Null and undefined values cause the key to be removed.
668
+ * Sets each header, replacing a header of the same name; a null or undefined value removes it.
665
669
  * @param {HeadersInit|Record<string,string|null|undefined>} hs
666
670
  * @returns {HttpRequestBuilder} this builder
667
671
  */
668
672
  headers(hs) {
669
673
  for (const [k, v] of rawEntries(hs)) {
670
- if (v == null) {
671
- this.#headers.delete(k);
672
- } else {
673
- this.#headers.set(k, v);
674
- }
674
+ this.header(k, v);
675
675
  }
676
676
  return this;
677
677
  }
678
678
  /**
679
- * Adds an header to the request, overriding it if it already exists. Null and undefined values cause the key to be removed
679
+ * Sets a header, replacing one of the same name; a null or undefined value removes it.
680
680
  * @param {string} k
681
- * @param {string} v
681
+ * @param {string|null|undefined} v
682
682
  * @returns {HttpRequestBuilder} this builder
683
683
  */
684
684
  header(k, v) {
@@ -690,7 +690,7 @@ var httpc = (function (exports) {
690
690
  return this;
691
691
  }
692
692
  /**
693
- * Add all query parameters to the request, overriding existing ones if that key already exists. Null and undefined values cause the key to be removed
693
+ * Sets each query parameter in place, replacing a parameter of the same name; a null or undefined value removes it.
694
694
  * @param {URLSearchParams|Record<string,string|null|undefined>|string[][]|string} ps
695
695
  * @returns {HttpRequestBuilder} this builder
696
696
  */
@@ -705,20 +705,14 @@ var httpc = (function (exports) {
705
705
  return this;
706
706
  }
707
707
  /**
708
- * Adds a query parameter to the request, overriding it if it already exists. An empty list, or one carrying only null and undefined values, causes the key to be removed; nullish entries among real values are skipped.
708
+ * Sets a query parameter to the values given, as repeated entries moved to the end of the query; nullish values are skipped, and none left removes the parameter.
709
709
  * @param {string} k
710
710
  * @param {...string} vs
711
711
  * @returns {HttpRequestBuilder} this builder
712
712
  */
713
713
  param(k, ...vs) {
714
- //overriding, as header, headers and params all do: pass every value in one
715
- //call to get a multi valued parameter
716
714
  this.#params.delete(k);
717
- const values = vs.filter((v) => v != null);
718
- if (values.length === 0) {
719
- return this;
720
- }
721
- for (const v of values) {
715
+ for (const v of vs.filter((v) => v != null)) {
722
716
  this.#params.append(k, v);
723
717
  }
724
718
  return this;
@@ -753,6 +747,7 @@ var httpc = (function (exports) {
753
747
  * Sets the request body as a FormData configured using the callback.
754
748
  * `Content-Type: multipart/form-data` header is automatically added by fetch if not explicitly set.
755
749
  * @param {(c: HttpMultipartRequestCustomizer) => void} callback
750
+ * @returns {HttpRequestBuilder} this builder
756
751
  */
757
752
  multipart(callback) {
758
753
  const formData = new FormData();
@@ -762,7 +757,7 @@ var httpc = (function (exports) {
762
757
  return this;
763
758
  }
764
759
  /**
765
- * Sets a fetch options for the request.
760
+ * Merges fetch options into the request's.
766
761
  * @param {Omit<RequestInit,"headers"|"method"|"body">} kvs
767
762
  * @returns {HttpRequestBuilder} this builder
768
763
  */
@@ -783,8 +778,8 @@ var httpc = (function (exports) {
783
778
  return this;
784
779
  }
785
780
  /**
786
- * Adds interceptors to the request.
787
- * @param {[HttpInterceptor]} is - the interceptor to be registered
781
+ * Adds interceptors run for this request only, after the client's.
782
+ * @param {HttpInterceptor[]} is
788
783
  * @returns {HttpRequestBuilder} this builder
789
784
  */
790
785
  interceptors(is) {
@@ -794,8 +789,8 @@ var httpc = (function (exports) {
794
789
  return this;
795
790
  }
796
791
  /**
797
- * Adds an interceptor to the request.
798
- * @param {HttpInterceptor} i - the interceptor to be registered
792
+ * Adds one interceptor run for this request only, after the client's.
793
+ * @param {HttpInterceptor} i
799
794
  * @returns {HttpRequestBuilder} this builder
800
795
  */
801
796
  interceptor(i) {
@@ -803,8 +798,8 @@ var httpc = (function (exports) {
803
798
  return this;
804
799
  }
805
800
  /**
806
- * Performs an HTTP exchange using the configured client, request and interceptors.
807
- * @returns {Promise<Response>} the response
801
+ * Sends the request and answers the Response, whatever its status.
802
+ * @returns {Promise<Response>} rejecting as `HttpClient.exchange` does
808
803
  */
809
804
  async exchange() {
810
805
  const query = this.#params.size ? `?${this.#params}` : '';
@@ -817,8 +812,12 @@ var httpc = (function (exports) {
817
812
  return await this.#client.exchange(`${this.#uri}${query}${this.#fragment}`, opts, this.#interceptors);
818
813
  }
819
814
  /**
820
- * Performs an HTTP exchange using the configured client request, and interceptors throwing a failure when response status is not in the 200-299 range.
821
- * @returns {Promise<Response>} the response
815
+ * Sends the request and answers the Response when its status is 200-299.
816
+ * Always rejects with an HttpClientError: `HttpClientError.fromResponse`
817
+ * for an error status, CONNECTION_PROBLEM for a transport failure, and
818
+ * UNEXPECTED_PROBLEM, carrying the error as its cause, for anything an
819
+ * interceptor throws that is not already a Failure.
820
+ * @returns {Promise<Response>}
822
821
  */
823
822
  async fetch() {
824
823
  try {
@@ -831,45 +830,40 @@ var httpc = (function (exports) {
831
830
  if (ex instanceof Failure) {
832
831
  throw ex;
833
832
  }
834
- //fetch() answers a Failure whatever happened, so a caller reading
835
- //`problems` never has to test the shape first. What reaches here is
836
- //not the transport, which labels its own failure below the chain: it
837
- //is a throw from the chain's own code, carried as the cause
838
833
  throw HttpClientError.of('UNEXPECTED_PROBLEM', ex);
839
834
  }
840
835
  }
841
836
  /**
842
- * Performs an HTTP exchange using the configured client request, and interceptors throwing a failure when response status is not in the 200-299 range.
843
- * @returns {Promise<string>} the response body, as text
837
+ * `fetch()`, then the body as text; a body that cannot be read rejects with UNMARSHALING_PROBLEM.
838
+ * @returns {Promise<string>}
844
839
  */
845
840
  async fetchText() {
846
841
  const response = await this.fetch();
847
842
  return await unmarshal(response, 'text');
848
843
  }
849
844
  /**
850
- * Performs an HTTP exchange using the configured client request, and interceptors throwing a failure when response status is not in the 200-299 range.
851
- * A 204 yields null without reading the body; any other empty body is an unmarshaling failure.
852
- * @returns {Promise<any>} the response body, deserialized as JSON
845
+ * `fetch()`, then the body parsed as json. A 204 yields null without reading
846
+ * the body; any other body that does not parse rejects with UNMARSHALING_PROBLEM.
847
+ * @returns {Promise<any>}
853
848
  */
854
849
  async fetchJson() {
855
850
  const response = await this.fetch();
856
851
  if (response.status === 204) {
857
- //a 204 declares no content: there is no body to decode
858
852
  return null;
859
853
  }
860
854
  return await unmarshal(response, 'json');
861
855
  }
862
856
  /**
863
- * Performs an HTTP exchange using the configured client request, and interceptors throwing a failure when response status is not in the 200-299 range.
864
- * @returns {Promise<Blob>} the response body, as a Blob
857
+ * `fetch()`, then the body as a Blob; a body that cannot be read rejects with UNMARSHALING_PROBLEM.
858
+ * @returns {Promise<Blob>}
865
859
  */
866
860
  async fetchBlob() {
867
861
  const response = await this.fetch();
868
862
  return await unmarshal(response, 'blob');
869
863
  }
870
864
  /**
871
- * Performs an HTTP exchange using the configured client request, and interceptors throwing a failure when response status is not in the 200-299 range.
872
- * @returns {Promise<ArrayBuffer>} the response body, as an ArrayBuffer
865
+ * `fetch()`, then the body as an ArrayBuffer; a body that cannot be read rejects with UNMARSHALING_PROBLEM.
866
+ * @returns {Promise<ArrayBuffer>}
873
867
  */
874
868
  async fetchArrayBuffer() {
875
869
  const response = await this.fetch();
@@ -880,10 +874,7 @@ var httpc = (function (exports) {
880
874
  /** Builds a multipart body: text fields, json parts, and single or repeated blobs. */
881
875
  class HttpMultipartRequestCustomizer {
882
876
  #formData;
883
- /**
884
- *
885
- * @param {FormData} formData
886
- */
877
+ /** @param {FormData} formData the body being built */
887
878
  constructor(formData) {
888
879
  this.#formData = formData;
889
880
  }
@@ -891,33 +882,30 @@ var httpc = (function (exports) {
891
882
  * Appends a value to the FormData.
892
883
  * @param {string} name
893
884
  * @param {*} value
894
- * @returns this builder
885
+ * @returns {HttpMultipartRequestCustomizer} this builder
895
886
  */
896
887
  field(name, value) {
897
888
  this.#formData.append(name, value);
898
889
  return this;
899
890
  }
900
891
  /**
901
- * Appends a Blob to the FormData.
902
- * If `filename` is omitted, FormData defaults are applied:
903
- * The default filename for Blob objects is "blob";
904
- * The default filename for File objects is the file's filename.
892
+ * Appends a Blob to the FormData. Without a filename a File keeps its own
893
+ * and any other Blob is named "blob".
905
894
  * @param {string} name
906
895
  * @param {Blob} value
907
- * @param {string|undefined} filename
908
- * @returns this builder
896
+ * @param {string} [filename]
897
+ * @returns {HttpMultipartRequestCustomizer} this builder
909
898
  */
910
899
  blob(name, value, filename) {
911
900
  this.#formData.append(name, value, filename);
912
901
  return this;
913
902
  }
914
903
  /**
915
- * Appends multiple Blobs to the FormData with the same name.
916
- * The default filename for Blob objects is "blob";
917
- * The default filename for File objects is the file's filename.
904
+ * Appends each Blob under the same name, a File keeping its own filename and
905
+ * any other Blob named "blob".
918
906
  * @param {string} name
919
907
  * @param {Blob[]} values
920
- * @returns this builder
908
+ * @returns {HttpMultipartRequestCustomizer} this builder
921
909
  */
922
910
  blobs(name, values) {
923
911
  for (const v of values) {
@@ -926,11 +914,11 @@ var httpc = (function (exports) {
926
914
  return this;
927
915
  }
928
916
  /**
929
- * Appends a JSON serialized blob to the FormData.
917
+ * Appends the value serialized as an `application/json` blob.
930
918
  * @param {string} name
931
919
  * @param {any} value
932
- * @param {string|undefined} filename
933
- * @returns this builder
920
+ * @param {string} [filename]
921
+ * @returns {HttpMultipartRequestCustomizer} this builder
934
922
  */
935
923
  json(name, value, filename) {
936
924
  const blob = new Blob([JSON.stringify(value)], { type: 'application/json' });