import type { Command, CommandMethods, Envelope, MaybePromise, Message, MessageTypes } from '../../types/index.ts' import { type Closable, isClosable } from '../closable.ts' import type { Sender } from '../sender.ts' export type SenderFactory = () => MaybePromise const isSenderFactory = (value: Sender | SenderFactory): value is SenderFactory => value instanceof Function type SenderEntry = { filter: (envelope: Envelope) => boolean | undefined factory: () => Promise resolved?: Sender resolving?: Promise } export class MultiSender implements Sender, Closable { private senders: Array /** * @param initializers Array of objects containing a filter function and a sender or a factory function that returns a sender. * The filter function is used to determine if the sender should be used for a given envelope. * If no filter is provided, the sender will be used for all envelopes. * The order of senders in the array determines the order in which they are tried. */ constructor( initializers: Array<{ filter?: (envelope: Envelope) => boolean | undefined sender: Sender | SenderFactory }>, ) { this.senders = initializers.map(({ filter, sender }) => { const isFactory = isSenderFactory(sender) return { filter: filter ?? (() => true), factory: isFactory ? () => Promise.resolve(sender()) : () => Promise.resolve(sender), resolved: isFactory ? undefined : sender, } }) } async sendMessage(message: Message): Promise { const sender = await this.getSender(message) return sender.sendMessage(message) } async sendCommand(command: Command): Promise { const sender = await this.getSender(command) return sender.sendCommand(command) } public async close(): Promise { await Promise.all( this.senders .map(({ resolved }) => resolved) .filter((sender): sender is Sender & Closable => sender !== undefined && isClosable(sender)) .map((sender) => sender.close()), ) } public findSender) => Sender>( proto: TProto, ): InstanceType { const matched = this.senders.find(({ resolved }) => resolved instanceof proto) if (!matched) throw new Error(`No sender found for constructor ${proto.name}`) return matched.resolved! as InstanceType } private getSender(envelope: Envelope): Promise { const matched = this.senders.find(({ filter }) => filter(envelope)) if (!matched) throw new Error('No sender matched for envelope') return this.resolveSender(matched) } private resolveSender(entry: SenderEntry): Promise { if (entry.resolved) return Promise.resolve(entry.resolved) if (!entry.resolving) { entry.resolving = entry .factory() .then((sender) => { entry.resolved = sender entry.resolving = undefined return sender }) .catch((error) => { entry.resolving = undefined throw error }) } return entry.resolving } }