onceAsync
Ensures an async function is only executed once. Subsequent calls return the same Promise. Includes a .reset()
method to allow re-invocation.
1/**
2 * Ensures an async function is only executed once.
3 * Subsequent calls return the same Promise.
4 * Includes a .reset() method to allow re-invocation.
5 *
6 * @param fn - The async function to wrap.
7 * @returns A function that only runs once, with .reset() support.
8 */
9export function onceAsync<T extends (...args: any[]) => Promise<any>>(
10 fn: T
11): (() => ReturnType<T>) & { reset: () => void } {
12 let promise: ReturnType<T> | null = null;
13
14 const wrapper = () => {
15 if (!promise) {
16 promise = fn();
17 }
18 return promise;
19 };
20
21 wrapper.reset = () => {
22 promise = null;
23 };
24
25 return wrapper;
26}
Single Execution Guarantee
Ensures the wrapped async function is only triggered once, even across concurrent invocations.
Promise Reuse
Returns the same
Promise
for all subsequent calls, preventing duplicate work and unnecessary async operations.Resettable
Provides a
.reset()
method to clear cached execution, enabling controlled re-execution when needed.Concurrency Safe
Avoids race conditions by storing the in-progress promise, ensuring only one instance of the async function runs at a time.
Tests | Examples
1test('onceAsync - only runs the function once', async () => {
2 const fn = jest.fn().mockResolvedValue('ok');
3 const onceFn = onceAsync(fn);
4
5 const result1 = await onceFn();
6 const result2 = await onceFn();
7
8 expect(result1).toBe('ok');
9 expect(result2).toBe('ok');
10 expect(fn).toHaveBeenCalledTimes(1);
11});
12
13test('onceAsync - multiple calls before resolve return same promise', async () => {
14 let resolver: (v: string) => void = () => {};
15 const promise = new Promise<string>(res => { resolver = res; });
16
17 const fn = jest.fn().mockReturnValue(promise);
18 const onceFn = onceAsync(fn);
19
20 const call1 = onceFn();
21 const call2 = onceFn();
22
23 resolver('resolved');
24
25 const [res1, res2] = await Promise.all([call1, call2]);
26
27 expect(res1).toBe('resolved');
28 expect(res2).toBe('resolved');
29 expect(fn).toHaveBeenCalledTimes(1);
30});
31
32test('onceAsync - reset allows function to be called again', async () => {
33 const fn = jest.fn()
34 .mockResolvedValueOnce('first')
35 .mockResolvedValueOnce('second');
36 const onceFn = onceAsync(fn);
37
38 const first = await onceFn();
39 expect(first).toBe('first');
40 expect(fn).toHaveBeenCalledTimes(1);
41
42 onceFn.reset();
43
44 const second = await onceFn();
45 expect(second).toBe('second');
46 expect(fn).toHaveBeenCalledTimes(2);
47});
Common Use Cases
Lazy Initialization
Initialize resources like database connections, API clients, or WebSocket sessions only once per lifecycle.
Memoized Async Configuration
Load configuration or environment data once and share it across your application.
Singleton Service Setup
Run costly setup tasks (e.g. authentication bootstrap, schema loading) a single time on first use.
Prevent Duplicate API Calls
Avoid redundant network requests caused by multiple parallel triggers for the same operation.
Testing and Mocking
Control async mocks or stubs so they only resolve once per test run unless explicitly reset.