Skip to content

⚡ Bolt: [performance improvement] Refactor preview server to use asynchronous I/O#89

Merged
iotserver24 merged 1 commit into
mainfrom
bolt-async-preview-server-11167938262434567867
May 16, 2026
Merged

⚡ Bolt: [performance improvement] Refactor preview server to use asynchronous I/O#89
iotserver24 merged 1 commit into
mainfrom
bolt-async-preview-server-11167938262434567867

Conversation

@iotserver24
Copy link
Copy Markdown
Owner

@iotserver24 iotserver24 commented May 15, 2026

💡 What: Refactored packages/desktop/src/main/preview-server.ts to replace synchronous file operations (fs.existsSync, fs.statSync, fs.readFileSync) with their asynchronous counterparts (fs.promises.access, fs.promises.stat, fs.createReadStream).

🎯 Why: The preview server runs in the Electron main process. Using synchronous operations blocks the main Node.js event loop. This leads to poor concurrency, hangs, and UI micro-stuttering, especially when serving large or numerous static assets (like JS bundles, videos, or images). Also, reading large files into memory using fs.readFileSync creates unnecessary memory overhead.

📊 Impact:

  • Eliminates event loop blocking during static file serving in the preview window.
  • Decreases memory consumption by converting full-file buffers to streamed responses using fs.createReadStream().pipe().
  • Unblocks concurrent requests when loading complex frontend assets with many sub-dependencies.

🔬 Measurement:
Run the desktop app (pnpm dev in packages/desktop), start a preview server for a large build output folder containing numerous JS chunks or large media files. Open the Network tab and observe the concurrent requests no longer stalling each other. CPU and Memory profiling in DevTools will show a smoothed out event loop with fewer large memory spikes during file access.


PR created automatically by Jules for task 11167938262434567867 started by @iotserver24

Summary by CodeRabbit

  • Chores
    • Optimized preview server file handling for improved performance and responsiveness.

Review Change Stack

@google-labs-jules
Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

📝 Walkthrough

Walkthrough

Preview HTTP server refactored from synchronous to asynchronous file operations. A new existsAsync utility replaces fs.existsSync checks; the request handler becomes async and now streams file responses via fs.createReadStream instead of readFileSync, reducing event-loop blocking in the Electron main process.

Changes

Electron Preview Server Async File Operations

Layer / File(s) Summary
Async file existence utility
packages/desktop/src/main/preview-server.ts
New existsAsync(p) helper returns a boolean by awaiting fs.promises.access, converting access errors to false.
Request handler path resolution with async checks
packages/desktop/src/main/preview-server.ts
Request handler becomes async and uses await existsAsync to resolve requested file paths, try .html fallbacks, and asynchronously check for index.html inside directories before serving or returning 404/403.
Streamed file response with error handling
packages/desktop/src/main/preview-server.ts
File serving replaces readFileSync with fs.createReadStream, wiring open to set headers, error to send 500 when headers not yet sent, and pipe to stream the response body.
Developer guidance on async patterns
.jules/bolt.md
Guidance note documents that synchronous fs methods block the Node.js event loop in Electron main process and recommends async fs.promises and stream piping for HTTP responses.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 The server hops from sync to streams,
No more blocking the event loop dreams,
Async/await keeps the main thread free,
While createReadStream flows gracefully,
Performance gains for this electron spree!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the refactoring details and impact, but is missing the required template sections (Type of Change checkboxes, Testing checklist, and Checklist items). Add the missing template sections including Type of Change (mark Refactoring), Testing checkboxes for unit/E2E/TypeScript, and standard code review checklist items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: refactoring the preview server to use asynchronous I/O, which directly matches the changeset modifications.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 bolt-async-preview-server-11167938262434567867

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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.

@iotserver24 iotserver24 marked this pull request as ready for review May 16, 2026 01:36
Copilot AI review requested due to automatic review settings May 16, 2026 01:36
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors the Electron main-process preview server to avoid blocking synchronous filesystem calls while serving static assets, improving responsiveness and concurrency when loading preview content.

Changes:

  • Replaced existsSync/statSync checks with async equivalents (fs.promises.access/fs.promises.stat).
  • Switched from readFileSync responses to streaming files via fs.createReadStream().pipe(res) to reduce memory overhead.
  • Documented the “avoid sync fs in Electron main process” performance learning in .jules/bolt.md.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
packages/desktop/src/main/preview-server.ts Converts preview server filesystem operations to async + stream-based responses.
.jules/bolt.md Adds a Bolt learning entry about avoiding sync fs calls in Electron main process code paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +82 to +86
private async handleRequest(req: any, res: any): Promise<void> {
const urlPath = req.url.split('?')[0];
let filePath = path.join(this.rootDir, urlPath === '/' ? 'index.html' : urlPath);

if (!fs.existsSync(filePath)) {
// ⚡ Bolt: Replace synchronous file operations with async promises to prevent blocking the main thread
Comment on lines +82 to 85
private async handleRequest(req: any, res: any): Promise<void> {
const urlPath = req.url.split('?')[0];
let filePath = path.join(this.rootDir, urlPath === '/' ? 'index.html' : urlPath);

Comment on lines 129 to +133
'Content-Type': contentType,
'Cache-Control': 'no-cache',
});
res.end(content);
} catch {
res.writeHead(500);
res.end('Failed to read file');
}
});
readStream.pipe(res);
Comment thread .jules/bolt.md
@@ -61,3 +61,6 @@
## 2026-05-19 - Top-Level Interval Anti-Pattern (SpinnerVerb)
**Learning:** Storing a slow interval state like `spinnerVerb` (every 2.4s) at the top-level `App` component still forces a full application tree O(N) cascade re-render every tick. Even if it is not as fast as a 250ms timer, it causes noticeable micro-stuttering during UI interactions and when large lists are rendered because it triggers diffs across the entire application including Heavy `ChatPanel` and `TabbedRightPanel`.
**Action:** Always extract even slow interval-driven decorative states into isolated leaf components (like `SpinnerVerbDisplay`) wrapped in `React.memo()`. Removing the prop-drilling entirely isolates the re-renders specifically to the UI span rendering the changing string.
Copy link
Copy Markdown

@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: 2

🤖 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 `@packages/desktop/src/main/preview-server.ts`:
- Line 82: The async method handleRequest can reject and its caller currently
invokes it without awaiting or catching the returned Promise; update either the
caller or handleRequest so rejections are always handled: either (A) change the
caller that calls handleRequest to await it or call handleRequest(...).catch(err
=> { send an error response and log the error }) or (B) add a top-level
try/catch inside handleRequest to catch any errors and send an appropriate HTTP
response (and log) before returning; reference the handleRequest function and
the code path where it is invoked so you handle all promise rejections and
ensure the response is always sent.
- Around line 121-126: The readStream error handler currently always calls
res.end('Failed to read file'), which corrupts binary responses when headers
have already been sent; update the readStream.on('error') handler (the listener
attached to readStream) to check res.headersSent first: if headers have not been
sent, write a 500 status and call res.end with the error message, otherwise call
res.destroy() (or res.end() without writing additional body) to terminate the
connection; ensure the code references the existing readStream.on('error')
listener and uses res.headersSent, res.writeHead, and res.destroy to avoid
appending text to an in-progress binary response.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 788f4305-5ac9-4711-9e5f-c0b9939225df

📥 Commits

Reviewing files that changed from the base of the PR and between 0bb1903 and 90630ff.

📒 Files selected for processing (2)
  • .jules/bolt.md
  • packages/desktop/src/main/preview-server.ts

}

private handleRequest(req: any, res: any): void {
private async handleRequest(req: any, res: any): Promise<void> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n packages/desktop/src/main/preview-server.ts | head -150

Repository: iotserver24/Xibecode

Length of output: 4504


Handle rejected promises from the async request handler.

Line 82 makes handleRequest asynchronous, but line 45 invokes it without awaiting or catching the returned promise. Any uncaught error in handleRequest becomes an unhandled promise rejection, leaving requests without a proper response.

Suggested fix
-      this.server = createServer((req, res) => {
-        this.handleRequest(req, res);
-      });
+      this.server = createServer((req, res) => {
+        void this.handleRequest(req, res).catch(() => {
+          if (!res.headersSent) {
+            res.writeHead(500);
+          }
+          res.end('Internal server error');
+        });
+      });
🤖 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 `@packages/desktop/src/main/preview-server.ts` at line 82, The async method
handleRequest can reject and its caller currently invokes it without awaiting or
catching the returned Promise; update either the caller or handleRequest so
rejections are always handled: either (A) change the caller that calls
handleRequest to await it or call handleRequest(...).catch(err => { send an
error response and log the error }) or (B) add a top-level try/catch inside
handleRequest to catch any errors and send an appropriate HTTP response (and
log) before returning; reference the handleRequest function and the code path
where it is invoked so you handle all promise rejections and ensure the response
is always sent.

Comment on lines +121 to +126
readStream.on('error', () => {
if (!res.headersSent) {
res.writeHead(500);
}
res.end('Failed to read file');
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -name "preview-server.ts" -type f

Repository: iotserver24/Xibecode

Length of output: 110


🏁 Script executed:

sed -n '115,135p' ./packages/desktop/src/main/preview-server.ts

Repository: iotserver24/Xibecode

Length of output: 681


🌐 Web query:

Node.js res.end() after headersSent true behavior

💡 Result:

In Node.js, calling res.end when headersSent is true is a standard and safe operation [1][2]. The res.end method is the required finalizer for an HTTP response; it signals to the server that all headers and body data have been sent and that the response is complete [3][4]. When res.end is called: - If headers have not yet been sent (headersSent is false), Node.js will automatically send them before finalizing the response [1][5]. - If headers have already been sent (headersSent is true), res.end simply completes the response stream [1][3]. The common error ERR_HTTP_HEADERS_SENT occurs not because of calling res.end when headers are already sent, but because of attempting to modify headers (e.g., using res.setHeader or res.writeHead) or sending additional response data after the response has already been finalized [6][7][2]. Best practices for handling this state include: - Checking res.headersSent before attempting to set headers or send error responses in asynchronous code paths [2][8]. - Ensuring that each request handler has exactly one response path to avoid attempting to send multiple responses [7][2]. - Using return statements after calling res.end or other response-sending methods to prevent subsequent code from executing and attempting to modify the already-sent response [1][2][9].

Citations:


🌐 Web query:

Node.js stream error handler res.destroy vs res.end when piping

💡 Result:

When piping streams in Node.js, the choice between res.destroy and res.end depends on the context of the error and the state of the response [1][2]. The.pipe method does not automatically propagate errors or clean up streams [3][4]. If an error occurs in a piped stream, the destination (such as an HTTP response) is often left hanging, keeping the socket open and the client waiting [1][5]. res.destroy(err) Use this when a fatal error occurs that prevents the response from completing successfully [6][5]. Calling.destroy immediately closes the underlying socket, which is appropriate if you cannot send a valid response body or if the stream is in an invalid state [6][5]. It signals that the connection should be terminated abruptly [5]. res.end Use this when you need to gracefully terminate the response, typically after you have already sent some data or when you want to send a final error message (e.g., a 500 status code) before closing [1][2]. If headers have not been sent, you can set an error status code and then call.end to inform the client of the failure [5]. Best Practice: Use stream.pipeline The modern, recommended approach is to use stream.pipeline instead of.pipe [3][7][4]. The pipeline utility automatically handles error propagation and cleanup [4]. When an error occurs, it automatically calls.destroy on all streams in the pipeline, preventing resource leaks and hanging sockets [4]. Example using pipeline: const { pipeline } = require('node:stream'); pipeline(sourceStream, res, (err) => { if (err) { console.error('Pipeline failed:', err); // If headers haven't been sent, you can send a 500 error if (!res.headersSent) { res.statusCode = 500; res.end('Internal Server Error'); } else { // Otherwise, destroy the response to close the connection res.destroy(err); } } }); In summary, prefer pipeline to avoid manual error handling [3][8]. If you must use.pipe, you are responsible for manually attaching error listeners to all streams and deciding whether to call.end (for a graceful, potentially error-informed close) or.destroy (for an immediate, forced termination) based on whether headers have already been sent [1][3][5].

Citations:


🏁 Script executed:

head -n 130 ./packages/desktop/src/main/preview-server.ts | tail -n 20 | cat -n

Repository: iotserver24/Xibecode

Length of output: 857


Move the error response body inside the headersSent check.

Line 125 unconditionally calls res.end('Failed to read file') even when headersSent is true. If the stream errors after piping begins (after the open event at line 127), this appends a string to the binary response, corrupting the downloaded file. When headers are already sent, destroy the response instead.

Suggested fix
    readStream.on('error', (err) => {
      if (!res.headersSent) {
        res.writeHead(500);
        res.end('Failed to read file');
        return;
      }
      res.destroy(err);
    });
📝 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
readStream.on('error', () => {
if (!res.headersSent) {
res.writeHead(500);
}
res.end('Failed to read file');
});
readStream.on('error', (err) => {
if (!res.headersSent) {
res.writeHead(500);
res.end('Failed to read file');
return;
}
res.destroy(err);
});
🤖 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 `@packages/desktop/src/main/preview-server.ts` around lines 121 - 126, The
readStream error handler currently always calls res.end('Failed to read file'),
which corrupts binary responses when headers have already been sent; update the
readStream.on('error') handler (the listener attached to readStream) to check
res.headersSent first: if headers have not been sent, write a 500 status and
call res.end with the error message, otherwise call res.destroy() (or res.end()
without writing additional body) to terminate the connection; ensure the code
references the existing readStream.on('error') listener and uses
res.headersSent, res.writeHead, and res.destroy to avoid appending text to an
in-progress binary response.

@iotserver24 iotserver24 merged commit 151c75e into main May 16, 2026
9 checks passed
@iotserver24 iotserver24 deleted the bolt-async-preview-server-11167938262434567867 branch May 16, 2026 01:40
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.

2 participants