Files
pezkuwi-api/packages/api/src/promise/Combinator.ts
T
Satoshi Qazi Muhammed 5835a5383c Move to @pezkuwi/dev 0.86 (eslint 10 toolchain)
- @pezkuwi/dev, dev-test, dev-ts ^0.86.3; disable directives renamed
  for the new rule names, or removed where the rule no longer reports
- rethrown errors carry the original as `cause` (13 sites)
- optional chains where a guard expression tested the same value
- Init: the registry cache settles with the creator's own promise,
  dropping the entry in finally(), instead of re-wrapping it
- Combinator: a typed call instead of `Function`
- checkTypes.manual: synchronous checks run outside Promise.all
- sortValues, createTypeUnsafe: initial values that were never read
- typegen util: load docs.js for its side effect (it registers the
  `docs` Handlebars partial) now that its types are re-exported
  type-only
- MockProvider keeps its Header assertion: the package's own build
  does not load api-augment, so createType returns a plain Codec there
- Decorate keeps `delete queue[i]`: the hole marks a fired queue and
  later indexes must not shift
2026-10-09 16:32:37 +04:00

90 lines
2.4 KiB
TypeScript

// Copyright 2017-2026 @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 <T extends unknown[]> = Callback<T>;
export type CombinatorFunction = (cb: Callback<any>) => UnsubscribePromise;
export class Combinator<T extends unknown[] = unknown[]> {
#allHasFired = false;
#callback: CombinatorCallback<T>;
#fired: boolean[] = [];
#fns: CombinatorFunction[] = [];
#isActive = true;
#results: unknown[] = [];
#subscriptions: UnsubscribePromise[] = [];
constructor (fns: (CombinatorFunction | [CombinatorFunction, ...unknown[]])[], callback: CombinatorCallback<T>) {
this.#callback = callback;
this.#subscriptions = fns.map(async (input, index): UnsubscribePromise => {
const [fn, ...args] = Array.isArray(input)
? input
: [input];
this.#fired.push(false);
this.#fns.push(fn);
// the leading arguments vary per function, the callback is always last
return (fn as (...params: unknown[]) => UnsubscribePromise)(...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<void> => {
try {
const unsubscribe = await subscription;
if (isFunction(unsubscribe)) {
unsubscribe();
}
} catch {
// ignore
}
})).catch(() => {
// ignore, already ignored above, should never throw
});
}
}