diff --git a/README.md b/README.md index 22c6e19..60cfd33 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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(undefined) + .then(() => {}) + .then(() => {}); + +console.log(await peekPromiseState(p)); // pending +await flushPromises(); +console.log(await peekPromiseState(p)); // fulfilled ``` ### Queue/Stack diff --git a/deno.jsonc b/deno.jsonc index 2d12f52..976c35e 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -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", @@ -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", diff --git a/flush_promises.ts b/flush_promises.ts new file mode 100644 index 0000000..6fb70f2 --- /dev/null +++ b/flush_promises.ts @@ -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 { + return new Promise((resolve) => setTimeout(resolve)); +} diff --git a/flush_promises_test.ts b/flush_promises_test.ts new file mode 100644 index 0000000..1d046bd --- /dev/null +++ b/flush_promises_test.ts @@ -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); + }, +); diff --git a/notify_test.ts b/notify_test.ts index bd96902..b756ef9 100644 --- a/notify_test.ts +++ b/notify_test.ts @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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( @@ -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"); }, ); diff --git a/peek_promise_state.ts b/peek_promise_state.ts new file mode 100644 index 0000000..1abbd99 --- /dev/null +++ b/peek_promise_state.ts @@ -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(undefined) + * .then(() => {}) + * .then(() => {}); + * assertEquals(await peekPromiseState(p), "pending"); + * await flushPromises(); + * assertEquals(await peekPromiseState(p), "fulfilled"); + * ``` + */ +export function peekPromiseState(p: Promise): Promise { + return Promise.race([p, t]).then( + (v) => (v === t ? "pending" : "fulfilled"), + () => "rejected", + ); +} diff --git a/peek_promise_state_bench.ts b/peek_promise_state_bench.ts new file mode 100644 index 0000000..e6523d9 --- /dev/null +++ b/peek_promise_state_bench.ts @@ -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, +}); diff --git a/peek_promise_state_test.ts b/peek_promise_state_test.ts new file mode 100644 index 0000000..75c6eb2 --- /dev/null +++ b/peek_promise_state_test.ts @@ -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(undefined) + .then(() => {}) + .then(() => {}); + assertEquals(await peekPromiseState(p), "pending"); + await flushPromises(); + assertEquals(await peekPromiseState(p), "fulfilled"); +}); diff --git a/promise_state.ts b/promise_state.ts index 4be0408..631b93d 100644 --- a/promise_state.ts +++ b/promise_state.ts @@ -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 @@ -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): Promise { - // NOTE: - // This 0 delay promise is required to refresh internal states of promises - await new Promise((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 }; diff --git a/promise_state_bench.ts b/promise_state_bench.ts new file mode 100644 index 0000000..7116aa2 --- /dev/null +++ b/promise_state_bench.ts @@ -0,0 +1,29 @@ +import { promiseState } from "./promise_state.ts"; + +Deno.bench({ + name: "current", + fn: async () => { + await promiseState(Promise.resolve("fulfilled")); + }, + group: "promiseState (fulfilled)", + baseline: true, +}); + +Deno.bench({ + name: "current", + fn: async () => { + const p = Promise.reject("reject").catch(() => {}); + await promiseState(p); + }, + group: "promiseState (rejected)", + baseline: true, +}); + +Deno.bench({ + name: "current", + fn: async () => { + await promiseState(new Promise(() => {})); + }, + group: "promiseState (pending)", + baseline: true, +}); diff --git a/queue_test.ts b/queue_test.ts index a91d912..0f92f3d 100644 --- a/queue_test.ts +++ b/queue_test.ts @@ -1,7 +1,8 @@ import { test } from "@cross/test"; import { delay } from "@std/async/delay"; import { assertEquals, assertRejects } from "@std/assert"; -import { promiseState } from "./promise_state.ts"; +import { flushPromises } from "./flush_promises.ts"; +import { peekPromiseState } from "./peek_promise_state.ts"; import { Queue } from "./queue.ts"; test("Queue 'pop' returns pushed items", async () => { @@ -17,9 +18,11 @@ test("Queue 'pop' returns pushed items", async () => { test("Queue 'pop' waits for an item is pushed", async () => { const q = new Queue(); const popper = q.pop(); - assertEquals(await promiseState(popper), "pending"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "pending"); q.push(1); - assertEquals(await promiseState(popper), "fulfilled"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "fulfilled"); assertEquals(await popper, 1); }); @@ -27,7 +30,8 @@ test("Queue 'pop' with non-aborted signal", async () => { const controller = new AbortController(); const q = new Queue(); const popper = q.pop({ signal: controller.signal }); - assertEquals(await promiseState(popper), "pending"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "pending"); }); test("Queue 'pop' with signal aborted after delay", async () => { @@ -61,17 +65,21 @@ test("Queue 'pop' with signal already aborted", async () => { test("Queue with falsy value is accepted", async () => { const q = new Queue(); const popper = q.pop(); - assertEquals(await promiseState(popper), "pending"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "pending"); q.push(0); - assertEquals(await promiseState(popper), "fulfilled"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "fulfilled"); assertEquals(await popper, 0); }); test("Queue with null is accepted", async () => { const q = new Queue(); const popper = q.pop(); - assertEquals(await promiseState(popper), "pending"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "pending"); q.push(null); - assertEquals(await promiseState(popper), "fulfilled"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "fulfilled"); assertEquals(await popper, null); }); diff --git a/rw_lock_test.ts b/rw_lock_test.ts index 2893b4f..50b30cb 100644 --- a/rw_lock_test.ts +++ b/rw_lock_test.ts @@ -1,6 +1,7 @@ import { test } from "@cross/test"; import { assertEquals } from "@std/assert"; -import { promiseState } from "./promise_state.ts"; +import { flushPromises } from "./flush_promises.ts"; +import { peekPromiseState } from "./peek_promise_state.ts"; import { AsyncValue } from "./async_value.ts"; import { RwLock } from "./rw_lock.ts"; @@ -89,11 +90,13 @@ test( }; const r = reader(); const w = writer(); - assertEquals(await promiseState(r), "pending"); - assertEquals(await promiseState(w), "pending"); + await flushPromises(); + assertEquals(await peekPromiseState(r), "pending"); + assertEquals(await peekPromiseState(w), "pending"); resolve(); - assertEquals(await promiseState(r), "fulfilled"); - assertEquals(await promiseState(w), "fulfilled"); + await flushPromises(); + assertEquals(await peekPromiseState(r), "fulfilled"); + assertEquals(await peekPromiseState(w), "fulfilled"); }, ); @@ -114,10 +117,12 @@ test( }; const w = writer(); const r = reader(); - assertEquals(await promiseState(w), "pending"); - assertEquals(await promiseState(r), "pending"); + await flushPromises(); + assertEquals(await peekPromiseState(w), "pending"); + assertEquals(await peekPromiseState(r), "pending"); resolve(); - assertEquals(await promiseState(w), "fulfilled"); - assertEquals(await promiseState(r), "fulfilled"); + await flushPromises(); + assertEquals(await peekPromiseState(w), "fulfilled"); + assertEquals(await peekPromiseState(r), "fulfilled"); }, ); diff --git a/stack_test.ts b/stack_test.ts index 125d4b4..0e0d0af 100644 --- a/stack_test.ts +++ b/stack_test.ts @@ -1,7 +1,8 @@ import { test } from "@cross/test"; import { delay } from "@std/async/delay"; import { assertEquals, assertRejects } from "@std/assert"; -import { promiseState } from "./promise_state.ts"; +import { flushPromises } from "./flush_promises.ts"; +import { peekPromiseState } from "./peek_promise_state.ts"; import { Stack } from "./stack.ts"; test("Stack 'pop' returns pushed items", async () => { @@ -17,9 +18,11 @@ test("Stack 'pop' returns pushed items", async () => { test("Stack 'pop' waits for an item is pushed", async () => { const q = new Stack(); const popper = q.pop(); - assertEquals(await promiseState(popper), "pending"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "pending"); q.push(1); - assertEquals(await promiseState(popper), "fulfilled"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "fulfilled"); assertEquals(await popper, 1); }); @@ -27,7 +30,8 @@ test("Stack 'pop' with non-aborted signal", async () => { const controller = new AbortController(); const q = new Stack(); const popper = q.pop({ signal: controller.signal }); - assertEquals(await promiseState(popper), "pending"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "pending"); }); test("Stack 'pop' with signal aborted after delay", async () => { @@ -61,17 +65,21 @@ test("Stack 'pop' with signal already aborted", async () => { test("Stack with falsy value is accepted", async () => { const q = new Stack(); const popper = q.pop(); - assertEquals(await promiseState(popper), "pending"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "pending"); q.push(0); - assertEquals(await promiseState(popper), "fulfilled"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "fulfilled"); assertEquals(await popper, 0); }); test("Stack with null is accepted", async () => { const q = new Stack(); const popper = q.pop(); - assertEquals(await promiseState(popper), "pending"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "pending"); q.push(null); - assertEquals(await promiseState(popper), "fulfilled"); + await flushPromises(); + assertEquals(await peekPromiseState(popper), "fulfilled"); assertEquals(await popper, null); });