Skip to content

Commit

Permalink
feat: add promiseWithResolvers (#5808)
Browse files Browse the repository at this point in the history
Add new utility function to `@aztec/foundation` to helpt with hand-rolled promises.
  • Loading branch information
alexghr authored Apr 19, 2024
1 parent 27129e6 commit afeef17
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 61 deletions.
3 changes: 2 additions & 1 deletion yarn-project/foundation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"./noir": "./dest/noir/index.js",
"./testing": "./dest/testing/index.js",
"./array": "./dest/array/index.js",
"./validation": "./dest/validation/index.js"
"./validation": "./dest/validation/index.js",
"./promise": "./dest/promise/index.js"
},
"scripts": {
"build": "yarn clean && tsc -b",
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/foundation/src/promise/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './running-promise.js';
export * from './utils.js';
47 changes: 47 additions & 0 deletions yarn-project/foundation/src/promise/running-promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { InterruptibleSleep } from '../sleep/index.js';

/**
* RunningPromise is a utility class that helps manage the execution of an asynchronous function
* at a specified polling interval. It allows starting, stopping, and checking the status of the
* internally managed promise. The class also supports interrupting the polling process when stopped.
*/
export class RunningPromise {
private running = false;
private runningPromise = Promise.resolve();
private interruptibleSleep = new InterruptibleSleep();

constructor(private fn: () => Promise<void>, private pollingIntervalMS = 10000) {}

/**
* Starts the running promise.
*/
public start() {
this.running = true;

const poll = async () => {
while (this.running) {
await this.fn();
await this.interruptibleSleep.sleep(this.pollingIntervalMS);
}
};
this.runningPromise = poll();
}

/**
* Stops the running promise, resolves any pending interruptible sleep,
* and waits for the currently executing function to complete.
*/
async stop(): Promise<void> {
this.running = false;
this.interruptibleSleep.interrupt();
await this.runningPromise;
}

/**
* Checks if the running promise is currently active.
* @returns True if the promise is running.
*/
public isRunning() {
return this.running;
}
}
29 changes: 29 additions & 0 deletions yarn-project/foundation/src/promise/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export type PromiseWithResolvers<T> = {
promise: Promise<T>;
resolve: (value: T) => void;
reject: (reason?: any) => void;
};

/**
* A polyfill for the Promise.withResolvers proposed API.
* @see https://github.com/tc39/proposal-promise-with-resolvers
* @returns A promise with resolvers.
*/
export function promiseWithResolvers<T>(): PromiseWithResolvers<T> {
// use ! operator to avoid TS error
let resolve!: (value: T) => void;
let reject!: (reason?: any) => void;

// the ES spec guarantees that the promise executor is called synchronously
// so the resolve and reject functions will be defined
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});

return {
promise,
resolve,
reject,
};
}
61 changes: 1 addition & 60 deletions yarn-project/foundation/src/running-promise/index.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1 @@
/**
* RunningPromise is a utility class that helps manage the execution of an asynchronous function
* at a specified polling interval. It allows starting, stopping, and checking the status of the
* internally managed promise. The class also supports interrupting the polling process when stopped.
*/
export class RunningPromise {
private running = false;
private runningPromise = Promise.resolve();
private interruptPromise = Promise.resolve();
private interruptResolve = () => {};
constructor(private fn: () => Promise<void>, private pollingInterval = 10000) {}

/**
* Starts the running promise.
*/
public start() {
this.running = true;
this.interruptPromise = new Promise(resolve => (this.interruptResolve = resolve));

const poll = async () => {
while (this.running) {
await this.fn();
await this.interruptibleSleep(this.pollingInterval);
}
};
this.runningPromise = poll();
}

/**
* Stops the running promise, resolves any pending interruptible sleep,
* and waits for the currently executing function to complete.
*/
async stop(): Promise<void> {
this.running = false;
this.interruptResolve();
await this.runningPromise;
}

/**
* A sleep function that can be interrupted before the specified time.
* The sleep duration is determined by 'timeInMs', and it can be terminated early if the 'interruptPromise' is resolved.
* @param timeInMs - The time in milliseconds.
*/
private async interruptibleSleep(timeInMs: number) {
let timeout!: NodeJS.Timeout;
const sleepPromise = new Promise(resolve => {
timeout = setTimeout(resolve, timeInMs);
});
await Promise.race([sleepPromise, this.interruptPromise]);
clearTimeout(timeout);
}

/**
* Checks if the running promise is currently active.
* @returns True if the promise is running.
*/
public isRunning() {
return this.running;
}
}
export * from '../promise/running-promise.js';

0 comments on commit afeef17

Please sign in to comment.