| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| interface PromiseInfo<T> { |
| promise: Promise<T>; |
| resolve: (obj: T) => void; |
| reject: (error: Error) => void; |
| } |
| |
| /** |
| * A class that facilitates resolving a id to an object of type T. If the id does not yet resolve, a promise |
| * is created that gets resolved once `onResolve` is called with the corresponding id. |
| * |
| * This class enables clients to control the duration of the wait and the lifetime of the associated |
| * promises by using the `clear` method on this class. |
| */ |
| export abstract class ResolverBase<Id, T> { |
| #unresolvedIds = new Map<Id, PromiseInfo<T>>(); |
| |
| protected abstract getForId(id: Id): T|null; |
| protected abstract startListening(): void; |
| protected abstract stopListening(): void; |
| |
| /** |
| * Returns a promise that resolves once the `id` can be resolved to an object. |
| */ |
| async waitFor(id: Id): Promise<T> { |
| const obj = this.getForId(id); |
| if (!obj) { |
| return await this.getOrCreatePromise(id); |
| } |
| return obj; |
| } |
| |
| /** |
| * Resolve the `id`. Returns the object immediately if it can be resolved, |
| * and otherwise waits for the object to appear and calls `callback` once |
| * it is resolved. |
| */ |
| tryGet(id: Id, callback: (t: T) => void): T|null { |
| const obj = this.getForId(id); |
| if (!obj) { |
| const swallowTheError = (): void => {}; |
| void this.getOrCreatePromise(id).catch(swallowTheError).then(obj => { |
| if (obj) { |
| callback(obj); |
| } |
| }); |
| return null; |
| } |
| return obj; |
| } |
| |
| /** |
| * Aborts all waiting and rejects all unresolved promises. |
| */ |
| clear(): void { |
| this.stopListening(); |
| for (const [id, {reject}] of this.#unresolvedIds.entries()) { |
| reject(new Error(`Object with ${id} never resolved.`)); |
| } |
| this.#unresolvedIds.clear(); |
| } |
| |
| private getOrCreatePromise(id: Id): Promise<T> { |
| const promiseInfo = this.#unresolvedIds.get(id); |
| if (promiseInfo) { |
| return promiseInfo.promise; |
| } |
| const {resolve, reject, promise} = Promise.withResolvers<T>(); |
| this.#unresolvedIds.set(id, {promise, resolve, reject}); |
| this.startListening(); |
| return promise; |
| } |
| |
| protected onResolve(id: Id, t: T): void { |
| const promiseInfo = this.#unresolvedIds.get(id); |
| this.#unresolvedIds.delete(id); |
| if (this.#unresolvedIds.size === 0) { |
| this.stopListening(); |
| } |
| promiseInfo?.resolve(t); |
| } |
| } |