UNPKG

create-sekiban-app

Version:

Create a new Sekiban application with Dapr

290 lines (244 loc) • 9.27 kB
import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import compression from 'compression'; import morgan from 'morgan'; import pg from 'pg'; import { config } from './config/index.js'; import { cleanup } from './setup/executor.js'; import { errorHandler } from './middleware/error-handler.js'; import { healthRoutes } from './routes/health-routes.js'; import { taskRoutes } from './routes/task-routes.js'; import { eventRoutes } from './routes/event-routes.js'; import { DaprServer, DaprClient, CommunicationProtocolEnum, HttpMethod, ActorProxyBuilder, ActorId } from '@dapr/dapr'; import { AggregateActorFactory, AggregateEventHandlerActorFactory, type EventHandlingResponse } from '@sekiban/dapr'; import { InMemoryEventStore, StorageProviderType } from '@sekiban/core'; import { PostgresEventStore } from '@sekiban/postgres'; import { createCosmosEventStore } from '@sekiban/cosmos'; const { Pool } = pg; import { createTaskDomainTypes, AggregateEventHandlerActorBase } from '@dapr-sample/domain'; async function startServer() { const app = express(); // Middleware BEFORE DaprServer setup app.use(helmet()); app.use(cors({ origin: config.CORS_ORIGIN })); app.use(compression()); app.use(morgan(config.NODE_ENV === 'production' ? 'combined' : 'dev')); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // CRITICAL: Convert POST to PUT for actor method calls // This middleware MUST come before DaprServer setup app.use((req, res, next) => { if (req.path.includes('/method/') && req.method === 'POST') { req.method = 'PUT'; } next(); }); // Routes app.use('/', healthRoutes); app.use('/', eventRoutes); app.use(config.API_PREFIX, taskRoutes); // Error handling (must be last) app.use(errorHandler); // Executor will be initialized in setupDaprActorsWithApp // Create DaprServer and pass the Express app const daprServer = await setupDaprActorsWithApp(app); // Start the DaprServer await daprServer.start(); console.log(` šŸš€ Server is running! šŸ“” Environment: ${config.NODE_ENV} šŸ”— URL: http://localhost:${config.PORT} šŸ”— API: http://localhost:${config.PORT}${config.API_PREFIX} šŸŽ­ Dapr App ID: ${config.DAPR_APP_ID} šŸŽ­ Actors: AggregateActor `); // Graceful shutdown const gracefulShutdown = async (signal: string) => { console.log(`\n${signal} received, starting graceful shutdown...`); // Force exit after 30 seconds const forceExitTimeout = setTimeout(() => { console.error('Forced shutdown after timeout'); process.exit(1); }, 30000); try { // Stop DaprServer await daprServer.stop(); console.log('DaprServer stopped'); // Cleanup database connections await cleanup(); console.log('Cleanup completed'); clearTimeout(forceExitTimeout); } catch (error) { console.error('Error during cleanup:', error); } process.exit(0); }; process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); } async function setupDaprActorsWithApp(app: express.Express) { console.log('Setting up Dapr actors with storage type:', config.STORAGE_TYPE); // Initialize domain types const domainTypes = createTaskDomainTypes(); // Choose storage type based on configuration let eventStore: any; switch (config.STORAGE_TYPE) { case 'postgres': { // Initialize PostgreSQL event store console.log('Initializing PostgreSQL event store...'); console.log('Database URL:', config.DATABASE_URL); const pool = new Pool({ connectionString: config.DATABASE_URL }); eventStore = new PostgresEventStore(pool); // Initialize the database schema try { const result = await eventStore.initialize(); if (!result.isOk()) { console.error('Failed to initialize PostgreSQL schema:', result.error); throw result.error; } console.log('PostgreSQL event store initialized successfully'); } catch (error) { console.error('Failed to initialize PostgreSQL:', error); throw error; } break; } case 'cosmos': { // Initialize Cosmos DB event store if (!config.COSMOS_CONNECTION_STRING) { throw new Error('COSMOS_CONNECTION_STRING environment variable is required for Cosmos DB storage'); } // Use createCosmosEventStore helper function const result = await createCosmosEventStore({ type: StorageProviderType.CosmosDB, connectionString: config.COSMOS_CONNECTION_STRING, databaseName: config.COSMOS_DATABASE, enableLogging: true }); if (!result.isOk()) { console.error('Failed to create Cosmos DB event store:', result.error); throw result.error; } eventStore = result.value; break; } case 'inmemory': default: { // Initialize in-memory event store eventStore = new InMemoryEventStore({ type: StorageProviderType.InMemory, enableLogging: config.NODE_ENV === 'development' }); break; } } // Create DaprClient for actor proxy factory const daprClient = new DaprClient({ daprHost: "127.0.0.1", daprPort: String(config.DAPR_HTTP_PORT) }); // Create actor proxy factory that uses DaprClient with ActorProxyBuilder const actorProxyFactory = { createActorProxy: <T>(actorId: any, actorType: string): T => { const actorIdStr = actorId.id || actorId; // Use ActorProxyBuilder for all actor types if (actorType === 'AggregateEventHandlerActor') { // For cross-service actor communication, create a custom proxy using service invocation const proxy = { appendEventsAsync: async (expectedLastSortableUniqueId: string, events: any[]) => { try { const result = await daprClient.invoker.invoke( 'sekiban-sample-event-handler', // Target app-id where the actor is hosted `/actors/${actorType}/${actorIdStr}/method/appendEventsAsync`, HttpMethod.PUT, [expectedLastSortableUniqueId, events] ); return result; } catch (error) { console.error('Failed to invoke AggregateEventHandlerActor:', error); throw error; } }, getAllEventsAsync: async () => { try { const result = await daprClient.invoker.invoke( 'sekiban-sample-event-handler', // Target app-id where the actor is hosted `/actors/${actorType}/${actorIdStr}/method/getAllEventsAsync`, HttpMethod.PUT, [] ); return result; } catch (error) { console.error('Failed to invoke AggregateEventHandlerActor:', error); throw error; } } }; return proxy as T; } else if (actorType === 'AggregateActor') { const AggregateActorClass = AggregateActorFactory.createActorClass(); const builder = new ActorProxyBuilder(AggregateActorClass, daprClient); return builder.build(new ActorId(actorIdStr)) as T; } else { // Unknown actor type throw new Error(`Unknown actor type: ${actorType}`); } } }; // Create a simple serialization service const serializationService = { async deserializeAggregateAsync(surrogate: any) { return surrogate; }, async serializeAggregateAsync(aggregate: any) { return aggregate; } }; // Configure actor factories AggregateActorFactory.configure( domainTypes, {}, // service provider actorProxyFactory, serializationService, eventStore ); AggregateEventHandlerActorFactory.configure(eventStore); // Set the event store in executor for later use const { setEventStore, createExecutor } = await import('./setup/executor.js'); setEventStore(eventStore); // Initialize executor with the configured event store await createExecutor(); // Create actor classes before DaprServer initialization const AggregateActorClass = AggregateActorFactory.createActorClass(); // Create DaprServer const daprServer = new DaprServer({ serverHost: "127.0.0.1", serverPort: String(config.PORT), serverHttp: app, // Pass the configured Express app communicationProtocol: CommunicationProtocolEnum.HTTP, clientOptions: { daprHost: "127.0.0.1", daprPort: String(config.DAPR_HTTP_PORT), communicationProtocol: CommunicationProtocolEnum.HTTP } }); // Register all actors explicitly before starting the server await daprServer.actor.registerActor(AggregateActorClass); // Only register AggregateActor in this service // AggregateEventHandlerActor is now in a separate service // Initialize actor runtime await daprServer.actor.init(); return daprServer; } // Start the server startServer().catch((error) => { console.error('Failed to start server:', error); process.exit(1); });