rea/src/application/binary/CompositeProvider.ts

228 lines
7.6 KiB
TypeScript

import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { err, ok } from "../../domain/result.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type {
AnalysisClient,
AnalysisClientContext,
AnalysisProvider,
CapabilityDescriptor,
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
/** Synthetic identity for a deterministic provider set. */
const compositeProviderIdentity = (
identities: readonly ProviderIdentity[],
): ProviderIdentity => ({
id: `composite:${identities
.map(({ id }) => id)
.sort()
.join("+")}`,
name: "REA composite analysis provider",
version: null,
});
const closeCompositeClients =
(
identity: ProviderIdentity,
clients: ReadonlyMap<AnalysisProvider, AnalysisClient>,
): AnalysisClient["close"] =>
async (options) => {
const outcomes = await Promise.all(
[...clients.entries()].map(([provider, client]) =>
closeAnalysisClient(client, provider.identity().id, options).then(
(result) => ({ providerId: provider.identity().id, result }),
),
),
);
const failures = outcomes.flatMap(({ providerId, result }) =>
result.ok ? [] : [{ providerId, error: result.error }],
);
if (failures.length === 0) return ok(null);
if (failures.length === 1) {
const failure = failures[0];
if (failure !== undefined) return err(failure.error);
}
const resources = [
...new Set(failures.flatMap(({ error }) => error.cleanupResources)),
];
return err(
new ProviderCleanupError(
identity.id,
resources,
{
reason:
"Multiple composed provider clients could not be confirmed closed.",
failures: failures.map(({ providerId, error }) => ({
provider_id: providerId,
message: error.message,
resources: [...error.cleanupResources],
...(error.diagnostics === undefined
? {}
: { diagnostics: error.diagnostics }),
})),
},
{
cause: new AggregateError(
failures.map(({ error }) => error),
"Composed provider cleanup failed.",
),
},
),
);
};
const executeCompositeHealth = async (
identity: ProviderIdentity,
clients: ReadonlyMap<AnalysisProvider, AnalysisClient>,
options: ExecutionOptions | undefined,
): ReturnType<AnalysisClient["execute"]> => {
for (const [provider, client] of clients) {
const health = client.operationHealthSnapshot?.();
if (
health !== undefined &&
health.state !== "idle" &&
health.state !== "busy"
)
return err(
new ProviderAdapterError(provider.identity().id, "health", {
diagnostics: {
reason: `instantiated provider client is ${health.state}`,
},
}),
);
const checked = await client.execute("health", {}, options);
if (!checked.ok) return checked;
}
return ok({
result: null,
rawResult: null,
provider: identity,
limitations: [],
locations: [],
subject: null,
});
};
/** Deterministically route disjoint operations without eagerly starting children. */
export class CompositeProvider implements AnalysisProvider {
readonly #identity: ProviderIdentity;
readonly #capabilities: readonly CapabilityDescriptor[];
readonly #providerByOperation: ReadonlyMap<string, AnalysisProvider>;
constructor(readonly providers: readonly AnalysisProvider[]) {
if (providers.length === 0)
throw new RangeError("CompositeProvider requires at least one provider");
this.#identity = Object.freeze(
compositeProviderIdentity(
providers.map((provider) => provider.identity()),
),
);
const routes = new Map<string, AnalysisProvider>();
const capabilities: CapabilityDescriptor[] = [];
for (const provider of providers) {
for (const descriptor of provider.capabilities()) {
if (routes.has(descriptor.operation))
throw new TypeError(
`Multiple providers declare operation ${descriptor.operation}`,
);
routes.set(descriptor.operation, provider);
capabilities.push(descriptor);
}
}
this.#providerByOperation = routes;
this.#capabilities = Object.freeze(
capabilities.sort(
(left, right) =>
compareUnicodeCodePoints(left.operation, right.operation) ||
compareUnicodeCodePoints(left.provider.id, right.provider.id),
),
);
}
identity(): ProviderIdentity {
return this.#identity;
}
capabilities(): readonly CapabilityDescriptor[] {
return this.#capabilities;
}
createClient(
target: BinaryTarget,
profile?: AnalysisProfileCommitment,
context?: AnalysisClientContext,
): AnalysisClient {
const clients = new Map<AnalysisProvider, AnalysisClient>();
const clientFor = (provider: AnalysisProvider): AnalysisClient => {
const existing = clients.get(provider);
if (existing !== undefined) return existing;
const created = provider.createClient(
target,
profile?.provider.id === provider.identity().id ? profile : undefined,
context,
);
clients.set(provider, created);
return created;
};
const close = closeCompositeClients(this.#identity, clients);
return {
executeIfReady: (operation, parameters, options) => {
const provider = this.#providerByOperation.get(operation);
const client =
provider === undefined ? undefined : clients.get(provider);
return (
client?.executeIfReady?.(operation, parameters, options) ??
Promise.resolve(undefined)
);
},
execute: async (operation, parameters, options) => {
if (operation === "health")
return executeCompositeHealth(this.#identity, clients, options);
const provider = this.#providerByOperation.get(operation);
return provider === undefined
? Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
this.#identity.id,
operation,
"operation is not declared by this provider set",
),
),
)
: clientFor(provider).execute(operation, parameters, options);
},
runtimeLineageSnapshots: () =>
[...clients.values()]
.flatMap((client) => client.runtimeLineageSnapshots?.() ?? [])
.sort((left, right) =>
compareUnicodeCodePoints(left.provider.id, right.provider.id),
),
requestActivitySnapshots: () =>
[...clients.values()]
.flatMap((client) => client.requestActivitySnapshots?.() ?? [])
.sort((left, right) =>
compareUnicodeCodePoints(left.provider.id, right.provider.id),
),
operationHealthSnapshot: () => {
const snapshots = [...clients.values()].map((client) =>
client.operationHealthSnapshot?.(),
);
if (snapshots.some((snapshot) => snapshot === undefined))
return undefined;
return (
snapshots.find(
(snapshot) => snapshot !== undefined && snapshot.state !== "idle",
) ?? snapshots[0]
);
},
close,
};
}
}