Skip to content

Conversation

@harlan-zw
Copy link
Collaborator

@harlan-zw harlan-zw commented Dec 24, 2025

Branch to track Unhead v3 that will introduce support for streaming and numerous performance and DX improvmeents.

PRs

Performance

Bundle

Build v3 size main size Δ size v3 gz main gz Δ gz
client 10254 11513 -1259 (-10.9%) 4235 4769 -534 (-11.2%)
server 9894 10361 -467 (-4.5%) 4060 4254 -194 (-4.6%)
vueClient 11323 12567 -1244 (-9.9%) 4676 5209 -533 (-10.2%)
vueServer 10849 11312 -463 (-4.1%) 4460 4651 -191 (-4.1%)

✨ 11.2% smaller client

Bench

e2e benchmark

Suite main (hz) v3 (hz) main v3 Δ ms Δ %
@unhead/vue 9,891 13,878 0.106ms 0.072ms -0.034ms 32% faster
bench 11,333 13,758 0.088ms 0.073ms -0.015ms 17% faster

simple benchmark

Suite main (avg hz) v3 (avg hz) main mean v3 mean Δ ms Δ %
@unhead/vue 113,881 153,648 0.0088ms 0.0065ms -0.0023ms 26% faster
bench 110,724 154,459 0.0090ms 0.0065ms -0.0025ms 28% faster

✨ ~32% faster for mid-size site

@harlan-zw harlan-zw changed the title chore: release v3.0.0-beta.1 v3.0.0 Dec 24, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Dec 24, 2025

Bundle Size Analysis

Bundle Size Gzipped
Client (Minimal) 11.2 kB → 10.1 kB 🟢 1.2 kB 4.7 kB → 4.2 kB 🟢 0.5 kB
Server (Minimal) 10.1 kB → 9.7 kB 🟢 0.5 kB 4.2 kB → 4 kB 🟢 0.2 kB
Vue Client (Minimal) 12.3 kB → 11.1 kB 🟢 1.2 kB 5.1 kB → 4.6 kB 🟢 0.5 kB
Vue Server (Minimal) 11 kB → 10.6 kB 🟢 0.5 kB 4.5 kB → 4.4 kB 🟢 0.2 kB

* fix(unhead)!: sync resolve tag engine

* chore: clean up

* chore: fix types
* perf(unhead)!: composable `resolveTags()`

* chore: tidy up

* chore: tidy up

* perf: reduce codde
* fix!: drop deprecations

* chore: sync

* chore: sync

* chore: progress

* chore: progress
* perf(unhead): client only capo sorting

* chore: build

* chore: exports

* chore: capo

* chore: simplify
* feat: `useHead()` type narrowing

* chore: types

* chore: types
* fix!: sync `renderDOMHead`

* doc: missing
* fix!: sync `renderDOMHead`

* doc: missing

* fix!: sync `renderSSRHead()`

* chore: conflict
* fix!: sync `renderDOMHead`

* doc: missing

* fix!: sync `renderSSRHead()`

* chore: conflict

* fix!: pluggable render functions

* fix!: pluggable render functions
* fix: switch to `HookableCore`

* fix: pure unhead core

* chore: test
Comment on lines +54 to +87
app.use('/{*path}', async (req, res) => {
try {
const url = req.originalUrl

let template, render
if (!isProd) {
template = fs.readFileSync(resolve('index.html'), 'utf-8')
template = await vite.transformIndexHtml(url, template)
render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render
}
else {
template = indexProd
render = (await import('./dist/server/entry-server.js')).render
}

const stream = render(url, template)

res.status(200).set({ 'Content-Type': 'text/html; charset=utf-8' })
const reader = stream.getReader()

while (true) {
const { done, value } = await reader.read()
if (done) break
if (res.closed) break
res.write(value)
}
res.end()
}
catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a file system access
, but is not rate-limited.

Copilot Autofix

AI 11 days ago

In general, this should be fixed by adding a rate‑limiting middleware to the Express app so that the expensive SSR route cannot be invoked arbitrarily often. A standard way is to use a well‑known package like express-rate-limit, configure a reasonable window and maximum request count, and apply it to the relevant route (or globally, if appropriate).

For this file, the minimal change without altering existing behavior is:

  1. Import express-rate-limit at the top of examples/vite-ssr-solidjs-streaming/server.js, alongside the existing imports.
  2. Define a limiter (e.g., 100 requests per 15 minutes per IP, as in the example) after creating app.
  3. Apply the limiter only to the expensive catch‑all route by adding it as middleware in app.use('/{*path}', limiter, async (req, res) => { ... }). This way, dev/prod logic, SSR behavior, and existing middleware order remain unchanged; only the number of requests per client to this route is constrained.

Concretely:

  • Add import rateLimit from 'express-rate-limit' after the existing express import.
  • After const app = express(), define const ssrLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }).
  • Change the route at line 54 from app.use('/{*path}', async (req, res) => { ... }) to app.use('/{*path}', ssrLimiter, async (req, res) => { ... }).
Suggested changeset 2
examples/vite-ssr-solidjs-streaming/server.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/vite-ssr-solidjs-streaming/server.js b/examples/vite-ssr-solidjs-streaming/server.js
--- a/examples/vite-ssr-solidjs-streaming/server.js
+++ b/examples/vite-ssr-solidjs-streaming/server.js
@@ -3,6 +3,7 @@
 import path from 'node:path'
 import { fileURLToPath } from 'node:url'
 import express from 'express'
+import rateLimit from 'express-rate-limit'
 
 const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD
 
@@ -19,6 +20,10 @@
     : ''
 
   const app = express()
+  const ssrLimiter = rateLimit({
+    windowMs: 15 * 60 * 1000,
+    max: 100,
+  })
 
   /** @type {import('vite').ViteDevServer} */
   let vite
@@ -51,7 +56,7 @@
     )
   }
 
-  app.use('/{*path}', async (req, res) => {
+  app.use('/{*path}', ssrLimiter, async (req, res) => {
     try {
       const url = req.originalUrl
 
EOF
@@ -3,6 +3,7 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import express from 'express'
import rateLimit from 'express-rate-limit'

const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD

@@ -19,6 +20,10 @@
: ''

const app = express()
const ssrLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
})

/** @type {import('vite').ViteDevServer} */
let vite
@@ -51,7 +56,7 @@
)
}

app.use('/{*path}', async (req, res) => {
app.use('/{*path}', ssrLimiter, async (req, res) => {
try {
const url = req.originalUrl

examples/vite-ssr-solidjs-streaming/package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/vite-ssr-solidjs-streaming/package.json b/examples/vite-ssr-solidjs-streaming/package.json
--- a/examples/vite-ssr-solidjs-streaming/package.json
+++ b/examples/vite-ssr-solidjs-streaming/package.json
@@ -16,7 +16,8 @@
     "@unhead/solid-js": "workspace:*",
     "compression": "^1.8.1",
     "express": "^5.2.1",
-    "solid-js": "^1.9.10"
+    "solid-js": "^1.9.10",
+    "express-rate-limit": "^8.2.1"
   },
   "devDependencies": {
     "@playwright/test": "^1.57.0",
EOF
@@ -16,7 +16,8 @@
"@unhead/solid-js": "workspace:*",
"compression": "^1.8.1",
"express": "^5.2.1",
"solid-js": "^1.9.10"
"solid-js": "^1.9.10",
"express-rate-limit": "^8.2.1"
},
"devDependencies": {
"@playwright/test": "^1.57.0",
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 8.2.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +54 to +87
app.use('/{*path}', async (req, res) => {
try {
const url = req.originalUrl

let template, render
if (!isProd) {
template = fs.readFileSync(resolve('index.html'), 'utf-8')
template = await vite.transformIndexHtml(url, template)
render = (await vite.ssrLoadModule('/src/entry-server.ts')).render
}
else {
template = indexProd
render = (await import('./dist/server/entry-server.js')).render
}

const stream = await render(url, template)

res.status(200).set({ 'Content-Type': 'text/html; charset=utf-8' })
const reader = stream.getReader()

while (true) {
const { done, value } = await reader.read()
if (done) break
if (res.closed) break
res.write(value)
}
res.end()
}
catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a file system access
, but is not rate-limited.

Copilot Autofix

AI 11 days ago

To fix this, introduce a rate-limiting middleware using a well-known library such as express-rate-limit, and apply it specifically to the expensive route at app.use('/{*path}', ...) (or globally if desired). This controls how many requests a client can make in a time window, mitigating denial-of-service risks from repeated filesystem and SSR operations.

The best minimal-impact change within the shown file is:

  1. Import express-rate-limit at the top of examples/vite-ssr-svelte-streaming/server.js.
  2. Define a limiter instance (e.g., 100 requests per 15 minutes per IP) near where the Express app is created, after const app = express().
  3. Apply this limiter to the expensive route by passing it as middleware before the existing async handler, i.e., change app.use('/{*path}', async (req, res) => { ... }) to app.use('/{*path}', limiter, async (req, res) => { ... }).

This preserves existing functionality and structure; only the addition of the middleware and its import are needed.

Suggested changeset 2
examples/vite-ssr-svelte-streaming/server.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/vite-ssr-svelte-streaming/server.js b/examples/vite-ssr-svelte-streaming/server.js
--- a/examples/vite-ssr-svelte-streaming/server.js
+++ b/examples/vite-ssr-svelte-streaming/server.js
@@ -3,6 +3,7 @@
 import path from 'node:path'
 import { fileURLToPath } from 'node:url'
 import express from 'express'
+import rateLimit from 'express-rate-limit'
 
 const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD
 
@@ -20,6 +21,11 @@
 
   const app = express()
 
+  const ssrLimiter = rateLimit({
+    windowMs: 15 * 60 * 1000, // 15 minutes
+    max: 100, // limit each IP to 100 SSR requests per window
+  })
+
   /** @type {import('vite').ViteDevServer} */
   let vite
   if (!isProd) {
@@ -51,7 +57,7 @@
     )
   }
 
-  app.use('/{*path}', async (req, res) => {
+  app.use('/{*path}', ssrLimiter, async (req, res) => {
     try {
       const url = req.originalUrl
 
EOF
@@ -3,6 +3,7 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import express from 'express'
import rateLimit from 'express-rate-limit'

const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD

@@ -20,6 +21,11 @@

const app = express()

const ssrLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 SSR requests per window
})

/** @type {import('vite').ViteDevServer} */
let vite
if (!isProd) {
@@ -51,7 +57,7 @@
)
}

app.use('/{*path}', async (req, res) => {
app.use('/{*path}', ssrLimiter, async (req, res) => {
try {
const url = req.originalUrl

examples/vite-ssr-svelte-streaming/package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/vite-ssr-svelte-streaming/package.json b/examples/vite-ssr-svelte-streaming/package.json
--- a/examples/vite-ssr-svelte-streaming/package.json
+++ b/examples/vite-ssr-svelte-streaming/package.json
@@ -15,7 +15,8 @@
     "@unhead/svelte": "workspace:*",
     "compression": "^1.8.1",
     "express": "^5.2.1",
-    "sirv": "^3.0.2"
+    "sirv": "^3.0.2",
+    "express-rate-limit": "^8.2.1"
   },
   "devDependencies": {
     "@playwright/test": "^1.57.0",
EOF
@@ -15,7 +15,8 @@
"@unhead/svelte": "workspace:*",
"compression": "^1.8.1",
"express": "^5.2.1",
"sirv": "^3.0.2"
"sirv": "^3.0.2",
"express-rate-limit": "^8.2.1"
},
"devDependencies": {
"@playwright/test": "^1.57.0",
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 8.2.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)

Check warning

Code scanning / CodeQL

Exception text reinterpreted as HTML Medium

Exception text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI 11 days ago

In general, never send raw exception messages or stack traces directly to the browser as HTML. For user-facing responses, either (a) send a generic error page/message that does not include user input, or (b) if you must include details, escape/encode the text appropriately (for example, HTML-escape it, or serve it with a safe content type like text/plain).

For this specific code, the safest and simplest change without altering core functionality is:

  • Stop writing e.stack directly as HTML.
  • Instead, respond with a generic error message to the client (for example, Internal Server Error) and continue logging the full stack trace to the server console.
  • Optionally, if you still want to expose some detail (e.g., in dev), you can encode the stack trace before sending it or change the content type to text/plain. Since we should not change surrounding behavior more than necessary and avoid adding new configuration toggles, the minimal secure fix is to replace res.status(500).end(e.stack) with a generic constant string response such as res.status(500).end('Internal Server Error').

Concretely:

  • In examples/vite-ssr-solidjs-streaming/server.js, within the catch (e) block around line 82, keep vite && vite.ssrFixStacktrace(e) and console.log(e.stack) as-is, but change the final response so it no longer includes e.stack. No new imports or helper functions are required.
Suggested changeset 1
examples/vite-ssr-solidjs-streaming/server.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/vite-ssr-solidjs-streaming/server.js b/examples/vite-ssr-solidjs-streaming/server.js
--- a/examples/vite-ssr-solidjs-streaming/server.js
+++ b/examples/vite-ssr-solidjs-streaming/server.js
@@ -82,7 +82,7 @@
     catch (e) {
       vite && vite.ssrFixStacktrace(e)
       console.log(e.stack)
-      res.status(500).end(e.stack)
+      res.status(500).end('Internal Server Error')
     }
   })
 
EOF
@@ -82,7 +82,7 @@
catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
res.status(500).end('Internal Server Error')
}
})

Copilot is powered by AI and may make mistakes. Always verify output.
catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)

Check warning

Code scanning / CodeQL

Information exposure through a stack trace Medium

This information exposed to the user depends on
stack trace information
.

Copilot Autofix

AI 11 days ago

In general, the fix is to avoid sending the stack trace (or detailed error object) to the client, and instead log it on the server while returning a generic error message and status code to the user. This preserves debuggability without exposing internal implementation details.

Specifically for this file, we should:

  • Keep server-side logging (console.log or similar) of the full error/stack trace.
  • Replace res.status(500).end(e.stack) with a generic message like res.status(500).end('Internal Server Error') (or localized/alternative wording) that does not depend on e or e.stack.
  • Optionally slightly improve logging by logging both the error and its stack, but without changing behavior observed by the client.

All required functionality can be implemented within examples/vite-ssr-solidjs-streaming/server.js without new imports. The only change is in the catch block of the app.use('/{*path}', ...) handler around lines 82–86.

Suggested changeset 1
examples/vite-ssr-solidjs-streaming/server.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/vite-ssr-solidjs-streaming/server.js b/examples/vite-ssr-solidjs-streaming/server.js
--- a/examples/vite-ssr-solidjs-streaming/server.js
+++ b/examples/vite-ssr-solidjs-streaming/server.js
@@ -82,7 +82,7 @@
     catch (e) {
       vite && vite.ssrFixStacktrace(e)
       console.log(e.stack)
-      res.status(500).end(e.stack)
+      res.status(500).end('Internal Server Error')
     }
   })
 
EOF
@@ -82,7 +82,7 @@
catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
res.status(500).end('Internal Server Error')
}
})

Copilot is powered by AI and may make mistakes. Always verify output.
catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)

Check warning

Code scanning / CodeQL

Exception text reinterpreted as HTML Medium

Exception text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI 11 days ago

In general, to fix this class of issue you should avoid sending raw exception messages or stack traces directly in an HTML response. For production, it’s safer to send a generic error page or message that contains no user-controllable data. For development, if you really need to expose the error content to the browser, you must HTML‑escape (encode) it so that any <, >, &, quotes, etc. are rendered as text instead of being interpreted as HTML or JavaScript.

The best fix here without changing existing functionality too much is:

  • In the catch block, keep logging the full stack trace to the server console (console.log(e.stack)).
  • Change the HTTP response so that it no longer sends e.stack directly. Instead:
    • In production (isProd === true), send a generic error message (e.g., "Internal Server Error").
    • In non‑production, either:
      • send an HTML‑escaped version of e.stack, or
      • for maximum simplicity and safety, also send a generic message.
  • Implement a small HTML-escaping helper within this file if you want to preserve error content in dev. This avoids adding new dependencies and keeps the change localized.

Concretely, in examples/vite-ssr-svelte-streaming/server.js:

  • Add a simple escapeHtml helper function near the top of the file.
  • Replace res.status(500).end(e.stack) with something like:
    • const msg = isProd ? 'Internal Server Error' : escapeHtml(String(e.stack || e));
    • res.status(500).set({ 'Content-Type': 'text/html; charset=utf-8' }).end(msg);
  • Make sure the catch block doesn’t introduce any new tainted pathway (i.e., only escapes or discards exception content).
Suggested changeset 1
examples/vite-ssr-svelte-streaming/server.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/vite-ssr-svelte-streaming/server.js b/examples/vite-ssr-svelte-streaming/server.js
--- a/examples/vite-ssr-svelte-streaming/server.js
+++ b/examples/vite-ssr-svelte-streaming/server.js
@@ -6,6 +6,15 @@
 
 const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD
 
+function escapeHtml(str) {
+  return String(str)
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;')
+}
+
 export async function createServer(
   root = process.cwd(),
   isProd = process.env.NODE_ENV === 'production',
@@ -81,8 +90,14 @@
     }
     catch (e) {
       vite && vite.ssrFixStacktrace(e)
-      console.log(e.stack)
-      res.status(500).end(e.stack)
+      console.log(e && e.stack ? e.stack : e)
+      const message = isProd
+        ? 'Internal Server Error'
+        : escapeHtml(e && e.stack ? e.stack : String(e))
+      res
+        .status(500)
+        .set({ 'Content-Type': 'text/html; charset=utf-8' })
+        .end(message)
     }
   })
 
EOF
@@ -6,6 +6,15 @@

const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD

function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}

export async function createServer(
root = process.cwd(),
isProd = process.env.NODE_ENV === 'production',
@@ -81,8 +90,14 @@
}
catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
console.log(e && e.stack ? e.stack : e)
const message = isProd
? 'Internal Server Error'
: escapeHtml(e && e.stack ? e.stack : String(e))
res
.status(500)
.set({ 'Content-Type': 'text/html; charset=utf-8' })
.end(message)
}
})

Copilot is powered by AI and may make mistakes. Always verify output.
catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)

Check warning

Code scanning / CodeQL

Information exposure through a stack trace Medium

This information exposed to the user depends on
stack trace information
.

Copilot Autofix

AI 11 days ago

In general, the fix is to stop sending full stack traces to the client and instead send a generic error message while logging the stack trace on the server. The server logs retain the detail needed for debugging without exposing it over HTTP.

For this specific file, the best minimal change is inside the catch (e) block of the app.use('/{*path}', ...) handler. We should:

  • Keep vite && vite.ssrFixStacktrace(e) as-is (it adjusts stack traces for server-side logging and debugging).
  • Keep logging the error stack to the server console (or potentially improve logging later), but not include it in the HTTP response.
  • Replace res.status(500).end(e.stack) with a response that sends a generic message such as "Internal Server Error" or "An error occurred" without any stack/implementation details.

No new imports or external libraries are strictly required. All changes are localized to the existing catch block in examples/vite-ssr-svelte-streaming/server.js, around lines 82–86.

Suggested changeset 1
examples/vite-ssr-svelte-streaming/server.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/vite-ssr-svelte-streaming/server.js b/examples/vite-ssr-svelte-streaming/server.js
--- a/examples/vite-ssr-svelte-streaming/server.js
+++ b/examples/vite-ssr-svelte-streaming/server.js
@@ -82,7 +82,7 @@
     catch (e) {
       vite && vite.ssrFixStacktrace(e)
       console.log(e.stack)
-      res.status(500).end(e.stack)
+      res.status(500).end('Internal Server Error')
     }
   })
 
EOF
@@ -82,7 +82,7 @@
catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
res.status(500).end('Internal Server Error')
}
})

Copilot is powered by AI and may make mistakes. Always verify output.
@harlan-zw harlan-zw marked this pull request as ready for review January 5, 2026 13:39
harlan-zw and others added 11 commits January 6, 2026 13:14
Fixes issue where patches called before first render (e.g., in Vue's onMounted)
were lost due to timing between patch and render.

Changes:
- Replace `_dirty` flag with cache invalidation via `delete entry._tags`
- Defer client-side patches using `_pending` until next render cycle
- Track `_originalInput` for SSR class side-effect pre-registration
- Add streaming client parity for hydration tracking
* chore: progress

* perf(client): reduce bundle size by ~30 bytes gzip

- Minify internal property names (_t, _e, _p, _s for DOM state)
- Simplify conditional logic and reduce code duplication
- Fix unused regex capturing group in dedupe.ts
- Update test to use renamed internal field
…636)

When `patch()` is called during or after a render cycle, the render may
complete and set `dirty = false` before the debounced render fires.
This caused the debounced render to skip processing `_pending` patches.

The fix checks for pending patches as a fallback when `dirty` is false,
ensuring reactive updates are always rendered.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
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