/** * Lambda handler factories — mirrors Cloudflare's handler-factories.ts. * * Creates combined Lambda handlers based on which handler types a unit needs * (fetch, queue, scheduled). Services are cached across warm invocations. */ import { LocalVariablesService, LocalSecretService } from '@pikku/core/services' import type { CoreSingletonServices } from '@pikku/core/types' import type { ChannelStore } from '@pikku/core/channel' import { runFetchV2 } from './http/run-fetch-v2.js' import { runSQSQueueWorker } from './queue/sqs-worker.js' import { runLambdaScheduled } from './scheduled/run-scheduled.js' import type { APIGatewayEvent, APIGatewayProxyEventV2, SQSEvent, ScheduledEvent, } from 'aws-lambda' export interface LambdaServiceFactories { createConfig: ( variables: LocalVariablesService, ...args: unknown[] ) => Promise createSingletonServices: ( config: unknown, existingServices?: Partial> ) => Promise /** * Optional callback to create platform services from environment. * Returns services to merge into existingServices. * Generated by the deploy codegen based on unit capabilities. */ createPlatformServices?: () => Record | Promise> } let cachedServices: CoreSingletonServices | null = null async function setupServices( factories: LambdaServiceFactories ): Promise { if (cachedServices) return cachedServices const variables = new LocalVariablesService( process.env as Record ) const config = await factories.createConfig(variables) const secrets = new LocalSecretService(variables) const platformServices = factories.createPlatformServices ? await factories.createPlatformServices() : {} cachedServices = await factories.createSingletonServices(config, { variables, secrets, ...platformServices, }) return cachedServices } /** * Creates a combined Lambda handler based on which handler types the unit needs. * * Returns an object with named exports that serverless.yml references: * - `handler` for HTTP (API Gateway) * - `queue` for SQS * - `scheduled` for EventBridge */ export function createLambdaHandler( factories: LambdaServiceFactories, handlerTypes: string[] ) { const result: Record = {} if (handlerTypes.includes('fetch')) { result.handler = async (event: APIGatewayProxyEventV2) => { await setupServices(factories) return runFetchV2(event) } } if (handlerTypes.includes('queue')) { result.queue = async (event: SQSEvent) => { const services = await setupServices(factories) return runSQSQueueWorker(services.logger, event) } } if (handlerTypes.includes('scheduled')) { result.scheduled = async (event: ScheduledEvent) => { await setupServices(factories) return runLambdaScheduled(event) } } // If no fetch handler was requested, add a health check if (!handlerTypes.includes('fetch')) { const activeHandlers = handlerTypes.join(', ') result.handler = async () => ({ statusCode: 200, body: `Lambda active (handlers: ${activeHandlers})`, }) } return result } /** * Creates a Lambda handler with a fetch() entrypoint for HTTP, agent, RPC, * and workflow-orchestrator units. */ export function createLambdaWorkerHandler(factories: LambdaServiceFactories) { return { async handler(event: APIGatewayProxyEventV2) { await setupServices(factories) return runFetchV2(event) }, } } /** * Creates a Lambda handler for WebSocket channel units. * Exports separate handlers for $connect, $disconnect, and $default routes. */ export function createLambdaWebSocketHandler( factories: LambdaServiceFactories ) { // Import dynamically to avoid pulling in websocket deps for non-channel units const getChannelStore = () => { if (!cachedServices) { throw new Error('Services not initialized for WebSocket handler') } const channelStore = (cachedServices as unknown as Record) .channelStore as ChannelStore | undefined if (!channelStore) { throw new Error( 'channelStore not found in singleton services. Ensure it is configured for channel units.' ) } return channelStore } return { async connect(event: APIGatewayEvent) { await setupServices(factories) const { connectWebsocket } = await import('./websocket/index.js') return connectWebsocket(event, { channelStore: getChannelStore() }) }, async disconnect(event: APIGatewayEvent) { await setupServices(factories) const { disconnectWebsocket } = await import('./websocket/index.js') return disconnectWebsocket(event, { channelStore: getChannelStore() }) }, async default(event: APIGatewayEvent) { await setupServices(factories) const { processWebsocketMessage } = await import('./websocket/index.js') return processWebsocketMessage(event, { channelStore: getChannelStore(), }) }, } }