Skip to content

Conversation

Vicentesan
Copy link

@Vicentesan Vicentesan commented Sep 29, 2025

addreses #1383

This PR enhances CF Workers support in Elysia, with a particular focus on lifecycle hooks and the setImmediate polyfill

Summary by CodeRabbit

  • New Features

    • Graceful shutdown support on Cloudflare Workers with stop lifecycle handling.
    • Environment-aware background task scheduling (auto-detecting setImmediate + Cloudflare execution context support).
  • Bug Fixes

    • After-response hooks run reliably without blocking across Cloudflare Workers, Node.js, and other environments.
    • Start hooks now trigger after routes are compiled for more consistent startup on Cloudflare.

Copy link

coderabbitai bot commented Sep 29, 2025

Walkthrough

Adds Cloudflare Worker execution-context support and an auto-detecting setImmediate, updates beforeCompile to polyfill and attach it, overrides fetch to propagate ExecutionContext, compiles routes and triggers onStart, adds stop to run onStop, and changes afterResponse scheduling to prefer ctx.waitUntil then setImmediate then Promise.

Changes

Cohort / File(s) Summary
Cloudflare Worker adapter
src/adapter/cloudflare-worker/index.ts
Added beforeCompile(app) to polyfill/attach an auto-detecting setImmediate, override global/globalThis, propagate ExecutionContext via fetch, compile routes, and invoke onStart. Added stop(app, closeActiveConnections) to invoke onStop. Exposed createAutoDetectingSetImmediate() and injected setImmediate into composeHandler.inject.
Request handler & after-response scheduling
src/dynamic-handle.ts
Handler signature extended to accept optional RequestContext. Replaced immediate scheduling logic with runAfterResponse that schedules via ExecutionContext.waitUntil (from requestContext or global), falling back to setImmediate, then to Promise.then. Declares global __cloudflareExecutionContext.
Adapter types
src/adapter/types.ts
Added exported interfaces: ExecutionContext<Props = unknown> (with waitUntil, passThroughOnException, props) and RequestContext (optional executionContext?: ExecutionContext).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App
  participant Adapter as CloudflareAdapter
  participant Global as global/globalThis
  participant Routes as Routes Compiler

  Note over Adapter,Global: beforeCompile flow

  App->>Adapter: beforeCompile(app)
  Adapter->>Global: createAutoDetectingSetImmediate() -> attach to global
  Adapter->>Adapter: override fetch to capture/propagate ExecutionContext
  Adapter->>Routes: compile routes
  Adapter-->>App: invoke onStart hooks
Loading
sequenceDiagram
  autonumber
  participant Handler
  participant EnvDetector as Environment
  participant Worker as Cloudflare Worker
  participant Node as Node.js
  participant Micro as Microtask (Promise)

  Note over Handler,EnvDetector: afterResponse scheduling

  Handler->>EnvDetector: schedule runAfterResponse(requestContext?)
  alt ExecutionContext available
    EnvDetector->>Worker: waitUntil(runAfterResponse)
  else setImmediate available
    EnvDetector->>Node: setImmediate(runAfterResponse)
  else
    EnvDetector->>Micro: Promise.resolve().then(runAfterResponse)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I twitched my whiskers at compile-time light,
setImmediate found its cloud-edge flight.
waitUntil hums the after-response tune,
onStart springs up, onStop tips the moon.
Hops and hooks — a rabbit’s patch of bright. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately describes the primary changes—adding lifecycle hooks and a setImmediate polyfill for Cloudflare Workers—which directly aligns with the pull request’s objectives and main implementation details.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 668c7a1 and 91b6246.

📒 Files selected for processing (2)
  • src/adapter/cloudflare-worker/index.ts (2 hunks)
  • src/dynamic-handle.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/dynamic-handle.ts (1)
test/types/index.ts (2)
  • afterResponse (1999-2009)
  • afterResponse (2473-2475)
src/adapter/cloudflare-worker/index.ts (1)
src/adapter/web-standard/index.ts (1)
  • WebStandardAdapter (10-179)

@Vicentesan Vicentesan marked this pull request as draft September 29, 2025 18:51
@Vicentesan
Copy link
Author

@coderabbitai review this

Copy link

coderabbitai bot commented Sep 29, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the PR diff to review the actual changes
gh pr diff 1447 --repo elysiajs/elysia

Length of output: 7192


@Vicentesan, here's a comprehensive review of your changes:

🎯 Overall Assessment

Good initiative to add Cloudflare Workers lifecycle support! However, there are several architectural and implementation concerns that should be addressed before merging.

🔴 Critical Issues

1. Global State Management & Memory Leaks

Location: src/adapter/cloudflare-worker/index.ts (Lines 14-15, 38-42) & src/dynamic-handle.ts (Lines 696-699)

The globalThis.__cloudflareExecutionContext is set but never cleaned up after the request completes. This creates a risk of:

  • Memory leaks in long-running workers
  • Context bleeding between concurrent requests
  • Race conditions where one request's context could be used by another

Recommendation: Clear the context after use:

app.fetch = function (request: Request, env?: any, ctx?: ExecutionContext) {
    try {
        if (ctx) {
            globalThis.__cloudflareExecutionContext = ctx
        }
        return originalFetch(request)
    } finally {
        // Clean up to prevent memory leaks and context bleeding
        globalThis.__cloudflareExecutionContext = undefined
    }
}

2. Type Safety Violations

Location: Multiple @ts-ignore comments (Lines 99, 126, 141)

The @ts-ignore comments suppress legitimate type errors. This:

  • Hides potential runtime errors
  • Makes refactoring dangerous
  • Reduces IDE support

Recommendation: Use proper type declarations or augment global types:

declare global {
    var setImmediate: ((callback: () => void) => void) | undefined
}

3. Async/Sync Inconsistency in Lifecycle Hooks

Location: src/adapter/cloudflare-worker/index.ts (Lines 100-108 vs 151-154)

The stop() method correctly uses async/await for lifecycle hooks, but beforeCompile() calls onStart hooks synchronously without awaiting:

// Line 151-154 - NOT awaited
if (app.event.start)
    for (let i = 0; i < app.event.start.length; i++)
        app.event.start[i].fn(app)  // ❌ What if this is async?

Recommendation: Make beforeCompile async and await the hooks:

async beforeCompile(app) {
    // ... setup code ...
    
    if (app.event.start)
        for (let i = 0; i < app.event.start.length; i++)
            await app.event.start[i].fn(app)
}

⚠️ Major Concerns

4. Fetch Override Side Effects

Location: Lines 127-141

Overriding app.fetch mutates the application object in beforeCompile, which:

  • Makes the behavior harder to understand
  • Could interfere with other adapters
  • Changes the function signature without updating types

Consider: Is there a way to pass ExecutionContext through the existing request flow without mutation?

5. Complex Fallback Logic

Location: src/dynamic-handle.ts (Lines 678-711)

The afterResponse scheduling has four different fallback paths:

  1. requestContext?.executionContext.waitUntil
  2. globalThis.__cloudflareExecutionContext.waitUntil
  3. setImmediate
  4. Promise.resolve().then

This complexity increases the risk of bugs and makes testing difficult.

Recommendation: Consolidate the logic and prefer passing context explicitly over globals.

6. Error Handling in setImmediate Polyfill

Location: Lines 38-45, 47-54

The polyfill catches and logs errors but:

  • Errors are silently swallowed (just logged to console)
  • No way for application code to handle errors
  • Could hide critical bugs in afterResponse hooks

Recommendation: Consider whether errors should propagate or at least be reported through an error handler.

💡 Minor Issues

7. Missing Type for requestContext Parameter

Location: src/dynamic-handle.ts (Line 68)

The requestContext parameter should have a proper type definition instead of inline interface:

type RequestContext = {
    executionContext?: ExecutionContext
}

return async (request: Request, requestContext?: RequestContext): Promise<Response> => {

8. Duplicate ExecutionContext Interface

Location: Both files define the same interface

Consider extracting this to a shared types file to maintain DRY principle.

9. Missing Global Type Declaration

Location: src/dynamic-handle.ts

The file uses globalThis.__cloudflareExecutionContext but doesn't declare it like the cloudflare-worker file does.

📋 Questions

  1. Testing: Are there tests for this functionality? How do you verify the ExecutionContext is properly captured and used?

  2. Adapter Detection: Should this behavior only activate when the Cloudflare adapter is actually being used? Currently, the dynamic-handle.ts changes affect all adapters.

  3. Backward Compatibility: Does this change maintain compatibility with existing Cloudflare Worker deployments?

🎓 Recommendations Summary

  1. ✅ Add cleanup for __cloudflareExecutionContext
  2. ✅ Fix type safety issues (remove @ts-ignore)
  3. ✅ Make lifecycle hooks consistently async
  4. ✅ Simplify the fallback logic in afterResponse
  5. ✅ Add proper error handling/reporting
  6. ✅ Extract shared types
  7. ✅ Add tests for Cloudflare Worker scenarios

Would you like me to help plan code changes for any of these issues?

@Vicentesan
Copy link
Author

@coderabbitai review this

Copy link

coderabbitai bot commented Oct 3, 2025

@Vicentesan, I'll trigger a fresh review of the PR for you.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91b6246 and 2db8cb7.

📒 Files selected for processing (3)
  • src/adapter/cloudflare-worker/index.ts (3 hunks)
  • src/adapter/types.ts (1 hunks)
  • src/dynamic-handle.ts (4 hunks)

Comment on lines +131 to +146
// Override app.fetch to accept ExecutionContext and set it globally
const originalFetch = app.fetch.bind(app)
app.fetch = function (
request: Request,
env?: any,
ctx?: ExecutionContext
) {
try {
if (ctx) {
globalThis.__cloudflareExecutionContext = ctx
}
const result = originalFetch(request)
// Clean up context after request to prevent memory leaks and context bleeding
globalThis.__cloudflareExecutionContext = undefined
return result
} catch (error) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Keep ExecutionContext alive until the request truly finishes

We set globalThis.__cloudflareExecutionContext = ctx, call originalFetch(request), and then immediately clear the global before the returned promise settles. All downstream async work (handlers, finally blocks, afterResponse, etc.) therefore run with the context wiped out, so waitUntil is never available and the feature still falls back to the Promise branch. The same happens on errors—cleanup runs before hooks fire.

Wrap the originalFetch call so the context remains set for the entire lifetime of the request and only restore/clear it in a finally after the promise resolves, e.g.:

-		const result = originalFetch(request)
-		// Clean up context after request to prevent memory leaks and context bleeding
-		globalThis.__cloudflareExecutionContext = undefined
-		return result
+		const previous = globalThis.__cloudflareExecutionContext
+		if (ctx) globalThis.__cloudflareExecutionContext = ctx
+
+		const result = originalFetch(request)
+
+		const cleanup = () => {
+			if (previous === undefined) {
+				delete globalThis.__cloudflareExecutionContext
+			} else {
+				globalThis.__cloudflareExecutionContext = previous
+			}
+		}
+
+		return result instanceof Promise ? result.finally(cleanup) : (cleanup(), result)

This keeps waitUntil reachable for afterResponse while still preventing leaks and restores any prior context to avoid cross-request bleeding.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Override app.fetch to accept ExecutionContext and set it globally
const originalFetch = app.fetch.bind(app)
app.fetch = function (
request: Request,
env?: any,
ctx?: ExecutionContext
) {
try {
if (ctx) {
globalThis.__cloudflareExecutionContext = ctx
}
const result = originalFetch(request)
// Clean up context after request to prevent memory leaks and context bleeding
globalThis.__cloudflareExecutionContext = undefined
return result
} catch (error) {
// Override app.fetch to accept ExecutionContext and set it globally
const originalFetch = app.fetch.bind(app)
app.fetch = function (
request: Request,
env?: any,
ctx?: ExecutionContext
) {
try {
if (ctx) {
globalThis.__cloudflareExecutionContext = ctx
}
const previous = globalThis.__cloudflareExecutionContext
if (ctx) globalThis.__cloudflareExecutionContext = ctx
const result = originalFetch(request)
const cleanup = () => {
if (previous === undefined) {
delete globalThis.__cloudflareExecutionContext
} else {
globalThis.__cloudflareExecutionContext = previous
}
}
return result instanceof Promise
? result.finally(cleanup)
: (cleanup(), result)
} catch (error) {
// existing error handling...
}
}
🤖 Prompt for AI Agents
In src/adapter/cloudflare-worker/index.ts around lines 131 to 146, the code sets
globalThis.__cloudflareExecutionContext before calling originalFetch(request)
but clears it immediately after the call, which tears down the context before
the returned promise settles; change this so the context remains set for the
full lifetime of the request by saving the previous context, assigning
globalThis.__cloudflareExecutionContext = ctx, calling originalFetch(request)
and then attaching a finally handler (or awaiting the promise) that restores the
previous context (or sets undefined if none) only after the promise resolves or
rejects; ensure errors are re-thrown/passed through so behavior is unchanged and
memory/context leaks are prevented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant