@fgv/ts-web-extras 5.1.0-1 → 5.1.0-3
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/.rush/temp/61c1d4a91d98f048b475a5bb3e6541c5d27819de.tar.log +237 -0
- package/.rush/temp/chunked-rush-logs/ts-web-extras.build.chunks.jsonl +17 -37
- package/.rush/temp/operation/build/all.log +17 -37
- package/.rush/temp/operation/build/log-chunks.jsonl +17 -37
- package/.rush/temp/operation/build/state.json +1 -1
- package/dist/packlets/file-tree/httpTreeAccessors.js +68 -11
- package/dist/packlets/file-tree/httpTreeAccessors.js.map +1 -1
- package/dist/test/unit/httpTreeAccessors.test.js +303 -74
- package/dist/test/unit/httpTreeAccessors.test.js.map +1 -1
- package/dist/ts-web-extras.d.ts +14 -0
- package/lib/packlets/file-tree/httpTreeAccessors.d.ts +14 -0
- package/lib/packlets/file-tree/httpTreeAccessors.d.ts.map +1 -1
- package/lib/packlets/file-tree/httpTreeAccessors.js +68 -11
- package/lib/packlets/file-tree/httpTreeAccessors.js.map +1 -1
- package/lib/test/unit/httpTreeAccessors.test.js +303 -74
- package/lib/test/unit/httpTreeAccessors.test.js.map +1 -1
- package/package.json +24 -24
- package/rush-logs/ts-web-extras.build.cache.log +3 -0
- package/rush-logs/ts-web-extras.build.log +17 -37
- package/src/packlets/file-tree/httpTreeAccessors.ts +79 -12
- package/src/test/unit/httpTreeAccessors.test.ts +377 -84
- package/temp/build/typescript/ts_8nwakTlr.json +1 -1
- package/temp/coverage/crypto-utils/browserCryptoProvider.ts.html +1 -1
- package/temp/coverage/crypto-utils/browserHashProvider.ts.html +1 -1
- package/temp/coverage/crypto-utils/index.html +1 -1
- package/temp/coverage/file-tree/directoryHandleStore.ts.html +1 -1
- package/temp/coverage/file-tree/fileApiTreeAccessors.ts.html +1 -1
- package/temp/coverage/file-tree/fileSystemAccessTreeAccessors.ts.html +1 -1
- package/temp/coverage/file-tree/httpTreeAccessors.ts.html +369 -168
- package/temp/coverage/file-tree/index.html +11 -11
- package/temp/coverage/file-tree/localStorageTreeAccessors.ts.html +1 -1
- package/temp/coverage/helpers/fileTreeHelpers.ts.html +1 -1
- package/temp/coverage/helpers/index.html +1 -1
- package/temp/coverage/index.html +11 -11
- package/temp/coverage/lcov-report/crypto-utils/browserCryptoProvider.ts.html +1 -1
- package/temp/coverage/lcov-report/crypto-utils/browserHashProvider.ts.html +1 -1
- package/temp/coverage/lcov-report/crypto-utils/index.html +1 -1
- package/temp/coverage/lcov-report/file-tree/directoryHandleStore.ts.html +1 -1
- package/temp/coverage/lcov-report/file-tree/fileApiTreeAccessors.ts.html +1 -1
- package/temp/coverage/lcov-report/file-tree/fileSystemAccessTreeAccessors.ts.html +1 -1
- package/temp/coverage/lcov-report/file-tree/httpTreeAccessors.ts.html +369 -168
- package/temp/coverage/lcov-report/file-tree/index.html +11 -11
- package/temp/coverage/lcov-report/file-tree/localStorageTreeAccessors.ts.html +1 -1
- package/temp/coverage/lcov-report/helpers/fileTreeHelpers.ts.html +1 -1
- package/temp/coverage/lcov-report/helpers/index.html +1 -1
- package/temp/coverage/lcov-report/index.html +11 -11
- package/temp/coverage/lcov-report/url-utils/index.html +1 -1
- package/temp/coverage/lcov-report/url-utils/urlParams.ts.html +1 -1
- package/temp/coverage/lcov.info +479 -379
- package/temp/coverage/url-utils/index.html +1 -1
- package/temp/coverage/url-utils/urlParams.ts.html +1 -1
- package/temp/test/jest/haste-map-7492f1b44480e0cdd1f220078fb3afd8-c8dd6c3430605adeb2f1cadf4f75e791-8c9336785555d572065b28c111982ba4 +0 -0
- package/temp/test/jest/perf-cache-7492f1b44480e0cdd1f220078fb3afd8-da39a3ee5e6b4b0d3255bfef95601890 +1 -0
- package/temp/ts-web-extras.api.json +1 -1
- package/.rush/temp/chunked-rush-logs/ts-web-extras.test.chunks.jsonl +0 -70
- package/.rush/temp/operation/build/error.log +0 -18
- package/.rush/temp/operation/test/all.log +0 -70
- package/.rush/temp/operation/test/error.log +0 -16
- package/.rush/temp/operation/test/log-chunks.jsonl +0 -70
- package/.rush/temp/operation/test/state.json +0 -3
- package/rush-logs/ts-web-extras.build.error.log +0 -18
- package/rush-logs/ts-web-extras.test.cache.log +0 -1
- package/rush-logs/ts-web-extras.test.error.log +0 -16
- package/rush-logs/ts-web-extras.test.log +0 -70
- package/temp/coverage/crypto/browserHashProvider.ts.html +0 -304
- package/temp/coverage/crypto/index.html +0 -116
- package/temp/coverage/lcov-report/crypto/browserHashProvider.ts.html +0 -304
- package/temp/coverage/lcov-report/crypto/index.html +0 -116
- package/temp/test/jest/haste-map-b931e4e63102f86c5bd4949f7dced44f-9d713eb41149188b4e5c0ae3d86d0a57-2ad8e16b24e391b8cdbe50b55c137169 +0 -0
- package/temp/test/jest/perf-cache-b931e4e63102f86c5bd4949f7dced44f-da39a3ee5e6b4b0d3255bfef95601890 +0 -1
- /package/temp/test/jest/{jest-transform-cache-b931e4e63102f86c5bd4949f7dced44f-79ef2876fae7ca75eedb2aa53dc48338/b5/package_b5f57afc9ec2c011239b1608ee5bdfa5 → jest-transform-cache-7492f1b44480e0cdd1f220078fb3afd8-79ef2876fae7ca75eedb2aa53dc48338/d6/package_d6e3b0fa94752e16b0b2a2777739b973} +0 -0
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
Invoking: heft test --clean
|
|
2
2
|
---- build started ----
|
|
3
|
-
[build:clean] Deleted 0 files and 3 folders
|
|
4
3
|
[build:typescript] The TypeScript compiler version 5.9.3 is newer than the latest version that was tested with Heft (5.8); it may not work correctly.
|
|
5
4
|
[build:typescript] Using TypeScript version 5.9.3
|
|
6
5
|
[build:api-extractor] Using API Extractor version 7.57.6
|
|
7
6
|
[build:api-extractor] Analysis will use the bundled TypeScript version 5.8.2
|
|
8
7
|
[build:api-extractor] *** The target project appears to use TypeScript 5.9.3 which is newer than the bundled compiler engine; consider upgrading API Extractor.
|
|
9
|
-
---- build finished (
|
|
8
|
+
---- build finished (24.830s) ----
|
|
10
9
|
---- test started ----
|
|
11
|
-
[test:clean] Deleted 0 files and 1 folder
|
|
12
10
|
[test:jest] Using Jest version 29.5.0
|
|
13
11
|
[test:jest]
|
|
14
12
|
[test:jest] Run start. 9 test suites
|
|
@@ -16,60 +14,42 @@ Invoking: heft test --clean
|
|
|
16
14
|
[test:jest] START lib/test/unit/httpTreeAccessors.test.js
|
|
17
15
|
[test:jest] START lib/test/unit/localStorageTreeAccessors.test.js
|
|
18
16
|
[test:jest] START lib/test/unit/fileSystemAccessTreeAccessors.test.js
|
|
17
|
+
[test:jest] PASS lib/test/unit/localStorageTreeAccessors.test.js (duration: 1.749s, 44 passed, 0 failed)
|
|
19
18
|
[test:jest] START lib/test/unit/fileTreeHelpers.test.js
|
|
19
|
+
[test:jest] PASS lib/test/unit/fileApiTreeAccessors.test.js (duration: 2.021s, 65 passed, 0 failed)
|
|
20
20
|
[test:jest] START lib/test/unit/fileApiTypes.test.js
|
|
21
|
+
[test:jest] PASS lib/test/unit/httpTreeAccessors.test.js (duration: 2.316s, 57 passed, 0 failed)
|
|
21
22
|
[test:jest] START lib/test/unit/urlParams.test.js
|
|
23
|
+
[test:jest] PASS lib/test/unit/fileSystemAccessTreeAccessors.test.js (duration: 1.352s, 44 passed, 0 failed)
|
|
22
24
|
[test:jest] START lib/test/unit/directoryHandleStore.test.js
|
|
25
|
+
[test:jest] PASS lib/test/unit/fileTreeHelpers.test.js (duration: 1.538s, 35 passed, 0 failed)
|
|
23
26
|
[test:jest] START lib/test/unit/browserHashProvider.test.js
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
(node:72598) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
|
|
29
|
-
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
30
|
-
[test:jest] PASS lib/test/unit/httpTreeAccessors.test.js (duration: 0.858s, 52 passed, 0 failed)
|
|
31
|
-
[test:jest] PASS lib/test/unit/fileSystemAccessTreeAccessors.test.js (duration: 0.900s, 44 passed, 0 failed)
|
|
32
|
-
[test:jest] PASS lib/test/unit/browserHashProvider.test.js (duration: 0.969s, 15 passed, 0 failed)
|
|
33
|
-
(node:72594) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
|
|
34
|
-
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
35
|
-
[test:jest] PASS lib/test/unit/fileTreeHelpers.test.js (duration: 0.968s, 35 passed, 0 failed)
|
|
36
|
-
[test:jest] PASS lib/test/unit/directoryHandleStore.test.js (duration: 0.948s, 19 passed, 0 failed)
|
|
37
|
-
(node:72597) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
|
|
38
|
-
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
39
|
-
(node:72590) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
|
|
40
|
-
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
41
|
-
(node:72596) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
|
|
42
|
-
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
43
|
-
(node:72592) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
|
|
44
|
-
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
45
|
-
[test:jest] PASS lib/test/unit/urlParams.test.js (duration: 0.916s, 60 passed, 0 failed)
|
|
46
|
-
[test:jest] PASS lib/test/unit/fileApiTreeAccessors.test.js (duration: 0.987s, 65 passed, 0 failed)
|
|
47
|
-
[test:jest] PASS lib/test/unit/localStorageTreeAccessors.test.js (duration: 0.974s, 44 passed, 0 failed)
|
|
48
|
-
(node:72595) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
|
|
49
|
-
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
50
|
-
[test:jest] PASS lib/test/unit/fileApiTypes.test.js (duration: 0.933s, 36 passed, 0 failed)
|
|
27
|
+
[test:jest] PASS lib/test/unit/fileApiTypes.test.js (duration: 1.440s, 36 passed, 0 failed)
|
|
28
|
+
[test:jest] PASS lib/test/unit/urlParams.test.js (duration: 1.637s, 60 passed, 0 failed)
|
|
29
|
+
[test:jest] PASS lib/test/unit/directoryHandleStore.test.js (duration: 1.412s, 19 passed, 0 failed)
|
|
30
|
+
[test:jest] PASS lib/test/unit/browserHashProvider.test.js (duration: 1.586s, 15 passed, 0 failed)
|
|
51
31
|
[test:jest]
|
|
52
32
|
[test:jest] Tests finished:
|
|
53
|
-
[test:jest] Successes:
|
|
33
|
+
[test:jest] Successes: 375
|
|
54
34
|
[test:jest] Failures: 0
|
|
55
|
-
[test:jest] Total:
|
|
35
|
+
[test:jest] Total: 376
|
|
56
36
|
-----------------------------------|---------|----------|---------|---------|-------------------
|
|
57
37
|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
|
58
38
|
-----------------------------------|---------|----------|---------|---------|-------------------
|
|
59
|
-
All files | 100 |
|
|
39
|
+
All files | 100 | 99.62 | 100 | 100 |
|
|
60
40
|
crypto-utils | 100 | 100 | 100 | 100 |
|
|
61
41
|
browserCryptoProvider.ts | 100 | 100 | 100 | 100 |
|
|
62
42
|
browserHashProvider.ts | 100 | 100 | 100 | 100 |
|
|
63
|
-
file-tree | 100 |
|
|
43
|
+
file-tree | 100 | 99.57 | 100 | 100 |
|
|
64
44
|
directoryHandleStore.ts | 100 | 100 | 100 | 100 |
|
|
65
45
|
fileApiTreeAccessors.ts | 100 | 100 | 100 | 100 |
|
|
66
46
|
fileSystemAccessTreeAccessors.ts | 100 | 100 | 100 | 100 |
|
|
67
|
-
httpTreeAccessors.ts | 100 |
|
|
47
|
+
httpTreeAccessors.ts | 100 | 98.41 | 100 | 100 | 339
|
|
68
48
|
localStorageTreeAccessors.ts | 100 | 100 | 100 | 100 |
|
|
69
49
|
helpers | 100 | 100 | 100 | 100 |
|
|
70
50
|
fileTreeHelpers.ts | 100 | 100 | 100 | 100 |
|
|
71
51
|
url-utils | 100 | 100 | 100 | 100 |
|
|
72
52
|
urlParams.ts | 100 | 100 | 100 | 100 |
|
|
73
53
|
-----------------------------------|---------|----------|---------|---------|-------------------
|
|
74
|
-
---- test finished (
|
|
75
|
-
-------------------- Finished (
|
|
54
|
+
---- test finished (8.132s) ----
|
|
55
|
+
-------------------- Finished (32.975s) --------------------
|
|
@@ -79,6 +79,9 @@ export class HttpTreeAccessors<TCT extends string = string>
|
|
|
79
79
|
private readonly _userId: string | undefined;
|
|
80
80
|
private readonly _logger: Logging.LogReporter<unknown>;
|
|
81
81
|
|
|
82
|
+
/** Guards against concurrent syncToDisk calls (thundering herd from autoSync). */
|
|
83
|
+
private _syncPromise: Promise<Result<void>> | undefined;
|
|
84
|
+
|
|
82
85
|
private constructor(files: FileTree.IInMemoryFile<TCT>[], params: IHttpTreeParams<TCT>) {
|
|
83
86
|
super(files, params);
|
|
84
87
|
this._baseUrl = params.baseUrl.replace(/\/$/, '');
|
|
@@ -117,21 +120,45 @@ export class HttpTreeAccessors<TCT extends string = string>
|
|
|
117
120
|
|
|
118
121
|
/**
|
|
119
122
|
* Synchronizes all dirty files to the HTTP backend.
|
|
123
|
+
*
|
|
124
|
+
* Uses a concurrency guard: if a sync is already in progress, callers
|
|
125
|
+
* await the existing operation rather than starting a parallel one.
|
|
126
|
+
* This prevents the thundering herd that occurs when autoSync fires
|
|
127
|
+
* for every file written during a bulk operation (e.g. restore).
|
|
128
|
+
*
|
|
120
129
|
* @returns A promise that resolves to a result indicating success or failure.
|
|
121
130
|
*/
|
|
122
131
|
public async syncToDisk(): Promise<Result<void>> {
|
|
132
|
+
if (this._syncPromise) {
|
|
133
|
+
return this._syncPromise;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this._syncPromise = this._doSync().finally(() => {
|
|
137
|
+
this._syncPromise = undefined;
|
|
138
|
+
});
|
|
139
|
+
return this._syncPromise;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async _doSync(): Promise<Result<void>> {
|
|
123
143
|
if (this._dirtyFiles.size === 0 && this._pendingDeletions.size === 0) {
|
|
124
144
|
return succeed(undefined);
|
|
125
145
|
}
|
|
126
146
|
|
|
127
|
-
|
|
147
|
+
// Snapshot and clear dirty sets so that changes arriving during
|
|
148
|
+
// the async sync are not dropped when we finish.
|
|
149
|
+
const deletions = new Set(this._pendingDeletions);
|
|
150
|
+
const dirty = new Set(this._dirtyFiles);
|
|
151
|
+
this._pendingDeletions.clear();
|
|
152
|
+
this._dirtyFiles.clear();
|
|
153
|
+
|
|
154
|
+
for (const path of deletions) {
|
|
128
155
|
const query = new URLSearchParams();
|
|
129
156
|
query.set('path', path);
|
|
130
157
|
if (this._namespace) {
|
|
131
158
|
query.set('namespace', this._namespace);
|
|
132
159
|
}
|
|
133
160
|
|
|
134
|
-
const deleteResult = await this.
|
|
161
|
+
const deleteResult = await this._requestWithRetry<{ deleted: boolean }>(`/file?${query.toString()}`, {
|
|
135
162
|
method: 'DELETE'
|
|
136
163
|
});
|
|
137
164
|
if (deleteResult.isFailure()) {
|
|
@@ -139,7 +166,7 @@ export class HttpTreeAccessors<TCT extends string = string>
|
|
|
139
166
|
}
|
|
140
167
|
}
|
|
141
168
|
|
|
142
|
-
for (const path of
|
|
169
|
+
for (const path of dirty) {
|
|
143
170
|
const contentsResult = this.getFileContents(path);
|
|
144
171
|
if (contentsResult.isFailure()) {
|
|
145
172
|
return fail(`${path}: ${contentsResult.message}`);
|
|
@@ -153,7 +180,7 @@ export class HttpTreeAccessors<TCT extends string = string>
|
|
|
153
180
|
body.namespace = this._namespace;
|
|
154
181
|
}
|
|
155
182
|
|
|
156
|
-
const saveResult = await this.
|
|
183
|
+
const saveResult = await this._requestWithRetry<IHttpStorageFileResponse>('/file', {
|
|
157
184
|
method: 'PUT',
|
|
158
185
|
body: JSON.stringify(body)
|
|
159
186
|
});
|
|
@@ -162,15 +189,12 @@ export class HttpTreeAccessors<TCT extends string = string>
|
|
|
162
189
|
}
|
|
163
190
|
}
|
|
164
191
|
|
|
165
|
-
this._pendingDeletions.clear();
|
|
166
|
-
this._dirtyFiles.clear();
|
|
167
|
-
|
|
168
192
|
const syncBody: Record<string, unknown> = {};
|
|
169
193
|
if (this._namespace) {
|
|
170
194
|
syncBody.namespace = this._namespace;
|
|
171
195
|
}
|
|
172
196
|
|
|
173
|
-
const syncResult = await this.
|
|
197
|
+
const syncResult = await this._requestWithRetry<IHttpStorageSyncResponse>('/sync', {
|
|
174
198
|
method: 'POST',
|
|
175
199
|
body: JSON.stringify(syncBody)
|
|
176
200
|
});
|
|
@@ -269,7 +293,10 @@ export class HttpTreeAccessors<TCT extends string = string>
|
|
|
269
293
|
}
|
|
270
294
|
|
|
271
295
|
if (!response.ok) {
|
|
272
|
-
const
|
|
296
|
+
const body = await response.text().catch(() => '');
|
|
297
|
+
const message = body
|
|
298
|
+
? `HTTP ${response.status}: ${body}`
|
|
299
|
+
: `HTTP ${response.status} ${response.statusText}`;
|
|
273
300
|
return fail(message);
|
|
274
301
|
}
|
|
275
302
|
|
|
@@ -280,6 +307,44 @@ export class HttpTreeAccessors<TCT extends string = string>
|
|
|
280
307
|
return succeed(json as T);
|
|
281
308
|
}
|
|
282
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Wraps `_request` with retry logic for transient failures
|
|
312
|
+
* (network errors, 503 service unavailable, etc.).
|
|
313
|
+
*/
|
|
314
|
+
private async _requestWithRetry<T>(resourcePath: string, init?: RequestInit): Promise<Result<T>> {
|
|
315
|
+
const maxAttempts = 3;
|
|
316
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
317
|
+
const result = await this._request<T>(resourcePath, init);
|
|
318
|
+
if (result.isSuccess() || attempt === maxAttempts) {
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
// Retry on transient-looking errors
|
|
322
|
+
const msg = result.message;
|
|
323
|
+
const lowerMsg = msg.toLowerCase();
|
|
324
|
+
const isTransient =
|
|
325
|
+
msg.includes('503') ||
|
|
326
|
+
msg.includes('502') ||
|
|
327
|
+
msg.includes('429') ||
|
|
328
|
+
lowerMsg.includes('disconnect') ||
|
|
329
|
+
lowerMsg.includes('econnreset') ||
|
|
330
|
+
lowerMsg.includes('failed to fetch') ||
|
|
331
|
+
lowerMsg.includes('network');
|
|
332
|
+
if (!isTransient) {
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
// Exponential backoff: 500ms, 1000ms
|
|
336
|
+
const delayMs = 500 * Math.pow(2, attempt - 1);
|
|
337
|
+
this._logger.detail(
|
|
338
|
+
`Retrying ${
|
|
339
|
+
init?.method ?? 'GET'
|
|
340
|
+
} ${resourcePath} after ${delayMs}ms (attempt ${attempt}/${maxAttempts})`
|
|
341
|
+
);
|
|
342
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
343
|
+
}
|
|
344
|
+
/* c8 ignore next 1 - defensive coding: loop always returns */
|
|
345
|
+
return fail('retry loop exited unexpectedly');
|
|
346
|
+
}
|
|
347
|
+
|
|
283
348
|
/**
|
|
284
349
|
* Loads files from the HTTP backend for the specified directory path.
|
|
285
350
|
* @param params - Configuration parameters for the HTTP tree accessors.
|
|
@@ -352,8 +417,7 @@ export class HttpTreeAccessors<TCT extends string = string>
|
|
|
352
417
|
}
|
|
353
418
|
|
|
354
419
|
const fetchImpl = normalizeFetch(params.fetchImpl);
|
|
355
|
-
|
|
356
|
-
const userIdHeaders: RequestInit | undefined = params.userId
|
|
420
|
+
const userIdHeaders: RequestInit | undefined = /* c8 ignore next */ params.userId
|
|
357
421
|
? { headers: { 'X-User-Id': params.userId } }
|
|
358
422
|
: undefined;
|
|
359
423
|
const response = await fetchImpl(
|
|
@@ -367,7 +431,10 @@ export class HttpTreeAccessors<TCT extends string = string>
|
|
|
367
431
|
}
|
|
368
432
|
|
|
369
433
|
if (!response.ok) {
|
|
370
|
-
const
|
|
434
|
+
const body = await response.text().catch(() => '');
|
|
435
|
+
const message = body
|
|
436
|
+
? `HTTP ${response.status}: ${body}`
|
|
437
|
+
: `HTTP ${response.status} ${response.statusText}`;
|
|
371
438
|
return fail(message);
|
|
372
439
|
}
|
|
373
440
|
|