"use strict";
/**
 * Capability dispatcher — the typed channel analyzers use to request a
 * capability (lint, depVulns, coverage, …) by descriptor. The dispatcher
 * runs every registered provider concurrently, filters nulls and
 * rejections, and aggregates the survivors using the descriptor's
 * bespoke aggregator.
 *
 * Caching is in-memory and per-dispatcher-instance: the same `(cwd, capId)`
 * request inside one analyzer run reuses the same Promise, so two reports
 * (e.g. `health` + `vulnerabilities`) sharing a process don't double-shell
 * out to `pip-audit`. Process exit clears the cache; persistent caching
 * is out of scope.
 *
 * Provider failures are isolated: one provider throwing does not abort the
 * dispatch. The thrown error is logged via the optional `onProviderError`
 * hook and that provider's contribution is dropped from the aggregate.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultDispatcher = exports.CapabilityDispatcher = void 0;
const deadline_1 = require("./tools/deadline");
class CapabilityDispatcher {
    cache = new Map();
    opts;
    constructor(opts = {}) {
        this.opts = opts;
    }
    async gather(cwd, cap, providers) {
        return (await this.gatherWithProvenance(cwd, cap, providers)).envelope;
    }
    /**
     * Same gather + cache semantics as `gather()`, plus per-provider
     * provenance (which sources we attempted, which returned null).
     * D080 (2.4.7): consumed by quality/health report renderers so the
     * "Tool" label honestly reflects which providers were attempted even
     * when one returned null silently.
     */
    async gatherWithProvenance(cwd, cap, providers) {
        const cacheKey = `${cwd}::${cap.id}`;
        const cached = this.cache.get(cacheKey);
        if (cached)
            return cached;
        const promise = this.run(cwd, cap, providers);
        this.cache.set(cacheKey, promise);
        return promise;
    }
    /** Test seam — drop the in-memory cache between runs. */
    clearCache() {
        this.cache.clear();
    }
    async run(cwd, cap, providers) {
        if (providers.length === 0) {
            return {
                envelope: null,
                attempted: [],
                succeeded: [],
                skipped: [],
                skipReasons: {},
            };
        }
        const attempted = providers.map((p) => p.source);
        // Each provider's gather pathway: prefer the discriminant-returning
        // `gatherOutcome` (Lint / DepVulns / Licenses providers) when it
        // exists, since it carries the "unavailable reason" the user-
        // facing report wants. Fall back to plain `gather()` for legacy
        // providers — the only loss is per-pack reason text; success/skip
        // attribution still works.
        //
        // Every provider's promise is wrapped in a deadline so a single
        // never-settling Promise can't keep `Promise.allSettled` pending
        // forever (the silent-failure shape observed when the capabilities
        // gather hung and the parent exited rc=0 with no work done). A
        // stall is materialised as `{ value: null, reason: "stalled at
        // >Ns (deadline)" }` so the existing skip/skipReason channels
        // carry it through to `toolsUnavailable` without any consumer
        // change.
        const deadlineMs = this.opts.providerDeadlineMs ?? deadline_1.DEFAULT_PROVIDER_DEADLINE_MS;
        const settled = await Promise.allSettled(providers.map((p) => {
            const candidate = p;
            const inner = typeof candidate.gatherOutcome === 'function'
                ? candidate.gatherOutcome(cwd).then((o) => {
                    if (o.kind === 'success') {
                        return { value: o.envelope, reason: null };
                    }
                    return {
                        value: null,
                        reason: o.reason ?? null,
                    };
                })
                : p.gather(cwd).then((v) => ({ value: v, reason: null }));
            return (0, deadline_1.withDeadline)(inner, deadlineMs).then((outcome) => {
                if (outcome.stalled) {
                    const stalledSeconds = Math.round(outcome.stalledMs / 1000);
                    const onStall = this.opts.onProviderStall ?? defaultOnProviderStall;
                    onStall(cap.id, p.source, outcome.stalledMs);
                    return {
                        value: null,
                        reason: `stalled at >${stalledSeconds}s (deadline)`,
                    };
                }
                return outcome.value;
            });
        }));
        const succeeded = [];
        const skipped = [];
        const skipReasons = {};
        const successful = [];
        for (let i = 0; i < settled.length; i++) {
            const r = settled[i];
            const source = providers[i].source;
            if (r.status === 'rejected') {
                this.opts.onProviderError?.(cap.id, source, r.reason);
                skipped.push(source);
                continue;
            }
            const { value, reason } = r.value;
            if (value !== null) {
                successful.push(value);
                succeeded.push(source);
            }
            else {
                skipped.push(source);
                if (reason !== null)
                    skipReasons[source] = reason;
            }
        }
        const envelope = successful.length === 0 ? null : cap.aggregate(successful);
        return { envelope, attempted, succeeded, skipped, skipReasons };
    }
}
exports.CapabilityDispatcher = CapabilityDispatcher;
/**
 * Default stall notifier — emits a single stderr line so a stalled
 * provider is visible in `vyuh-dxkit report --verbose` and CI logs.
 * Tests pass `onProviderStall` to suppress the write.
 */
function defaultOnProviderStall(capId, source, stalledMs) {
    const seconds = Math.round(stalledMs / 1000);
    // Stderr keeps the stall visible in --verbose + CI logs without
    // polluting subprocess stdout (the orchestrator parses stdout).
    process.stderr.write(`[dxkit] capability "${capId}" provider "${source}" stalled after >${seconds}s (deadline) — treating as skipped\n`);
}
/** Process-wide singleton. Tests should construct their own instance. */
exports.defaultDispatcher = new CapabilityDispatcher();
//# sourceMappingURL=dispatcher.js.map