@nomicfoundation/hardhat-utils 4.0.5 → 4.1.1

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 (69) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/src/bytecode.d.ts +4 -0
  3. package/dist/src/bytecode.d.ts.map +1 -1
  4. package/dist/src/bytecode.js +12 -7
  5. package/dist/src/bytecode.js.map +1 -1
  6. package/dist/src/crypto.d.ts.map +1 -1
  7. package/dist/src/crypto.js +10 -2
  8. package/dist/src/crypto.js.map +1 -1
  9. package/dist/src/debug.d.ts +40 -8
  10. package/dist/src/debug.d.ts.map +1 -1
  11. package/dist/src/debug.js +54 -18
  12. package/dist/src/debug.js.map +1 -1
  13. package/dist/src/fast-semver.d.ts +49 -0
  14. package/dist/src/fast-semver.d.ts.map +1 -0
  15. package/dist/src/fast-semver.js +73 -0
  16. package/dist/src/fast-semver.js.map +1 -0
  17. package/dist/src/fs.d.ts +47 -0
  18. package/dist/src/fs.d.ts.map +1 -1
  19. package/dist/src/fs.js +204 -48
  20. package/dist/src/fs.js.map +1 -1
  21. package/dist/src/internal/bytecode.d.ts +11 -0
  22. package/dist/src/internal/bytecode.d.ts.map +1 -1
  23. package/dist/src/internal/bytecode.js +39 -2
  24. package/dist/src/internal/bytecode.js.map +1 -1
  25. package/dist/src/internal/debug.d.ts +35 -0
  26. package/dist/src/internal/debug.d.ts.map +1 -0
  27. package/dist/src/internal/debug.js +91 -0
  28. package/dist/src/internal/debug.js.map +1 -0
  29. package/dist/src/internal/fs.d.ts +15 -0
  30. package/dist/src/internal/fs.d.ts.map +1 -1
  31. package/dist/src/internal/fs.js +44 -0
  32. package/dist/src/internal/fs.js.map +1 -1
  33. package/dist/src/internal/global-dir.d.ts +2 -2
  34. package/dist/src/internal/global-dir.d.ts.map +1 -1
  35. package/dist/src/internal/global-dir.js +7 -1
  36. package/dist/src/internal/global-dir.js.map +1 -1
  37. package/dist/src/internal/lang.d.ts +3 -2
  38. package/dist/src/internal/lang.d.ts.map +1 -1
  39. package/dist/src/internal/lang.js +12 -6
  40. package/dist/src/internal/lang.js.map +1 -1
  41. package/dist/src/lang.js +2 -2
  42. package/dist/src/lang.js.map +1 -1
  43. package/dist/src/request.d.ts +11 -11
  44. package/dist/src/request.d.ts.map +1 -1
  45. package/dist/src/request.js +6 -6
  46. package/dist/src/request.js.map +1 -1
  47. package/dist/src/subprocess.d.ts +2 -0
  48. package/dist/src/subprocess.d.ts.map +1 -1
  49. package/dist/src/subprocess.js +1 -0
  50. package/dist/src/subprocess.js.map +1 -1
  51. package/dist/src/synchronization.d.ts +97 -0
  52. package/dist/src/synchronization.d.ts.map +1 -1
  53. package/dist/src/synchronization.js +177 -8
  54. package/dist/src/synchronization.js.map +1 -1
  55. package/package.json +3 -4
  56. package/src/bytecode.ts +15 -6
  57. package/src/crypto.ts +15 -2
  58. package/src/debug.ts +73 -28
  59. package/src/fast-semver.ts +95 -0
  60. package/src/fs.ts +282 -76
  61. package/src/internal/bytecode.ts +64 -5
  62. package/src/internal/debug.ts +119 -0
  63. package/src/internal/fs.ts +70 -0
  64. package/src/internal/global-dir.ts +10 -4
  65. package/src/internal/lang.ts +15 -6
  66. package/src/lang.ts +2 -2
  67. package/src/request.ts +16 -16
  68. package/src/subprocess.ts +2 -0
  69. package/src/synchronization.ts +217 -9
@@ -1,6 +1,7 @@
1
1
  import type { Dirent } from "node:fs";
2
2
 
3
3
  import fsPromises from "node:fs/promises";
4
+ import path from "node:path";
4
5
 
5
6
  import { ensureNodeErrnoExceptionError } from "../error.js";
6
7
  import { FileSystemAccessError, NotADirectoryError } from "../errors/fs.js";
@@ -55,3 +56,72 @@ export async function isDirectoryDirentAware(
55
56
 
56
57
  return await isDirectory(absolutePath);
57
58
  }
59
+
60
+ /**
61
+ * Recursively walk the directory tree rooted at `dirFrom`, appending the
62
+ * absolute paths of every file accepted by `matches` to `results`. Descent
63
+ * into a subdirectory is gated by `directoryFilter`. When either callback is
64
+ * omitted, every file or directory is accepted.
65
+ */
66
+ export async function collectAllFilesMatching(
67
+ dirFrom: string,
68
+ results: string[],
69
+ matches?: (absolutePathToFile: string) => Promise<boolean> | boolean,
70
+ directoryFilter?: (absolutePathToDir: string) => Promise<boolean> | boolean,
71
+ ): Promise<void> {
72
+ const dirContent = await readdirWithFileTypesOrEmpty(dirFrom);
73
+
74
+ await Promise.all(
75
+ dirContent.map(async (dirent) => {
76
+ const absolutePathToFile = path.join(dirFrom, dirent.name);
77
+ if (await isDirectoryDirentAware(absolutePathToFile, dirent)) {
78
+ if (
79
+ directoryFilter === undefined ||
80
+ (await directoryFilter(absolutePathToFile))
81
+ ) {
82
+ await collectAllFilesMatching(
83
+ absolutePathToFile,
84
+ results,
85
+ matches,
86
+ directoryFilter,
87
+ );
88
+ }
89
+
90
+ return;
91
+ } else if (matches === undefined || (await matches(absolutePathToFile))) {
92
+ results.push(absolutePathToFile);
93
+ }
94
+ }),
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Recursively walk the directory tree rooted at `dirFrom`, appending the
100
+ * absolute paths of every directory accepted by `matches` to `results`. When
101
+ * a directory matches, its descendants are not explored; when it does not,
102
+ * the walk continues into its subdirectories. When `matches` is omitted,
103
+ * every directory is accepted and recursion stops at the top level.
104
+ */
105
+ export async function collectAllDirectoriesMatching(
106
+ dirFrom: string,
107
+ results: string[],
108
+ matches?: (absolutePathToDir: string) => Promise<boolean> | boolean,
109
+ ): Promise<void> {
110
+ const dirContent = await readdirWithFileTypesOrEmpty(dirFrom);
111
+
112
+ await Promise.all(
113
+ dirContent.map(async (dirent) => {
114
+ const absolutePathToFile = path.join(dirFrom, dirent.name);
115
+ if (!(await isDirectoryDirentAware(absolutePathToFile, dirent))) {
116
+ return;
117
+ }
118
+
119
+ if (matches === undefined || (await matches(absolutePathToFile))) {
120
+ results.push(absolutePathToFile);
121
+ return;
122
+ }
123
+
124
+ await collectAllDirectoriesMatching(absolutePathToFile, results, matches);
125
+ }),
126
+ );
127
+ }
@@ -1,9 +1,15 @@
1
- import envPaths from "env-paths";
1
+ import type { Paths } from "env-paths";
2
2
 
3
3
  export const HARDHAT_PACKAGE_NAME = "hardhat";
4
4
 
5
- export async function generatePaths(
6
- packageName: string,
7
- ): Promise<envPaths.Paths> {
5
+ // We don't load env-paths on startup because this module is transitively
6
+ // imported from many places but generatePaths is rarely called during
7
+ // bootstrap.
8
+ let envPaths: ((name: string) => Paths) | undefined;
9
+
10
+ export async function generatePaths(packageName: string): Promise<Paths> {
11
+ if (envPaths === undefined) {
12
+ ({ default: envPaths } = await import("env-paths"));
13
+ }
8
14
  return envPaths(packageName);
9
15
  }
@@ -1,11 +1,15 @@
1
- import { createCustomEqual } from "fast-equals";
2
- import rfdc from "rfdc";
1
+ import type Rfdc from "rfdc";
3
2
 
4
3
  import { isObject } from "../lang.js";
5
4
 
6
- let clone: ReturnType<typeof rfdc> | null = null;
7
- export function getDeepCloneFunction(): <T>(input: T) => T {
8
- if (clone === null) {
5
+ // We don't load rfdc on startup because it adds unnecessary
6
+ // overhead when this module is transitively imported but deep clone
7
+ // is not used.
8
+ let clone: ReturnType<typeof Rfdc> | undefined;
9
+
10
+ export async function getDeepCloneFunction(): Promise<ReturnType<typeof Rfdc>> {
11
+ if (clone === undefined) {
12
+ const { default: rfdc } = await import("rfdc");
9
13
  clone = rfdc();
10
14
  }
11
15
 
@@ -53,6 +57,9 @@ export function deepMergeImpl<T extends object, S extends object>(
53
57
  return result;
54
58
  }
55
59
 
60
+ // We don't load fast-equals on startup because it adds unnecessary
61
+ // overhead when this module is transitively imported but deep equal
62
+ // is not used.
56
63
  let cachedCustomEqual: ((a: unknown, b: unknown) => boolean) | undefined;
57
64
 
58
65
  /**
@@ -62,11 +69,13 @@ let cachedCustomEqual: ((a: unknown, b: unknown) => boolean) | undefined;
62
69
  * @param y The second value to compare.
63
70
  * @returns True if the values are deeply equal, false otherwise.
64
71
  */
65
- export function customFastEqual<T>(x: T, y: T): boolean {
72
+ export async function customFastEqual<T>(x: T, y: T): Promise<boolean> {
66
73
  if (cachedCustomEqual !== undefined) {
67
74
  return cachedCustomEqual(x, y);
68
75
  }
69
76
 
77
+ const { createCustomEqual } = await import("fast-equals");
78
+
70
79
  cachedCustomEqual = createCustomEqual({
71
80
  createCustomConfig: (defaultConfig) => ({
72
81
  areTypedArraysEqual: (a, b, state) => {
package/src/lang.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  * @returns The deep clone of the provided value.
12
12
  */
13
13
  export async function deepClone<T>(value: T): Promise<T> {
14
- const _deepClone = getDeepCloneFunction();
14
+ const _deepClone = await getDeepCloneFunction();
15
15
 
16
16
  return _deepClone<T>(value);
17
17
  }
@@ -24,7 +24,7 @@ export async function deepClone<T>(value: T): Promise<T> {
24
24
  * @returns True if the values are deeply equal, false otherwise.
25
25
  */
26
26
  export async function deepEqual<T>(x: T, y: T): Promise<boolean> {
27
- return customFastEqual(x, y);
27
+ return await customFastEqual(x, y);
28
28
  }
29
29
 
30
30
  /**
package/src/request.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import type EventEmitter from "node:events";
2
2
  import type { FileHandle } from "node:fs/promises";
3
3
  import type { ParsedUrlQueryInput } from "node:querystring";
4
- import type * as UndiciT from "undici";
4
+ import type * as Undici from "undici";
5
5
 
6
6
  import { open } from "node:fs/promises";
7
7
  import querystring from "node:querystring";
8
- import stream from "node:stream/promises";
8
+ import { pipeline } from "node:stream/promises";
9
9
 
10
10
  import { ensureError } from "./error.js";
11
11
  import {
@@ -24,18 +24,18 @@ import {
24
24
  handleError,
25
25
  } from "./internal/request.js";
26
26
 
27
- export const DEFAULT_TIMEOUT_IN_MILLISECONDS = 300_000; // Aligned with unidici
27
+ export const DEFAULT_TIMEOUT_IN_MILLISECONDS = 300_000; // Aligned with undici
28
28
  export const DEFAULT_MAX_REDIRECTS = 10;
29
29
  export const DEFAULT_POOL_MAX_CONNECTIONS = 128;
30
30
  export const DEFAULT_USER_AGENT = "Hardhat";
31
31
 
32
- export type Dispatcher = UndiciT.Dispatcher;
33
- export type TestDispatcher = UndiciT.MockAgent;
34
- export type Interceptable = UndiciT.Interceptable;
32
+ export type Dispatcher = Undici.Dispatcher;
33
+ export type TestDispatcher = Undici.MockAgent;
34
+ export type Interceptable = Undici.Interceptable;
35
35
 
36
36
  // We don't load undici on startup because this package is transitively imported
37
37
  // from too many places and it's too complex to optimize case by case.
38
- let undici: typeof UndiciT | undefined;
38
+ let undici: typeof Undici | undefined;
39
39
 
40
40
  /**
41
41
  * Options to configure the dispatcher.
@@ -90,7 +90,7 @@ export interface HttpResponse {
90
90
  export async function getRequest(
91
91
  url: string,
92
92
  requestOptions: RequestOptions = {},
93
- dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
93
+ dispatcherOrDispatcherOptions?: Undici.Dispatcher | DispatcherOptions,
94
94
  ): Promise<HttpResponse> {
95
95
  if (undici === undefined) {
96
96
  undici = await import("undici");
@@ -132,7 +132,7 @@ export async function postJsonRequest(
132
132
  url: string,
133
133
  body: unknown,
134
134
  requestOptions: RequestOptions = {},
135
- dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
135
+ dispatcherOrDispatcherOptions?: Undici.Dispatcher | DispatcherOptions,
136
136
  ): Promise<HttpResponse> {
137
137
  if (undici === undefined) {
138
138
  undici = await import("undici");
@@ -179,7 +179,7 @@ export async function postFormRequest(
179
179
  url: string,
180
180
  body: unknown,
181
181
  requestOptions: RequestOptions = {},
182
- dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
182
+ dispatcherOrDispatcherOptions?: Undici.Dispatcher | DispatcherOptions,
183
183
  ): Promise<HttpResponse> {
184
184
  if (undici === undefined) {
185
185
  undici = await import("undici");
@@ -225,7 +225,7 @@ export async function download(
225
225
  url: string,
226
226
  destination: string,
227
227
  requestOptions: RequestOptions = {},
228
- dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
228
+ dispatcherOrDispatcherOptions?: Undici.Dispatcher | DispatcherOptions,
229
229
  ): Promise<void> {
230
230
  let statusCode: number | undefined;
231
231
  let tempFilePath: string | undefined;
@@ -240,7 +240,7 @@ export async function download(
240
240
  url,
241
241
  requestOptions,
242
242
  dispatcherOrDispatcherOptions,
243
- )) as UndiciT.Dispatcher.ResponseData;
243
+ )) as Undici.Dispatcher.ResponseData;
244
244
  const { body } = response;
245
245
  statusCode = response.statusCode;
246
246
 
@@ -257,7 +257,7 @@ export async function download(
257
257
 
258
258
  const fileStream = fileHandle.createWriteStream();
259
259
 
260
- await stream.pipeline(body, fileStream);
260
+ await pipeline(body, fileStream);
261
261
  } finally {
262
262
  // NOTE: Historically, not closing the file handle caused issues on Windows,
263
263
  // for example, when trying to move the file previously written to by this function
@@ -284,9 +284,9 @@ export async function download(
284
284
 
285
285
  /**
286
286
  * Creates a dispatcher based on the provided options.
287
- * If the `proxy` option is set, it creates a {@link UndiciT.ProxyAgent} dispatcher.
288
- * If the `pool` option is set to `true`, it creates a {@link UndiciT.Pool} dispatcher.
289
- * Otherwise, it creates a basic {@link UndiciT.Agent} dispatcher.
287
+ * If the `proxy` option is set, it creates a {@link Undici.ProxyAgent} dispatcher.
288
+ * If the `pool` option is set to `true`, it creates a {@link Undici.Pool} dispatcher.
289
+ * Otherwise, it creates a basic {@link Undici.Agent} dispatcher.
290
290
  *
291
291
  * @param url The url to make requests to.
292
292
  * @param options The options to configure the dispatcher. See {@link DispatcherOptions}.
package/src/subprocess.ts CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  } from "./errors/subprocess.js";
7
7
  import { exists, isDirectory } from "./fs.js";
8
8
 
9
+ export { SubprocessFileNotFoundError, SubprocessPathIsDirectoryError };
10
+
9
11
  /**
10
12
  * Spawns a detached subprocess to execute a given file with optional arguments.
11
13
  *
@@ -1,10 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
-
6
- import debug from "debug";
2
+ import fs from "node:fs";
3
+ import { hostname } from "node:os";
4
+ import path from "node:path";
7
5
 
6
+ import { createDebug } from "./debug.js";
8
7
  import { ensureError, ensureNodeErrnoExceptionError } from "./error.js";
9
8
  import {
10
9
  BaseMultiProcessMutexError,
@@ -30,7 +29,7 @@ export {
30
29
  StaleMultiProcessMutexError,
31
30
  } from "./errors/synchronization.js";
32
31
 
33
- const log = debug("hardhat:util:multi-process-mutex");
32
+ const log = createDebug("hardhat:utils:synchronization");
34
33
 
35
34
  const PROCESS_SESSION_ID = randomUUID();
36
35
  const DEFAULT_TIMEOUT_MS = 60_000;
@@ -383,11 +382,11 @@ export class MultiProcessMutex {
383
382
  }
384
383
 
385
384
  // Different hostname — can't verify PID remotely
386
- if (metadata.hostname !== os.hostname()) {
385
+ if (metadata.hostname !== hostname()) {
387
386
  throw new IncompatibleHostnameMultiProcessMutexError(
388
387
  lockPath,
389
388
  metadata.hostname,
390
- os.hostname(),
389
+ hostname(),
391
390
  );
392
391
  }
393
392
 
@@ -476,7 +475,7 @@ export class MultiProcessMutex {
476
475
  #buildMetadata(): LockMetadata {
477
476
  return {
478
477
  pid: process.pid,
479
- hostname: os.hostname(),
478
+ hostname: hostname(),
480
479
  createdAt: Date.now(),
481
480
  ...(process.getuid !== undefined ? { uid: process.getuid() } : {}),
482
481
  platform: process.platform,
@@ -622,3 +621,212 @@ export class AsyncMutex {
622
621
  });
623
622
  }
624
623
  }
624
+
625
+ interface SharedPromiseSuccessfulExecutionResult<ValueT> {
626
+ success: true;
627
+ value: ValueT;
628
+ }
629
+
630
+ type SharedPromiseExecutionResult<ValueT> =
631
+ | SharedPromiseSuccessfulExecutionResult<ValueT>
632
+ | { success: false; error: unknown };
633
+
634
+ type SharedPromiseCachedResult<ValueT> =
635
+ // We wrapped the resolved value so that we can distinguish not-cached from
636
+ // `undefined` cached values, and between Promise and value.
637
+ | SharedPromiseSuccessfulExecutionResult<ValueT>
638
+ | Promise<SharedPromiseExecutionResult<ValueT>>;
639
+
640
+ /**
641
+ * A class that deduplicates the concurrent computations of an asynchronous
642
+ * operation based on a string key, by sharing the same Promise for all
643
+ * concurrent calls.
644
+ *
645
+ * This class is useful when the operation is expensive, or you need to
646
+ * guarantee that it's only executed once per key.
647
+ *
648
+ * For this cache to work correctly, the operation has to be either idempotent,
649
+ * or virtually idempotent (e.g. reading the same file multiple times in a very
650
+ * short time, in a context where it shouldn't be changed).
651
+ *
652
+ * The results of the first operation run per key are cached during the lifetime
653
+ * of the class, or until the `delete` or `clear` methods are called.
654
+ *
655
+ * Note that you should always use the same function/operation per cache key. If
656
+ * you call `getOrCompute` with the same key but different functions, the
657
+ * first function will be used for all the calls.
658
+ *
659
+ * Notes on async stack traces: This class is designed to preserve as much as
660
+ * possible of the producer async stack trace, including the place where the
661
+ * first `getOrCompute` computation is started and where the producer throws. To
662
+ * achieve this, you should provide an async lambda of the shape
663
+ * `async () => await myFunction()`, instead of directly passing `myFunction`.
664
+ * You should also try to immediately await the calls to `getOrCompute`.
665
+ *
666
+ * Concurrent callers for the same in-flight computation receive the same
667
+ * original thrown value, so their own `getOrCompute` call site won't
668
+ * necessarily be present in the error stack.
669
+ *
670
+ * Notes on concurrency: `getOrCompute` stores the in-flight promise in the
671
+ * cache before invoking the producer function. Subsequent calls with the same
672
+ * key, including synchronous same-key reentrant calls from the producer,
673
+ * observe that promise and await the same computation instead of invoking their
674
+ * own function. A producer must still not await a same-key reentrant call
675
+ * before completing, because it would wait on its own in-flight result.
676
+ *
677
+ * This guarantee only applies while the cache entry remains installed. Calling
678
+ * `delete` or `clear` during an in-flight computation lets later callers start
679
+ * a new computation, and the older in-flight result won't repopulate the cache.
680
+ */
681
+ export class SharedPromiseCache<ValueT> {
682
+ readonly #cache = new Map<string, SharedPromiseCachedResult<ValueT>>();
683
+
684
+ /**
685
+ * Returns the cached value associated to the key, or computes it if needed,
686
+ * guaranteeing that it's only computed once per key.
687
+ *
688
+ * @param key The cache key associated to the value.
689
+ * @param fn The function that computes the value when it's not cached. Please
690
+ * read the class docs to understand the requirements it should meet.
691
+ * @returns The value.
692
+ * @throws The original value thrown or rejected by the function. Concurrent
693
+ * callers for the same in-flight computation observe the same thrown value, so
694
+ * their stack reflects the original computation, not necessarily each
695
+ * awaiting call site.
696
+ */
697
+ public async getOrCompute(
698
+ key: string,
699
+ fn: () => Promise<ValueT>,
700
+ ): Promise<ValueT> {
701
+ const cached = this.#cache.get(key);
702
+ if (cached !== undefined) {
703
+ if (!(cached instanceof Promise)) {
704
+ return cached.value;
705
+ }
706
+
707
+ const cachedResult = await cached;
708
+
709
+ this.#updateResolvedPromiseCache(key, cached, cachedResult);
710
+
711
+ if (cachedResult.success) {
712
+ return cachedResult.value;
713
+ }
714
+
715
+ throw cachedResult.error;
716
+ }
717
+
718
+ // Create and cache the in-flight promise before invoking `fn`, because
719
+ // `fn` can synchronously re-enter `getOrCompute` with the same key. The
720
+ // reentrant call must observe this promise instead of a cache miss, or it
721
+ // could start a second computation for the same key.
722
+ //
723
+ // `Promise.withResolvers` lets the first caller still await `fn` directly,
724
+ // preserving the producer async stack.
725
+ const { promise, resolve } =
726
+ Promise.withResolvers<SharedPromiseExecutionResult<ValueT>>();
727
+
728
+ this.#cache.set(key, promise);
729
+
730
+ let result: SharedPromiseExecutionResult<ValueT>;
731
+
732
+ try {
733
+ const value = await fn();
734
+ result = { success: true, value };
735
+ } catch (error) {
736
+ result = { success: false, error };
737
+ }
738
+
739
+ resolve(result);
740
+
741
+ this.#updateResolvedPromiseCache(key, promise, result);
742
+
743
+ if (result.success) {
744
+ return result.value;
745
+ }
746
+
747
+ throw result.error;
748
+ }
749
+
750
+ /**
751
+ * Returns the cached value associated to the key without invoking any
752
+ * producer. If the entry is in-flight, the value is not yet available and
753
+ * this method returns `undefined` exactly as it would for a missing key.
754
+ *
755
+ * Use this for synchronous fast-path lookups; if the result is `undefined`
756
+ * and you still want to compute the value, fall through to `getOrCompute`.
757
+ *
758
+ * Note that if `ValueT` includes `undefined` as a valid value, you won't be
759
+ * able to distinguish between a cached `undefined` and a missing/in-flight
760
+ * entry, but that's an intentional tradeoff to keep this method simple.
761
+ *
762
+ * @param key The cache key.
763
+ * @returns The cached value, or `undefined` if the key is missing or
764
+ * in-flight.
765
+ */
766
+ public peek(key: string): ValueT | undefined {
767
+ const cached = this.#cache.get(key);
768
+ if (cached === undefined || cached instanceof Promise) {
769
+ return undefined;
770
+ }
771
+ return cached.value;
772
+ }
773
+
774
+ /**
775
+ * Iterates over the entries that are successfully resolved, yielding
776
+ * `[key, value]` pairs. In-flight entries are skipped because their value
777
+ * is not yet known, and failed ones are removed from the cache.
778
+ *
779
+ * Producers are never invoked.
780
+ */
781
+ public *resolvedEntries(): IterableIterator<[string, ValueT]> {
782
+ for (const [key, cached] of this.#cache) {
783
+ if (!(cached instanceof Promise)) {
784
+ yield [key, cached.value];
785
+ }
786
+ }
787
+ }
788
+
789
+ /**
790
+ * Deletes the cached value associated to the key, if any. Note that this does
791
+ * not cancel any ongoing operation. Callers that already observed the
792
+ * in-flight promise will still wait for the original operation to complete,
793
+ * but callers that start after the deletion may start a new computation.
794
+ *
795
+ * @param key The cache key to delete.
796
+ */
797
+ public delete(key: string): void {
798
+ this.#cache.delete(key);
799
+ }
800
+
801
+ /**
802
+ * Clears the cache, removing all the stored values, but without cancelling
803
+ * any ongoing operation.
804
+ */
805
+ public clear(): void {
806
+ this.#cache.clear();
807
+ }
808
+
809
+ /**
810
+ * Updates the cache if needed once a promise got resolved.
811
+ *
812
+ * @param key The cache key.
813
+ * @param promise The promise that got resolved.
814
+ * @param result The result of the resolved promise.
815
+ */
816
+ #updateResolvedPromiseCache(
817
+ key: string,
818
+ promise: Promise<SharedPromiseExecutionResult<ValueT>>,
819
+ result: SharedPromiseExecutionResult<ValueT>,
820
+ ): void {
821
+ // We only update the cache if the cached promise is still the one that got
822
+ // resolved, which may not always be the case. The reason is that the
823
+ // resolved promise may have been deleted and/or replaced in the cache.
824
+ if (this.#cache.get(key) === promise) {
825
+ if (result.success) {
826
+ this.#cache.set(key, { success: true, value: result.value });
827
+ } else {
828
+ this.#cache.delete(key);
829
+ }
830
+ }
831
+ }
832
+ }