From 5178a72c523397f27c01306f26f0fcdfb7564fab Mon Sep 17 00:00:00 2001 From: Jonathan Tran Date: Tue, 3 Dec 2024 13:12:46 -0500 Subject: [PATCH] Add registry for background promises --- src/lib/settle.ts | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/lib/settle.ts diff --git a/src/lib/settle.ts b/src/lib/settle.ts new file mode 100644 index 000000000..d5416fa44 --- /dev/null +++ b/src/lib/settle.ts @@ -0,0 +1,90 @@ +/** + * A registry for tracking background work and reacting once all work has + * settled. + */ +export class PromiseRegistry { + outstanding: Array> + settleCallbacks: Array<() => void> + + constructor() { + this.outstanding = [] + this.settleCallbacks = [] + } + + track(promise: Promise, onSettle?: () => void) { + // Since built-in Promises don't have a way to synchronously check if + // they're settled, it cannot start out settled. + this.outstanding.push( + new TrackedPromise(promise, this.attemptCleanUp.bind(this)) + ) + if (onSettle) { + this.settleCallbacks.push(onSettle) + } + } + + /** + * Returns a promise that resolves when all promises have settled. + */ + waitForSettle(): Promise { + return new Promise((resolve) => { + this.addSettleCallback(resolve) + }) + } + + /** + * Add a callback to be called when all promises have settled. + * + * @see waitForSettle for a Promise-based interface. + */ + addSettleCallback(onSettle: () => void) { + if (this.isSettled()) { + // Already settled, so schedule the callback. + setTimeout(onSettle, 0) + } else { + this.settleCallbacks.push(onSettle) + } + } + + isSettled(): boolean { + return this.outstanding.every((p) => p.settled) + } + + private attemptCleanUp() { + if (this.outstanding.length === 0) { + return + } + // Garbage collect. + const unsettled = this.outstanding.filter((p) => !p.settled) + this.outstanding = unsettled + // Transition to settled. We could move this into TrackedPromise. + // It's a trade-off between reducing latency and reducing overhead. + if (unsettled.length === 0) { + for (const cb of this.settleCallbacks) { + cb() + } + this.settleCallbacks = [] + } + } +} + +/** + * Native Promises don't have a way to synchronously detect if they're settled. + */ +class TrackedPromise { + settled: boolean + inner: Promise + + constructor(promise: Promise, onSettle: () => void) { + this.settled = false + this.inner = promise.finally(() => { + this.settled = true + // TODO: debounce? + setTimeout(onSettle, 0) + }) + } +} + +/** + * Singleton regsitry for the whole app. + */ +export const AppPromises = new PromiseRegistry()