import { clientWebsocketMessages, serverWebsocketMessages } from "@trigger.dev/core/v3"; import type { StructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger"; import type { MessageCatalogToSocketIoEvents } from "@trigger.dev/core/v3/zodMessageHandler"; import { ZodMessageHandler, ZodMessageSender } from "@trigger.dev/core/v3/zodMessageHandler"; import { Evt } from "evt"; import { randomUUID } from "node:crypto"; import type { DisconnectReason, Namespace, Socket } from "socket.io"; import { env } from "~/env.server"; import { logger } from "~/services/logger.server"; import { SharedQueueConsumer } from "./marqs/sharedQueueConsumer.server"; interface SharedQueueConsumerPoolOptions { sender: ZodMessageSender; poolSize: number; } class SharedQueueConsumerPool { #consumers: SharedQueueConsumer[]; constructor(opts: SharedQueueConsumerPoolOptions) { this.#consumers = Array(opts.poolSize) .fill(null) .map( () => new SharedQueueConsumer(opts.sender, { interval: env.SHARED_QUEUE_CONSUMER_INTERVAL_MS, nextTickInterval: env.SHARED_QUEUE_CONSUMER_NEXT_TICK_INTERVAL_MS, }) ); } async start() { await Promise.allSettled(this.#consumers.map((consumer) => consumer.start())); } async stop() { await Promise.allSettled(this.#consumers.map((consumer) => consumer.stop())); } } interface SharedSocketConnectionOptions { namespace: Namespace< MessageCatalogToSocketIoEvents, MessageCatalogToSocketIoEvents >; socket: Socket< MessageCatalogToSocketIoEvents, MessageCatalogToSocketIoEvents >; logger?: StructuredLogger; poolSize?: number; } export class SharedSocketConnection { public id: string; public onClose: Evt = new Evt(); private _sender: ZodMessageSender; private _sharedQueueConsumerPool: SharedQueueConsumerPool; private _messageHandler: ZodMessageHandler; private _defaultPoolSize = 10; constructor(opts: SharedSocketConnectionOptions) { this.id = randomUUID(); this._sender = new ZodMessageSender({ schema: serverWebsocketMessages, sender: async (message) => { return new Promise((resolve, reject) => { try { const { type, ...payload } = message; opts.namespace.emit(type, payload as any); resolve(); } catch (err) { reject(err); } }); }, canSendMessage() { // Return true if there is at least 1 connected socket on the namespace if (opts.namespace.sockets.size === 0) { return false; } return Array.from(opts.namespace.sockets.values()).some((socket) => socket.connected); }, }); logger.debug("Starting SharedQueueConsumer pool", { poolSize: opts.poolSize ?? this._defaultPoolSize, }); this._sharedQueueConsumerPool = new SharedQueueConsumerPool({ poolSize: opts.poolSize ?? this._defaultPoolSize, sender: this._sender, }); opts.socket.on("disconnect", this.#handleClose.bind(this)); opts.socket.on("error", this.#handleError.bind(this)); this._messageHandler = new ZodMessageHandler({ schema: clientWebsocketMessages, logger, messages: { READY_FOR_TASKS: async (payload) => { this._sharedQueueConsumerPool.start(); }, BACKGROUND_WORKER_DEPRECATED: async (payload) => { // await this._sharedConsumer.deprecateBackgroundWorker(payload.backgroundWorkerId); }, BACKGROUND_WORKER_MESSAGE: async (payload) => { switch (payload.data.type) { case "TASK_RUN_COMPLETED": { // handled in coordinator namespace break; } case "TASK_HEARTBEAT": { // handled in coordinator namespace break; } } }, }, }); this._messageHandler.registerHandlers(opts.socket, opts.logger ?? logger); } async initialize() { this._sender.send("SERVER_READY", { id: this.id }); } async #handleClose(ev: DisconnectReason) { await this._sharedQueueConsumerPool.stop(); this.onClose.post(ev); } async #handleError(ev: Error) { logger.error("Websocket error", { ev }); } }