Skip to content

feat(web): a server function reference can declare its own client send - #4170

Merged
ryansolid merged 3 commits into
nextfrom
claude/project-thread-4achkk
Oct 20, 2026
Merged

ryansolid merged 3 commits into
nextfrom
claude/project-thread-4achkk

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Summary

Before: an integration that answers some server-function requests itself had to install itself as the transport fetch. @solidjs/prerender does this to serve prerendered calls from static artifacts. It and an app's own configureServerFunctionsClient({ fetch }) then replaced each other, depending on which ran last. In the common order, data modules evaluate prerendered() before the client entry configures its fetch, so the artifacts were silently bypassed.

After: a reference can declare its own send in its metadata, withMeta(fn, { fetch: (address, init, next) => ... }). The transport uses it for that function's requests, with the configured send (the app's fetch, or the global) as next. The intent lives on the declaration and the app's fetch stays the app's, so there is nothing to order and no new global hook.

How: createRequest already has the reference's metadata, so the send becomes (meta.fetch || send)(base, init, send). The configured send receives a trailing third argument, which it ignores, as the global fetch does. This is documented on the fetch option. ServerFunctionMetadata.fetch is typed and documented, and the send's shape is exported as ServerFunctionsSend.

Size: +19 B minified on the server-component page scenarios and 0 B elsewhere, within the gate's 20 B allowance. The first cut was a global wrapServerFunctionsFetch layer; it was replaced by this per-reference send on review.

How did you test this change?

  • New spec in test/server/server-functions-extensions.spec.tsx, with the reference's send declared before the app configures its fetch:
    • It runs for that reference's calls.
    • It passes live calls to the app's fetch through next.
    • It answers its own requests without reaching the app's send.
    • Other references are untouched.
  • packages/web:
    • Default and hydrate vitest configs: all pass.
    • Server config: all pass except server-functions-adapter-request "rebuild failure", which fails only on Node 22.22 locally and passes on CI's Node 24.
    • pnpm test-types passes.
  • scripts/size: head vs base is +19 B minified on SC pages only.

Consumer: solidjs/prerender-crawler#11 declares its artifact send this way. Its SSG example, built against rc.15 patched with this one-line change, navigates from artifacts only in a browser.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NkjGmX4hAoiU9VhXNt7j3E

…the app's fetch

An integration that answers some server-function requests itself (a
prerendered build's static artifacts) had to install itself as the
transport fetch, so it and an app's own configureServerFunctionsClient
({ fetch }) replaced each other depending on which ran last. A layer wraps
a send that reads the configured fetch at call time, so the order no
longer matters.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkjGmX4hAoiU9VhXNt7j3E
@changeset-bot

changeset-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 27b6f34

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

This PR includes changesets to release 14 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
board-example Patch
server-dbmon-example 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 20, 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.37 KB 0 B 0 B 0 B 7.39 KB ✅
signals: + createStore 14.93 KB 0 B 0 B 0 B 14.95 KB ✅
signals: + isPending/latest 10.14 KB 0 B 0 B 0 B 10.16 KB ✅
signals: + until/refresh (truth readers, no optimism) 7.97 KB 0 B 0 B 0 B 7.98 KB ✅
app: render + one signal (the simple-app floor) 9.68 KB 0 B 0 B 0 B 9.70 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.31 KB 0 B 0 B 0 B 18.32 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 30.31 KB 0 B 0 B 0 B 30.32 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.26 KB 0 B 0 B 0 B 13.28 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 15.02 KB 0 B 0 B 0 B 15.03 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.61 KB 0 B 0 B 0 B 29.63 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 9.88 KB 0 B 0 B 0 B 9.89 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 26.09 KB 0 B 0 B 0 B 26.10 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.09 KB 0 B 0 B 0 B 32.13 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.75 KB 0 B 0 B 0 B 11.76 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.21 KB +17 B (+0.1%) +19 B +19 B 33.20 KB ⚠️ over by 7 B, 1 B minified headroom assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, trace.js 9.02 KB, wire.js 2.78 KB
page: live server components (base + live/GET + action + isPending/latest) 37.47 KB +7 B (+0.0%) +19 B +19 B 37.48 KB ✅ assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.06 KB, wire.js 2.78 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.28 KB +67 B (+0.2%) +19 B +19 B 35.23 KB ⚠️ over by 49 B, 1 B minified headroom assets.js 0.78 KB, decode.js 6.25 KB, regions.js 1.06 KB, sc-comments.js 0.20 KB, trace.js 9.06 KB, wire.js 2.77 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 41.31 KB +13 B (+0.0%) +19 B +19 B 41.31 KB ⚠️ over by 3 B, 1 B minified headroom eager (counted): web.js 21.93 KB; assets.js 0.78 KB, decode.js 6.25 KB, regions.js 1.06 KB, sc-comments.js 0.19 KB, trace.js 9.08 KB, wire.js 2.77 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 40.67 KB −33 B (−0.1%) +19 B +19 B 40.72 KB ✅ assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, server.js 1.02 KB, serverForms.js 3.59 KB, trace.js 9.13 KB, wire.js 2.77 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 46.87 KB −22 B (−0.0%) +19 B +19 B 46.91 KB ✅ eager (counted): client.js 27.07 KB; assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, server.js 1.02 KB, serverForms.js 3.30 KB, trace.js 9.06 KB, wire.js 2.80 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.62 KB 0 B 0 B −30 B 20.59 KB ⚠️ over by 31 B, 50 B minified headroom

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

  • page: base server components (hydrating + dynamic + frames + sf reference): over brotli cap by 7 B; minified 101,463 B vs 101,444 B recorded with the cap (+19 B) — 1 B of the 20 B minified allowance left; +19 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 49 B; minified 107,804 B vs 107,785 B recorded with the cap (+19 B) — 1 B of the 20 B minified allowance left; +19 B minified over this PR's base
  • page: compiled live server components (the compiled base page + live/GET + action + isPending/latest): over brotli cap by 3 B; minified 122,986 B vs 122,967 B recorded with the cap (+19 B) — 1 B of the 20 B minified allowance left; +19 B minified over this PR's base
  • server: renderToString (the server-render floor): over brotli cap by 31 B; minified 72,124 B vs 72,154 B recorded with the cap (−30 B) — 50 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).

ryansolid pushed a commit to solidjs/prerender-crawler that referenced this pull request Oct 20, 2026
Artifacts lived at the same url in every build, so a CDN or browser cache
could serve the previous deploy's answer. The crawl now takes a buildId
(the Vite plugin mints one per process and defines it as
import.meta.env.PRERENDER_BUILD); integrations get it as context.buildId and
@solidjs/prerender writes _static/<build>/<label>.<key>.txt.

prerendered() installed staticFetch as the transport fetch, so it and an
app's own configureServerFunctionsClient({ fetch }) replaced each other by
order. On a runtime with wrapServerFunctionsFetch (solidjs/solid#4170) it now
installs a send layer instead, which composes in either order; older
runtimes keep the previous behavior.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkjGmX4hAoiU9VhXNt7j3E
…s added

An unlayered app now ships no extra bytes: with wrapServerFunctionsFetch
tree-shaken, the send variable is never assigned and minifies away.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkjGmX4hAoiU9VhXNt7j3E
@coveralls

coveralls commented Oct 20, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 39456084970

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage remained the same at 77.555%

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: 1347
Covered Lines: 1103
Line Coverage: 81.89%
Relevant Branches: 1050
Covered Branches: 756
Branch Coverage: 72.0%
Branches in Coverage %: Yes
Coverage Strength: 27.67 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 11.81%

⚠️ 4 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 195 untouched benchmarks
⏩ 5 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ ⚠️ build + consume 776.3 µs 694.3 µs +11.81%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/project-thread-4achkk (27b6f34) with next (a2743fa)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

wrapServerFunctionsFetch added a second, global transport hook. A
reference can now declare its own send in its metadata instead
(withMeta(fn, { fetch: (address, init, next) => ... })): the transport uses
it for that function's requests with the app's configured send as next.
The intent lives on the declaration, the app's fetch stays the app's, and
there is nothing to order.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkjGmX4hAoiU9VhXNt7j3E
ryansolid pushed a commit to solidjs/prerender-crawler that referenced this pull request Oct 20, 2026
Instead of a global send layer, prerendered() declares the artifact send on
its reference (withMeta(fn, { fetch }), solidjs/solid#4170). The runtime
calls it for that function's requests with the app's transport fetch as
next, so the app's fetch is never displaced and carries the hybrid
fallback. Runtimes without reference sends wire staticFetch() by hand; on
current ones a wired staticFetch passes through, so artifacts are looked up
once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkjGmX4hAoiU9VhXNt7j3E
@ryansolid ryansolid changed the title feat(web): wrapServerFunctionsFetch, a send layer that composes with the app's fetch feat(web): a server function reference can declare its own client send Oct 20, 2026
@ryansolid
ryansolid marked this pull request as ready for review October 20, 2026 16:44
@ryansolid
ryansolid merged commit 7f8ab2d into next Oct 20, 2026
8 checks passed
ryansolid pushed a commit to solidjs/prerender-crawler that referenced this pull request Oct 20, 2026
rc.16 ships reference sends (solidjs/solid#4170), so prerendered()
serves artifacts on its own. The hand-wired staticFetch transport only
existed for older runtimes; remove it along with its server stub and
the test probe. Bump the SSG example and dev deps to rc.16 / router
next.41.

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

3 participants