@nomicfoundation/hardhat-utils 4.0.4 → 4.1.0
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/CHANGELOG.md +24 -0
- package/dist/src/debug.d.ts +40 -8
- package/dist/src/debug.d.ts.map +1 -1
- package/dist/src/debug.js +54 -18
- package/dist/src/debug.js.map +1 -1
- package/dist/src/fast-semver.d.ts +49 -0
- package/dist/src/fast-semver.d.ts.map +1 -0
- package/dist/src/fast-semver.js +73 -0
- package/dist/src/fast-semver.js.map +1 -0
- package/dist/src/fs.d.ts +57 -0
- package/dist/src/fs.d.ts.map +1 -1
- package/dist/src/fs.js +200 -25
- package/dist/src/fs.js.map +1 -1
- package/dist/src/internal/debug.d.ts +35 -0
- package/dist/src/internal/debug.d.ts.map +1 -0
- package/dist/src/internal/debug.js +91 -0
- package/dist/src/internal/debug.js.map +1 -0
- package/dist/src/internal/fs.d.ts +12 -0
- package/dist/src/internal/fs.d.ts.map +1 -0
- package/dist/src/internal/fs.js +42 -0
- package/dist/src/internal/fs.js.map +1 -0
- package/dist/src/runtime.d.ts +13 -0
- package/dist/src/runtime.d.ts.map +1 -0
- package/dist/src/runtime.js +34 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/subprocess.d.ts +2 -0
- package/dist/src/subprocess.d.ts.map +1 -1
- package/dist/src/subprocess.js +1 -0
- package/dist/src/subprocess.js.map +1 -1
- package/dist/src/synchronization.d.ts +97 -0
- package/dist/src/synchronization.d.ts.map +1 -1
- package/dist/src/synchronization.js +171 -2
- package/dist/src/synchronization.js.map +1 -1
- package/package.json +4 -4
- package/src/debug.ts +73 -28
- package/src/fast-semver.ts +95 -0
- package/src/fs.ts +272 -35
- package/src/internal/debug.ts +119 -0
- package/src/internal/fs.ts +57 -0
- package/src/runtime.ts +45 -0
- package/src/subprocess.ts +2 -0
- package/src/synchronization.ts +211 -3
package/src/synchronization.ts
CHANGED
|
@@ -3,8 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
|
|
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 =
|
|
32
|
+
const log = createDebug("hardhat:utils:synchronization");
|
|
34
33
|
|
|
35
34
|
const PROCESS_SESSION_ID = randomUUID();
|
|
36
35
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
@@ -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
|
+
}
|