create-sekiban-app
Version:
Create a new Sekiban application with Dapr
290 lines (244 loc) ⢠9.27 kB
text/typescript
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);
});