/** * 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 class Flow { private producers = new Map>(); private consumers = new Map(); private requestors = new Map>(); private parameters = new Map(); private compatibilityScope: Scope.Closeable | null = null; public readonly name: string; public readonly processorId: string; private readonly pubsub: PubSubBackend; private readonly definition: FlowDefinition; private readonly specifications: ReadonlyArray>; constructor( name: string, processorId: string, pubsub: PubSubBackend, definition: FlowDefinition, specifications: ReadonlyArray>, ) { this.name = name; this.processorId = processorId; this.pubsub = pubsub; this.definition = definition; this.specifications = specifications; } startEffect(): Effect.Effect { const flow = this; return Effect.gen(function* () { for (const spec of flow.specifications) { yield* spec.addEffect(flow, flow.definition); } }); } async start(): Promise { if (this.compatibilityScope !== null) { await this.stop(); } await this.runInCompatibilityScope( this.startEffect() as Effect.Effect, this.pubsub, ); } async stop(): Promise { const scope = this.compatibilityScope; this.compatibilityScope = null; if (scope !== null) { await Effect.runPromise(Scope.close(scope, Exit.void)); } this.clearResources(); } async runInCompatibilityScope( effect: Effect.Effect, pubsub: PubSubBackend, ): Promise { const scope = await this.ensureCompatibilityScope(); const pubsubService = makePubSubService(pubsub); 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 { this.producers.clear(); this.consumers.clear(); this.requestors.clear(); this.parameters.clear(); } registerProducer(name: string, producer: EffectProducer): void { this.producers.set(name, producer); } registerConsumer(name: string, consumer: EffectConsumer): void { this.consumers.set(name, consumer); } registerRequestor(name: string, rr: EffectRequestResponse): void { this.requestors.set(name, rr); } setParameter(name: string, value: unknown): void { this.parameters.set(name, value); } producerEffect(name: string): Effect.Effect, FlowResourceNotFoundError> { const p = this.producers.get(name); return p === undefined ? Effect.fail(flowResourceNotFoundError(this.name, "producer", name)) : Effect.succeed(p as EffectProducer); } consumerEffect(name: string): Effect.Effect { const c = this.consumers.get(name); return c === undefined ? Effect.fail(flowResourceNotFoundError(this.name, "consumer", name)) : Effect.succeed(c); } requestorEffect( name: string, ): Effect.Effect, FlowResourceNotFoundError> { const rr = this.requestors.get(name); return rr === undefined ? Effect.fail(flowResourceNotFoundError(this.name, "requestor", name)) : Effect.succeed(rr as EffectRequestResponse); } parameterEffect(name: string): Effect.Effect { const v = this.parameters.get(name); return v === undefined ? Effect.fail(flowResourceNotFoundError(this.name, "parameter", name)) : Effect.succeed(v as T); } producer(name: string): FlowProducer { const p = this.producers.get(name); if (p === undefined) throw flowResourceNotFoundError(this.name, "producer", name); 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(name: string): FlowConsumer { const c = this.consumers.get(name); if (c === undefined) throw flowResourceNotFoundError(this.name, "consumer", name); return { stop: () => Effect.runPromise(c.stop), }; } requestor(name: string): FlowRequestor { const rr = this.requestors.get(name); if (rr === undefined) throw flowResourceNotFoundError(this.name, "requestor", name); return { request: (request, options) => Effect.runPromise( (rr as EffectRequestResponse).request( request, this.toEffectRequestOptions(options), ), ), stop: () => Effect.runPromise(rr.stop), }; } parameter(name: string): T { const v = this.parameters.get(name); if (v === undefined) throw flowResourceNotFoundError(this.name, "parameter", name); return v as T; } private async ensureCompatibilityScope(): Promise { if (this.compatibilityScope !== null) { return this.compatibilityScope; } this.compatibilityScope = await Effect.runPromise(Scope.make()); return this.compatibilityScope; } private 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)), }), }; } }