Merge pull request #72 from kitpdr/fix/cloudflare-workers-compat
fix: cloudflare workers compatible initialization
1 file · +45 -13
M src/hooks.server.ts +45 -13
| @@ -33,21 +33,53 @@ async function setAllUsersOffline() { | |||
| 33 | 33 | } | |
| 34 | 34 | } | |
| 35 | 35 | ||
| 36 | - | // Initialize server | |
| 37 | - | Promise.resolve() | |
| 38 | - | .then(async () => { | |
| 39 | - | log.info('Initializing server...'); | |
| 40 | - | await ensureDefaultChatRoom(); | |
| 41 | - | await setAllUsersOffline(); | |
| 42 | - | log.info('Server initialized successfully'); | |
| 43 | - | }) | |
| 44 | - | .catch((error: unknown) => { | |
| 45 | - | log.error('Failed to initialize server:', { error }); | |
| 46 | - | process.exit(1); | |
| 47 | - | }); | |
| 36 | + | // Initialize server (deferred, non-blocking for Cloudflare Workers compatibility) | |
| 37 | + | let initPromise: Promise<void> | null = null; | |
| 38 | + | let lastFailedAttempt = 0; | |
| 39 | + | let failureCount = 0; | |
| 40 | + | const BASE_COOLDOWN = 5000; // 5 seconds | |
| 41 | + | const MAX_COOLDOWN = 60000; // 1 minute max | |
| 42 | + | ||
| 43 | + | function initializeServer(): Promise<void> { | |
| 44 | + | if (initPromise) return initPromise; | |
| 45 | + | ||
| 46 | + | // Exponential backoff after failures | |
| 47 | + | if (lastFailedAttempt > 0) { | |
| 48 | + | const cooldown = Math.min(BASE_COOLDOWN * Math.pow(2, failureCount - 1), MAX_COOLDOWN); | |
| 49 | + | if (Date.now() - lastFailedAttempt < cooldown) { | |
| 50 | + | return Promise.resolve(); | |
| 51 | + | } | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | initPromise = (async () => { | |
| 55 | + | try { | |
| 56 | + | log.info('Initializing server...'); | |
| 57 | + | await ensureDefaultChatRoom(); | |
| 58 | + | await setAllUsersOffline(); | |
| 59 | + | log.info('Server initialized successfully'); | |
| 60 | + | lastFailedAttempt = 0; | |
| 61 | + | failureCount = 0; | |
| 62 | + | } catch (error: unknown) { | |
| 63 | + | log.error('Failed to initialize server:', { error, failureCount: failureCount + 1 }); | |
| 64 | + | lastFailedAttempt = Date.now(); | |
| 65 | + | failureCount++; | |
| 66 | + | initPromise = null; | |
| 67 | + | } | |
| 68 | + | })(); | |
| 69 | + | ||
| 70 | + | return initPromise; | |
| 71 | + | } | |
| 48 | 72 | ||
| 49 | 73 | export const handle: Handle = async ({ event, resolve }) => { | |
| 50 | - | log.debug('Handling request', { path: event.url.pathname }); | |
| 74 | + | const path = event.url.pathname; | |
| 75 | + | ||
| 76 | + | // Skip initialization for static assets and non-API routes | |
| 77 | + | const needsInit = path.startsWith('/api/') && !path.startsWith('/api/health'); | |
| 78 | + | if (needsInit) { | |
| 79 | + | await initializeServer(); | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | log.debug('Handling request', { path }); | |
| 51 | 83 | ||
| 52 | 84 | // CSRF protection: validate Origin header for state-changing requests | |
| 53 | 85 | if (event.request.method !== 'GET' && event.request.method !== 'HEAD') { | |
| @@ -33,21 +33,53 @@ async function setAllUsersOffline() { | |||||
| 33 | } | 33 | } | ||
| 34 | } | 34 | } | ||
| 35 | 35 | ||||
| 36 | - | // Initialize server | 36 | + | // Initialize server (deferred, non-blocking for Cloudflare Workers compatibility) |
| 37 | - | Promise.resolve() | 37 | + | let initPromise: Promise<void> | null = null; |
| 38 | - | .then(async () => { | 38 | + | let lastFailedAttempt = 0; |
| 39 | - | log.info('Initializing server...'); | 39 | + | let failureCount = 0; |
| 40 | - | await ensureDefaultChatRoom(); | 40 | + | const BASE_COOLDOWN = 5000; // 5 seconds |
| 41 | - | await setAllUsersOffline(); | 41 | + | const MAX_COOLDOWN = 60000; // 1 minute max |
| 42 | - | log.info('Server initialized successfully'); | 42 | + | |
| 43 | - | }) | 43 | + | function initializeServer(): Promise<void> { |
| 44 | - | .catch((error: unknown) => { | 44 | + | if (initPromise) return initPromise; |
| 45 | - | log.error('Failed to initialize server:', { error }); | 45 | + | |
| 46 | - | process.exit(1); | 46 | + | // Exponential backoff after failures |
| 47 | - | }); | 47 | + | if (lastFailedAttempt > 0) { |
| 48 | + | const cooldown = Math.min(BASE_COOLDOWN * Math.pow(2, failureCount - 1), MAX_COOLDOWN); | |||
| 49 | + | if (Date.now() - lastFailedAttempt < cooldown) { | |||
| 50 | + | return Promise.resolve(); | |||
| 51 | + | } | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | initPromise = (async () => { | |||
| 55 | + | try { | |||
| 56 | + | log.info('Initializing server...'); | |||
| 57 | + | await ensureDefaultChatRoom(); | |||
| 58 | + | await setAllUsersOffline(); | |||
| 59 | + | log.info('Server initialized successfully'); | |||
| 60 | + | lastFailedAttempt = 0; | |||
| 61 | + | failureCount = 0; | |||
| 62 | + | } catch (error: unknown) { | |||
| 63 | + | log.error('Failed to initialize server:', { error, failureCount: failureCount + 1 }); | |||
| 64 | + | lastFailedAttempt = Date.now(); | |||
| 65 | + | failureCount++; | |||
| 66 | + | initPromise = null; | |||
| 67 | + | } | |||
| 68 | + | })(); | |||
| 69 | + | ||||
| 70 | + | return initPromise; | |||
| 71 | + | } | |||
| 48 | 72 | ||||
| 49 | export const handle: Handle = async ({ event, resolve }) => { | 73 | export const handle: Handle = async ({ event, resolve }) => { | ||
| 50 | - | log.debug('Handling request', { path: event.url.pathname }); | 74 | + | const path = event.url.pathname; |
| 75 | + | ||||
| 76 | + | // Skip initialization for static assets and non-API routes | |||
| 77 | + | const needsInit = path.startsWith('/api/') && !path.startsWith('/api/health'); | |||
| 78 | + | if (needsInit) { | |||
| 79 | + | await initializeServer(); | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | log.debug('Handling request', { path }); | |||
| 51 | 83 | ||||
| 52 | // CSRF protection: validate Origin header for state-changing requests | 84 | // CSRF protection: validate Origin header for state-changing requests | ||
| 53 | if (event.request.method !== 'GET' && event.request.method !== 'HEAD') { | 85 | if (event.request.method !== 'GET' && event.request.method !== 'HEAD') { | ||