Skip to content

Commit

Permalink
Merge pull request #38 from jsr-core/improve-promise-state
Browse files Browse the repository at this point in the history
feat: add `flushPromises` and `peekPromiseState` then deprecate `promiseState`
  • Loading branch information
lambdalisue authored Aug 21, 2024
2 parents 5950811 + 45c948e commit f5a9505
Show file tree
Hide file tree
Showing 13 changed files with 309 additions and 69 deletions.
48 changes: 41 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@ const p2 = ensurePromise("Not a promise");
console.log(await p2); // Not a promise
```

### flushPromises

`flushPromises` flushes all pending promises in the microtask queue.

```ts
import { flushPromises } from "@core/asyncutil/flush-promises";

let count = 0;
Array.from({ length: 5 }).forEach(() => {
Promise.resolve()
.then(() => count++)
.then(() => count++);
});

console.log(count); // 0
await flushPromises();
console.log(count); // 10
```

### Lock/RwLock

`Lock` is a mutual exclusion lock that provides safe concurrent access to a
Expand Down Expand Up @@ -156,22 +175,37 @@ assertEquals(await promiseState(waiter1), "fulfilled");
assertEquals(await promiseState(waiter2), "fulfilled");
```

### promiseState
### peekPromiseState

`promiseState` is used to determine the state of the promise. Mainly for testing
purpose.
`peekPromiseState` is used to determine the state of the promise. Mainly for
testing purpose.

```typescript
import { promiseState } from "@core/asyncutil/promise-state";
import { peekPromiseState } from "@core/asyncutil/peek-promise-state";

const p1 = Promise.resolve("Resolved promise");
console.log(await promiseState(p1)); // fulfilled
console.log(await peekPromiseState(p1)); // fulfilled

const p2 = Promise.reject("Rejected promise").catch(() => undefined);
console.log(await promiseState(p2)); // rejected
console.log(await peekPromiseState(p2)); // rejected

const p3 = new Promise(() => undefined);
console.log(await promiseState(p3)); // pending
console.log(await peekPromiseState(p3)); // pending
```

Use `flushPromises` to wait all pending promises to resolve.

```typescript
import { flushPromises } from "@core/asyncutil/flush-promises";
import { peekPromiseState } from "@core/asyncutil/peek-promise-state";

const p = Promise.resolve<void>(undefined)
.then(() => {})
.then(() => {});

console.log(await peekPromiseState(p)); // pending
await flushPromises();
console.log(await peekPromiseState(p)); // fulfilled
```

### Queue/Stack
Expand Down
4 changes: 4 additions & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
"./async-value": "./async_value.ts",
"./barrier": "./barrier.ts",
"./ensure-promise": "./ensure_promise.ts",
"./flush-promises": "./flush_promises.ts",
"./lock": "./lock.ts",
"./mutex": "./mutex.ts",
"./notify": "./notify.ts",
"./peek-promise-state": "./peek_promise_state.ts",
"./promise-state": "./promise_state.ts",
"./queue": "./queue.ts",
"./rw-lock": "./rw_lock.ts",
Expand Down Expand Up @@ -36,9 +38,11 @@
"@core/asyncutil/async-value": "./async_value.ts",
"@core/asyncutil/barrier": "./barrier.ts",
"@core/asyncutil/ensure-promise": "./ensure_promise.ts",
"@core/asyncutil/flush-promises": "./flush_promises.ts",
"@core/asyncutil/lock": "./lock.ts",
"@core/asyncutil/mutex": "./mutex.ts",
"@core/asyncutil/notify": "./notify.ts",
"@core/asyncutil/peek-promise-state": "./peek_promise_state.ts",
"@core/asyncutil/promise-state": "./promise_state.ts",
"@core/asyncutil/queue": "./queue.ts",
"@core/asyncutil/rw-lock": "./rw_lock.ts",
Expand Down
25 changes: 25 additions & 0 deletions flush_promises.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Flush all pending promises in the microtask queue.
*
* ```ts
* import { flushPromises } from "@core/asyncutil/flush-promises";
*
* let count = 0;
* Array.from({ length: 5 }).forEach(() => {
* Promise.resolve()
* .then(() => count++)
* .then(() => count++);
* });
*
* console.log(count); // 0
* await flushPromises();
* console.log(count); // 10
* ```
*
* The original idea comes from [flush-promises] package in npm.
*
* [flush-promises]: https://www.npmjs.com/package/flush-promises
*/
export function flushPromises(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve));
}
18 changes: 18 additions & 0 deletions flush_promises_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { test } from "@cross/test";
import { assertEquals } from "@std/assert";
import { flushPromises } from "./flush_promises.ts";

test(
"flushPromises() flushes all pending promises in the microtask queue",
async () => {
let count = 0;
Array.from({ length: 5 }).forEach(() => {
Promise.resolve()
.then(() => count++)
.then(() => count++);
});
assertEquals(count, 0);
await flushPromises();
assertEquals(count, 10);
},
);
53 changes: 30 additions & 23 deletions notify_test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { test } from "@cross/test";
import { delay } from "@std/async/delay";
import { assertEquals, assertRejects, assertThrows } from "@std/assert";
import { promiseState } from "./promise_state.ts";
import { flushPromises } from "./flush_promises.ts";
import { peekPromiseState } from "./peek_promise_state.ts";
import { Notify } from "./notify.ts";

test("Notify 'notify' wakes up a single waiter", async () => {
Expand All @@ -11,14 +12,16 @@ test("Notify 'notify' wakes up a single waiter", async () => {
assertEquals(notify.waiterCount, 2);

notify.notify();
await flushPromises();
assertEquals(notify.waiterCount, 1);
assertEquals(await promiseState(waiter1), "fulfilled");
assertEquals(await promiseState(waiter2), "pending");
assertEquals(await peekPromiseState(waiter1), "fulfilled");
assertEquals(await peekPromiseState(waiter2), "pending");

notify.notify();
await flushPromises();
assertEquals(notify.waiterCount, 0);
assertEquals(await promiseState(waiter1), "fulfilled");
assertEquals(await promiseState(waiter2), "fulfilled");
assertEquals(await peekPromiseState(waiter1), "fulfilled");
assertEquals(await peekPromiseState(waiter2), "fulfilled");
});

test("Notify 'notify' wakes up a multiple waiters", async () => {
Expand All @@ -31,28 +34,31 @@ test("Notify 'notify' wakes up a multiple waiters", async () => {
assertEquals(notify.waiterCount, 5);

notify.notify(2);
await flushPromises();
assertEquals(notify.waiterCount, 3);
assertEquals(await promiseState(waiter1), "fulfilled");
assertEquals(await promiseState(waiter2), "fulfilled");
assertEquals(await promiseState(waiter3), "pending");
assertEquals(await promiseState(waiter4), "pending");
assertEquals(await promiseState(waiter5), "pending");
assertEquals(await peekPromiseState(waiter1), "fulfilled");
assertEquals(await peekPromiseState(waiter2), "fulfilled");
assertEquals(await peekPromiseState(waiter3), "pending");
assertEquals(await peekPromiseState(waiter4), "pending");
assertEquals(await peekPromiseState(waiter5), "pending");

notify.notify(2);
await flushPromises();
assertEquals(notify.waiterCount, 1);
assertEquals(await promiseState(waiter1), "fulfilled");
assertEquals(await promiseState(waiter2), "fulfilled");
assertEquals(await promiseState(waiter3), "fulfilled");
assertEquals(await promiseState(waiter4), "fulfilled");
assertEquals(await promiseState(waiter5), "pending");
assertEquals(await peekPromiseState(waiter1), "fulfilled");
assertEquals(await peekPromiseState(waiter2), "fulfilled");
assertEquals(await peekPromiseState(waiter3), "fulfilled");
assertEquals(await peekPromiseState(waiter4), "fulfilled");
assertEquals(await peekPromiseState(waiter5), "pending");

notify.notify(2);
await flushPromises();
assertEquals(notify.waiterCount, 0);
assertEquals(await promiseState(waiter1), "fulfilled");
assertEquals(await promiseState(waiter2), "fulfilled");
assertEquals(await promiseState(waiter3), "fulfilled");
assertEquals(await promiseState(waiter4), "fulfilled");
assertEquals(await promiseState(waiter5), "fulfilled");
assertEquals(await peekPromiseState(waiter1), "fulfilled");
assertEquals(await peekPromiseState(waiter2), "fulfilled");
assertEquals(await peekPromiseState(waiter3), "fulfilled");
assertEquals(await peekPromiseState(waiter4), "fulfilled");
assertEquals(await peekPromiseState(waiter5), "fulfilled");
});

test("Notify 'notifyAll' wakes up all waiters", async () => {
Expand All @@ -62,9 +68,10 @@ test("Notify 'notifyAll' wakes up all waiters", async () => {
assertEquals(notify.waiterCount, 2);

notify.notifyAll();
await flushPromises();
assertEquals(notify.waiterCount, 0);
assertEquals(await promiseState(waiter1), "fulfilled");
assertEquals(await promiseState(waiter2), "fulfilled");
assertEquals(await peekPromiseState(waiter1), "fulfilled");
assertEquals(await peekPromiseState(waiter2), "fulfilled");
});

test(
Expand All @@ -74,7 +81,7 @@ test(
const notify = new Notify();

const waiter = notify.notified({ signal: controller.signal });
assertEquals(await promiseState(waiter), "pending");
assertEquals(await peekPromiseState(waiter), "pending");
},
);

Expand Down
41 changes: 41 additions & 0 deletions peek_promise_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const t = Symbol("pending mark");

/**
* Promise state
*/
export type PromiseState = "fulfilled" | "rejected" | "pending";

/**
* Peek the current state (fulfilled, rejected, or pending) of the promise.
*
* ```ts
* import { assertEquals } from "@std/assert";
* import { peekPromiseState } from "@core/asyncutil/peek-promise-state";
*
* assertEquals(await peekPromiseState(Promise.resolve("value")), "fulfilled");
* assertEquals(await peekPromiseState(Promise.reject("error")), "rejected");
* assertEquals(await peekPromiseState(new Promise(() => {})), "pending");
* ```
*
* Use {@linkcode https://jsr.io/@core/asyncutil/doc/flush-promises/~/flushPromises flushPromises}
* to wait for all pending promises to be resolved prior to calling this function.
*
* ```ts
* import { assertEquals } from "@std/assert";
* import { flushPromises } from "@core/asyncutil/flush-promises";
* import { peekPromiseState } from "@core/asyncutil/peek-promise-state";
*
* const p = Promise.resolve<void>(undefined)
* .then(() => {})
* .then(() => {});
* assertEquals(await peekPromiseState(p), "pending");
* await flushPromises();
* assertEquals(await peekPromiseState(p), "fulfilled");
* ```
*/
export function peekPromiseState(p: Promise<unknown>): Promise<PromiseState> {
return Promise.race([p, t]).then(
(v) => (v === t ? "pending" : "fulfilled"),
() => "rejected",
);
}
29 changes: 29 additions & 0 deletions peek_promise_state_bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { peekPromiseState } from "./peek_promise_state.ts";

Deno.bench({
name: "current",
fn: async () => {
await peekPromiseState(Promise.resolve("fulfilled"));
},
group: "peekPromiseState (fulfilled)",
baseline: true,
});

Deno.bench({
name: "current",
fn: async () => {
const p = Promise.reject("reject").catch(() => {});
await peekPromiseState(p);
},
group: "peekPromiseState (rejected)",
baseline: true,
});

Deno.bench({
name: "current",
fn: async () => {
await peekPromiseState(new Promise(() => {}));
},
group: "peekPromiseState (pending)",
baseline: true,
});
38 changes: 38 additions & 0 deletions peek_promise_state_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test } from "@cross/test";
import { assertEquals } from "@std/assert";
import { flushPromises } from "./flush_promises.ts";
import { peekPromiseState } from "./peek_promise_state.ts";

test(
"peekPromiseState() returns 'fulfilled' for resolved promise",
async () => {
const p = Promise.resolve("Resolved promise");
assertEquals(await peekPromiseState(p), "fulfilled");
},
);

test(
"peekPromiseState() returns 'rejected' for rejected promise",
async () => {
const p = Promise.reject("Rejected promise");
p.catch(() => undefined); // Avoid 'Uncaught (in promise) Rejected promise'
assertEquals(await peekPromiseState(p), "rejected");
},
);

test(
"peekPromiseState() returns 'pending' for not resolved promise",
async () => {
const p = new Promise(() => undefined);
assertEquals(await peekPromiseState(p), "pending");
},
);

test("peekPromiseState() return the current state of the promise", async () => {
const p = Promise.resolve<void>(undefined)
.then(() => {})
.then(() => {});
assertEquals(await peekPromiseState(p), "pending");
await flushPromises();
assertEquals(await peekPromiseState(p), "fulfilled");
});
22 changes: 8 additions & 14 deletions promise_state.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
/**
* Promise state
*/
export type PromiseState = "fulfilled" | "rejected" | "pending";
import { flushPromises } from "./flush_promises.ts";
import { peekPromiseState, type PromiseState } from "./peek_promise_state.ts";

/**
* Return state (fulfilled/rejected/pending) of a promise
Expand All @@ -14,16 +12,12 @@ export type PromiseState = "fulfilled" | "rejected" | "pending";
* assertEquals(await promiseState(Promise.reject("error")), "rejected");
* assertEquals(await promiseState(new Promise(() => {})), "pending");
* ```
*
* @deprecated Use {@linkcode https://jsr.io/@core/asyncutil/doc/peek-promise-state/~/peekPromiseState peekPromiseState} with {@linkcode https://jsr.io/@core/asyncutil/doc/flush-promises/~/flushPromises flushPromises} instead.
*/
export async function promiseState(p: Promise<unknown>): Promise<PromiseState> {
// NOTE:
// This 0 delay promise is required to refresh internal states of promises
await new Promise<void>((resolve) => {
setTimeout(() => resolve(), 0);
});
const t = {};
return Promise.race([p, t]).then(
(v) => (v === t ? "pending" : "fulfilled"),
() => "rejected",
);
await flushPromises();
return peekPromiseState(p);
}

export type { PromiseState };
Loading

0 comments on commit f5a9505

Please sign in to comment.