// Copyright 2017-2025 @pezkuwi/api authors & contributors // SPDX-License-Identifier: Apache-2.0 import type { Callback } from '@pezkuwi/types/types'; import type { UnsubscribePromise } from '../types/index.js'; import { isFunction, noop } from '@pezkuwi/util'; export type CombinatorCallback = Callback; export type CombinatorFunction = (cb: Callback) => UnsubscribePromise; export class Combinator { #allHasFired = false; #callback: CombinatorCallback; #fired: boolean[] = []; #fns: CombinatorFunction[] = []; #isActive = true; #results: unknown[] = []; #subscriptions: UnsubscribePromise[] = []; constructor (fns: (CombinatorFunction | [CombinatorFunction, ...unknown[]])[], callback: CombinatorCallback) { this.#callback = callback; // eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/require-await this.#subscriptions = fns.map(async (input, index): UnsubscribePromise => { const [fn, ...args] = Array.isArray(input) ? input : [input]; this.#fired.push(false); this.#fns.push(fn); // Not quite 100% how to have a variable number at the front here // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/ban-types return (fn as Function)(...args, this._createCallback(index)); }); } protected _allHasFired (): boolean { this.#allHasFired ||= this.#fired.filter((hasFired): boolean => !hasFired).length === 0; return this.#allHasFired; } protected _createCallback (index: number): (value: any) => void { return (value: unknown): void => { this.#fired[index] = true; this.#results[index] = value; this._triggerUpdate(); }; } protected _triggerUpdate (): void { if (!this.#isActive || !isFunction(this.#callback) || !this._allHasFired()) { return; } try { Promise .resolve(this.#callback(this.#results as T)) .catch(noop); } catch { // swallow, we don't want the handler to trip us up } } public unsubscribe (): void { if (!this.#isActive) { return; } this.#isActive = false; Promise .all(this.#subscriptions.map(async (subscription): Promise => { try { const unsubscribe = await subscription; if (isFunction(unsubscribe)) { unsubscribe(); } } catch { // ignore } })).catch(() => { // ignore, already ignored above, should never throw }); } }