Skip to content

fix(dev): await worker shutdown before replacing it - #4506

Open
danielroe wants to merge 1 commit into
mainfrom
fix/dev-close-await
Open

fix(dev): await worker shutdown before replacing it#4506
danielroe wants to merge 1 commit into
mainfrom
fix/dev-close-await

Conversation

@danielroe

@danielroe danielroe commented Aug 4, 2026

Copy link
Copy Markdown
Member

🔗 Linked issue

nuxt/nuxt#32928

related: #4088
partly resolve #2735

❓ Type of change

  • 📖 Documentation (updates to the documentation, readme, or JSdoc annotations)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

we currently don't await when shutting down a worker, meaning tasks or plugins shutdown can overlap with new startup (e.g. database connection). we've implemented waiting (+ timeout) in nuxt/cli for the nuxt dev server (nuxt/cli#1424) but I thought we should probably do the same for nitro workers.

let me know if you don't think this is desired behaviour, or if you want to move it to env-runner instead 🙏

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

@danielroe danielroe self-assigned this Aug 4, 2026
@danielroe
danielroe requested a review from pi0 as a code owner August 4, 2026 09:56
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nitro.build Ready Ready Preview Aug 4, 2026 9:56am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The dev server now performs coordinated worker shutdown during close and reload. It waits for worker exit, handles timeout and send failures, and suppresses recovery behavior for intentional worker closure.

Changes

Worker shutdown coordination

Layer / File(s) Summary
Worker shutdown protocol
src/dev/server.ts
The server tracks intentional shutdown state. #shutdownWorker() sends a shutdown message, waits for an exit event, handles send failures, and force-completes after five seconds.
Close and reload integration
src/dev/server.ts
close() and reload shut down the existing worker before releasing resources or starting a replacement runner. Intentional worker closure skips error recording and retry handling.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title follows Conventional Commits syntax and clearly describes awaiting worker shutdown before replacement.
Description check ✅ Passed The description explains the worker shutdown bug, the timeout-based fix, and the intended prevention of overlapping shutdown and startup tasks.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dev-close-await

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.

@pkg-pr-new

pkg-pr-new Bot commented Aug 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/nitro@4506

commit: 41d7d70

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/dev/server.ts (1)

211-243: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Extract worker lifecycle coordination.

Move the shutdown state and shutdown protocol into an internal module such as src/dev/_worker-lifecycle.ts. This file already exceeds 200 lines, and this change adds a separate lifecycle unit.

As per coding guidelines, “Split logic across files; avoid long single-file modules (>200 LoC).”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dev/server.ts` around lines 211 - 243, Extract the worker shutdown
coordination logic from the server.ts file into a new internal module such as
src/dev/_worker-lifecycle.ts. Move the `#shutdownWorker` method, the `#shuttingDown`
state field, and related shutdown protocol constants like SHUTDOWN_TIMEOUT into
the new module. Create a class or exported functions in the new module that
encapsulate this lifecycle management, then update the server.ts file to use the
extracted module, removing the original `#shutdownWorker` method and `#shuttingDown`
field while delegating to the new module.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/dev/server.ts`:
- Around line 235-237: Update the catch block around the shutdown message
delivery to capture the send error and issue a warning containing the shutdown
operation context and error message, then continue calling done() as before.
- Line 155: Update close() before the await this.#shutdownWorker() call to set a
permanent closing state, reject both new and already queued reload() operations,
and await any active reload represented by `#reloadPromise` before invoking
`#manager.close`(). Ensure reload cannot call `#manager.reload` after closing
begins, while preserving the existing shutdown and watcher-close sequence.
- Around line 82-84: Update the shutdown flow in `#shutdownWorker`() and local
close() so RunnerManager cleanup occurs before `#shuttingDown` is cleared, or keep
that flag set through manager.close(). Ensure registered onClose listeners
observe the intentional-shutdown state and do not call reload(), while
preserving listener cleanup ordering.

---

Nitpick comments:
In `@src/dev/server.ts`:
- Around line 211-243: Extract the worker shutdown coordination logic from the
server.ts file into a new internal module such as src/dev/_worker-lifecycle.ts.
Move the `#shutdownWorker` method, the `#shuttingDown` state field, and related
shutdown protocol constants like SHUTDOWN_TIMEOUT into the new module. Create a
class or exported functions in the new module that encapsulate this lifecycle
management, then update the server.ts file to use the extracted module, removing
the original `#shutdownWorker` method and `#shuttingDown` field while delegating to
the new module.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 203bb772-0c7f-4261-a187-31c373a3b410

📥 Commits

Reviewing files that changed from the base of the PR and between 52abde8 and 41d7d70.

📒 Files selected for processing (1)
  • src/dev/server.ts

Comment thread src/dev/server.ts
Comment on lines +82 to +84
if (this.#shuttingDown) {
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the RunnerManager implementation and its event ordering.
rg -n -C 6 'RunnerManager|onClose\(|onMessage\(|offMessage\(|sendMessage\(|reload\(' src

# Trace the local shutdown and exit-message protocol.
rg -n -C 6 --glob '*.{ts,js,mjs}' 'shutdown|event:\s*["'\'']exit["'\'']|event\s*===\s*["'\'']exit["'\'']' src

# Identify the locked external runner package if its implementation is not local.
rg -n -C 4 'env-runner|RunnerManager' package.json pnpm-lock.yaml

Repository: nitrojs/nitro

Length of output: 30874


🌐 Web query:

npm env-runner 0.1.16 RunnerManager onClose sendMessage

💡 Result:

In the env-runner package (version 0.1.16), the RunnerManager and IPC system provide bidirectional messaging and lifecycle management between a runner and its worker [1][2][3]. The onClose hook and sendMessage function function as follows: sendMessage The sendMessage function is a method used for bidirectional communication to send messages from one side of the IPC channel to the other [4]. - Within the worker (app code), it is provided as part of the context in the ipc.onOpen hook: ipc.onOpen({ sendMessage }) { sendMessage({ type: 'hello' }); } [1][2][3]. - Within the manager or runner instances, it is called directly: manager.sendMessage({ type: 'config' }) [1][2][3]. - Messages sent via the manager are queued if the runner is not yet ready [1][2][3]. onClose The onClose hook is used to handle the lifecycle event when a runner or worker shuts down [5][4]. - In the worker IPC configuration (ipc object), onClose is a callback defined to execute logic when the runner shuts down: ipc.onClose {... } [1][2][3]. - In the RunnerManager and general runner configuration, lifecycle hooks are defined under a hooks object, where onClose receives the runner instance and an optional cause: hooks: { onClose: (runner, cause) => {... } } [4][6]. RunnerManager handles hot-reloading by automatically closing the old runner and starting a new one when.reload is called, triggering the associated close lifecycle events [1][2][3]. All runners and the RunnerManager implement AsyncDisposable, allowing for automatic closure using explicit resource management (await using) [2][3].

Citations:


🌐 Web query:

github unjs env-runner v0.1.16 RunnerManager close onClose source

💡 Result:

In unjs/env-runner version 0.1.16, RunnerManager is a proxy manager designed to handle runner lifecycles, including hot-reloading and graceful shutdowns [1][2]. The RunnerManager and related EnvRunner implementations support explicit resource management via AsyncDisposable [2][3]. This means that RunnerManager instances can be closed automatically at the end of a scope using the await using syntax, or manually by calling the await manager.close() method [2][4]. When manager.reload() is called with a new runner, the previous runner is closed automatically [1][2]. Regarding the onClose hook, it is defined in the WorkerHooks interface (located in src/types.ts) as a lifecycle callback [3]. When observed on a runner, it allows you to react to the worker shutting down: onClose?: (worker: EnvRunner, cause?: unknown) => void; This hook is distinct from the RunnerManager's own close() method, which performs the actual shutdown operation [3]. While RunnerManager manages multiple runners, individual runners (such as NodeProcessEnvRunner) can be configured with hooks that include onClose to observe their specific state changes [1][4]. In broader contexts, such as IPC (Inter-Process Communication) configurations, there is also an ipc.onClose() callback specifically for handling the closure of the IPC channel [1][5]. Top results: 1, 2, 3, 7

Citations:


🌐 Web query:

ghcr.io unjs/env-runner 0.1.16 RunnerManager close onClose sendMessage source

💡 Result:

In version 0.1.16 of the unjs/env-runner package, RunnerManager and individual runner instances (such as NodeProcessEnvRunner) provide standardized methods for lifecycle management and bidirectional communication [1][2][3][4]. Key methods include: - close: Gracefully shuts down the runner or manager [2][3][4]. Both RunnerManager and all EnvRunner implementations implement AsyncDisposable, allowing for automatic closure using the await using syntax [3][4]. - sendMessage(message): Sends a message to the worker [2][3][4]. In RunnerManager, messages are queued until the runner is ready [2][3]. - onClose: This is a lifecycle hook, typically defined within a WorkerHooks object when configuring a runner [2][4]. It is called when the worker closes, optionally providing the cause of the closure [4]. For IPC communication from within the worker (the application being run), you can use the ipc object in your entry file to handle events: // Example of IPC hooks in the app entry file export default { ipc: { onOpen({ sendMessage }) { sendMessage({ type: "hello", from: "worker" }); }, onMessage(message) { console.log("Received:", message); }, onClose { console.log("Runner is shutting down"); }, }, }; These interfaces are defined in the package's type definitions, ensuring consistent behavior across different runner types (e.g., Node.js worker threads, child processes, Bun, Deno) [4][5]. The 0.1.16 release notably included enhancements for runtime-native WebSocket upgrade proxying [6].

Citations:


🌐 Web query:

npm env-runner 0.1.16 RunnerManager onClose source source env-runner 0.1.16 onMessage exit cause

💡 Result:

In the env-runner package (version 0.1.16), the RunnerManager and individual runner instances handle lifecycle events and IPC messaging through defined hooks and event-driven patterns. ### Lifecycle Hooks (onClose) The onClose hook is a lifecycle observer used to detect when a worker or runner shuts down [1]. - Definition: Defined in WorkerHooks, the onClose hook is typed as onClose?: (worker: EnvRunner, cause?: unknown) => void [1]. - Usage: When a runner shuts down, it executes this hook to signal the end of its lifecycle [2]. In implementation, it is often triggered during the close() method, which can accept an optional cause argument to describe why the shutdown occurred (e.g., worker exit code or manual termination) [1][3]. ### Message Handling (onMessage and Exit) Communication between the manager/parent process and the worker occurs via an IPC channel [2]. - onMessage: This hook allows the runner to receive and process custom messages sent from the worker or parent [2]. It explicitly filters out internal system messages—such as ping/pong and shutdown signals—to ensure that only user-defined logic is executed [2]. - Exit Cause: The "exit" event is a specific internal message used during the graceful shutdown sequence [2][4]. When a shutdown is requested (e.g., via close()), the runner sends a { event: "shutdown" } signal to the worker [4][3]. The worker, upon receiving this, performs its internal onClose cleanup and subsequently communicates an exit status back to the manager (often by emitting an exit event or fulfilling a promise indicating the worker has finished) [2][4][3]. ### Summary of Lifecycle Flow 1. Initiation: The manager triggers a shutdown (either manually via manager.close() or automatically during a hot-reload reload() call) [5][6]. 2. Shutdown Signal: The runner sends a shutdown event to the worker [4][3]. 3. Cleanup: The worker executes its defined ipc.onClose() logic [2][4]. 4. Exit: The worker notifies the manager of its termination (the "exit" event) [2][4]. If the process terminates, the manager detects the exit code and may pass it to the onClose hook as the cause parameter [3]. These mechanisms ensure that resources are cleaned up correctly during transitions or process termination in various environments (Node.js, Deno, Bun) supported by env-runner [2][4][7].

Citations:


🌐 Web query:

github cjk107/env-runner 0.1.16 source RunnerManager onLoad onClose exit event close

💡 Result:

In the env-runner library (including version 0.1.16), the RunnerManager and related runner components use a structured lifecycle for managing worker execution, hot-reloading, and termination [1][2][3]. Lifecycle Events and Management: 1. Initialization and Readiness: Runners and the RunnerManager support lifecycle hooks, including onReady, which is triggered when a worker is initialized and ready to accept requests [4][5]. 2. Closing and Exit: - Graceful Shutdown: Runners implement the AsyncDisposable interface [1][5]. You can trigger a graceful shutdown by calling await runner.close or await manager.close [1][2][3]. When using modern JavaScript environments, this can be handled automatically via the explicit resource management syntax (await using) [1][3]. - onClose Hook: The onClose hook is available within the WorkerHooks interface and is called when a worker is shutting down [4][5]. It receives the runner instance and an optional cause parameter [5]. - Termination Behavior: The system distinguishes between graceful shutdowns (via close) and immediate termination [4]. The internal implementation ensures that non-graceful supervisor deaths do not leave orphan processes by registering process-level cleanup handlers (e.g., listening for disconnect events) [4]. Version 0.1.16 includes general enhancements and fixes for the runner ecosystem [6]. The core API for managing these lifecycles has remained consistent, utilizing the AsyncDisposable pattern for cleanup and providing the onClose callback for observability [1][5][3].

Citations:


🌐 Web query:

@unjs/env-runner 0.1.16 RunnerManager close() RunnerManager reload() onClose exit event source

💡 Result:

In @unjs/env-runner 0.1.16, RunnerManager provides robust lifecycle management for runners, including support for hot-reloading and graceful shutdown [1][2][3]. RunnerManager Methods close: Awaits the shutdown of the currently active runner and cleans up the manager [1][4]. The RunnerManager implements AsyncDisposable, allowing it to be used with the await using syntax for automatic, graceful cleanup at the end of a scope [2][5]. reload(runner): Replaces the current runner with a new instance [1][3]. This method automatically calls close on the old runner to ensure a clean transition [1][2]. Lifecycle and Events onClose: While RunnerManager acts as a proxy, individual runner instances (which implement the EnvRunner interface) provide an onClose hook [4]. This hook is defined in the WorkerHooks interface as: onClose?: (worker: EnvRunner, cause?: unknown) => void; and is triggered when the worker shuts down [4]. exit event: The framework is designed to prevent orphaned processes by registering internal listeners. Specifically, Node.js and Bun process-based workers register process.on("disconnect", => process.exit(0)) internally before the entry module is imported. This ensures that if the supervisor process exits abruptly (e.g., via SIGKILL or a crash), the worker process is terminated automatically [6]. Summary of Usage For manual control, you can explicitly call await runner.close [1][5]. For modern resource management, utilizing await using with the manager or runner is the recommended pattern to handle lifecycle events and shutdown automatically [2][5]. [1][2][4][6]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the dev server lifecycle and message listener behavior without running repo code.
python3 - <<'PY'
from pathlib import Path
src = Path("src/dev/server.ts").read_text()
parts = {
    "class_body": src[src.index("class NitroDevServer"):],
    "onClose": src[src.index("this.#manager.onClose"):src.index("this.#manager.onMessage", src.index("this.#manager.onClose"))],
    "shutdown": src[src.index("async `#shutdownWorker`"):src.index("async close()"), src.index("async close()"):src.index("async `#reload`()"), src.index("async `#reload`()"):src.index("reload()", src.index("async `#reload`()")+1)],
}
for name, text in parts.items():
    print(f"\n--- {name} ---")
    print("\n".join(f"{i+1:4}: {line}" for i, line in enumerate(text.splitlines())))
    print()

# Deterministic probe of the flag/order pattern in src/dev/server.ts only.
lines = src.splitlines()
events = []
for i, line in enumerate(lines, 1):
    stripped = line.strip()
    if "this.#shuttingDown = true;" in stripped: events.append(("shutting_down_set", i))
    elif "if (this.#shuttingDown)" in stripped and "onClose" in "".join(lines[max(1,i-10):i]): 
        events.append(("onClose_flag_check", i))
    elif "onMessage((message" in stripped or "const listener" in stripped or '"event" === "exit"' in stripped or "message?.event === \"exit\"" in stripped:
        events.append(("exit_listener_found", i))
    elif "this.#manager.close();" in stripped: events.append(("manager_close", i))
    elif "this.#shuttingDown = false;" in stripped: events.append(("shutting_down_cleared", i))

print("flag_ordering_events:")
for ev in events:
    print(ev)

# Check whether any local close path is visible after `#shutdownWorker` and before `#manager.close`,
# and whether event listener cleanup can still receive events via onMessage after process close.
print("\nlocal_close_call_sites:")
for i, line in enumerate(lines, 1):
    if "this.#manager.close()" in line or "`#shutdownWorker`()" in line:
        print(f"{i:4}: {line.strip()}")
PY

# Search for any local runner implementation that might handle shutdown/exit/onClose locally.
rg -n -C 5 'event === ["'\'']exit["'\'']|message\.event === ["'\'']exit["'\'']|send\(.*exit|onMessage.*event|process\.on\(.*disconnect|process\.exit|ipc\.onClose|onClose\(' src || true

Repository: nitrojs/nitro

Length of output: 267


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant methods directly from src/dev/server.ts.
sed -n '1,95p' src/dev/server.ts
printf '\n--- shutdown/reload/close methods ---\n'
sed -n '154,242p' src/dev/server.ts

# Locate all local close() paths in src/dev/server.ts.
printf '\n--- close/shutdown call sites in src/dev/server.ts ---\n'
rg -n -C 4 'close\(\)|`#shutdownWorker`\(|`#manager`\.close\(\)|onClose\(|onMessage\(' src/dev/server.ts

# Check any local source for exit/closure message handling.
printf '\n--- local shutdown/exit/onClose source ---\n'
rg -n -C 4 'event["'\'']?\s*[:=]\s*["'$"'"']exit["'\'']|message["'\'']?.*["'$"'"']exit["'\'']|send\(.*exit|emit\(.*exit|ipc\.onClose|\.onClose\(|cause.*close|close\(.*cause|send\("exit"|send('"'"'exit'"'"')" src | true

Repository: nitrojs/nitro

Length of output: 7874


🌐 Web query:

"cjk107" "env-runner" "0.1.16" "RunnerManager" "onClose"

💡 Result:

In the unjs/env-runner library (version 0.1.16), RunnerManager.onClose is a method used to register a listener that executes when the managed runner is closed [1]. Key details regarding RunnerManager and the onClose hook: 1. Method Purpose: RunnerManager allows for hot-reloading runners [2][3]. The onClose listener is used to observe the lifecycle event when a runner shuts down [1]. 2. Implementation: The library transitioned from simple callback properties to a multi-listener event pattern [4]. You can register a listener using the manager's onClose method: manager.onClose((runner, cause) => { console.log("Runner closed", cause); }); 3. Lifecycle Management: Both RunnerManager and the individual runners (e.g., NodeProcessEnvRunner) implement AsyncDisposable, supporting the JavaScript explicit resource management pattern (await using) [2][3]. When using this pattern, the runners are closed automatically when the scope terminates [2][3]. 4. Individual Runner Hooks: If you are configuring an individual runner rather than the manager, the onClose hook is defined within the hooks object (e.g., in NodeProcessEnvRunner options) as: hooks: { onClose: (runner, cause) => console.log("Closed", cause), } [2][3] The type definition for WorkerHooks confirms that onClose accepts the runner instance and an optional cause [5].

Citations:


🌐 Web query:

npm registry env-runner 0.1.16 tarball

💡 Result:

The npm package env-runner version 0.1.16 is a legitimate release available on the npm registry, published on July 2, 2026 [1]. It is part of the unjs ecosystem [1][2], designed as a generic environment runner for JavaScript runtimes (e.g., Node.js, Bun, Deno, and edge platforms) [1]. You can download the tarball for this specific version directly from the npm registry using the following URL pattern: https://registry.npmjs.org/env-runner/-/env-runner-0.1.16.tgz The package is maintained in the unjs/env-runner GitHub repository [2], and the release included enhancements such as runtime-native WebSocket upgrade proxying [3]. Public CDNs like jsDelivr also host the files for this version [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d)"
cd "$tmp"

python3 - <<'PY'
import urllib.request
import tarfile
import io
url = "https://registry.npmjs.org/env-runner/-/env-runner-0.1.16.tgz"
resp = urllib.request.urlopen(url, timeout=20)
data = resp.read()
with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tar:
    names = [n for n in tar.getnames() if n.endswith((".ts", ".js", ".mjs", ".cjs"))]
    for n in sorted(names):
        print(n)
PY

printf '\n--- candidate env-runner source excerpts ---\n'
for f in \
  package/src/manager/RunnerManager.ts \
  package/src/manager/RunnerManager.js \
  package/dist/RunnerManager.mjs \
  package/dist/RunnerManager.cjs \
  package/src/runners/node.ts \
  package/src/runners/node-worker.ts \
  package/src/runners/node-worker.node.ts \
  package/src/runners/node.worker.mjs \
  package/src/runner.node.ts
do
  if tarfile_exists=$(python3 - "import urllib.request,io,tarfile; t=tarfile.open(fileobj=io.BytesIO(urllib.request.urlopen('https://registry.npmjs.org/env-runner/-/env-runner-0.1.16.tgz',timeout=20).read()), mode='r:gz'); print('yes' if next((m for m in t.getnames() if m == '$f'), None) else 'no', file='', end='')" 2>/dev/null); tarfile_exists="$(tar xzf env-runner-0.1.16.tgz "$f" >/dev/null 2>&1 && echo yes || echo no)"; then
    printf '\n### %s\n' "$f"
    sed -n '1,260p' "$f"
    printf '\n--- focused matches ---\n'
    rg -n -C 4 'onClose|sendMessage|ready|close\(|event:\s*["'"'"']exit["'"'"']|message\?.*event|exit|runner' "$f" || true
  else
    printf 'missing: %s\n' "$f"
  fi
done

Repository: nitrojs/nitro

Length of output: 2437


🌐 Web query:

unjs env-runner RunnerManager reload onClose close RunnerManager source lines

💡 Result:

In the unjs/env-runner library, the RunnerManager acts as a proxy manager for hot-reloading runners [1][2][3]. Key details regarding your query: 1. Lifecycle Management: RunnerManager and its associated runners implement AsyncDisposable [1][4]. This allows them to be used with the await using syntax (explicit resource management) for automatic closure, or managed manually via their.close method [1][2]. 2. reload(runner): When you call manager.reload(newRunner), the previous runner instance is automatically closed by the manager [1][2]. 3. onClose: The onClose hook is a lifecycle callback provided to the runner configuration [1]. It is triggered when a runner is closed, receiving arguments such as the runner instance itself and the cause of closure [1][2]. 4. Source Code Location: The core logic for RunnerManager resides in src/manager.ts within the unjs/env-runner repository [5][3]. If you are implementing custom runners or integrating this manager (e.g., as seen in Nitro's development server implementation), you typically attach listeners to handle the runner's lifecycle events: manager.onClose((runner, cause) => { console.log("Closed", cause); }); The library ensures that messaging and requests are handled gracefully during these reloads, often queueing messages until a new runner is ready [1][2].

Citations:


🌐 Web query:

github.com cjk107/env-runner/src/manager.ts

💡 Result:

The file manager.ts is part of the unjs/env-runner library [1][2]. It defines the RunnerManager class, which serves as a proxy manager responsible for coordinating hot-reloads, message queuing, and listener forwarding [2][3]. Key characteristics of the RunnerManager defined in manager.ts include: - Hot-Reloading: It facilitates the replacement of environment runners. When a new runner is loaded via manager.reload(newRunner), the old runner is automatically closed [2][3]. - Proxying: It acts as a proxy for fetch and WebSocket requests, routing them to the currently active runner [2][3]. - Messaging: It supports bidirectional communication with the runner. Messages sent before the runner is ready are queued and delivered once the connection is established [2][3]. - Resource Management: The RunnerManager implements AsyncDisposable, allowing it to be used with the explicit resource management pattern (await using) for automatic cleanup [2][4]. - Integration: It is used in conjunction with specific environment runner implementations (e.g., NodeWorkerEnvRunner, NodeProcessEnvRunner) to manage their lifecycle and interaction within the broader environment-agnostic runner architecture [2][5].

Citations:


🌐 Web query:

git show cjk107/env-runner:src/manager.ts | sed -n '1,260p'

💡 Result:

The repository cjk107/env-runner does not appear to exist in public records as of August 4, 2026. However, there is a well-known project titled "env-runner" maintained by the unjs organization (available at github.com/unjs/env-runner) [1][2]. The unjs/env-runner project is a generic environment runner for JavaScript runtimes that supports running server applications across various environments, including Node.js worker threads, child processes, Bun, Deno, Cloudflare Workers, Vercel, and Netlify [1]. The file src/manager.ts in the unjs/env-runner repository contains the implementation of the RunnerManager class, which acts as a proxy manager for hot-reloading, message queueing, and listener forwarding across runner swaps [3][4]. If you intended to access a specific private or non-indexed repository, please verify the organization or username, as cjk107 is not currently associated with the official env-runner project [1][5].

Citations:


🌐 Web query:

env-runner 0.1.16 RunnerManager reload onClose close cause source lines

💡 Result:

In the context of the env-runner library (v0.1.16), the RunnerManager and runner interfaces provide mechanisms for handling lifecycle events, including worker closure and reloading [1][2]. The onClose hook is a lifecycle function used to observe when a runner or worker terminates [1][3]. Its definition is as follows: onClose?: (worker: EnvRunner, cause?: unknown) => void; When a runner is closed, it may receive an optional cause argument, which typically captures the reason for the closure—such as an error, a shutdown command, or an unexpected exit [1][4][5]. Key technical aspects include: 1. RunnerManager.reload: When you call manager.reload(newRunner), the RunnerManager automatically handles the closure of the existing (old) runner [6][2]. 2. Cause Propagation: When a runner is terminated (e.g., via a graceful shutdown or an internal error), the cause of this closure is passed to the onClose hook [3][4]. In implementations like Nitro's dev server, this cause is often used for logging or to trigger automatic restarts if the worker crashes unexpectedly [5]. 3. Implementation details: - The EnvRunner interface defines the structure for lifecycle hooks [1]. - If an error occurs during execution, it can be passed as the cause, allowing the manager or host process to inspect why the runner stopped [4][5]. - In environments where IPC is used, closure events (like "shutdown" messages) are processed to trigger these hooks before the underlying worker or process exits [4][7]. In summary, the onClose hook acts as a receiver for the termination cause, which allows the RunnerManager to differentiate between graceful reloads, manual shutdowns, and runtime failures [1][4][5].

Citations:


🌐 Web query:

nodejs worker thread isTerminated exit event onexit cause onClose

💡 Result:

In Node.js, the worker_threads module provides specific mechanisms to handle the termination and exit lifecycle of worker threads [1][2]. Termination Behavior When you call worker.terminate, the worker thread stops all JavaScript execution as soon as possible [1][2]. This is an asynchronous operation that returns a Promise, which resolves when the exit event is emitted [1][2]. If the worker was terminated via this method, the exit event will be emitted with an exitCode of 1 [1][2]. Key Events 1. 'exit' Event: This event is emitted once the worker has stopped [1][3]. It is the final event emitted by any Worker instance [1][2]. The exitCode parameter indicates how the worker stopped: if it called process.exit, it receives that specific code; if it was terminated via worker.terminate, the code is 1 [1][4]. 2. 'close' Event: While the Worker class does not explicitly list 'close' as a core event for the Worker instance in the same way some other Node.js handles do (it is distinct from MessagePort.close or other stream-like closing events), the 'exit' event acts as the definitive indicator that the worker thread has completely ceased operations [1][2]. Important Considerations - Thread Shutdown: Calling worker.terminate stops the thread abruptly. If you require the worker to perform cleanup (e.g., closing database connections or file handles), you should design your architecture to send a "shutdown" message to the worker via postMessage, allowing the worker to perform its own cleanup and exit gracefully using process.exit [5]. - No "isTerminated" Property: There is no built-in property named isTerminated on the Worker instance. To track the state, you must listen to the 'exit' event [1][2]. - Errors: If an uncaught exception occurs within the worker, an 'error' event is emitted and the worker is automatically terminated [2][6]. In summary, use the 'exit' event to detect when a worker has stopped, and check the exitCode to determine if it was a normal exit or a termination [1][2]. If you need a worker to perform cleanup tasks before stopping, do not rely solely on worker.terminate; instead, use message passing to trigger an orderly shutdown [5].

Citations:


Preserve shutdown ordering between listener cleanup and manager close.

#shutdownWorker() resolves when event: "exit" arrives, then clears #shuttingDown, but local close() still calls #manager.close() afterward. If RunnerManager emits its registered onClose listeners as part of that close, this method can report an intentional shutdown as a failure and call reload() unnecessarily. Keep the runner closing after the listeners are cleared, or keep #shuttingDown true until manager cleanup is complete.

Also applies to lines 217-241.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dev/server.ts` around lines 82 - 84, Update the shutdown flow in
`#shutdownWorker`() and local close() so RunnerManager cleanup occurs before
`#shuttingDown` is cleared, or keep that flag set through manager.close(). Ensure
registered onClose listeners observe the intentional-shutdown state and do not
call reload(), while preserving listener cleanup ordering.

Comment thread src/dev/server.ts
}

async close() {
await this.#shutdownWorker();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Block reloads when close starts.

close() waits for shutdown before it closes #watcher. A file change during that wait can schedule reload(). #reloadPromise does not serialize with close(). The queued reload can call #manager.reload(runner) while close() calls #manager.close().

Set a permanent closing state before Line 155. Reject new and queued reloads. Await an active reload before closing the manager.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dev/server.ts` at line 155, Update close() before the await
this.#shutdownWorker() call to set a permanent closing state, reject both new
and already queued reload() operations, and await any active reload represented
by `#reloadPromise` before invoking `#manager.close`(). Ensure reload cannot call
`#manager.reload` after closing begins, while preserving the existing shutdown and
watcher-close sequence.

Comment thread src/dev/server.ts
Comment on lines +235 to +237
} catch {
done();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Log shutdown message delivery failures.

Capture the send error and write a warning with the shutdown operation and error message. The current catch block silently continues after a recoverable failure.

As per coding guidelines, “Prefer explicit errors over silent failures; use warnings for recoverable situations; throw for invalid states” and “Include actionable context in error messages.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dev/server.ts` around lines 235 - 237, Update the catch block around the
shutdown message delivery to capture the send error and issue a warning
containing the shutdown operation context and error message, then continue
calling done() as before.

Source: Coding guidelines

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.

Close Hook not awaited

1 participant