/** * Runtime flow instance — created by FlowProcessor for each configured flow. * * Python reference: trustgraph-base/trustgraph/base/flow.py */ import { Effect, Exit, Scope } from "effect"; import type { PubSubBackend } from "../backend/types.js"; import { makePubSubService } from "../backend/pubsub.js"; import { flowResourceNotFoundError, type FlowResourceNotFoundError, type PubSubError, } from "../errors.js"; import { ConsumerFactory, ProducerFactory, RequestResponseFactory, type EffectConsumer, type EffectProducer, type EffectRequestOptions, type EffectRequestResponse, makeConsumerFactoryService, makeProducerFactoryService, makeRequestResponseFactoryService, } from "../messaging/runtime.js"; import { loadMessagingRuntimeConfig } from "../runtime/messaging-config.js"; import type { Spec, SpecRuntimeRequirements } from "../spec/types.js"; export interface FlowDefinition { /** Topic overrides keyed by spec name */ topics?: Record; /** Parameter values keyed by spec name */ parameters?: Record; } export interface FlowProducer { readonly send: (id: string, message: T) => Promise; readonly flush: () => Promise; readonly stop: () => Promise; } export interface FlowConsumer { readonly stop: () => Promise; } export interface FlowRequestOptions { readonly timeoutMs?: number; readonly recipient?: (response: TRes) => Promise; } export interface FlowRequestor { readonly request: ( request: TReq, options?: FlowRequestOptions, ) => Promise; readonly stop: () => Promise; } export function makeFlow( name: string, processorId: string, pubsub: PubSubBackend, definition: FlowDefinition, specifications: ReadonlyArray>, ) { const producers = new Map>(); const consumers = new Map(); const requestors = new Map>(); const parameters = new Map(); let compatibilityScope: Scope.Closeable | null = null; const ensureCompatibilityScope = async (): Promise => { if (compatibilityScope !== null) { return compatibilityScope; } compatibilityScope = await Effect.runPromise(Scope.make()); return compatibilityScope; }; const toEffectRequestOptions = ( options: FlowRequestOptions | undefined, ): EffectRequestOptions | undefined => { if (options === undefined) { return undefined; } const recipient = options.recipient; return { ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(recipient === undefined ? {} : { recipient: (response: TRes) => Effect.promise(() => recipient(response)), }), }; }; const flow = { name, processorId, startEffect(): Effect.Effect { return Effect.gen(function* () { for (const spec of specifications) { yield* spec.addEffect(flow, definition); } }); }, async start(): Promise { if (compatibilityScope !== null) { await flow.stop(); } await flow.runInCompatibilityScope( flow.startEffect() as Effect.Effect, pubsub, ); }, async stop(): Promise { const scope = compatibilityScope; compatibilityScope = null; if (scope !== null) { await Effect.runPromise(Scope.close(scope, Exit.void)); } flow.clearResources(); }, async runInCompatibilityScope( effect: Effect.Effect, runtimePubsub: PubSubBackend, ): Promise { const scope = await ensureCompatibilityScope(); const pubsubService = makePubSubService(runtimePubsub); const messagingConfig = await Effect.runPromise(loadMessagingRuntimeConfig()); return await Effect.runPromise( effect.pipe( Effect.provideService(ProducerFactory, ProducerFactory.of(makeProducerFactoryService(pubsubService))), Effect.provideService(ConsumerFactory, ConsumerFactory.of(makeConsumerFactoryService(pubsubService, messagingConfig))), Effect.provideService( RequestResponseFactory, RequestResponseFactory.of(makeRequestResponseFactoryService(pubsubService, messagingConfig)), ), Scope.provide(scope), ), ); }, clearResources(): void { producers.clear(); consumers.clear(); requestors.clear(); parameters.clear(); }, registerProducer(registerName: string, producer: EffectProducer): void { producers.set(registerName, producer); }, registerConsumer(registerName: string, consumer: EffectConsumer): void { consumers.set(registerName, consumer); }, registerRequestor(registerName: string, rr: EffectRequestResponse): void { requestors.set(registerName, rr); }, setParameter(parameterName: string, value: unknown): void { parameters.set(parameterName, value); }, producerEffect(producerName: string): Effect.Effect, FlowResourceNotFoundError> { const p = producers.get(producerName); return p === undefined ? Effect.fail(flowResourceNotFoundError(name, "producer", producerName)) : Effect.succeed(p as EffectProducer); }, consumerEffect(consumerName: string): Effect.Effect { const c = consumers.get(consumerName); return c === undefined ? Effect.fail(flowResourceNotFoundError(name, "consumer", consumerName)) : Effect.succeed(c); }, requestorEffect( requestorName: string, ): Effect.Effect, FlowResourceNotFoundError> { const rr = requestors.get(requestorName); return rr === undefined ? Effect.fail(flowResourceNotFoundError(name, "requestor", requestorName)) : Effect.succeed(rr as EffectRequestResponse); }, parameterEffect(parameterName: string): Effect.Effect { const v = parameters.get(parameterName); return v === undefined ? Effect.fail(flowResourceNotFoundError(name, "parameter", parameterName)) : Effect.succeed(v as T); }, producer(producerName: string): FlowProducer { const p = producers.get(producerName); if (p === undefined) throw flowResourceNotFoundError(name, "producer", producerName); return { send: (id, message) => Effect.runPromise((p as EffectProducer).send(id, message)), flush: () => Effect.runPromise(p.flush), stop: () => Effect.runPromise(p.flush.pipe(Effect.flatMap(() => p.close))), }; }, consumer(consumerName: string): FlowConsumer { const c = consumers.get(consumerName); if (c === undefined) throw flowResourceNotFoundError(name, "consumer", consumerName); return { stop: () => Effect.runPromise(c.stop), }; }, requestor(requestorName: string): FlowRequestor { const rr = requestors.get(requestorName); if (rr === undefined) throw flowResourceNotFoundError(name, "requestor", requestorName); return { request: (request, options) => Effect.runPromise( (rr as EffectRequestResponse).request( request, toEffectRequestOptions(options), ), ), stop: () => Effect.runPromise(rr.stop), }; }, parameter(parameterName: string): T { const v = parameters.get(parameterName); if (v === undefined) throw flowResourceNotFoundError(name, "parameter", parameterName); return v as T; }, }; return flow; } export type Flow = ReturnType>; export const Flow = makeFlow as unknown as { new ( name: string, processorId: string, pubsub: PubSubBackend, definition: FlowDefinition, specifications: ReadonlyArray>, ): Flow; ( name: string, processorId: string, pubsub: PubSubBackend, definition: FlowDefinition, specifications: ReadonlyArray>, ): Flow; };