@optionfactory/fml 8.0.3 → 9.0.0-rc1
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/LICENSE.md +7 -0
- package/README.md +88 -0
- package/dist/client-errors.iife.js +30 -9
- package/dist/client-errors.iife.js.map +1 -1
- package/dist/client-errors.iife.min.js +1 -1
- package/dist/client-errors.iife.min.js.map +1 -1
- package/dist/custom-elements.json +1529 -435
- package/dist/fml.css +21 -10
- package/dist/fml.css.map +1 -1
- package/dist/fml.d.mts +3 -1322
- package/dist/fml.iife.js +5267 -2276
- 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/fml.min.mjs +1 -1
- package/dist/fml.min.mjs.map +1 -1
- package/dist/fml.mjs +6 -8737
- package/dist/fml.mjs.map +1 -1
- package/dist/ftl.d.mts +430 -92
- package/dist/ftl.iife.js +1314 -809
- 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 +1313 -810
- package/dist/ftl.mjs.map +1 -1
- package/dist/ful.css +21 -10
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +801 -252
- package/dist/ful.iife.js +3677 -1378
- 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 +3666 -1378
- package/dist/ful.mjs.map +1 -1
- package/dist/httpc.d.mts +114 -19
- package/dist/httpc.iife.js +253 -83
- 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 +250 -84
- package/dist/httpc.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +600 -58
- package/dist/web-types.json +1475 -380
- package/package.json +16 -8
package/dist/httpc.iife.js
CHANGED
|
@@ -4,6 +4,11 @@ var httpc = (function (exports) {
|
|
|
4
4
|
/**
|
|
5
5
|
* @typedef {{ type: string; context: string?; reason: string; details: any?; }} Problem
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* An error carrying a list of problems rather than one message. A problem's
|
|
9
|
+
* `context` names the field it belongs to, which is what lets a form show each
|
|
10
|
+
* one beside its own input instead of in a banner.
|
|
11
|
+
*/
|
|
7
12
|
class Failure extends Error {
|
|
8
13
|
/**
|
|
9
14
|
*
|
|
@@ -16,18 +21,55 @@ var httpc = (function (exports) {
|
|
|
16
21
|
this.name = 'Failure';
|
|
17
22
|
this.problems = problems;
|
|
18
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Returns a copy whose problems' contexts have the prefix removed, so a
|
|
26
|
+
* caller can rethrow namespaced problems as its own.
|
|
27
|
+
* @param {string} prefix
|
|
28
|
+
* @returns {Failure}
|
|
29
|
+
*/
|
|
19
30
|
dropping(prefix) {
|
|
20
31
|
return new Failure(this.message, Failure.dropProblemsContext(this.problems, prefix), this);
|
|
21
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* @param {Problem[]} problems
|
|
35
|
+
* @param {string} prefix
|
|
36
|
+
* @returns {Problem[]}
|
|
37
|
+
*/
|
|
22
38
|
static dropProblemsContext(problems, prefix) {
|
|
23
39
|
return problems.map(({ type, context, reason, details }) => {
|
|
24
40
|
const nctx = context?.startsWith(prefix) ? context.substring(prefix.length) : context;
|
|
25
41
|
return { type, context: nctx, reason, details };
|
|
26
42
|
});
|
|
27
43
|
}
|
|
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
|
+
}
|
|
28
59
|
}
|
|
29
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Base64 encoding and decoding over ArrayBuffers, in the STANDARD and URL_SAFE
|
|
63
|
+
* alphabets. The encoder never emits padding; the decoder accepts both padded
|
|
64
|
+
* and unpadded input, and rejects anything it cannot decode faithfully instead
|
|
65
|
+
* of corrupting silently.
|
|
66
|
+
*/
|
|
30
67
|
class Base64 {
|
|
68
|
+
/**
|
|
69
|
+
* @param {ArrayBuffer} arrayBuffer
|
|
70
|
+
* @param {string} [dialect] one of Base64.STANDARD or Base64.URL_SAFE, URL_SAFE by default
|
|
71
|
+
* @returns {string} the unpadded encoding
|
|
72
|
+
*/
|
|
31
73
|
static encode(arrayBuffer, dialect) {
|
|
32
74
|
const d = dialect || Base64.URL_SAFE;
|
|
33
75
|
const len = arrayBuffer.byteLength;
|
|
@@ -47,15 +89,32 @@ var httpc = (function (exports) {
|
|
|
47
89
|
}
|
|
48
90
|
return res;
|
|
49
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} str
|
|
94
|
+
* @param {string} [dialect] one of Base64.STANDARD or Base64.URL_SAFE, URL_SAFE by default
|
|
95
|
+
* @returns {ArrayBuffer}
|
|
96
|
+
*/
|
|
50
97
|
static decode(str, dialect) {
|
|
51
98
|
const d = dialect || Base64.URL_SAFE;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
99
|
+
//padding belongs at the tail only, two at most, and nothing outside the
|
|
100
|
+
//dialect decodes: reject instead of corrupting silently
|
|
101
|
+
let end = str.length;
|
|
102
|
+
while (end > 0 && str.charAt(end - 1) === '=') {
|
|
103
|
+
--end;
|
|
104
|
+
}
|
|
105
|
+
const unpadded = str.substring(0, end);
|
|
106
|
+
if (str.length - end > 2 || unpadded.includes('=') || (unpadded.length === 0 && str.length > 0)) {
|
|
107
|
+
throw new Error('invalid padding');
|
|
108
|
+
}
|
|
109
|
+
if (unpadded.length % 4 === 1) {
|
|
110
|
+
throw new Error('invalid length');
|
|
111
|
+
}
|
|
112
|
+
for (const c of unpadded) {
|
|
113
|
+
if (d.indexOf(c) === -1) {
|
|
114
|
+
throw new Error(`invalid character '${c}'`);
|
|
56
115
|
}
|
|
57
|
-
--nbytes;
|
|
58
116
|
}
|
|
117
|
+
const nbytes = Math.floor(unpadded.length * 0.75);
|
|
59
118
|
const view = new Uint8Array(nbytes);
|
|
60
119
|
|
|
61
120
|
let vi = 0;
|
|
@@ -64,10 +123,10 @@ var httpc = (function (exports) {
|
|
|
64
123
|
//one or two: writing them anyway would rely on typed arrays dropping writes
|
|
65
124
|
//past their length
|
|
66
125
|
while (vi < nbytes) {
|
|
67
|
-
const v1 = d.indexOf(
|
|
68
|
-
const v2 = d.indexOf(
|
|
69
|
-
const v3 = d.indexOf(
|
|
70
|
-
const v4 = d.indexOf(
|
|
126
|
+
const v1 = d.indexOf(unpadded.charAt(si++));
|
|
127
|
+
const v2 = d.indexOf(unpadded.charAt(si++));
|
|
128
|
+
const v3 = d.indexOf(unpadded.charAt(si++));
|
|
129
|
+
const v4 = d.indexOf(unpadded.charAt(si++));
|
|
71
130
|
view[vi++] = (v1 << 2) | (v2 >> 4);
|
|
72
131
|
if (vi === nbytes) {
|
|
73
132
|
break;
|
|
@@ -86,11 +145,21 @@ var httpc = (function (exports) {
|
|
|
86
145
|
Base64.STANDARD = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
87
146
|
Base64.URL_SAFE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
88
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Hex encoding and decoding over byte sequences, lowercase by default.
|
|
150
|
+
*/
|
|
89
151
|
class Hex {
|
|
152
|
+
/**
|
|
153
|
+
* @param {string} hex
|
|
154
|
+
* @returns {Uint8Array}
|
|
155
|
+
*/
|
|
90
156
|
static decode(hex) {
|
|
91
157
|
if (hex.length % 2 !== 0) {
|
|
92
158
|
throw new Error('invalid length');
|
|
93
159
|
}
|
|
160
|
+
if (!/^[0-9a-fA-F]*$/.test(hex)) {
|
|
161
|
+
throw new Error('invalid character');
|
|
162
|
+
}
|
|
94
163
|
const lenInBytes = hex.length / 2;
|
|
95
164
|
return new Uint8Array(lenInBytes).map((e, i) => {
|
|
96
165
|
const offset = i * 2;
|
|
@@ -98,6 +167,11 @@ var httpc = (function (exports) {
|
|
|
98
167
|
return parseInt(octet, 16);
|
|
99
168
|
});
|
|
100
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* @param {Iterable<number>} bytes
|
|
172
|
+
* @param {boolean} [upper]
|
|
173
|
+
* @returns {string}
|
|
174
|
+
*/
|
|
101
175
|
static encode(bytes, upper) {
|
|
102
176
|
return Array.from(bytes)
|
|
103
177
|
.map((b) => b.toString(16))
|
|
@@ -107,6 +181,7 @@ var httpc = (function (exports) {
|
|
|
107
181
|
}
|
|
108
182
|
}
|
|
109
183
|
|
|
184
|
+
/** A parsed `Content-Type`: the type and subtype without the parameters, so a comparison is not defeated by a charset. */
|
|
110
185
|
class MediaType {
|
|
111
186
|
#type;
|
|
112
187
|
#subtype;
|
|
@@ -124,7 +199,7 @@ var httpc = (function (exports) {
|
|
|
124
199
|
return this.#subtype;
|
|
125
200
|
}
|
|
126
201
|
/**
|
|
127
|
-
*
|
|
202
|
+
* Parses a Content-Type header value into its type/subtype pair, dropping any parameter.
|
|
128
203
|
* @param {string|null|undefined} v
|
|
129
204
|
* @returns
|
|
130
205
|
*/
|
|
@@ -133,8 +208,11 @@ var httpc = (function (exports) {
|
|
|
133
208
|
return new MediaType('unknown', 'unknown');
|
|
134
209
|
}
|
|
135
210
|
const [prefix, _] = v.split(';');
|
|
136
|
-
const [ptype, psubtype] = prefix.trim().split('/');
|
|
137
|
-
|
|
211
|
+
const [ptype, psubtype] = prefix.trim().toLowerCase().split('/');
|
|
212
|
+
if (!ptype || !psubtype) {
|
|
213
|
+
return new MediaType('unknown', 'unknown');
|
|
214
|
+
}
|
|
215
|
+
return new MediaType(ptype, psubtype);
|
|
138
216
|
}
|
|
139
217
|
}
|
|
140
218
|
|
|
@@ -146,6 +224,11 @@ var httpc = (function (exports) {
|
|
|
146
224
|
* @property {(url: URL, init: RequestInit|undefined, chain: HttpInterceptorChain) => Promise<Response>} intercept
|
|
147
225
|
*/
|
|
148
226
|
|
|
227
|
+
/**
|
|
228
|
+
* A Failure from an http exchange, carrying the status that was served. Status
|
|
229
|
+
* 0 means no response was served at all: the transport failed, or a body the
|
|
230
|
+
* server did send could not be read.
|
|
231
|
+
*/
|
|
149
232
|
class HttpClientError extends Failure {
|
|
150
233
|
/**
|
|
151
234
|
* @param {string} message
|
|
@@ -158,29 +241,33 @@ var httpc = (function (exports) {
|
|
|
158
241
|
this.name = 'HttpClientError';
|
|
159
242
|
this.status = status;
|
|
160
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Returns a copy whose problems' contexts have the prefix removed, keeping
|
|
246
|
+
* this error's status.
|
|
247
|
+
* @param {string} prefix
|
|
248
|
+
* @returns {HttpClientError}
|
|
249
|
+
*/
|
|
161
250
|
dropping(prefix) {
|
|
162
251
|
return new HttpClientError(this.message, this.status, Failure.dropProblemsContext(this.problems, prefix), this);
|
|
163
252
|
}
|
|
164
253
|
/**
|
|
165
|
-
*
|
|
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.
|
|
256
|
+
* @param {string} type
|
|
257
|
+
* @param {string} reason
|
|
258
|
+
*/
|
|
259
|
+
static problem(type, reason) {
|
|
260
|
+
return { type, context: null, reason, details: null };
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Creates a client failure carrying no status, wrapping the cause and its message.
|
|
166
264
|
* @param {string} type
|
|
167
265
|
* @param {any} cause
|
|
168
266
|
* @returns
|
|
169
267
|
*/
|
|
170
268
|
static of(type, cause) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
0,
|
|
174
|
-
[
|
|
175
|
-
{
|
|
176
|
-
type,
|
|
177
|
-
context: null,
|
|
178
|
-
reason: cause.message,
|
|
179
|
-
details: null,
|
|
180
|
-
},
|
|
181
|
-
],
|
|
182
|
-
cause,
|
|
183
|
-
);
|
|
269
|
+
const reason = String(cause?.message ?? cause ?? 'unknown failure');
|
|
270
|
+
return new HttpClientError(reason, 0, [HttpClientError.problem(type, reason)], cause);
|
|
184
271
|
}
|
|
185
272
|
/**
|
|
186
273
|
* Creates an HttpClientError from a Response.
|
|
@@ -190,55 +277,86 @@ var httpc = (function (exports) {
|
|
|
190
277
|
static async fromResponse(response) {
|
|
191
278
|
switch (MediaType.parse(response.headers.get('Content-Type')).normalized) {
|
|
192
279
|
case 'application/failures+json': {
|
|
193
|
-
const data = await response.json();
|
|
280
|
+
const data = await response.json().catch(() => HttpClientError.#unreadable);
|
|
281
|
+
if (data === HttpClientError.#unreadable) {
|
|
282
|
+
return HttpClientError.#undecodable(response);
|
|
283
|
+
}
|
|
284
|
+
if (!Array.isArray(data)) {
|
|
285
|
+
return HttpClientError.#undecodable(response, 'as a failures array');
|
|
286
|
+
}
|
|
194
287
|
const message = `${response.status} ${response.statusText}: ${data.length} failures`;
|
|
195
288
|
return new HttpClientError(message, response.status, data);
|
|
196
289
|
}
|
|
197
290
|
case 'application/problem+json': {
|
|
198
|
-
const data = await response.json();
|
|
291
|
+
const data = await response.json().catch(() => HttpClientError.#unreadable);
|
|
292
|
+
if (data === HttpClientError.#unreadable) {
|
|
293
|
+
return HttpClientError.#undecodable(response);
|
|
294
|
+
}
|
|
295
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
296
|
+
return HttpClientError.#undecodable(response, 'as a problem object');
|
|
297
|
+
}
|
|
199
298
|
const message = `${response.status} ${response.statusText}: ${data.title} ${data.detail}`;
|
|
200
299
|
return new HttpClientError(
|
|
201
300
|
message,
|
|
202
301
|
response.status,
|
|
203
|
-
data.problems || [
|
|
204
|
-
{
|
|
205
|
-
type: 'GENERIC_PROBLEM',
|
|
206
|
-
context: null,
|
|
207
|
-
reason: message,
|
|
208
|
-
details: null,
|
|
209
|
-
},
|
|
210
|
-
],
|
|
302
|
+
data.problems || [HttpClientError.problem('GENERIC_PROBLEM', message)],
|
|
211
303
|
);
|
|
212
304
|
}
|
|
213
305
|
default: {
|
|
214
|
-
|
|
215
|
-
const message = `${response.status} ${response.statusText}: ${text}`;
|
|
216
|
-
return new HttpClientError(message, response.status, [
|
|
217
|
-
{
|
|
218
|
-
type: 'GENERIC_PROBLEM',
|
|
219
|
-
context: null,
|
|
220
|
-
reason: message,
|
|
221
|
-
details: null,
|
|
222
|
-
},
|
|
223
|
-
]);
|
|
306
|
+
return HttpClientError.#generic(response);
|
|
224
307
|
}
|
|
225
308
|
}
|
|
226
309
|
}
|
|
310
|
+
/** marks a body whose json() rejected, telling it apart from a body decoding to json null */
|
|
311
|
+
static #unreadable = Symbol('unreadable body');
|
|
312
|
+
/**
|
|
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
|
+
* @param {Response} response
|
|
317
|
+
* @param {string} [as] - what the body does not decode as
|
|
318
|
+
*/
|
|
319
|
+
static #undecodable(response, as = 'as json') {
|
|
320
|
+
const mediaType = MediaType.parse(response.headers.get('Content-Type')).normalized;
|
|
321
|
+
const message = `${response.status} ${response.statusText}: the ${mediaType} body does not decode ${as}`;
|
|
322
|
+
return new HttpClientError(message, response.status, [HttpClientError.problem('GENERIC_PROBLEM', message)]);
|
|
323
|
+
}
|
|
324
|
+
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
|
+
const text = await response.text().catch(() => null);
|
|
329
|
+
const message =
|
|
330
|
+
text === null
|
|
331
|
+
? `${response.status} ${response.statusText}: the body could not be read`
|
|
332
|
+
: `${response.status} ${response.statusText}: ${text}`;
|
|
333
|
+
return new HttpClientError(message, response.status, [HttpClientError.problem('GENERIC_PROBLEM', message)]);
|
|
334
|
+
}
|
|
227
335
|
}
|
|
228
336
|
|
|
337
|
+
const metaContent = (name) =>
|
|
338
|
+
globalThis.document?.querySelector(`meta[name="${name}"]`)?.getAttribute('content') ?? undefined;
|
|
339
|
+
|
|
229
340
|
/**
|
|
230
341
|
* @implements {HttpInterceptor}
|
|
231
342
|
*/
|
|
343
|
+
/**
|
|
344
|
+
* Sends the csrf header named by the page's `_csrf_header` meta, with the token
|
|
345
|
+
* from `_csrf`. Both are read per request, so metas replaced after the client
|
|
346
|
+
* was built are honoured, and the header is sent to the page's own origin only.
|
|
347
|
+
*/
|
|
232
348
|
class CsrfTokenInterceptor {
|
|
233
|
-
#k;
|
|
234
|
-
#v;
|
|
235
|
-
constructor() {
|
|
236
|
-
this.#k = document.querySelector("meta[name='_csrf_header']")?.getAttribute('content');
|
|
237
|
-
this.#v = document.querySelector("meta[name='_csrf']")?.getAttribute('content');
|
|
238
|
-
}
|
|
239
349
|
async intercept(url, request, chain) {
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
if (url.origin !== (globalThis.window?.location?.origin ?? url.origin)) {
|
|
354
|
+
return await chain.proceed(url, request);
|
|
355
|
+
}
|
|
356
|
+
const csrfHeader = metaContent('_csrf_header');
|
|
357
|
+
const csrfToken = metaContent('_csrf');
|
|
358
|
+
if (csrfHeader && csrfToken) {
|
|
359
|
+
request.headers.set(csrfHeader, csrfToken);
|
|
242
360
|
}
|
|
243
361
|
return await chain.proceed(url, request);
|
|
244
362
|
}
|
|
@@ -246,6 +364,11 @@ var httpc = (function (exports) {
|
|
|
246
364
|
/**
|
|
247
365
|
* @implements {HttpInterceptor}
|
|
248
366
|
*/
|
|
367
|
+
/**
|
|
368
|
+
* Navigates to a login url when a response comes back 401. The promise it
|
|
369
|
+
* returns never settles, so callers keep waiting while the page unloads
|
|
370
|
+
* instead of showing a failure nobody will be present to read.
|
|
371
|
+
*/
|
|
249
372
|
class RedirectOnUnauthorizedInterceptor {
|
|
250
373
|
#redirectUri;
|
|
251
374
|
/**
|
|
@@ -258,12 +381,17 @@ var httpc = (function (exports) {
|
|
|
258
381
|
const response = await chain.proceed(url, request);
|
|
259
382
|
if (response.status === 401) {
|
|
260
383
|
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
|
|
261
388
|
return new Promise(() => {});
|
|
262
389
|
}
|
|
263
390
|
return response;
|
|
264
391
|
}
|
|
265
392
|
}
|
|
266
393
|
|
|
394
|
+
/** Collects the interceptors an HttpClient will run, in the order they are added. */
|
|
267
395
|
class HttpClientBuilder {
|
|
268
396
|
/**
|
|
269
397
|
* @type {HttpInterceptor[]}
|
|
@@ -295,12 +423,21 @@ var httpc = (function (exports) {
|
|
|
295
423
|
/**
|
|
296
424
|
* @implements {HttpInterceptor}
|
|
297
425
|
*/
|
|
426
|
+
/** The last interceptor in every chain: the one that performs the request. */
|
|
298
427
|
class HttpCall {
|
|
299
428
|
async intercept(url, request, chain) {
|
|
300
|
-
|
|
429
|
+
try {
|
|
430
|
+
return await fetch(url, request);
|
|
431
|
+
} 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
|
+
throw HttpClientError.of('CONNECTION_PROBLEM', ex);
|
|
436
|
+
}
|
|
301
437
|
}
|
|
302
438
|
}
|
|
303
439
|
|
|
440
|
+
/** One request's position in the interceptor list: `proceed` runs the next interceptor, the last of which performs the request. */
|
|
304
441
|
class HttpInterceptorChain {
|
|
305
442
|
#interceptors;
|
|
306
443
|
#current;
|
|
@@ -329,6 +466,11 @@ var httpc = (function (exports) {
|
|
|
329
466
|
}
|
|
330
467
|
}
|
|
331
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Performs http requests through a fixed list of interceptors. The verbs
|
|
471
|
+
* return a request builder; `exchange` is the lower-level entry that returns
|
|
472
|
+
* the Response itself without treating an error status as a failure.
|
|
473
|
+
*/
|
|
332
474
|
class HttpClient {
|
|
333
475
|
#interceptors;
|
|
334
476
|
/**
|
|
@@ -357,7 +499,8 @@ var httpc = (function (exports) {
|
|
|
357
499
|
const is = [...this.#interceptors, ...(interceptors || []), new HttpCall()];
|
|
358
500
|
const chain = new HttpInterceptorChain(is, 0);
|
|
359
501
|
const url = new URL(new Request(uri).url);
|
|
360
|
-
|
|
502
|
+
const request = { ...options, headers: new Headers(options?.headers) };
|
|
503
|
+
return await chain.proceed(url, request);
|
|
361
504
|
}
|
|
362
505
|
/**
|
|
363
506
|
* Creates a request builder.
|
|
@@ -419,7 +562,7 @@ var httpc = (function (exports) {
|
|
|
419
562
|
}
|
|
420
563
|
|
|
421
564
|
/**
|
|
422
|
-
*
|
|
565
|
+
* Reads the response body as the given type, wrapping a failed read as an UNMARSHALING_PROBLEM.
|
|
423
566
|
* @param {Response} response
|
|
424
567
|
* @param {'text'|'json'|'blob'|'arrayBuffer'} type
|
|
425
568
|
* @returns
|
|
@@ -452,6 +595,12 @@ var httpc = (function (exports) {
|
|
|
452
595
|
return Object.entries(source);
|
|
453
596
|
};
|
|
454
597
|
|
|
598
|
+
/**
|
|
599
|
+
* One request under construction: method, url, parameters, headers and body,
|
|
600
|
+
* with a `fetch*` method per body type. Every configuration method returns the
|
|
601
|
+
* builder, and a `fetch*` rejects with an HttpClientError for any status
|
|
602
|
+
* outside 200-299.
|
|
603
|
+
*/
|
|
455
604
|
class HttpRequestBuilder {
|
|
456
605
|
#client;
|
|
457
606
|
#method;
|
|
@@ -461,6 +610,7 @@ var httpc = (function (exports) {
|
|
|
461
610
|
#body;
|
|
462
611
|
#options;
|
|
463
612
|
#interceptors;
|
|
613
|
+
#fragment;
|
|
464
614
|
/**
|
|
465
615
|
* Creates an HttpRequestBuilder.
|
|
466
616
|
* @param {HttpClient} client
|
|
@@ -469,16 +619,22 @@ var httpc = (function (exports) {
|
|
|
469
619
|
* @returns {HttpRequestBuilder} the builder
|
|
470
620
|
*/
|
|
471
621
|
static create(client, method, uri) {
|
|
472
|
-
|
|
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
|
+
const hashIndex = uri.indexOf('#');
|
|
625
|
+
const fragment = hashIndex === -1 ? '' : uri.slice(hashIndex);
|
|
626
|
+
const withoutFragment = hashIndex === -1 ? uri : uri.slice(0, hashIndex);
|
|
627
|
+
const queryIndex = withoutFragment.indexOf('?');
|
|
473
628
|
return new HttpRequestBuilder(
|
|
474
629
|
client,
|
|
475
630
|
method,
|
|
476
|
-
|
|
477
|
-
new URLSearchParams(
|
|
631
|
+
queryIndex === -1 ? withoutFragment : withoutFragment.slice(0, queryIndex),
|
|
632
|
+
new URLSearchParams(queryIndex === -1 ? '' : withoutFragment.slice(queryIndex + 1)),
|
|
478
633
|
new Headers(),
|
|
479
634
|
undefined,
|
|
480
635
|
{},
|
|
481
636
|
[],
|
|
637
|
+
fragment,
|
|
482
638
|
);
|
|
483
639
|
}
|
|
484
640
|
/**
|
|
@@ -491,8 +647,9 @@ var httpc = (function (exports) {
|
|
|
491
647
|
* @param {any} body
|
|
492
648
|
* @param {Omit<RequestInit,"headers"|"method"|"body">} options
|
|
493
649
|
* @param {HttpInterceptor[]} interceptors
|
|
650
|
+
* @param {string} [fragment]
|
|
494
651
|
*/
|
|
495
|
-
constructor(client, method, uri, params, headers, body, options, interceptors) {
|
|
652
|
+
constructor(client, method, uri, params, headers, body, options, interceptors, fragment = '') {
|
|
496
653
|
this.#client = client;
|
|
497
654
|
this.#method = method;
|
|
498
655
|
this.#uri = uri;
|
|
@@ -501,6 +658,7 @@ var httpc = (function (exports) {
|
|
|
501
658
|
this.#headers = headers;
|
|
502
659
|
this.#options = options;
|
|
503
660
|
this.#interceptors = interceptors;
|
|
661
|
+
this.#fragment = fragment;
|
|
504
662
|
}
|
|
505
663
|
/**
|
|
506
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.
|
|
@@ -547,7 +705,7 @@ var httpc = (function (exports) {
|
|
|
547
705
|
return this;
|
|
548
706
|
}
|
|
549
707
|
/**
|
|
550
|
-
* Adds a query parameter to the request, overriding it if it already exists.
|
|
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.
|
|
551
709
|
* @param {string} k
|
|
552
710
|
* @param {...string} vs
|
|
553
711
|
* @returns {HttpRequestBuilder} this builder
|
|
@@ -556,10 +714,11 @@ var httpc = (function (exports) {
|
|
|
556
714
|
//overriding, as header, headers and params all do: pass every value in one
|
|
557
715
|
//call to get a multi valued parameter
|
|
558
716
|
this.#params.delete(k);
|
|
559
|
-
|
|
717
|
+
const values = vs.filter((v) => v != null);
|
|
718
|
+
if (values.length === 0) {
|
|
560
719
|
return this;
|
|
561
720
|
}
|
|
562
|
-
for (const v of
|
|
721
|
+
for (const v of values) {
|
|
563
722
|
this.#params.append(k, v);
|
|
564
723
|
}
|
|
565
724
|
return this;
|
|
@@ -582,8 +741,12 @@ var httpc = (function (exports) {
|
|
|
582
741
|
* @returns {HttpRequestBuilder} this builder
|
|
583
742
|
*/
|
|
584
743
|
json(body) {
|
|
744
|
+
if (body === undefined) {
|
|
745
|
+
return this;
|
|
746
|
+
}
|
|
747
|
+
const serialized = JSON.stringify(body);
|
|
585
748
|
this.#headers.set('Content-Type', 'application/json');
|
|
586
|
-
this.#body =
|
|
749
|
+
this.#body = serialized;
|
|
587
750
|
return this;
|
|
588
751
|
}
|
|
589
752
|
/**
|
|
@@ -621,7 +784,7 @@ var httpc = (function (exports) {
|
|
|
621
784
|
}
|
|
622
785
|
/**
|
|
623
786
|
* Adds interceptors to the request.
|
|
624
|
-
* @param {[HttpInterceptor]} is - the interceptor to be
|
|
787
|
+
* @param {[HttpInterceptor]} is - the interceptor to be registered
|
|
625
788
|
* @returns {HttpRequestBuilder} this builder
|
|
626
789
|
*/
|
|
627
790
|
interceptors(is) {
|
|
@@ -632,7 +795,7 @@ var httpc = (function (exports) {
|
|
|
632
795
|
}
|
|
633
796
|
/**
|
|
634
797
|
* Adds an interceptor to the request.
|
|
635
|
-
* @param {HttpInterceptor} i - the interceptor to be
|
|
798
|
+
* @param {HttpInterceptor} i - the interceptor to be registered
|
|
636
799
|
* @returns {HttpRequestBuilder} this builder
|
|
637
800
|
*/
|
|
638
801
|
interceptor(i) {
|
|
@@ -644,29 +807,22 @@ var httpc = (function (exports) {
|
|
|
644
807
|
* @returns {Promise<Response>} the response
|
|
645
808
|
*/
|
|
646
809
|
async exchange() {
|
|
647
|
-
const
|
|
810
|
+
const query = this.#params.size ? `?${this.#params}` : '';
|
|
648
811
|
const opts = {
|
|
649
812
|
...this.#options,
|
|
650
813
|
headers: this.#headers,
|
|
651
814
|
method: this.#method,
|
|
652
815
|
body: this.#body,
|
|
653
816
|
};
|
|
654
|
-
return await this.#client.exchange(uri
|
|
817
|
+
return await this.#client.exchange(`${this.#uri}${query}${this.#fragment}`, opts, this.#interceptors);
|
|
655
818
|
}
|
|
656
819
|
/**
|
|
657
|
-
* Performs an HTTP exchange using the configured client request, and
|
|
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.
|
|
658
821
|
* @returns {Promise<Response>} the response
|
|
659
822
|
*/
|
|
660
823
|
async fetch() {
|
|
661
|
-
const uri = this.#params.size ? `${this.#uri}?${this.#params}` : this.#uri;
|
|
662
|
-
const opts = {
|
|
663
|
-
...this.#options,
|
|
664
|
-
headers: this.#headers,
|
|
665
|
-
method: this.#method,
|
|
666
|
-
body: this.#body,
|
|
667
|
-
};
|
|
668
824
|
try {
|
|
669
|
-
const response = await this
|
|
825
|
+
const response = await this.exchange();
|
|
670
826
|
if (!response.ok) {
|
|
671
827
|
throw await HttpClientError.fromResponse(response);
|
|
672
828
|
}
|
|
@@ -675,11 +831,15 @@ var httpc = (function (exports) {
|
|
|
675
831
|
if (ex instanceof Failure) {
|
|
676
832
|
throw ex;
|
|
677
833
|
}
|
|
678
|
-
|
|
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
|
+
throw HttpClientError.of('UNEXPECTED_PROBLEM', ex);
|
|
679
839
|
}
|
|
680
840
|
}
|
|
681
841
|
/**
|
|
682
|
-
* Performs an HTTP exchange using the configured client request, and
|
|
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.
|
|
683
843
|
* @returns {Promise<string>} the response body, as text
|
|
684
844
|
*/
|
|
685
845
|
async fetchText() {
|
|
@@ -687,15 +847,20 @@ var httpc = (function (exports) {
|
|
|
687
847
|
return await unmarshal(response, 'text');
|
|
688
848
|
}
|
|
689
849
|
/**
|
|
690
|
-
* Performs an HTTP exchange using the configured client request, and
|
|
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.
|
|
691
852
|
* @returns {Promise<any>} the response body, deserialized as JSON
|
|
692
853
|
*/
|
|
693
854
|
async fetchJson() {
|
|
694
855
|
const response = await this.fetch();
|
|
856
|
+
if (response.status === 204) {
|
|
857
|
+
//a 204 declares no content: there is no body to decode
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
695
860
|
return await unmarshal(response, 'json');
|
|
696
861
|
}
|
|
697
862
|
/**
|
|
698
|
-
* Performs an HTTP exchange using the configured client request, and
|
|
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.
|
|
699
864
|
* @returns {Promise<Blob>} the response body, as a Blob
|
|
700
865
|
*/
|
|
701
866
|
async fetchBlob() {
|
|
@@ -703,7 +868,7 @@ var httpc = (function (exports) {
|
|
|
703
868
|
return await unmarshal(response, 'blob');
|
|
704
869
|
}
|
|
705
870
|
/**
|
|
706
|
-
* Performs an HTTP exchange using the configured client request, and
|
|
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.
|
|
707
872
|
* @returns {Promise<ArrayBuffer>} the response body, as an ArrayBuffer
|
|
708
873
|
*/
|
|
709
874
|
async fetchArrayBuffer() {
|
|
@@ -712,6 +877,7 @@ var httpc = (function (exports) {
|
|
|
712
877
|
}
|
|
713
878
|
}
|
|
714
879
|
|
|
880
|
+
/** Builds a multipart body: text fields, json parts, and single or repeated blobs. */
|
|
715
881
|
class HttpMultipartRequestCustomizer {
|
|
716
882
|
#formData;
|
|
717
883
|
/**
|
|
@@ -777,7 +943,11 @@ var httpc = (function (exports) {
|
|
|
777
943
|
exports.Failure = Failure;
|
|
778
944
|
exports.Hex = Hex;
|
|
779
945
|
exports.HttpClient = HttpClient;
|
|
946
|
+
exports.HttpClientBuilder = HttpClientBuilder;
|
|
780
947
|
exports.HttpClientError = HttpClientError;
|
|
948
|
+
exports.HttpInterceptorChain = HttpInterceptorChain;
|
|
949
|
+
exports.HttpMultipartRequestCustomizer = HttpMultipartRequestCustomizer;
|
|
950
|
+
exports.HttpRequestBuilder = HttpRequestBuilder;
|
|
781
951
|
exports.MediaType = MediaType;
|
|
782
952
|
|
|
783
953
|
return exports;
|