@@ -34,15 +34,91 @@ export function makeSafeRunner(onError: (error: unknown) => void) {
3434
3535type AsyncOperation < T > = ( ) => Promise < T > ;
3636
37+ /**
38+ * A mutex for asynchronous operations that ensures only one operation runs at a time.
39+ *
40+ * When multiple callers attempt to execute operations simultaneously, they will all
41+ * receive the same promise from the currently running operation, effectively deduplicating
42+ * concurrent calls. This is useful for expensive operations like API calls, file operations,
43+ * or database queries that should not be executed multiple times concurrently.
44+ *
45+ * @template T - The default return type for operations when using a default operation
46+ *
47+ * @example
48+ * // Basic usage with explicit operations
49+ * const mutex = new AsyncMutex();
50+ *
51+ * // Multiple concurrent calls will deduplicate
52+ * const [result1, result2, result3] = await Promise.all([
53+ * mutex.do(() => fetch('/api/data')),
54+ * mutex.do(() => fetch('/api/data')), // Same request, will get same promise
55+ * mutex.do(() => fetch('/api/data')) // Same request, will get same promise
56+ * ]);
57+ * // Only one fetch actually happens
58+ *
59+ * @example
60+ * // Usage with a default operation
61+ * const dataLoader = new AsyncMutex(() =>
62+ * fetch('/api/expensive-data').then(res => res.json())
63+ * );
64+ *
65+ * // Multiple components can call this without duplication
66+ * const data1 = await dataLoader.do(); // Executes the fetch
67+ * const data2 = await dataLoader.do(); // Gets the same promise result
68+ */
3769export class AsyncMutex < T = unknown > {
3870 private currentOperation : Promise < any > | null = null ;
3971 private defaultOperation ?: AsyncOperation < T > ;
4072
73+ /**
74+ * Creates a new AsyncMutex instance.
75+ *
76+ * @param operation - Optional default operation to execute when calling `do()` without arguments.
77+ * This is useful when you have a specific operation that should be deduplicated.
78+ *
79+ * @example
80+ * // Without default operation
81+ * const mutex = new AsyncMutex();
82+ * await mutex.do(() => someAsyncWork());
83+ *
84+ * @example
85+ * // With default operation
86+ * const dataMutex = new AsyncMutex(() => loadExpensiveData());
87+ * await dataMutex.do(); // Executes loadExpensiveData()
88+ */
4189 constructor ( operation ?: AsyncOperation < T > ) {
4290 this . defaultOperation = operation ;
4391 }
4492
93+ /**
94+ * Executes the default operation if one was provided in the constructor.
95+ * @returns Promise that resolves with the result of the default operation
96+ * @throws Error if no default operation was set in the constructor
97+ */
4598 do ( ) : Promise < T > ;
99+ /**
100+ * Executes the provided operation, ensuring only one runs at a time.
101+ *
102+ * If an operation is already running, all subsequent calls will receive
103+ * the same promise from the currently running operation. This effectively
104+ * deduplicates concurrent calls to the same expensive operation.
105+ *
106+ * @param operation - Optional operation to execute. If not provided, uses the default operation.
107+ * @returns Promise that resolves with the result of the operation
108+ * @throws Error if no operation is provided and no default operation was set
109+ *
110+ * @example
111+ * const mutex = new AsyncMutex();
112+ *
113+ * // These will all return the same promise
114+ * const promise1 = mutex.do(() => fetch('/api/data'));
115+ * const promise2 = mutex.do(() => fetch('/api/other')); // Still gets first promise!
116+ * const promise3 = mutex.do(() => fetch('/api/another')); // Still gets first promise!
117+ *
118+ * // After the first operation completes, new operations can run
119+ * await promise1;
120+ * const newPromise = mutex.do(() => fetch('/api/new')); // This will execute
121+ */
46122 do < U > ( operation : AsyncOperation < U > ) : Promise < U > ;
47123 do < U = T > ( operation ?: AsyncOperation < U > ) : Promise < U | T > {
48124 if ( ! operation && ! this . defaultOperation ) {
0 commit comments