Skip to content

fix(web): unwrap respond() on direct calls; make its envelope a Response - #3910

Merged
ryansolid merged 1 commit into
nextfrom
fix/server-function-unwraps-respond
Oct 8, 2026
Merged

ryansolid merged 1 commit into
nextfrom
fix/server-function-unwraps-respond

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Summary

respond() behaved differently depending on who called the server function, and its types described the object it builds rather than what callers receive. This fixes the runtime and the types together in @solidjs/web.

Found while moving the hackernews server-components example to GET (#3717): story.tsx returns respond(() => <View />, { headers: { "cache-control": "public, max-age=60" } }), and during SSR the page broke.

1. Direct calls unwrap the envelope

A server function called in-process during SSR (the direct leg in createServerReference) handed its caller the ResponseEnvelope itself, while an HTTP caller decoded value. For a server component wrapped for its headers, the envelope reached the render as a plain object: frameTransformDirectResult never branded it as a server component, and hydration serialization failed on it.

The direct leg now resolves with the envelope's value (a thrown envelope rejects or throws with it) before transformDirectResult runs. Of the envelope's metadata, only Set-Cookie is appended to the render's response head: a cookie is state the function established, and the browser must receive it whichever leg ran the call. The other headers describe the function's own address (a GET's Cache-Control is about that URL, not the page composed from it), and the status is the page's, so neither is applied to the document.

2. The envelope is a Response

ResponseEnvelope now extends Response, carrying the given response's body, status and headers (multiple Set-Cookie values preserved). A consumer that knows nothing about Solid, such as filesystem-routing's API dispatch (result instanceof Response ? result : Response.json(result)), now answers with it as-is instead of JSON-encoding the wrapper object. Solid's own consumers already check the isResponseEnvelope() brand before instanceof Response, so they're unaffected.

The subclass is built on first construction: an extends Response evaluated at module load would throw wherever the global is missing, and a top-level class with a prototype write would pin itself into every bundle importing the module.

3. The helpers are typed by what they mean to the caller

  • respond<T>(value, init) returns T. Every caller of the function returning it, over HTTP or in-process, receives the value.
  • redirect() and reload() are <T = never>(…): T. They're control flow for the integration, not values. never vanishes from the inferred return type's union, and where the context names a type (an annotated return, a Response-typed request handler), T takes it. A literal never return would also mark code after a bare call unreachable; a type parameter doesn't.

So a bare "use server" function returning respond(item) on one branch and redirect("/login") on another is typed Promise<Item>, with no wrapper narrowing needed (the router's NarrowResponse existed to do this after the fact). In normal use the client runtime already acts on redirect and revalidate responses: mutations deliver them to the registered flight consumers and resolve with the value. A raw Response only reaches code that reads them deliberately, such as a reading integration, which is typed against Response anyway. A hand-built new Response(...) stays Response-typed; wrappers that handle responses can filter it with Exclude<T, Response>.

Public API changes

  • Direct server-function calls that return or throw respond(value) now receive value instead of the ResponseEnvelope. Integrations that inspected the envelope on the direct leg (for example the router's query) no longer see it there.
  • During SSR, a directly called function's respond() Set-Cookie headers are appended to the page's response. Its other headers and its status are dropped on that leg.
  • ResponseEnvelope extends Response. new ResponseEnvelope(response, value) keeps its signature. envelope.response is now the envelope itself, or undefined when constructed without a response. instanceof Response is now true for envelopes.
  • respond() is typed T (was ResponseEnvelope<T>).
  • redirect() and reload() are typed <T = never>(…): T (were Response). Code that uses the result as a Response passes the type explicitly (reload<Response>(…)) or gets it from context.

Tests

  • New test/server/server-functions-direct-respond.spec.tsx (11 tests). It covers:

    • the envelope as a Response: brand, value, status, headers, cookies and JSON body; a bodiless 304; a plain fetch-style dispatcher; and both constructor forms;
    • the direct leg: async and sync returns, thrown envelopes, cookie-only forwarding with the status untouched, and frameTransformDirectResult branding respond(View).

    All 6 direct-leg tests failed before the fix.

  • New test/response-helpers.type-tests.ts. It covers:

    • respond's value type;
    • redirect/reload inferring never and accepting an explicit Response;
    • mixed redirect, reload and respond branches resolving to the value type;
    • annotated and Response-typed contexts;
    • GET(async () => respond(() => "view")) on client and server.
  • server-functions-outcome-digest.spec.tsx: one reload<Response>(…) where the test reads headers off the result.

  • Web server suite: 162 files, 1,535 passed. Web test type-check clean.

Follow-ups (not in this PR)

  • An unserializable async value during SSR with no onError throws from seroval's promise continuation, which hangs the render or kills the process instead of reaching an Errored boundary.
  • The router can drop NarrowResponse once it depends on this release.

A server function called in-process during SSR that returned
respond(value) handed its caller the ResponseEnvelope itself, while an
HTTP caller decoded the value. A server component wrapped for its headers
(respond(View, { headers })) reached the render as an object, so the
frames policy never branded it and hydration serialization failed on it.
The direct leg now resolves with the value (a thrown envelope rejects
with it) before transformDirectResult runs, and appends the envelope's
Set-Cookie headers to the render's response head; other headers and the
status are the function's address's and the page's, not the document's.

ResponseEnvelope now extends Response (built lazily, so the module stays
free of side effects and safe where the global is missing), carrying the
given response's body, status and headers. A fetch-style consumer with no
Solid knowledge, such as a filesystem router's API dispatch, answers with
it as-is instead of JSON-encoding the wrapper.

The helpers are typed by what they mean to the caller: respond<T>()
returns T, and redirect()/reload() are <T = never>(...): T, vanishing
from inferred unions while taking a type from context (annotated returns,
Response-typed handlers). A bare "use server" function's signature is its
callers' type, with no wrapper narrowing needed.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Oct 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5f9e777

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
test-integration Patch
todos-server-example Patch
@solidjs/compiler Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/universal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.43 KB 0 B 0 B +15 B 7.45 KB ✅
signals: + createStore 14.69 KB 0 B 0 B 0 B 14.70 KB ✅
signals: + isPending/latest 9.63 KB 0 B 0 B +15 B 9.65 KB ✅
app: render + one signal (the simple-app floor) 9.92 KB 0 B 0 B +15 B 9.93 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.89 KB 0 B 0 B +15 B 17.91 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.17 KB 0 B 0 B +55 B 29.19 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 12.92 KB 0 B 0 B +15 B 12.96 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.53 KB 0 B 0 B +15 B 14.53 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.85 KB 0 B 0 B +15 B 28.89 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.12 KB 0 B 0 B +15 B 10.13 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.22 KB 0 B 0 B +55 B 25.24 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.16 KB 0 B 0 B 0 B 31.17 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.11 KB 0 B 0 B 0 B 11.13 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.91 KB 0 B 0 B +161 B 33.92 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.20 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 37.61 KB 0 B 0 B +15 B 37.59 KB ⚠️ over by 20 B, 5 B minified headroom assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.19 KB, wire.js 0.93 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.15 KB 0 B 0 B +15 B 35.13 KB ⚠️ over by 16 B, 5 B minified headroom assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, regions.js 0.80 KB, sc-comments.js 0.20 KB, trace.js 8.18 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.65 KB 0 B 0 B +15 B 40.66 KB ✅ eager (counted): web.js 22.02 KB; assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.20 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 46.00 KB 0 B 0 B +15 B 46.02 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, server.js 1.02 KB, trace.js 8.19 KB, wire.js 0.93 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.33 KB 0 B 0 B +15 B 47.29 KB ⚠️ over by 36 B, 5 B minified headroom assets.js 0.78 KB, bind.js 1.84 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.81 KB, server.js 1.02 KB, trace.js 8.20 KB, wire.js 0.94 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.40 KB 0 B 0 B +4 B 20.42 KB ✅

⚠️ Over the brotli cap within the minified allowance (passes)

  • page: live server components (base + live/GET + action + isPending/latest): over brotli cap by 20 B; minified 117,456 B vs 117,441 B recorded with the cap (+15 B) — 5 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread): over brotli cap by 16 B; minified 109,461 B vs 109,446 B recorded with the cap (+15 B) — 5 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 36 B; minified 148,683 B vs 148,668 B recorded with the cap (+15 B) — 5 B of the 20 B minified allowance left; +0 B minified over this PR's base

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 37751642091

Coverage remained the same at 76.43%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1227
Covered Lines: 996
Line Coverage: 81.17%
Relevant Branches: 958
Covered Branches: 674
Branch Coverage: 70.35%
Branches in Coverage %: Yes
Coverage Strength: 28.37 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 8, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 188 untouched benchmarks


Comparing fix/server-function-unwraps-respond (5f9e777) with next (8d23a5a)

Open in CodSpeed

ryansolid added a commit that referenced this pull request Oct 8, 2026
The server-components twin's routes are GET reads that answer through
respond(View, cacheable): a minute of public HTTP caching, so a link
hover's preload is the response the click then reads from the browser's
cache. Both twins drop the routing dim: the current page stays up until
the next is ready.

Depends on respond() being typed as its value and envelopes unwrapping
on the direct leg (#3910).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit 49bed5c into next Oct 8, 2026
8 of 9 checks passed
ryansolid added a commit that referenced this pull request Oct 8, 2026
The server-components twin's routes are GET reads that answer through
respond(View, cacheable): a minute of public HTTP caching, so a link
hover's preload is the response the click then reads from the browser's
cache. Both twins drop the routing dim: the current page stays up until
the next is ready.

Depends on respond() being typed as its value and envelopes unwrapping
on the direct leg (#3910).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit that referenced this pull request Oct 9, 2026
The server-components twin's routes are GET reads that answer through
respond(View, cacheable): a minute of public HTTP caching, so a link
hover's preload is the response the click then reads from the browser's
cache. Both twins drop the routing dim: the current page stays up until
the next is ready.

Depends on respond() being typed as its value and envelopes unwrapping
on the direct leg (#3910).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit that referenced this pull request Oct 12, 2026
The server-components twin's routes are GET reads that answer through
respond(View, cacheable): a minute of public HTTP caching, so a link
hover's preload is the response the click then reads from the browser's
cache. Both twins drop the routing dim: the current page stays up until
the next is ready.

Depends on respond() being typed as its value and envelopes unwrapping
on the direct leg (#3910).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit that referenced this pull request Oct 12, 2026
The server-components twin's routes are GET reads that answer through
respond(View, cacheable): a minute of public HTTP caching, so a link
hover's preload is the response the click then reads from the browser's
cache. Both twins drop the routing dim: the current page stays up until
the next is ready.

Depends on respond() being typed as its value and envelopes unwrapping
on the direct leg (#3910).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit that referenced this pull request Oct 12, 2026
…3717)

* examples: hackernews collapse as a binding slot, twins in the authoring layout

The thread's collapse was a `Toggle` client component wrapping a template
slot. It is now a binding slot on server markup: the recursive `Comment` is
a server component, and each comment with replies calls
`props.toggle({ $key: c.id })` and binds the toggle's `open` class name,
its `onClick`, its label (a text position) and the replies' `display`. The
fill is the SPA twin's `Toggle` without its markup: a signal in the body,
getters over it, a plain handler. No client components remain.

Both twins take the authoring layout: each route file holds its query with
the server function inline (markup in one twin, data in the other), and
`src/server/hn.ts` is a plain module behind `import "server-only"`, with
the boundary types referenced from `vite-env.d.ts` (TypeScript 6 rejects
the undeclared side-effect import). `lib/`, `api.ts`, `views.tsx` and
`components/toggle.tsx` go. `CommentDefinition` gains `id` and
`StoryDefinition.id` is a number, as the data carries.

The READMEs lead with the collapse, name their coordinates, and correct the
bundle check (grep the client JavaScript; `app.css` carries both class
names). A server spec pins the class-array form at a binding position on
both faces.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: hackernews server routes, nav outside Loading, router next.31

The feeds and the user page have no client half, so they become server
routes: `serverRouteComponent(query(...))`, with the router making the
call from the match on navigation and on link hover, so they need no route
component and no `preload`. The feeds are one `/:type?` route filtered to
the five feed names, with `page` read through a hand-written search schema.
The story route keeps its component because of the collapse fill. The nav
leaves the `Loading` boundary, since it does no I/O and the shell can wait
on it. The SPA twin uses the same route pattern and filter, and parses
`?page` itself.

Router 2.0.0-next.29 sent a schema-less server route's args as
`{ params, search: undefined }`. The JSON argument check rejects that, so
client navigation to the user page silently never happened. next.30
fixed it (#615), so every example that uses the router moves to next.31.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: hackernews collapse as native details, navigation dim

The collapse only hides replies, which is what <details> does natively, so
the binding slot was a heavier way to do what the browser already does.
Both twins now render the same <details> with a CSS label swap: the server
twin's thread has no client code, its story route becomes a server route
like the feeds and user page, and the SPA's Toggle component is deleted.
The toggle styles move to the <summary> so font sizes don't compound with
nesting. Markup stays byte-identical between the twins.

Both roots dim the page while a navigation is pending (`useIsRouting()`),
with a short delay so a navigation that hover preloading already made fast
never flickers. The old page stays up instead of going blank.

The plan records the revision, and that the occlusion proof (content a
client component doesn't place ships once, as a record) has had no demo or
end-to-end test since the early HN demo's deep-collapsed replies were
dropped.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: notes in React's shape, end-to-end occlusion specs

The notes demo moves to the authoring layout and toward the React
original. The shell is a client component, as App.js is; only the
sidebar list and the note preview are server components. The server
shell had existed only to host the search field as a binding slot, and
with the shell on the client the field is a plain client component. The
editor's data is a plain query (getNoteEdit) instead of a server
component used as a data loader. router.tsx holds the routes, the root
preload and getNoteList, and server-config.ts passes that Router to the
flight collector, as in the fullstack template.

The excerpt now mounts only while expanded, as in the demo, so a
collapsed excerpt ships once as an sc:region record and expanding it
makes no request. The list is newest first and the excerpt is the
rendered markdown as plain text, both matching the demo; the excerpt
renders on the server, so marked stays in the lazy editor chunk only.

The occlusion specs prove the single-copy claim end to end. The server
spec checks each excerpt ships once (markup where placed, a record where
not) and that late placement is locked to records. The hydration spec
adopts the document with the network stubbed to throw, then expands,
collapses and re-expands, with each text on screen once.

The README, the grid plan and the principles doc are updated: the
notes search field is no longer a binding-slot example, and the
hackernews occlusion gap is closed.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: todos README names its coordinate, small cleanups

The README opens with the example's place on the grid: the client-owned,
request/response corner beside hackernews-spa, twin of todos-server, and
the baseline for the planned board example. It points at todos.ts as the
file to read.

The todos.ts header keeps its instruction to read the file for its
layering rather than as a list of React-to-Solid renames, but drops the
rebuke. The error map moves from module scope into createTodos, so each
instance has its own; the unused TodoActions type goes; the row checkbox
uses onChange like toggle-all. Vite 7 -> 8, as in the other grid
examples. The grid plan marks todos built.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: todos-server as forms with binding slots, router next.34, vite-plugin next.47

todos-server rebuilt around forms: every control is a form that works
without JavaScript (`.with()` binds a row's id), and the server answers
the page it was posted from in one round trip. Two binding slots, `row`
and `list`, add back the optimism a form cannot express (toggle-all,
clear-completed, the live count) from one intent store the actions'
onSubmit hooks write. Failures are returned `{ error }` values read
through useSubmissions; the no-JS path reads the same answers from the
router's flash cookie. The README is rewritten around that shape.

Dependencies across the examples:
- @solidjs/router 2.0.0-next.34 in notes, room, hackernews,
  hackernews-spa and todos-server (server-rendered `.with()` forms find
  the registered action; the flash decode runs once per request).
- @solidjs/vite-plugin 3.0.0-next.47 in all eleven, so they compile with
  the workspace compiler and babel plugin; next.47 is also the first to
  inject the flash-cookie secret. effect, migrating-element, rendering and
  sierpinski move to vite ^8, which next.47 requires (transformWithOxc).

The plan's verification note records V1-V4 holding and the two router
fixes they needed.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: router next.35

Actions now settle at the commit of their transition (#649): a form's
aria-busy, the onSettled hooks and the submission record release when the
result is on screen, not when the action body returns. Busy state is keyed
by the form's action URL, so it survives a server-component morph. Also
picks up the link-state fixes (aria-current compares the query, base path
on a segment boundary, non-ASCII paths).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: todos-server shows intent through aria-busy, not an optimistic store

The list no longer predicts outcomes. While a form's submission is in
flight the router marks it aria-busy, and app.css shows the request from
that alone: a pressed toggle, a dimmed row, and through :has() every row
toggle-all or clear-completed touches. A mark waiting over two seconds
pulses. The server's answer replaces the mark; the SPA twin keeps the
predicting version.

Failures are answers the server words (it knows the todo's title): the
actions return { error } with the message, and the route lists them under
the list with a retry, cleared when the next attempt at the same thing
settles. The fills, the binding-slot bindings and the optimistic store are
gone; the list has no client code.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: todos-server dismisses errors a later success answered

A failed toggle's error stayed up after mark-all completed the todo, or
after the todo was deleted. A settled attempt already replaced earlier
answers to the same question within one action; now a successful delete
also dismisses that todo's toggle errors, and a successful mark-all
dismisses every row's.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: todos-server README names the render-mode condition for no-JS

Without JavaScript the page has to arrive whole: streamed content that
settles after the first flush needs a script to swap in. The example's
reads land before the flush, which is timing, not a guarantee; an app
that must work without JavaScript renders in the vite plugin's async
mode. Also brings the toggle snippet in line with the markup
(aria-pressed).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: router next.37, server components mount through dynamicComponent

Every example moves to @solidjs/router 2.0.0-next.37, and the server
component mounts in chat, notes, room and todos-server use
dynamicComponent, the documented mount for a server component.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: hackernews routes answer cacheable GETs, no navigation dim

The server-components twin's routes are GET reads that answer through
respond(View, cacheable): a minute of public HTTP caching, so a link
hover's preload is the response the click then reads from the browser's
cache. Both twins drop the routing dim: the current page stays up until
the next is ready.

Depends on respond() being typed as its value and envelopes unwrapping
on the direct leg (#3910).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: router 2.0.0-next.38, vite-plugin 3.0.0-next.48

Link preloading is opt-in in next.38, so each example's router passes
intentPreload() and keeps the hover, focus, and touch preload it had.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: vite-plugin 3.0.0-next.49

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* examples: record the grid handoff

The apps that exist are as far as the runtime can take them. The plan
names what landed, what stays, and what is handed off, and the chat,
room, and migrating-element READMEs say the same.

Co-authored-by: Grok via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* test(web): occlusion spec makes the regions tier resident before hydrate

The spec predates #3860, which moved `{$frame}` slot regions into the lazily
loaded regions tier: a record naming a region waits for the tier, so the slot
fills never mounted and expanding item a rendered nothing. Load it before
hydrate (as welcome-status-parity does for trace), assert the document
announces it, and re-record the artifact, which predated the announcement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PDt8Jv6uf5xRDsrux4UwF

---------

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <noreply@anthropic.com>
@ryansolid
ryansolid deleted the fix/server-function-unwraps-respond branch October 12, 2026 18:20
ryansolid pushed a commit to brenelz/solid-router that referenced this pull request Oct 13, 2026
Moves the dev deps, lockfile and peer floor to rc.15 and updates the
tests that relied on rc.14 behaviour:

- claimElement is once per element since solidjs/solid#3924, so tests
  that simulate a morph re-claim now call claimElementTree (the sweep
  frames use), and the href re-claim test writes href through the
  compiler-owned setAttribute.
- respond() is typed as its value and redirect() defaults to never
  (solidjs/solid#3910), so two server specs and one hash-history spec
  annotate the Response they use.
- The solidjs#655 characterization's superseded-navigation timeline now lists
  data-pending attribute writes before the useLinkState render effects
  in the same flush, matching the single-navigation ordering.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R2FCNsvaipU3xXx9p83VUs
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