/** * A proxy to handle awaiting a core promise before executing methods. * Supports inheritance and composition. * * For inheritance, extend AwaitProxy and use `withAwaited` to wrap methods. * * For composition, create an instance of AwaitProxy and use `apply` to * wrap methods of the target instance. */ export class AwaitProxy { private _awaited: T | undefined; constructor( private readonly _promise: Promise, private readonly _bindTo: any = this, ) { _promise.then((v) => { this._awaited = v; }); } get awaited(): T { if (!this._awaited) { throw new Error( 'Promise not resolved yet. Did you wrap the accessing method in withAwaited or use apply on your class instance?', ); } return this._awaited; } /** * Wraps all methods of the target instance to await the core promise before executing. * Or pass a list of method names to apply to. */ apply = ( target: TTarget, onlyNames?: (keyof TTarget)[], ) => { const names = Object.getOwnPropertyNames(target); for (const key of names) { if (key === 'constructor') continue; const desc = Object.getOwnPropertyDescriptor(target, key); if ( desc && typeof desc.value === 'function' && (!onlyNames || onlyNames.includes(key as keyof TTarget)) ) { // Wrap method target[key as keyof TTarget] = this.afterAwaited(desc.value) as any; } } }; /** * Extends a method to await the core promise and passes it as the * first argument. */ withAwaited( fn: (awaited: T, ...args: TArgs) => TReturn, ): (...args: TArgs) => Promise> { return async (...args: TArgs): Promise> => { const awaited = await this._promise; return fn.apply(this._bindTo, [awaited, ...args]) as Unpacked; }; } /** * Wraps a method to await the core promise but does not change * its arguments. */ afterAwaited( fn: (...args: TArgs) => TReturn, ): (...args: TArgs) => Promise> { return async (...args: TArgs): Promise> => { await this._promise; return fn.apply(this._bindTo, args) as Unpacked; }; } } type Unpacked = T extends Promise ? U : T;