Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions addon/src/build-waiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ function getNextToken(): Token {
return new Token();
}

/**
* A promise that carries its own resolver, so a collection of pending
* operations holds nothing but the promises themselves.
*/
type ResolvablePromise = Promise<void> & { resolve: () => void };

function resolvablePromise(): ResolvablePromise {
let resolve!: () => void;
const promise = new Promise<void>((r) => (resolve = r)) as ResolvablePromise;

promise.resolve = resolve;

return promise;
}

class TestWaiterImpl<T extends object | Primitive = Token> implements TestWaiter<T> {
public name: WaiterName;
private nextToken: () => T;
Expand All @@ -24,6 +39,14 @@ class TestWaiterImpl<T extends object | Primitive = Token> implements TestWaiter
completedOperationsForTokens = new WeakMap<Token, boolean>();
completedOperationsForPrimitives = new Map<Primitive, boolean>();

/**
* The completion promise for each pending operation, keyed by token so
* `endAsync` can settle its own. Values are the promises themselves,
* each carrying its resolver, and an entry is dropped as it resolves --
* so this only ever holds operations still in flight.
*/
private pendingPromises = new Map<T, ResolvablePromise>();

constructor(name: WaiterName, nextToken?: () => T) {
this.name = name;
// @ts-ignore
Expand All @@ -47,6 +70,8 @@ class TestWaiterImpl<T extends object | Primitive = Token> implements TestWaiter
label,
});

this.pendingPromises.set(token, resolvablePromise());

return token;
}

Expand All @@ -64,6 +89,13 @@ class TestWaiterImpl<T extends object | Primitive = Token> implements TestWaiter
// Mark when a waiter operation has completed so we can distinguish
// whether endAsync is being called before a prior beginAsync call above.
this._getCompletedOperations(token).set(token, true);

const pending = this.pendingPromises.get(token);

if (pending !== undefined) {
this.pendingPromises.delete(token);
pending.resolve();
}
}

waitUntil(): boolean {
Expand All @@ -80,8 +112,20 @@ class TestWaiterImpl<T extends object | Primitive = Token> implements TestWaiter
return result;
}

settled(): Promise<unknown> {
return Promise.all(this.pendingPromises.values());
}

reset(): void {
this.items.clear();

// anything awaiting these would otherwise wait on operations this
// waiter has stopped tracking
for (const pending of this.pendingPromises.values()) {
pending.resolve();
}

this.pendingPromises.clear();
}

private _register(): void {
Expand Down
1 change: 1 addition & 0 deletions addon/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export {
_reset,
getPendingWaiterState,
hasPendingWaiters,
waitersSettled,
} from './waiter-manager.ts';

export { default as buildWaiter, _resetWaiterNames } from './build-waiter.ts';
Expand Down
15 changes: 15 additions & 0 deletions addon/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ export interface Waiter {
* @returns {TestWaiterDebugInfo}
*/
debugInfo(): TestWaiterDebugInfo[];

/**
* Resolves when the operations this waiter is currently tracking have
* completed, so callers can await completion rather than polling
* `waitUntil`. Operations begun after the call are not included --
* callers that need a fixpoint should re-check.
*
* Optional: a waiter that cannot know when it goes quiet may omit it,
* and `waitersSettled` will treat this waiter as un-announceable.
*
* @public
* @method settled
* @returns {Promise<unknown>} resolves when the tracked operations complete
*/
settled?(): Promise<unknown>;
}

/**
Expand Down
35 changes: 35 additions & 0 deletions addon/src/waiter-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,38 @@ export function hasPendingWaiters(): boolean {

return state.pending > 0;
}

/**
* Never resolves. Returned when a pending waiter cannot announce its own
* completion, so callers fall through to whatever fallback they race
* this against rather than being told, wrongly, that things are quiet.
*/
const NEVER: Promise<unknown> = new Promise(() => {});

/**
* Resolves when the operations all waiters are currently tracking have
* completed, composed from the waiters' own completion promises rather
* than by polling `hasPendingWaiters`.
*
* Operations begun after this call are not included: settling can start
* more work, so a caller that needs a true fixpoint re-checks. And a
* pending waiter that does not implement `settled` cannot announce
* anything, so this never resolves while one is outstanding -- race it
* against a fallback tick if you must tolerate those.
*
* @public
* @returns {Promise<unknown>} resolves when the tracked operations complete
*/
export function waitersSettled(): Promise<unknown> {
const settled: Promise<unknown>[] = [];

for (const waiter of getWaiters()) {
if (typeof waiter.settled === 'function') {
settled.push(waiter.settled());
} else if (!waiter.waitUntil()) {
return NEVER;
}
}

return Promise.all(settled);
}