Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 83 additions & 33 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
// ── All imports must be at the top of the file (ESM requirement) ─────────────
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';
import http from 'node:http';
import express, { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';

Sentry.init({
dsn: process.env.SENTRY_DSN || '',
integrations: [
nodeProfilingIntegration(),
],
tracesSampleRate: 1.0,
profilesSampleRate: 1.0,
environment: process.env.NODE_ENV || 'development',
beforeSend(event, hint) {
if (event.exception && hint.originalException) {
const error = hint.originalException as Error;
if (error && error.message && error.message.includes('Database connection timeout')) {
event.fingerprint = ['database-timeout'];
}
}
return event;
}
});
import cors from 'cors';
import { tokenBucketRateLimit } from './middleware/rate-limit.js';
import { compressionMiddleware, getCompressionMetrics } from './middleware/compression.js';
import { poolMetrics } from './config/database.js';
import { config } from './config.js';
import { versionMiddleware } from './middleware/versioning.js';
import { verificationRouter } from './routes/verification.js';
import { invoiceRouter } from './routes/invoice.js';
import { stellarRouter } from './routes/stellar.js';
Expand Down Expand Up @@ -54,7 +37,6 @@ import { slaTrackingMiddleware } from './middleware/slaTracking.js';
import { requestIdMiddleware, REQUEST_ID_HEADER } from './middleware/requestId.js';
import { validateEnv, config as getConfig } from './config/env.js';
import { flagsRouter } from './routes/flags.js';
import { rateLimitAnalyticsRouter } from './routes/rate-limit-analytics.js';
import { emailRouter } from './routes/email.js';
import { portfolioRouter } from './routes/portfolio.js';
import { backupRouter } from './routes/backup.js';
Expand All @@ -63,19 +45,18 @@ import { ipAllowlistRouter } from './routes/ip-allowlist.js';
import { nfcRouter } from './routes/nfc.js';
import { cacheRouter } from './routes/cache.js';
import { ipAllowlistMiddleware, initIpAllowlist } from './middleware/ip-allowlist.js';
import { sessionsRouter } from './routes/sessions.js';
import { sessionMiddleware } from './middleware/session.js';
import { notificationsRouter } from './routes/notifications.js';
import { auditRouter } from './routes/audit.js';
import { hedgingRouter } from './routes/hedging.js';
import { complianceRouter } from './routes/compliance.js';
import { gdprRouter } from './routes/gdpr.js';
import { kybRouter } from './routes/kyb.js';
import { batchRouter } from './routes/batch.js';
import { relayerRouter } from './routes/relayer.js';
import { paymentQueueRouter } from './routes/payment-queue.js';
import { disputeRoutes } from './disputes/index.js';
import { disputeService } from './disputes/disputeService.js';
import http from 'node:http';
import { attachWebSocketServer } from './websocket/server.js';
import { createWebSocketRouter } from './routes/websocket.js';
import { bindWebSocketServer } from './events/event-bus.js';
Expand All @@ -98,10 +79,6 @@ import { analyticsService } from './services/analytics.js';
import { createAnalyticsRouter } from './routes/analytics.js';
import { paymentQueue } from './queue/payment-queue.js';
import './events/projections.js';
import { stripeRouter } from './routes/stripe.js';
import { SecurityMiddleware, SecurityMonitor } from './middleware/security.js';
import { sanitizeInput, contentSecurityPolicy } from './middleware/sanitize.js';
import { signaturesRouter } from './routes/signatures.js';
import { createSandboxRouter } from './routes/sandbox.js';
import { circuitBreakerRouter } from './routes/circuit-breaker.js';
import SandboxManager from './services/sandbox.js';
Expand All @@ -119,10 +96,26 @@ import { startScheduledRotation, stopScheduledRotation } from './config/credenti
validateEnv();
const env = getConfig();

// Initialize sandbox services
const sandboxManager = new SandboxManager(env.NODE_ENV || 'development');
const mockPaymentProcessor = new MockPaymentProcessor();
const testDataSeeder = new TestDataSeeder();
// ── Lazy sandbox service initialization ───────────────────────────────────────
// SandboxManager, MockPaymentProcessor, and TestDataSeeder are only needed in
// development/sandbox environments. Deferring their construction avoids paying
// the instantiation cost on every cold start in production.
let _sandboxManager: InstanceType<typeof SandboxManager> | null = null;
let _mockPaymentProcessor: InstanceType<typeof MockPaymentProcessor> | null = null;
let _testDataSeeder: InstanceType<typeof TestDataSeeder> | null = null;

function getSandboxManager(): InstanceType<typeof SandboxManager> {
if (!_sandboxManager) _sandboxManager = new SandboxManager(env.NODE_ENV || 'development');
return _sandboxManager;
}
function getMockPaymentProcessor(): InstanceType<typeof MockPaymentProcessor> {
if (!_mockPaymentProcessor) _mockPaymentProcessor = new MockPaymentProcessor();
return _mockPaymentProcessor;
}
function getTestDataSeeder(): InstanceType<typeof TestDataSeeder> {
if (!_testDataSeeder) _testDataSeeder = new TestDataSeeder();
return _testDataSeeder;
}

// Initialize IP allowlist from environment
if (env.IP_ALLOWLIST_ENABLED || env.IP_ALLOWLIST) {
Expand Down Expand Up @@ -159,6 +152,9 @@ console.error = (...args) => originalConsole.error(...formatMessage(args));

const app = express();

// ── Cold start tracking — mount first so every request is measured ────────────
app.use(coldStartMiddleware);

// Token-bucket rate limiter (replaces fixed-window tieredRateLimit)
const apiRateLimiter = tokenBucketRateLimit({ keyPrefix: 'rl:api' });
// Stricter limiter for invoice endpoint
Expand Down Expand Up @@ -228,7 +224,8 @@ app.use((req: Request, res: Response, next: NextFunction) => {

app.use(healthRouter);

import { versionMiddleware } from './middleware/versioning.js';
// Cold start monitoring dashboard — available before auth/rate-limit middleware
app.use('/api/v1/cold-start', coldStartMonitorRouter);

app.use('/api/', apiRateLimiter);

Expand Down Expand Up @@ -327,7 +324,7 @@ app.use('/api/v1/tax', taxRouter);
app.use('/api/v1/projects', projectsRouter);

// Sandbox environment for testing (with relaxed rate limits)
const sandboxRouter = createSandboxRouter(sandboxManager, mockPaymentProcessor, testDataSeeder);
const sandboxRouter = createSandboxRouter(getSandboxManager(), getMockPaymentProcessor(), getTestDataSeeder());
app.use('/api/v1/sandbox', sandboxRateLimiter, sandboxRouter);

// Email system v2 with templates, analytics, and localization
Expand Down Expand Up @@ -415,6 +412,59 @@ const analyticsInterval = setInterval(() => {
server.listen(config.server.port, () => {
console.log(`AgenticPay backend running on port ${config.server.port} [${config.env}]`);
console.log(`WebSocket server listening on path /ws (max ${wsServer.metrics.activeConnections}/${wsServer.metrics.acceptedConnections})`);

// ── Deferred startup: run after the server is accepting requests ────────────
// These services are not needed to serve the first request. Starting them
// after listen() means the process is ready to handle traffic immediately,
// and the background work happens concurrently without blocking the hot path.
setImmediate(() => {
// Load Sentry profiling integration now that the process is warm
loadProfilingIntegration();

// Job scheduler
if (config.jobs.enabled) {
startJobs();

createBullMQScheduler(getScheduledTasks()).then((scheduler) => {
if (scheduler) {
console.log('[bullmq] Distributed scheduler active');
} else {
console.log('[scheduler] Using in-process node-cron (Redis not configured)');
}
}).catch((err) => {
console.error('[bullmq] Scheduler startup error:', err);
});
}

// Queue processors
registerDefaultProcessors();
if (config.queue.enabled) {
messageQueue.start();
paymentQueue.start();
}

// Webhook worker
startWebhookWorker();

// Auto-escalation cron
setInterval(async () => {
const count = await disputeService.processEscalations();
if (count > 0) console.log(`Escalated ${count} disputes`);
}, 5 * 60 * 1000);

// Batch processor
if (featureFlags.evaluate('batch-operations')) {
batchProcessor.start();
console.log('[BatchProcessor] Started');
}

// Redis cache connection (non-blocking — falls back to in-memory)
getRedisCache().connect().then(() => {
console.log('[RedisCache] Connection initialized');
}).catch(() => {
console.log('[RedisCache] Not available, using in-memory cache only');
});
});
});

const shutdown = (signal: string) => {
Expand Down
94 changes: 94 additions & 0 deletions backend/src/lib/lazy-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* lazy-loader.ts
*
* Utility for lazy initialization of heavy dependencies.
* Modules are only loaded on first access, not at process startup.
* This reduces cold start time by deferring expensive require/import
* calls until the code path that actually needs them is hit.
*
* Usage:
* const getOpenAI = lazyLoad(() => import('openai').then(m => new m.default({ apiKey: ... })));
* // Later, on first call:
* const client = await getOpenAI();
*/

export type LazyFactory<T> = () => Promise<T>;

/**
* Creates a lazy-initialized singleton.
* The factory is called at most once; subsequent calls return the cached instance.
*/
export function lazyLoad<T>(factory: LazyFactory<T>): () => Promise<T> {
let instance: T | undefined;
let pending: Promise<T> | undefined;

return async (): Promise<T> => {
if (instance !== undefined) return instance;
if (pending) return pending;

pending = factory().then((result) => {
instance = result;
pending = undefined;
return result;
});

return pending;
};
}

/**
* Synchronous lazy singleton — for modules that initialize synchronously.
*/
export function lazySyncLoad<T>(factory: () => T): () => T {
let instance: T | undefined;
return (): T => {
if (instance === undefined) {
instance = factory();
}
return instance;
};
}

/**
* Registry of all lazy-loaded modules with their load status.
* Used by the cold-start monitor to report which modules have been initialized.
*/
interface LazyModuleEntry {
name: string;
loaded: boolean;
loadedAt?: number;
loadDurationMs?: number;
}

const registry = new Map<string, LazyModuleEntry>();

/**
* Creates a tracked lazy loader that registers itself in the module registry.
* Useful for monitoring which heavy deps have been loaded.
*/
export function trackedLazyLoad<T>(name: string, factory: LazyFactory<T>): () => Promise<T> {
registry.set(name, { name, loaded: false });

let instance: T | undefined;
let pending: Promise<T> | undefined;

return async (): Promise<T> => {
if (instance !== undefined) return instance;
if (pending) return pending;

const start = Date.now();
pending = factory().then((result) => {
const duration = Date.now() - start;
instance = result;
pending = undefined;
registry.set(name, { name, loaded: true, loadedAt: Date.now(), loadDurationMs: duration });
return result;
});

return pending;
};
}

export function getLazyModuleRegistry(): LazyModuleEntry[] {
return Array.from(registry.values());
}
Loading