Skip to content
Draft
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
58 changes: 58 additions & 0 deletions packages/core/src/middleware/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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'

Expand Down Expand Up @@ -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<void>((resolve) => {
releaseMigration = resolve
})
const migrationStarted = new Promise<void>((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()
Expand All @@ -258,6 +315,7 @@ describe('bootstrapMiddleware', () => {

describe('resetBootstrap', () => {
beforeEach(() => {
resetBootstrap()
vi.clearAllMocks()
})

Expand Down
97 changes: 53 additions & 44 deletions packages/core/src/middleware/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Bindings = {

// Track if bootstrap has been run in this worker instance
let bootstrapComplete = false;
let bootstrapInFlight: Promise<void> | null = null;

/**
* Verify security-critical environment configuration at startup.
Expand Down Expand Up @@ -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)
Expand All @@ -159,4 +167,5 @@ export function bootstrapMiddleware(config: SonicJSConfig = {}) {
*/
export function resetBootstrap() {
bootstrapComplete = false;
bootstrapInFlight = null;
}