diff --git a/packages/core/src/middleware/bootstrap.test.ts b/packages/core/src/middleware/bootstrap.test.ts index 587c54d37..dfb6634b1 100644 --- a/packages/core/src/middleware/bootstrap.test.ts +++ b/packages/core/src/middleware/bootstrap.test.ts @@ -11,6 +11,10 @@ vi.mock('../services/collection-sync', () => ({ syncCollections: vi.fn().mockResolvedValue([]) })) +vi.mock('../services/form-collection-sync', () => ({ + syncAllFormCollections: vi.fn().mockResolvedValue(undefined) +})) + vi.mock('../services/migrations', () => { const mockRunPendingMigrations = vi.fn().mockResolvedValue(undefined) return { @@ -35,6 +39,7 @@ vi.mock('../services/plugin-bootstrap', () => { // Import the mocked modules after mocking import { syncCollections } from '../services/collection-sync' +import { syncAllFormCollections } from '../services/form-collection-sync' import { MigrationService } from '../services/migrations' import { PluginBootstrapService } from '../services/plugin-bootstrap' @@ -232,6 +237,58 @@ describe('bootstrapMiddleware', () => { expect(consoleSpy).toHaveBeenCalledWith('[Bootstrap] Plugin bootstrap skipped (disableAll is true)') }) + it('should run bootstrap only once for concurrent cold-start requests', async () => { + const app = new Hono() + const env = createMockEnv() + + let releaseMigration: (() => void) | undefined + let markMigrationStarted: (() => void) | undefined + const migrationGate = new Promise((resolve) => { + releaseMigration = resolve + }) + const migrationStarted = new Promise((resolve) => { + markMigrationStarted = resolve + }) + + const migrationServiceMock = vi.mocked(MigrationService) + migrationServiceMock.mockImplementation(function() { + this.runPendingMigrations = vi.fn().mockImplementation(async () => { + markMigrationStarted?.() + await migrationGate + }) + return this + }) + + app.use('*', async (c, next) => { + c.env = env as any + await next() + }) + app.use('*', bootstrapMiddleware()) + app.get('/test', (c) => c.json({ ok: true })) + + const firstRequest = app.request('/test') + await migrationStarted + const secondRequest = app.request('/test') + + expect(MigrationService).toHaveBeenCalledTimes(1) + + releaseMigration?.() + const [firstResponse, secondResponse] = await Promise.all([firstRequest, secondRequest]) + + expect(firstResponse.status).toBe(200) + expect(secondResponse.status).toBe(200) + expect(MigrationService).toHaveBeenCalledTimes(1) + expect(syncCollections).toHaveBeenCalledTimes(1) + expect(syncAllFormCollections).toHaveBeenCalledTimes(1) + expect(PluginBootstrapService).toHaveBeenCalledTimes(1) + + migrationServiceMock.mockReset() + migrationServiceMock.mockImplementation(function() { + this.runPendingMigrations = vi.fn().mockResolvedValue(undefined) + return this + }) + }) + it('should continue on fatal bootstrap error', async () => { const app = new Hono() const env = createMockEnv() @@ -258,6 +315,7 @@ describe('bootstrapMiddleware', () => { describe('resetBootstrap', () => { beforeEach(() => { + resetBootstrap() vi.clearAllMocks() }) diff --git a/packages/core/src/middleware/bootstrap.ts b/packages/core/src/middleware/bootstrap.ts index bf6fc131e..d6be969ae 100644 --- a/packages/core/src/middleware/bootstrap.ts +++ b/packages/core/src/middleware/bootstrap.ts @@ -15,6 +15,7 @@ type Bindings = { // Track if bootstrap has been run in this worker instance let bootstrapComplete = false; +let bootstrapInFlight: Promise | null = null; /** * Verify security-critical environment configuration at startup. @@ -99,52 +100,59 @@ export function bootstrapMiddleware(config: SonicJSConfig = {}) { return next(); } - try { - console.log("[Bootstrap] Starting system initialization..."); - - // 1. Run database migrations first - console.log("[Bootstrap] Running database migrations..."); - const migrationService = new MigrationService(c.env.DB); - await migrationService.runPendingMigrations(); - - // 2. Sync collection configurations - console.log("[Bootstrap] Syncing collection configurations..."); - try { - await syncCollections(c.env.DB); - } catch (error) { - console.error("[Bootstrap] Error syncing collections:", error); - // Continue bootstrap even if collection sync fails - } - - // 2b. Sync form-derived shadow collections - console.log("[Bootstrap] Syncing form collections..."); - try { - await syncAllFormCollections(c.env.DB); - } catch (error) { - console.error("[Bootstrap] Error syncing form collections:", error); - } - - // 3. Bootstrap core plugins (unless disableAll is set) - if (!config.plugins?.disableAll) { - console.log("[Bootstrap] Bootstrapping core plugins..."); - const bootstrapService = new PluginBootstrapService(c.env.DB); - - // Check if bootstrap is needed - const needsBootstrap = await bootstrapService.isBootstrapNeeded(); - if (needsBootstrap) { - await bootstrapService.bootstrapCorePlugins(); + if (!bootstrapInFlight) { + bootstrapInFlight = (async () => { + try { + console.log("[Bootstrap] Starting system initialization..."); + + // 1. Run database migrations first + console.log("[Bootstrap] Running database migrations..."); + const migrationService = new MigrationService(c.env.DB); + await migrationService.runPendingMigrations(); + + // 2. Sync collection configurations + console.log("[Bootstrap] Syncing collection configurations..."); + try { + await syncCollections(c.env.DB); + } catch (error) { + console.error("[Bootstrap] Error syncing collections:", error); + // Continue bootstrap even if collection sync fails + } + + // 2b. Sync form-derived shadow collections + console.log("[Bootstrap] Syncing form collections..."); + try { + await syncAllFormCollections(c.env.DB); + } catch (error) { + console.error("[Bootstrap] Error syncing form collections:", error); + } + + // 3. Bootstrap core plugins (unless disableAll is set) + if (!config.plugins?.disableAll) { + console.log("[Bootstrap] Bootstrapping core plugins..."); + const bootstrapService = new PluginBootstrapService(c.env.DB); + + // Check if bootstrap is needed + const needsBootstrap = await bootstrapService.isBootstrapNeeded(); + if (needsBootstrap) { + await bootstrapService.bootstrapCorePlugins(); + } + } else { + console.log("[Bootstrap] Plugin bootstrap skipped (disableAll is true)"); + } + + // Mark bootstrap as complete for this worker instance + bootstrapComplete = true; + console.log("[Bootstrap] System initialization completed"); + } catch (error) { + console.error("[Bootstrap] Error during system initialization:", error); + // Don't prevent the app from starting, but log the error + } finally { + bootstrapInFlight = null; } - } else { - console.log("[Bootstrap] Plugin bootstrap skipped (disableAll is true)"); - } - - // Mark bootstrap as complete for this worker instance - bootstrapComplete = true; - console.log("[Bootstrap] System initialization completed"); - } catch (error) { - console.error("[Bootstrap] Error during system initialization:", error); - // Don't prevent the app from starting, but log the error + })(); } + await bootstrapInFlight; // 4. Verify security configuration (outside try/catch so critical // errors in production propagate and prevent insecure deployments) @@ -159,4 +167,5 @@ export function bootstrapMiddleware(config: SonicJSConfig = {}) { */ export function resetBootstrap() { bootstrapComplete = false; + bootstrapInFlight = null; }