@optionfactory/fml 9.0.0-rc14 → 9.0.0-rc16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -88
- package/dist/client-errors.iife.js +17 -26
- package/dist/client-errors.iife.js.map +1 -1
- package/dist/client-errors.iife.min.js.map +1 -1
- package/dist/custom-elements.json +70 -30
- package/dist/fml.css +20 -16
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +3612 -2138
- package/dist/fml.iife.js.map +1 -1
- package/dist/fml.iife.min.js +1 -1
- package/dist/fml.iife.min.js.map +1 -1
- package/dist/ftl.d.mts +395 -224
- package/dist/ftl.iife.js +400 -330
- package/dist/ftl.iife.js.map +1 -1
- package/dist/ftl.iife.min.js +1 -1
- package/dist/ftl.iife.min.js.map +1 -1
- package/dist/ftl.min.mjs +1 -1
- package/dist/ftl.min.mjs.map +1 -1
- package/dist/ftl.mjs +400 -330
- package/dist/ftl.mjs.map +1 -1
- package/dist/ful.css +20 -16
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +2601 -678
- package/dist/ful.iife.js +3042 -1610
- package/dist/ful.iife.js.map +1 -1
- package/dist/ful.iife.min.js +1 -1
- package/dist/ful.iife.min.js.map +1 -1
- package/dist/ful.min.mjs +1 -1
- package/dist/ful.min.mjs.map +1 -1
- package/dist/ful.mjs +3042 -1611
- package/dist/ful.mjs.map +1 -1
- package/dist/httpc.d.mts +139 -121
- package/dist/httpc.iife.js +137 -166
- package/dist/httpc.iife.js.map +1 -1
- package/dist/httpc.iife.min.js +1 -1
- package/dist/httpc.iife.min.js.map +1 -1
- package/dist/httpc.min.mjs +1 -1
- package/dist/httpc.min.mjs.map +1 -1
- package/dist/httpc.mjs +137 -166
- package/dist/httpc.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +36 -18
- package/dist/web-types.json +67 -35
- package/package.json +1 -1
package/dist/httpc.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* One problem of a failure: its `type` (a code such as `FIELD_ERROR` or
|
|
3
|
+
* `GENERIC_PROBLEM`), the `context` naming the field it belongs to or null, the
|
|
4
|
+
* human `reason`, and optional `details`.
|
|
2
5
|
* @typedef {{ type: string; context: string?; reason: string; details: any?; }} Problem
|
|
3
6
|
*/
|
|
4
7
|
/**
|
|
@@ -8,10 +11,9 @@
|
|
|
8
11
|
*/
|
|
9
12
|
class Failure extends Error {
|
|
10
13
|
/**
|
|
11
|
-
*
|
|
12
14
|
* @param {string} message
|
|
13
15
|
* @param {Problem[]} problems
|
|
14
|
-
* @param {*} cause
|
|
16
|
+
* @param {*} [cause]
|
|
15
17
|
*/
|
|
16
18
|
constructor(message, problems, cause) {
|
|
17
19
|
super(message, { cause });
|
|
@@ -28,6 +30,7 @@ class Failure extends Error {
|
|
|
28
30
|
return new Failure(this.message, Failure.dropProblemsContext(this.problems, prefix), this);
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
33
|
+
* Copies of the problems, each context losing the prefix where it starts with it.
|
|
31
34
|
* @param {Problem[]} problems
|
|
32
35
|
* @param {string} prefix
|
|
33
36
|
* @returns {Problem[]}
|
|
@@ -38,21 +41,6 @@ class Failure extends Error {
|
|
|
38
41
|
return { type, context: nctx, reason, details };
|
|
39
42
|
});
|
|
40
43
|
}
|
|
41
|
-
/**
|
|
42
|
-
* The one reading of a failure: its problems' reasons, one per line, or
|
|
43
|
-
* the fallback when the value carries none. An empty problems array
|
|
44
|
-
* carries nothing: the failure's own message reads instead.
|
|
45
|
-
*
|
|
46
|
-
* @param {any} cause
|
|
47
|
-
* @param {string|null} [fallback]
|
|
48
|
-
* @returns {string}
|
|
49
|
-
*/
|
|
50
|
-
static problemsText(cause, fallback = null) {
|
|
51
|
-
if (cause?.problems?.length) {
|
|
52
|
-
return cause.problems.map((p) => `${p.reason}`).join('\n');
|
|
53
|
-
}
|
|
54
|
-
return fallback ?? `${cause?.message ?? cause}`;
|
|
55
|
-
}
|
|
56
44
|
}
|
|
57
45
|
|
|
58
46
|
/**
|
|
@@ -62,6 +50,10 @@ class Failure extends Error {
|
|
|
62
50
|
* of corrupting silently.
|
|
63
51
|
*/
|
|
64
52
|
class Base64 {
|
|
53
|
+
/** The RFC 4648 alphabet, with `+` and `/`. */
|
|
54
|
+
static STANDARD = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
55
|
+
/** The RFC 4648 url and filename safe alphabet, with `-` and `_`. */
|
|
56
|
+
static URL_SAFE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
65
57
|
/**
|
|
66
58
|
* @param {ArrayBuffer} arrayBuffer
|
|
67
59
|
* @param {string} [dialect] one of Base64.STANDARD or Base64.URL_SAFE, URL_SAFE by default
|
|
@@ -87,14 +79,14 @@ class Base64 {
|
|
|
87
79
|
return res;
|
|
88
80
|
}
|
|
89
81
|
/**
|
|
90
|
-
* @param {string} str
|
|
82
|
+
* @param {string} str padded or unpadded
|
|
91
83
|
* @param {string} [dialect] one of Base64.STANDARD or Base64.URL_SAFE, URL_SAFE by default
|
|
92
84
|
* @returns {ArrayBuffer}
|
|
85
|
+
* @throws on padding anywhere but the tail or longer than two, on an
|
|
86
|
+
* impossible length, and on a character outside the dialect
|
|
93
87
|
*/
|
|
94
88
|
static decode(str, dialect) {
|
|
95
89
|
const d = dialect || Base64.URL_SAFE;
|
|
96
|
-
//padding belongs at the tail only, two at most, and nothing outside the
|
|
97
|
-
//dialect decodes: reject instead of corrupting silently
|
|
98
90
|
let end = str.length;
|
|
99
91
|
while (end > 0 && str.charAt(end - 1) === '=') {
|
|
100
92
|
--end;
|
|
@@ -116,9 +108,6 @@ class Base64 {
|
|
|
116
108
|
|
|
117
109
|
let vi = 0;
|
|
118
110
|
let si = 0;
|
|
119
|
-
//every group yields three bytes, but a padded input stops short of the last
|
|
120
|
-
//one or two: writing them anyway would rely on typed arrays dropping writes
|
|
121
|
-
//past their length
|
|
122
111
|
while (vi < nbytes) {
|
|
123
112
|
const v1 = d.indexOf(unpadded.charAt(si++));
|
|
124
113
|
const v2 = d.indexOf(unpadded.charAt(si++));
|
|
@@ -139,16 +128,14 @@ class Base64 {
|
|
|
139
128
|
}
|
|
140
129
|
}
|
|
141
130
|
|
|
142
|
-
Base64.STANDARD = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
143
|
-
Base64.URL_SAFE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
144
|
-
|
|
145
131
|
/**
|
|
146
132
|
* Hex encoding and decoding over byte sequences, lowercase by default.
|
|
147
133
|
*/
|
|
148
134
|
class Hex {
|
|
149
135
|
/**
|
|
150
|
-
* @param {string} hex
|
|
136
|
+
* @param {string} hex either case
|
|
151
137
|
* @returns {Uint8Array}
|
|
138
|
+
* @throws on an odd length or a character that is not a hex digit
|
|
152
139
|
*/
|
|
153
140
|
static decode(hex) {
|
|
154
141
|
if (hex.length % 2 !== 0) {
|
|
@@ -166,8 +153,8 @@ class Hex {
|
|
|
166
153
|
}
|
|
167
154
|
/**
|
|
168
155
|
* @param {Iterable<number>} bytes
|
|
169
|
-
* @param {boolean} [upper]
|
|
170
|
-
* @returns {string}
|
|
156
|
+
* @param {boolean} [upper] uppercase digits instead of lowercase
|
|
157
|
+
* @returns {string} two digits per byte
|
|
171
158
|
*/
|
|
172
159
|
static encode(bytes, upper) {
|
|
173
160
|
return Array.from(bytes)
|
|
@@ -186,19 +173,22 @@ class MediaType {
|
|
|
186
173
|
this.#type = type;
|
|
187
174
|
this.#subtype = subtype;
|
|
188
175
|
}
|
|
176
|
+
/** @returns {string} `type/subtype`, lowercase */
|
|
189
177
|
get normalized() {
|
|
190
178
|
return `${this.#type}/${this.#subtype}`;
|
|
191
179
|
}
|
|
180
|
+
/** @returns {string} */
|
|
192
181
|
get type() {
|
|
193
182
|
return this.#type;
|
|
194
183
|
}
|
|
184
|
+
/** @returns {string} */
|
|
195
185
|
get subtype() {
|
|
196
186
|
return this.#subtype;
|
|
197
187
|
}
|
|
198
188
|
/**
|
|
199
189
|
* Parses a Content-Type header value into its type/subtype pair, dropping any parameter.
|
|
200
190
|
* @param {string|null|undefined} v
|
|
201
|
-
* @returns
|
|
191
|
+
* @returns {MediaType} `unknown/unknown` for a missing or malformed value
|
|
202
192
|
*/
|
|
203
193
|
static parse(v) {
|
|
204
194
|
if (!v) {
|
|
@@ -248,28 +238,35 @@ class HttpClientError extends Failure {
|
|
|
248
238
|
return new HttpClientError(this.message, this.status, Failure.dropProblemsContext(this.problems, prefix), this);
|
|
249
239
|
}
|
|
250
240
|
/**
|
|
251
|
-
*
|
|
252
|
-
* same shape, and the server's arrive already shaped from the wire.
|
|
241
|
+
* A problem with no context and no details.
|
|
253
242
|
* @param {string} type
|
|
254
243
|
* @param {string} reason
|
|
244
|
+
* @returns {{ type: string; context: null; reason: string; details: null; }}
|
|
255
245
|
*/
|
|
256
246
|
static problem(type, reason) {
|
|
257
247
|
return { type, context: null, reason, details: null };
|
|
258
248
|
}
|
|
259
249
|
/**
|
|
260
|
-
*
|
|
250
|
+
* A failure with status 0 and one problem of the given type, whose reason is
|
|
251
|
+
* the cause's message.
|
|
261
252
|
* @param {string} type
|
|
262
253
|
* @param {any} cause
|
|
263
|
-
* @returns
|
|
254
|
+
* @returns {HttpClientError}
|
|
264
255
|
*/
|
|
265
256
|
static of(type, cause) {
|
|
266
257
|
const reason = String(cause?.message ?? cause ?? 'unknown failure');
|
|
267
258
|
return new HttpClientError(reason, 0, [HttpClientError.problem(type, reason)], cause);
|
|
268
259
|
}
|
|
269
260
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
261
|
+
* The failure an error response stands for, keeping its status. An
|
|
262
|
+
* `application/failures+json` body is the problems list; an
|
|
263
|
+
* `application/problem+json` body contributes its `problems`, or one
|
|
264
|
+
* GENERIC_PROBLEM made of its title and detail. Any other body is one
|
|
265
|
+
* GENERIC_PROBLEM carrying the body text. A json body that does not decode,
|
|
266
|
+
* or decodes to the wrong shape, and a body that cannot be read at all,
|
|
267
|
+
* each become one GENERIC_PROBLEM saying so.
|
|
268
|
+
* @param {Response} response consumed
|
|
269
|
+
* @returns {Promise<HttpClientError>}
|
|
273
270
|
*/
|
|
274
271
|
static async fromResponse(response) {
|
|
275
272
|
switch (MediaType.parse(response.headers.get('Content-Type')).normalized) {
|
|
@@ -304,14 +301,10 @@ class HttpClientError extends Failure {
|
|
|
304
301
|
}
|
|
305
302
|
}
|
|
306
303
|
}
|
|
307
|
-
/** marks a body whose json() rejected, telling it apart from a body decoding to json null */
|
|
308
304
|
static #unreadable = Symbol('unreadable body');
|
|
309
305
|
/**
|
|
310
|
-
* A json body that failed to decode, or that decoded to something other than
|
|
311
|
-
* the declared contract, has consumed its stream: there is no text left to
|
|
312
|
-
* embed, the status, the declared media type and the shape are the report.
|
|
313
306
|
* @param {Response} response
|
|
314
|
-
* @param {string} [as]
|
|
307
|
+
* @param {string} [as] what the body does not decode as
|
|
315
308
|
*/
|
|
316
309
|
static #undecodable(response, as = 'as json') {
|
|
317
310
|
const mediaType = MediaType.parse(response.headers.get('Content-Type')).normalized;
|
|
@@ -319,9 +312,6 @@ class HttpClientError extends Failure {
|
|
|
319
312
|
return new HttpClientError(message, response.status, [HttpClientError.problem('GENERIC_PROBLEM', message)]);
|
|
320
313
|
}
|
|
321
314
|
static async #generic(response) {
|
|
322
|
-
//a body that cannot be read (the connection cut mid-body) must not
|
|
323
|
-
//masquerade as a connection problem: the response was served, its
|
|
324
|
-
//status is the report
|
|
325
315
|
const text = await response.text().catch(() => null);
|
|
326
316
|
const message =
|
|
327
317
|
text === null
|
|
@@ -334,19 +324,14 @@ class HttpClientError extends Failure {
|
|
|
334
324
|
const metaContent = (name) =>
|
|
335
325
|
globalThis.document?.querySelector(`meta[name="${name}"]`)?.getAttribute('content') ?? undefined;
|
|
336
326
|
|
|
337
|
-
/**
|
|
338
|
-
* @implements {HttpInterceptor}
|
|
339
|
-
*/
|
|
340
327
|
/**
|
|
341
328
|
* Sends the csrf header named by the page's `_csrf_header` meta, with the token
|
|
342
329
|
* from `_csrf`. Both are read per request, so metas replaced after the client
|
|
343
330
|
* was built are honoured, and the header is sent to the page's own origin only.
|
|
331
|
+
* @implements {HttpInterceptor}
|
|
344
332
|
*/
|
|
345
333
|
class CsrfTokenInterceptor {
|
|
346
334
|
async intercept(url, request, chain) {
|
|
347
|
-
//the token is the page's own: it travels to the page's origin only, and it
|
|
348
|
-
//is read at request time, so metas landed after the client was built (a
|
|
349
|
-
//login flow) are honored without a rebuild
|
|
350
335
|
if (url.origin !== (globalThis.window?.location?.origin ?? url.origin)) {
|
|
351
336
|
return await chain.proceed(url, request);
|
|
352
337
|
}
|
|
@@ -358,13 +343,12 @@ class CsrfTokenInterceptor {
|
|
|
358
343
|
return await chain.proceed(url, request);
|
|
359
344
|
}
|
|
360
345
|
}
|
|
361
|
-
/**
|
|
362
|
-
* @implements {HttpInterceptor}
|
|
363
|
-
*/
|
|
364
346
|
/**
|
|
365
347
|
* Navigates to a login url when a response comes back 401. The promise it
|
|
366
348
|
* returns never settles, so callers keep waiting while the page unloads
|
|
367
|
-
* instead of showing a failure
|
|
349
|
+
* instead of showing a failure; where the navigation is blocked, as by a
|
|
350
|
+
* beforeunload prompt, they stay pending until the page leaves.
|
|
351
|
+
* @implements {HttpInterceptor}
|
|
368
352
|
*/
|
|
369
353
|
class RedirectOnUnauthorizedInterceptor {
|
|
370
354
|
#redirectUri;
|
|
@@ -378,10 +362,6 @@ class RedirectOnUnauthorizedInterceptor {
|
|
|
378
362
|
const response = await chain.proceed(url, request);
|
|
379
363
|
if (response.status === 401) {
|
|
380
364
|
window.location.href = this.#redirectUri;
|
|
381
|
-
//the page is navigating away: a promise that never settles keeps the
|
|
382
|
-
//callers' spinners up instead of flashing a failure nobody will read.
|
|
383
|
-
//Where the navigation is blocked (a beforeunload gate), they stay
|
|
384
|
-
//pending until the page actually leaves
|
|
385
365
|
return new Promise(() => {});
|
|
386
366
|
}
|
|
387
367
|
return response;
|
|
@@ -397,38 +377,48 @@ class HttpClientBuilder {
|
|
|
397
377
|
constructor() {
|
|
398
378
|
this.#interceptors = [];
|
|
399
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Adds the CsrfTokenInterceptor.
|
|
382
|
+
* @returns {HttpClientBuilder} this builder
|
|
383
|
+
*/
|
|
400
384
|
withCsrfToken() {
|
|
401
385
|
this.#interceptors.push(new CsrfTokenInterceptor());
|
|
402
386
|
return this;
|
|
403
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Adds the RedirectOnUnauthorizedInterceptor.
|
|
390
|
+
* @param {string} redirectUri where a 401 navigates to
|
|
391
|
+
* @returns {HttpClientBuilder} this builder
|
|
392
|
+
*/
|
|
404
393
|
withRedirectOnUnauthorized(redirectUri) {
|
|
405
394
|
this.#interceptors.push(new RedirectOnUnauthorizedInterceptor(redirectUri));
|
|
406
395
|
return this;
|
|
407
396
|
}
|
|
408
397
|
/**
|
|
398
|
+
* Adds interceptors, run in the order given after the ones added before.
|
|
409
399
|
* @param {...HttpInterceptor} interceptors
|
|
400
|
+
* @returns {HttpClientBuilder} this builder
|
|
410
401
|
*/
|
|
411
402
|
withInterceptors(...interceptors) {
|
|
412
403
|
this.#interceptors.push(...interceptors);
|
|
413
404
|
return this;
|
|
414
405
|
}
|
|
406
|
+
/** @returns {HttpClient} a client running the interceptors added so far */
|
|
415
407
|
build() {
|
|
416
408
|
return new HttpClient(this.#interceptors);
|
|
417
409
|
}
|
|
418
410
|
}
|
|
419
411
|
|
|
420
412
|
/**
|
|
413
|
+
* The last interceptor in every chain, performing the request with fetch. A
|
|
414
|
+
* fetch rejection becomes an HttpClientError with a CONNECTION_PROBLEM.
|
|
421
415
|
* @implements {HttpInterceptor}
|
|
422
416
|
*/
|
|
423
|
-
/** The last interceptor in every chain: the one that performs the request. */
|
|
424
417
|
class HttpCall {
|
|
425
418
|
async intercept(url, request, chain) {
|
|
426
419
|
try {
|
|
427
420
|
return await fetch(url, request);
|
|
428
421
|
} catch (ex) {
|
|
429
|
-
//the one place a connection problem is a connection problem: the
|
|
430
|
-
//transport itself refused to deliver. Everything above this is code,
|
|
431
|
-
//and code that throws has a different story to tell
|
|
432
422
|
throw HttpClientError.of('CONNECTION_PROBLEM', ex);
|
|
433
423
|
}
|
|
434
424
|
}
|
|
@@ -439,19 +429,18 @@ class HttpInterceptorChain {
|
|
|
439
429
|
#interceptors;
|
|
440
430
|
#current;
|
|
441
431
|
/**
|
|
442
|
-
*
|
|
443
432
|
* @param {HttpInterceptor[]} interceptors
|
|
444
|
-
* @param {number} current
|
|
433
|
+
* @param {number} current the index of the interceptor `proceed` runs
|
|
445
434
|
*/
|
|
446
435
|
constructor(interceptors, current) {
|
|
447
436
|
this.#interceptors = interceptors;
|
|
448
437
|
this.#current = current;
|
|
449
438
|
}
|
|
450
439
|
/**
|
|
451
|
-
*
|
|
440
|
+
* Runs the next interceptor.
|
|
452
441
|
* @param {URL} url
|
|
453
442
|
* @param {RequestInit} request
|
|
454
|
-
* @returns {Promise<Response>}
|
|
443
|
+
* @returns {Promise<Response>}
|
|
455
444
|
*/
|
|
456
445
|
async proceed(url, request) {
|
|
457
446
|
const interceptor = this.#interceptors[this.#current];
|
|
@@ -485,12 +474,14 @@ class HttpClient {
|
|
|
485
474
|
this.#interceptors = interceptors || [];
|
|
486
475
|
}
|
|
487
476
|
/**
|
|
488
|
-
* Performs an HTTP exchange
|
|
489
|
-
*
|
|
490
|
-
* @param {string} uri
|
|
491
|
-
* @param {RequestInit|undefined} options
|
|
492
|
-
* @param {HttpInterceptor[]|undefined} interceptors
|
|
493
|
-
* @returns {Promise<Response>} the response
|
|
477
|
+
* Performs an HTTP exchange through the client's interceptors, then the
|
|
478
|
+
* given ones.
|
|
479
|
+
* @param {string} uri the request url, relative to the page's
|
|
480
|
+
* @param {RequestInit|undefined} options fetch options
|
|
481
|
+
* @param {HttpInterceptor[]|undefined} interceptors run for this exchange only
|
|
482
|
+
* @returns {Promise<Response>} the response, whatever its status; rejecting
|
|
483
|
+
* with an HttpClientError CONNECTION_PROBLEM when the transport fails, or
|
|
484
|
+
* with what an interceptor throws
|
|
494
485
|
*/
|
|
495
486
|
async exchange(uri, options, interceptors) {
|
|
496
487
|
const is = [...this.#interceptors, ...(interceptors || []), new HttpCall()];
|
|
@@ -500,58 +491,58 @@ class HttpClient {
|
|
|
500
491
|
return await chain.proceed(url, request);
|
|
501
492
|
}
|
|
502
493
|
/**
|
|
503
|
-
*
|
|
504
|
-
* @param {string} method
|
|
505
|
-
* @param {string} uri
|
|
506
|
-
* @returns {HttpRequestBuilder}
|
|
494
|
+
* Starts a request with any method.
|
|
495
|
+
* @param {string} method
|
|
496
|
+
* @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
|
|
497
|
+
* @returns {HttpRequestBuilder}
|
|
507
498
|
*/
|
|
508
499
|
request(method, uri) {
|
|
509
500
|
return HttpRequestBuilder.create(this, method, uri);
|
|
510
501
|
}
|
|
511
502
|
/**
|
|
512
|
-
*
|
|
513
|
-
* @param {string} uri
|
|
514
|
-
* @returns {HttpRequestBuilder}
|
|
503
|
+
* Starts a GET request.
|
|
504
|
+
* @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
|
|
505
|
+
* @returns {HttpRequestBuilder}
|
|
515
506
|
*/
|
|
516
507
|
get(uri) {
|
|
517
508
|
return HttpRequestBuilder.create(this, 'GET', uri);
|
|
518
509
|
}
|
|
519
510
|
/**
|
|
520
|
-
*
|
|
521
|
-
* @param {string} uri
|
|
522
|
-
* @returns {HttpRequestBuilder}
|
|
511
|
+
* Starts a HEAD request.
|
|
512
|
+
* @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
|
|
513
|
+
* @returns {HttpRequestBuilder}
|
|
523
514
|
*/
|
|
524
515
|
head(uri) {
|
|
525
516
|
return HttpRequestBuilder.create(this, 'HEAD', uri);
|
|
526
517
|
}
|
|
527
518
|
/**
|
|
528
|
-
*
|
|
529
|
-
* @param {string} uri
|
|
530
|
-
* @returns {HttpRequestBuilder}
|
|
519
|
+
* Starts a POST request.
|
|
520
|
+
* @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
|
|
521
|
+
* @returns {HttpRequestBuilder}
|
|
531
522
|
*/
|
|
532
523
|
post(uri) {
|
|
533
524
|
return HttpRequestBuilder.create(this, 'POST', uri);
|
|
534
525
|
}
|
|
535
526
|
/**
|
|
536
|
-
*
|
|
537
|
-
* @param {string} uri
|
|
538
|
-
* @returns {HttpRequestBuilder}
|
|
527
|
+
* Starts a PUT request.
|
|
528
|
+
* @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
|
|
529
|
+
* @returns {HttpRequestBuilder}
|
|
539
530
|
*/
|
|
540
531
|
put(uri) {
|
|
541
532
|
return HttpRequestBuilder.create(this, 'PUT', uri);
|
|
542
533
|
}
|
|
543
534
|
/**
|
|
544
|
-
*
|
|
545
|
-
* @param {string} uri
|
|
546
|
-
* @returns {HttpRequestBuilder}
|
|
535
|
+
* Starts a PATCH request.
|
|
536
|
+
* @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
|
|
537
|
+
* @returns {HttpRequestBuilder}
|
|
547
538
|
*/
|
|
548
539
|
patch(uri) {
|
|
549
540
|
return HttpRequestBuilder.create(this, 'PATCH', uri);
|
|
550
541
|
}
|
|
551
542
|
/**
|
|
552
|
-
*
|
|
553
|
-
* @param {string} uri
|
|
554
|
-
* @returns {HttpRequestBuilder}
|
|
543
|
+
* Starts a DELETE request.
|
|
544
|
+
* @param {string} uri the request url, relative to the page's; a query and a fragment in it are kept
|
|
545
|
+
* @returns {HttpRequestBuilder}
|
|
555
546
|
*/
|
|
556
547
|
delete(uri) {
|
|
557
548
|
return HttpRequestBuilder.create(this, 'DELETE', uri);
|
|
@@ -562,7 +553,7 @@ class HttpClient {
|
|
|
562
553
|
* Reads the response body as the given type, wrapping a failed read as an UNMARSHALING_PROBLEM.
|
|
563
554
|
* @param {Response} response
|
|
564
555
|
* @param {'text'|'json'|'blob'|'arrayBuffer'} type
|
|
565
|
-
* @returns
|
|
556
|
+
* @returns {Promise<any>}
|
|
566
557
|
*/
|
|
567
558
|
const unmarshal = async (response, type) => {
|
|
568
559
|
try {
|
|
@@ -573,9 +564,7 @@ const unmarshal = async (response, type) => {
|
|
|
573
564
|
};
|
|
574
565
|
|
|
575
566
|
/**
|
|
576
|
-
*
|
|
577
|
-
* normalizing through `Headers`/`URLSearchParams` first would stringify them to
|
|
578
|
-
* "null"/"undefined" instead of removing the key.
|
|
567
|
+
* The entries of a headers or params initializer with nullish values kept as they are.
|
|
579
568
|
* @param {any} source
|
|
580
569
|
* @returns {Iterable<[string, any]>}
|
|
581
570
|
*/
|
|
@@ -609,15 +598,14 @@ class HttpRequestBuilder {
|
|
|
609
598
|
#interceptors;
|
|
610
599
|
#fragment;
|
|
611
600
|
/**
|
|
612
|
-
*
|
|
601
|
+
* A builder for the url: its query becomes the initial params, split at the
|
|
602
|
+
* first `?`, and its fragment, from the first `#`, is kept for the final url.
|
|
613
603
|
* @param {HttpClient} client
|
|
614
|
-
* @param {string} method
|
|
615
|
-
* @param {string} uri
|
|
616
|
-
* @returns {HttpRequestBuilder}
|
|
604
|
+
* @param {string} method
|
|
605
|
+
* @param {string} uri
|
|
606
|
+
* @returns {HttpRequestBuilder}
|
|
617
607
|
*/
|
|
618
608
|
static create(client, method, uri) {
|
|
619
|
-
//'/a#frag?p=1' parses as hash '#frag?p=1' with an empty query, and a '?'
|
|
620
|
-
//may appear in a query itself: only the first of each splits
|
|
621
609
|
const hashIndex = uri.indexOf('#');
|
|
622
610
|
const fragment = hashIndex === -1 ? '' : uri.slice(hashIndex);
|
|
623
611
|
const withoutFragment = hashIndex === -1 ? uri : uri.slice(0, hashIndex);
|
|
@@ -635,10 +623,9 @@ class HttpRequestBuilder {
|
|
|
635
623
|
);
|
|
636
624
|
}
|
|
637
625
|
/**
|
|
638
|
-
* Creates an HttpRequestBuilder.
|
|
639
626
|
* @param {HttpClient} client
|
|
640
|
-
* @param {string} method
|
|
641
|
-
* @param {string} uri
|
|
627
|
+
* @param {string} method
|
|
628
|
+
* @param {string} uri the url without query and fragment
|
|
642
629
|
* @param {URLSearchParams} params
|
|
643
630
|
* @param {Headers} headers
|
|
644
631
|
* @param {any} body
|
|
@@ -658,24 +645,20 @@ class HttpRequestBuilder {
|
|
|
658
645
|
this.#fragment = fragment;
|
|
659
646
|
}
|
|
660
647
|
/**
|
|
661
|
-
*
|
|
648
|
+
* Sets each header, replacing a header of the same name; a null or undefined value removes it.
|
|
662
649
|
* @param {HeadersInit|Record<string,string|null|undefined>} hs
|
|
663
650
|
* @returns {HttpRequestBuilder} this builder
|
|
664
651
|
*/
|
|
665
652
|
headers(hs) {
|
|
666
653
|
for (const [k, v] of rawEntries(hs)) {
|
|
667
|
-
|
|
668
|
-
this.#headers.delete(k);
|
|
669
|
-
} else {
|
|
670
|
-
this.#headers.set(k, v);
|
|
671
|
-
}
|
|
654
|
+
this.header(k, v);
|
|
672
655
|
}
|
|
673
656
|
return this;
|
|
674
657
|
}
|
|
675
658
|
/**
|
|
676
|
-
*
|
|
659
|
+
* Sets a header, replacing one of the same name; a null or undefined value removes it.
|
|
677
660
|
* @param {string} k
|
|
678
|
-
* @param {string} v
|
|
661
|
+
* @param {string|null|undefined} v
|
|
679
662
|
* @returns {HttpRequestBuilder} this builder
|
|
680
663
|
*/
|
|
681
664
|
header(k, v) {
|
|
@@ -687,7 +670,7 @@ class HttpRequestBuilder {
|
|
|
687
670
|
return this;
|
|
688
671
|
}
|
|
689
672
|
/**
|
|
690
|
-
*
|
|
673
|
+
* Sets each query parameter in place, replacing a parameter of the same name; a null or undefined value removes it.
|
|
691
674
|
* @param {URLSearchParams|Record<string,string|null|undefined>|string[][]|string} ps
|
|
692
675
|
* @returns {HttpRequestBuilder} this builder
|
|
693
676
|
*/
|
|
@@ -702,20 +685,14 @@ class HttpRequestBuilder {
|
|
|
702
685
|
return this;
|
|
703
686
|
}
|
|
704
687
|
/**
|
|
705
|
-
*
|
|
688
|
+
* 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.
|
|
706
689
|
* @param {string} k
|
|
707
690
|
* @param {...string} vs
|
|
708
691
|
* @returns {HttpRequestBuilder} this builder
|
|
709
692
|
*/
|
|
710
693
|
param(k, ...vs) {
|
|
711
|
-
//overriding, as header, headers and params all do: pass every value in one
|
|
712
|
-
//call to get a multi valued parameter
|
|
713
694
|
this.#params.delete(k);
|
|
714
|
-
const
|
|
715
|
-
if (values.length === 0) {
|
|
716
|
-
return this;
|
|
717
|
-
}
|
|
718
|
-
for (const v of values) {
|
|
695
|
+
for (const v of vs.filter((v) => v != null)) {
|
|
719
696
|
this.#params.append(k, v);
|
|
720
697
|
}
|
|
721
698
|
return this;
|
|
@@ -750,6 +727,7 @@ class HttpRequestBuilder {
|
|
|
750
727
|
* Sets the request body as a FormData configured using the callback.
|
|
751
728
|
* `Content-Type: multipart/form-data` header is automatically added by fetch if not explicitly set.
|
|
752
729
|
* @param {(c: HttpMultipartRequestCustomizer) => void} callback
|
|
730
|
+
* @returns {HttpRequestBuilder} this builder
|
|
753
731
|
*/
|
|
754
732
|
multipart(callback) {
|
|
755
733
|
const formData = new FormData();
|
|
@@ -759,7 +737,7 @@ class HttpRequestBuilder {
|
|
|
759
737
|
return this;
|
|
760
738
|
}
|
|
761
739
|
/**
|
|
762
|
-
*
|
|
740
|
+
* Merges fetch options into the request's.
|
|
763
741
|
* @param {Omit<RequestInit,"headers"|"method"|"body">} kvs
|
|
764
742
|
* @returns {HttpRequestBuilder} this builder
|
|
765
743
|
*/
|
|
@@ -780,8 +758,8 @@ class HttpRequestBuilder {
|
|
|
780
758
|
return this;
|
|
781
759
|
}
|
|
782
760
|
/**
|
|
783
|
-
* Adds interceptors
|
|
784
|
-
* @param {[
|
|
761
|
+
* Adds interceptors run for this request only, after the client's.
|
|
762
|
+
* @param {HttpInterceptor[]} is
|
|
785
763
|
* @returns {HttpRequestBuilder} this builder
|
|
786
764
|
*/
|
|
787
765
|
interceptors(is) {
|
|
@@ -791,8 +769,8 @@ class HttpRequestBuilder {
|
|
|
791
769
|
return this;
|
|
792
770
|
}
|
|
793
771
|
/**
|
|
794
|
-
* Adds
|
|
795
|
-
* @param {HttpInterceptor} i
|
|
772
|
+
* Adds one interceptor run for this request only, after the client's.
|
|
773
|
+
* @param {HttpInterceptor} i
|
|
796
774
|
* @returns {HttpRequestBuilder} this builder
|
|
797
775
|
*/
|
|
798
776
|
interceptor(i) {
|
|
@@ -800,8 +778,8 @@ class HttpRequestBuilder {
|
|
|
800
778
|
return this;
|
|
801
779
|
}
|
|
802
780
|
/**
|
|
803
|
-
*
|
|
804
|
-
* @returns {Promise<Response>}
|
|
781
|
+
* Sends the request and answers the Response, whatever its status.
|
|
782
|
+
* @returns {Promise<Response>} rejecting as `HttpClient.exchange` does
|
|
805
783
|
*/
|
|
806
784
|
async exchange() {
|
|
807
785
|
const query = this.#params.size ? `?${this.#params}` : '';
|
|
@@ -814,8 +792,12 @@ class HttpRequestBuilder {
|
|
|
814
792
|
return await this.#client.exchange(`${this.#uri}${query}${this.#fragment}`, opts, this.#interceptors);
|
|
815
793
|
}
|
|
816
794
|
/**
|
|
817
|
-
*
|
|
818
|
-
*
|
|
795
|
+
* Sends the request and answers the Response when its status is 200-299.
|
|
796
|
+
* Always rejects with an HttpClientError: `HttpClientError.fromResponse`
|
|
797
|
+
* for an error status, CONNECTION_PROBLEM for a transport failure, and
|
|
798
|
+
* UNEXPECTED_PROBLEM, carrying the error as its cause, for anything an
|
|
799
|
+
* interceptor throws that is not already a Failure.
|
|
800
|
+
* @returns {Promise<Response>}
|
|
819
801
|
*/
|
|
820
802
|
async fetch() {
|
|
821
803
|
try {
|
|
@@ -828,45 +810,40 @@ class HttpRequestBuilder {
|
|
|
828
810
|
if (ex instanceof Failure) {
|
|
829
811
|
throw ex;
|
|
830
812
|
}
|
|
831
|
-
//fetch() answers a Failure whatever happened, so a caller reading
|
|
832
|
-
//`problems` never has to test the shape first. What reaches here is
|
|
833
|
-
//not the transport, which labels its own failure below the chain: it
|
|
834
|
-
//is a throw from the chain's own code, carried as the cause
|
|
835
813
|
throw HttpClientError.of('UNEXPECTED_PROBLEM', ex);
|
|
836
814
|
}
|
|
837
815
|
}
|
|
838
816
|
/**
|
|
839
|
-
*
|
|
840
|
-
* @returns {Promise<string>}
|
|
817
|
+
* `fetch()`, then the body as text; a body that cannot be read rejects with UNMARSHALING_PROBLEM.
|
|
818
|
+
* @returns {Promise<string>}
|
|
841
819
|
*/
|
|
842
820
|
async fetchText() {
|
|
843
821
|
const response = await this.fetch();
|
|
844
822
|
return await unmarshal(response, 'text');
|
|
845
823
|
}
|
|
846
824
|
/**
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
* @returns {Promise<any>}
|
|
825
|
+
* `fetch()`, then the body parsed as json. A 204 yields null without reading
|
|
826
|
+
* the body; any other body that does not parse rejects with UNMARSHALING_PROBLEM.
|
|
827
|
+
* @returns {Promise<any>}
|
|
850
828
|
*/
|
|
851
829
|
async fetchJson() {
|
|
852
830
|
const response = await this.fetch();
|
|
853
831
|
if (response.status === 204) {
|
|
854
|
-
//a 204 declares no content: there is no body to decode
|
|
855
832
|
return null;
|
|
856
833
|
}
|
|
857
834
|
return await unmarshal(response, 'json');
|
|
858
835
|
}
|
|
859
836
|
/**
|
|
860
|
-
*
|
|
861
|
-
* @returns {Promise<Blob>}
|
|
837
|
+
* `fetch()`, then the body as a Blob; a body that cannot be read rejects with UNMARSHALING_PROBLEM.
|
|
838
|
+
* @returns {Promise<Blob>}
|
|
862
839
|
*/
|
|
863
840
|
async fetchBlob() {
|
|
864
841
|
const response = await this.fetch();
|
|
865
842
|
return await unmarshal(response, 'blob');
|
|
866
843
|
}
|
|
867
844
|
/**
|
|
868
|
-
*
|
|
869
|
-
* @returns {Promise<ArrayBuffer>}
|
|
845
|
+
* `fetch()`, then the body as an ArrayBuffer; a body that cannot be read rejects with UNMARSHALING_PROBLEM.
|
|
846
|
+
* @returns {Promise<ArrayBuffer>}
|
|
870
847
|
*/
|
|
871
848
|
async fetchArrayBuffer() {
|
|
872
849
|
const response = await this.fetch();
|
|
@@ -877,10 +854,7 @@ class HttpRequestBuilder {
|
|
|
877
854
|
/** Builds a multipart body: text fields, json parts, and single or repeated blobs. */
|
|
878
855
|
class HttpMultipartRequestCustomizer {
|
|
879
856
|
#formData;
|
|
880
|
-
/**
|
|
881
|
-
*
|
|
882
|
-
* @param {FormData} formData
|
|
883
|
-
*/
|
|
857
|
+
/** @param {FormData} formData the body being built */
|
|
884
858
|
constructor(formData) {
|
|
885
859
|
this.#formData = formData;
|
|
886
860
|
}
|
|
@@ -888,33 +862,30 @@ class HttpMultipartRequestCustomizer {
|
|
|
888
862
|
* Appends a value to the FormData.
|
|
889
863
|
* @param {string} name
|
|
890
864
|
* @param {*} value
|
|
891
|
-
* @returns this builder
|
|
865
|
+
* @returns {HttpMultipartRequestCustomizer} this builder
|
|
892
866
|
*/
|
|
893
867
|
field(name, value) {
|
|
894
868
|
this.#formData.append(name, value);
|
|
895
869
|
return this;
|
|
896
870
|
}
|
|
897
871
|
/**
|
|
898
|
-
* Appends a Blob to the FormData.
|
|
899
|
-
*
|
|
900
|
-
* The default filename for Blob objects is "blob";
|
|
901
|
-
* The default filename for File objects is the file's filename.
|
|
872
|
+
* Appends a Blob to the FormData. Without a filename a File keeps its own
|
|
873
|
+
* and any other Blob is named "blob".
|
|
902
874
|
* @param {string} name
|
|
903
875
|
* @param {Blob} value
|
|
904
|
-
* @param {string
|
|
905
|
-
* @returns this builder
|
|
876
|
+
* @param {string} [filename]
|
|
877
|
+
* @returns {HttpMultipartRequestCustomizer} this builder
|
|
906
878
|
*/
|
|
907
879
|
blob(name, value, filename) {
|
|
908
880
|
this.#formData.append(name, value, filename);
|
|
909
881
|
return this;
|
|
910
882
|
}
|
|
911
883
|
/**
|
|
912
|
-
* Appends
|
|
913
|
-
*
|
|
914
|
-
* The default filename for File objects is the file's filename.
|
|
884
|
+
* Appends each Blob under the same name, a File keeping its own filename and
|
|
885
|
+
* any other Blob named "blob".
|
|
915
886
|
* @param {string} name
|
|
916
887
|
* @param {Blob[]} values
|
|
917
|
-
* @returns this builder
|
|
888
|
+
* @returns {HttpMultipartRequestCustomizer} this builder
|
|
918
889
|
*/
|
|
919
890
|
blobs(name, values) {
|
|
920
891
|
for (const v of values) {
|
|
@@ -923,11 +894,11 @@ class HttpMultipartRequestCustomizer {
|
|
|
923
894
|
return this;
|
|
924
895
|
}
|
|
925
896
|
/**
|
|
926
|
-
* Appends
|
|
897
|
+
* Appends the value serialized as an `application/json` blob.
|
|
927
898
|
* @param {string} name
|
|
928
899
|
* @param {any} value
|
|
929
|
-
* @param {string
|
|
930
|
-
* @returns this builder
|
|
900
|
+
* @param {string} [filename]
|
|
901
|
+
* @returns {HttpMultipartRequestCustomizer} this builder
|
|
931
902
|
*/
|
|
932
903
|
json(name, value, filename) {
|
|
933
904
|
const blob = new Blob([JSON.stringify(value)], { type: 'application/json' });
|