@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.
Files changed (71) hide show
  1. package/.rush/temp/61c1d4a91d98f048b475a5bb3e6541c5d27819de.tar.log +237 -0
  2. package/.rush/temp/chunked-rush-logs/ts-web-extras.build.chunks.jsonl +17 -37
  3. package/.rush/temp/operation/build/all.log +17 -37
  4. package/.rush/temp/operation/build/log-chunks.jsonl +17 -37
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/dist/packlets/file-tree/httpTreeAccessors.js +68 -11
  7. package/dist/packlets/file-tree/httpTreeAccessors.js.map +1 -1
  8. package/dist/test/unit/httpTreeAccessors.test.js +303 -74
  9. package/dist/test/unit/httpTreeAccessors.test.js.map +1 -1
  10. package/dist/ts-web-extras.d.ts +14 -0
  11. package/lib/packlets/file-tree/httpTreeAccessors.d.ts +14 -0
  12. package/lib/packlets/file-tree/httpTreeAccessors.d.ts.map +1 -1
  13. package/lib/packlets/file-tree/httpTreeAccessors.js +68 -11
  14. package/lib/packlets/file-tree/httpTreeAccessors.js.map +1 -1
  15. package/lib/test/unit/httpTreeAccessors.test.js +303 -74
  16. package/lib/test/unit/httpTreeAccessors.test.js.map +1 -1
  17. package/package.json +24 -24
  18. package/rush-logs/ts-web-extras.build.cache.log +3 -0
  19. package/rush-logs/ts-web-extras.build.log +17 -37
  20. package/src/packlets/file-tree/httpTreeAccessors.ts +79 -12
  21. package/src/test/unit/httpTreeAccessors.test.ts +377 -84
  22. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  23. package/temp/coverage/crypto-utils/browserCryptoProvider.ts.html +1 -1
  24. package/temp/coverage/crypto-utils/browserHashProvider.ts.html +1 -1
  25. package/temp/coverage/crypto-utils/index.html +1 -1
  26. package/temp/coverage/file-tree/directoryHandleStore.ts.html +1 -1
  27. package/temp/coverage/file-tree/fileApiTreeAccessors.ts.html +1 -1
  28. package/temp/coverage/file-tree/fileSystemAccessTreeAccessors.ts.html +1 -1
  29. package/temp/coverage/file-tree/httpTreeAccessors.ts.html +369 -168
  30. package/temp/coverage/file-tree/index.html +11 -11
  31. package/temp/coverage/file-tree/localStorageTreeAccessors.ts.html +1 -1
  32. package/temp/coverage/helpers/fileTreeHelpers.ts.html +1 -1
  33. package/temp/coverage/helpers/index.html +1 -1
  34. package/temp/coverage/index.html +11 -11
  35. package/temp/coverage/lcov-report/crypto-utils/browserCryptoProvider.ts.html +1 -1
  36. package/temp/coverage/lcov-report/crypto-utils/browserHashProvider.ts.html +1 -1
  37. package/temp/coverage/lcov-report/crypto-utils/index.html +1 -1
  38. package/temp/coverage/lcov-report/file-tree/directoryHandleStore.ts.html +1 -1
  39. package/temp/coverage/lcov-report/file-tree/fileApiTreeAccessors.ts.html +1 -1
  40. package/temp/coverage/lcov-report/file-tree/fileSystemAccessTreeAccessors.ts.html +1 -1
  41. package/temp/coverage/lcov-report/file-tree/httpTreeAccessors.ts.html +369 -168
  42. package/temp/coverage/lcov-report/file-tree/index.html +11 -11
  43. package/temp/coverage/lcov-report/file-tree/localStorageTreeAccessors.ts.html +1 -1
  44. package/temp/coverage/lcov-report/helpers/fileTreeHelpers.ts.html +1 -1
  45. package/temp/coverage/lcov-report/helpers/index.html +1 -1
  46. package/temp/coverage/lcov-report/index.html +11 -11
  47. package/temp/coverage/lcov-report/url-utils/index.html +1 -1
  48. package/temp/coverage/lcov-report/url-utils/urlParams.ts.html +1 -1
  49. package/temp/coverage/lcov.info +479 -379
  50. package/temp/coverage/url-utils/index.html +1 -1
  51. package/temp/coverage/url-utils/urlParams.ts.html +1 -1
  52. package/temp/test/jest/haste-map-7492f1b44480e0cdd1f220078fb3afd8-c8dd6c3430605adeb2f1cadf4f75e791-8c9336785555d572065b28c111982ba4 +0 -0
  53. package/temp/test/jest/perf-cache-7492f1b44480e0cdd1f220078fb3afd8-da39a3ee5e6b4b0d3255bfef95601890 +1 -0
  54. package/temp/ts-web-extras.api.json +1 -1
  55. package/.rush/temp/chunked-rush-logs/ts-web-extras.test.chunks.jsonl +0 -70
  56. package/.rush/temp/operation/build/error.log +0 -18
  57. package/.rush/temp/operation/test/all.log +0 -70
  58. package/.rush/temp/operation/test/error.log +0 -16
  59. package/.rush/temp/operation/test/log-chunks.jsonl +0 -70
  60. package/.rush/temp/operation/test/state.json +0 -3
  61. package/rush-logs/ts-web-extras.build.error.log +0 -18
  62. package/rush-logs/ts-web-extras.test.cache.log +0 -1
  63. package/rush-logs/ts-web-extras.test.error.log +0 -16
  64. package/rush-logs/ts-web-extras.test.log +0 -70
  65. package/temp/coverage/crypto/browserHashProvider.ts.html +0 -304
  66. package/temp/coverage/crypto/index.html +0 -116
  67. package/temp/coverage/lcov-report/crypto/browserHashProvider.ts.html +0 -304
  68. package/temp/coverage/lcov-report/crypto/index.html +0 -116
  69. package/temp/test/jest/haste-map-b931e4e63102f86c5bd4949f7dced44f-9d713eb41149188b4e5c0ae3d86d0a57-2ad8e16b24e391b8cdbe50b55c137169 +0 -0
  70. package/temp/test/jest/perf-cache-b931e4e63102f86c5bd4949f7dced44f-da39a3ee5e6b4b0d3255bfef95601890 +0 -1
  71. /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 (3.991s) ----
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
- (node:72593) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
25
- (Use `node --trace-deprecation ...` to show where the warning was created)
26
- (node:72591) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
27
- (Use `node --trace-deprecation ...` to show where the warning was created)
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: 370
33
+ [test:jest] Successes: 375
54
34
  [test:jest] Failures: 0
55
- [test:jest] Total: 371
35
+ [test:jest] Total: 376
56
36
  -----------------------------------|---------|----------|---------|---------|-------------------
57
37
  File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
58
38
  -----------------------------------|---------|----------|---------|---------|-------------------
59
- All files | 100 | 100 | 100 | 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 | 100 | 100 | 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 | 100 | 100 | 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 (2.336s) ----
75
- -------------------- Finished (6.331s) --------------------
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
- for (const path of this._pendingDeletions) {
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._request<{ deleted: boolean }>(`/file?${query.toString()}`, {
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 this._dirtyFiles) {
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._request<IHttpStorageFileResponse>('/file', {
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._request<IHttpStorageSyncResponse>('/sync', {
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 message = await response.text().catch(() => `HTTP ${response.status}`);
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
- /* c8 ignore next 3 - userId header in static _requestWithParams; covered by userId tests via fromHttp */
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 message = await response.text().catch(() => `HTTP ${response.status}`);
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