@solidjs/web@2.0.0-rc.4
Pre-releaseMinor Changes
- 475744c: Add
invoke(fn, options, ...args)— the per-call server function invocator (#3057). Applies one call with invocation-scoped options:signal(aborting rejects the call and cancels the request; ends a live source's iteration across reconnects),keepalive, andpriority. Longer-lived concerns are refused with a redirect to their home (prepareRequest,withMeta/GET, the data layer viasignal) — never aRequestInitpassthrough. Dispatch rides a registered-symbol invocation channel (SERVER_FUNCTION_INVOKE) that wrappers forward like declaration metadata, soinvokecomposes throughGET,live, and integration wrappers that adapt it. On the server the call runs in-process:signalrejects the caller, transport hints are no-ops. - 8d249c7: Patch-mode list hydration: claim + register only. The list driver claims each
server row positionally through the row's own_hkkey (a row-scoped
explicit-id owner makes the compiled template's getNextElement resolve it),
and patchDriver skips the initial force-apply while hydrating — server HTML
stays the truth until the first transition. All driver-sideeachreads and
the probe are id-isolated (throwaway/private explicit-id owners), so lazily
minted prop-getter memos can no longer shift the ambient hydration id chain
on either the engage or decline path. - 8d249c7: Patch-mode list driver: keyed
<For>over a store array is offered to the
runtime's row-ops driver (create/bind at op-apply, LIS moves, node removal —
no mapArray, no per-row owners, no DOM-side reconcile).Forcarries$ll
metadata on a lazy classic accessor so unaware renderers and declined lists
(non-store subject, impure rows proven by a bind-time owner probe, fallback
or index usage) fall through to today's mapArray path unchanged. Array
identity swaps keep keyed semantics by raw-identity matching. Adds
ownerIsBlank(signals) for the purity probe anddriveList(web, rxcore
seam) for the runtime. - 8d249c7: Close two list-driver coverage gaps found by the JFB store scenario: setter-
channel structural mutation (push/splice/index assignment/permutation) now
emits identity-keyed row ops at the fold — a driven list stays DOM-correct
for stores mutated without reconcile — and empty-initial lists engage
TENTATIVELY, deferring the purity probe to the first created row, with a
late decline handing the region to the classic mapArray path through the
runtime's re-entry thunk - 8d249c7: Shallow store lists through the compiled driver: slot patches graduate from
prototype to channel semantics (key-aligned value-replaced slots only —
structure rides row ops — queued at effect phase under the registration
owner), and the list driver collects a shallow row's compiled bodies at bind
(rows are raw; nothing to register on) and dispatches them from the array's
slot channel, rebasing indices with structural ops. Adds storeIsShallow;
kind-changing subject swaps (shallow <-> deep) hand off to classic.
Patch Changes
-
8d249c7: External-audit fixes on the patch-list driver surface: family (projection/optimistic) arrays now decline the driver — their structural changes emit no row/slot ops and the proxy identity is stable, so an engaged list would freeze on optimistic or projection structure (classic mapArray handles them correctly, including on identity-swap handoff). Shallow slot-patch registration is now multi-consumer — two driven lists over one shallow array previously overwrote each other's channel. Adds
storeHasFamily(with server stub) and regression tests for both. -
54506e0: Clarify invoke's wrapper contract: declaration wrappers (GET, live) forward the invocation channel mechanically (1:1 call mapping); wrappers that share calls (deduping caches, multicast channels) opt in deliberately or decline, and invoke's error now directs callers to the underlying reference or the wrapper's own idioms.
-
0043643: Document two boundaries of the client
fetchoption's contract: a retrying wrapper may re-send a request that got no response but must never replay one whose response ended (mid-body death may have executed a mutation; live-source reconnection is the runtime's job), and the call-to-request mapping is delivery detail, not contract. -
c9b4f2a: SSR
<select value>resolution now handles empty-string bound values (#3013 follow-up). Empty attribute values serialize as bare attributes (<select value>,<option value>), which the flush-time pass didn't recognize — a bound''never marked thevalue=""placeholder optionselected, so the pre-hydration page showed the first option while app state said''. The pass now reads the bare form as the empty string on both the select and its options, matching React's SSR output for the single-select placeholder pattern. -
8c48a2e: Fix whole-document hydration dying when
useHeadcoexists with shell-authored<head>children (#3081). A charset/base registration is spliced as a prelude immediately after the<head>open tag — a deliberate byte-placement constraint — landing it ahead of every head child the shell authored itself. The compiled head traversal is positional (rawfirstChild/nextSiblingchains in production), so the prepended tag shifted every read by one and hydration for the whole document died on a null read.hydrate()now moves the registry-inserted leading run (data-dhwithout thedata-dhfin-place-rewrite stash) to the end of head before any claiming: the parser already consumed the byte-placement guarantees, the moved metas are inert in an unrendered element, and the walk sees exactly the shell's authored children. The in-place rewritten static<title>keeps its stash, its position, and its claim. -
8d249c7: The list driver's identity matching unwraps store proxies on both sides — draft-authored permutations store row proxies verbatim, and matching them against raw records rebuilt every surviving row (caught by the JFB keyed-reorder identity gate).
-
2f01f23: Module-level "use server" exports now register by value: the server build registers each export's evaluated terminal initializer whole, so server-side wrappers compose onto every call path —
export const getUser = withValidation(schema, fn)applies the wrapper to HTTP dispatch and in-process SSR calls alike, and patterns likewithDelay(fn, 400)work for server mocks. The client build always emits bare references, so wrappers, schemas, and helpers stay server-only by construction. The compiler never inspects the initializer's shape;registerServerReferencenow throws at module eval when handed a non-function, turning stray non-function exports into loud boot errors instead of dead references. Anonymous default expressions (export default withDelay(...),export default async () => ...) get a synthesized binding and register too — previously they were silently dropped from both builds. Supersedes the unreleased wrapped-export compile error. -
8d249c7: Optimistic family arrays are drivable by the patch-mode list driver, completing the family channel: structural optimism (push/splice/reorder/replace in optimistic drafts) emits identity-diffed row ops at lane timing from the override channel — visible in flight, bypassing the transition stash like optimistic record patches — and reverts emit an identity RESYNC the driver resolves against the live post-revert view. The driver binds optimistic lists from the optimistic view (classic reads the same view through the proxy), and the identity-swap matcher is shared between swaps and resyncs. Equivalence matrix extended with async optimistic scenarios (mounted → in-flight → settled, revert and land, element-level and parent-key structural writes).
-
8d249c7: Patch channel is pay-for-use: the list driver and
patchDrivermoved out of the always-retained web runtime intopatch-driver.ts, arming the insert seam lazily fromrowProof/patchDriver(which only compiled patch-mode output imports); the store's emitters ride hooks installed at first registration (patch-hooks.ts) instead of static imports. Apps without patch-mode output retain only a ~100 B insert hook; the store write-path seams cost ~490 B on the store floor. Before this, every client app carried the full driver (~2.4 KB brotli). -
8d249c7: Second re-audit hardening of the patch channel: adoption seams demote accessor-bearing adoptees to tracked effects in development, with a loud diagnostic (production emits directly — per-adoption accessor scans cost ~12% of dbmon's tick, and getter-bearing adoptees on patched records are a development-caught shape); setter-returned root replacements and chained-store swaps emit their patches and row ops at fold commit; the list driver's ops application builds every new row before any destructive step (a throwing row factory leaves DOM and bookkeeping atomically unchanged); patch errors route to the nearest computed ancestor so
Errored.reset()can recompute it (reset also skips non-computed sources), and unhandled patch errors halt like unhandled effect errors; key equality is SameValueZero and occurrence-aware everywhere keys compare — NaN keys stay retained and duplicate keys adopt per occurrence on both channels; same-batch duplicate patch emissions coalesce (one application per batch, effect parity). -
8d249c7: Third re-audit hardening of the patch channel: same-batch coalescing updates the queued entry in place (latest
nextwins — adoption replaces the captured object, so dropping later emissions applied stale state) and the drain clears the channel stamps (no batch retention on quiet records); the adoption remainder window builds from the misalignment point so prefix-consumed rows are never re-offered to duplicate keys; optimistic tentative matching gains SameValueZero + occurrence-aware parity with the plain channel; a failed row-ops application forces an identity resync on the next update (the store committed the failed topology while DOM kept the old one — positional ops would mis-index) and suppresses slot ticks until the baseline is restored; a throwing row factory also severs its own partial registrations. -
8d249c7: Fifth-round hardening of the patch channel: no-op adoptions (A→B→A in one batch) clear the adopted flag so later setter row ops never freeze a driven list; transition merges retarget the moved entries' coalescing stamps (post-merge emissions coalesce instead of double-applying at commit); multi-consumer patch dispatch snapshots the registration list (a callback unbinding a sibling no longer skips consumers); the list driver's initial construction severs partial registrations on throw like update-time builds (one failed initial render no longer elevates patchCount globally); a failed apply actively resyncs from the next slot tick instead of waiting for a structural update; and identity swaps register the new subject's channels before applying so a throwing swap stays recoverable.
-
8d249c7: Patch-channel contract hardening from the stage-2 re-audit: ordinary
patchDriverregistrations unbind with their owner (entries no longer leak past unmount); merged transitions move their held-patch stash so no patch strands; the optimistic drain shares the normal drain's per-entry error isolation and boundary routing; accessor-bearing records are excluded at admission (scan-before-trust) and records that acquire accessors demote their patches to tracked effect fallbacks; writable projection arrays emit setter row ops at their fold-commit visibility moment; row-ops/slot registrations resolve chained backings to the ultimate owner; duplicate keys match occurrence-aware instead of first-wins; the production dev-token typo (_DX_DEV_) is fixed;patchDriver: truenormalizes identically in Babel and the native loader, the option is typed inTransformOptions, and adom-patchparity tier ratchets patch-mode output across both compilers (currently byte-identical on all fixtures). -
8d249c7: Patch-channel semantics completion: a throwing patch now routes through its
registering owner's queue chain to the enclosing error boundary (render-
effect parity; sibling isolation preserved, unhandled errors still rethrow),
and the dual-driver effect fallback splits phases with the same compiled
body — a next===prev read pass tracks in compute, the force apply writes in
the effect phase where transitions and batching expect DOM writes -
8d249c7: Patch-mode lists now implement the identity semantics the view declares instead of the reconcile key's. Deep lists are unaffected (adoption preserves proxy identity, so key ops and reference semantics coincide). Shallow reference-keyed lists rebuild rows whose records were replaced — matching classic
mapArrayexactly, where the driver previously patched them in place (a default-on compiler mode must never change observable DOM identity).Forforwards itskeyedprop on the list metadata; explicitkeyed={fn}lists decline the driver until the accessor-row binding contract lands. -
8d249c7: Patch-channel arming is two-tier so the default-on cost stays proportional:
patchDriverno longer retains the list driver (onlyrowProof— the compiled marker of a patch-mode list — arms the insert seam), and the store emitters split into value hooks (armed byregisterPatch) and row hooks (armed by list registrations), so non-list patch templates never retain row binding, LIS, or reconcile's diff builders. Flip-preview size scenarios pin both tiers. -
8d249c7: Patch-mode lists retain per-row unbind handles: a record the app keeps beyond its row's life no longer holds a live patch registration updating detached DOM — registrations are severed on row removal, contract-leave handoffs, and list disposal. Dev builds also warn when a stamped row's build attaches computations or cleanups to the shared list owner (owned work in handler/attribute value position is unsupported in patch-mode rows).
-
8d249c7: Projection (non-optimistic) family arrays are drivable by the patch-mode list driver: their recomputes walk reconcile, whose row/slot emissions were never family-gated and ride the transition-stamped apply queue. The blanket family decline narrows to optimistic families only (
storeHasOptimisticFamily), whose user writes ride node overrides and emit no structural ops. Fixes chained-backing patch registration: a projection wrapper's backing is another store's proxy, soregisterPatch/patchableRawnow resolve through the chain to the ultimate owner target — patches registered on wrapped projection rows previously never fired (value transitions fold on the source). Equivalence matrix extended with 13 projection scenarios including recompute-driven structure and retention topology. -
8d249c7: Patch-mode list admission moves entirely to compile time: driveList engages only for row functions carrying the compiler's
rowProofstamp (exported from @solidjs/web), and the runtime purity probe is deleted — no speculative execution of user row code, no probeMark/probeGate seams, no ownerIsBlank, no tentative empty-list engagement with late decline. Unstamped rows take the classic mapArray path before any DOM work;lateClassicremains only for engaged lists whose subject later leaves the contract (identity swap to a derived array, shallow/deep kind switch). -
258c76a: Harden the server function handler's HTTP layer. The method gate is now an allowlist: POST always dispatches, GET and HEAD dispatch only to
GET-declared functions, and every other verb answers 405 — previously a HEAD (or PUT/DELETE/PATCH) request bypassed the GET gate entirely and executed any registered function with attacker-chosen query arguments (#3069). HEAD runs the function like GET and strips the body per spec. Responses now default toCache-Control: no-storeunless the function set its own cache policy, and GET/HEAD requests toGET-declared functions skip the CSRF origin gate so their responses no longer carry theVary: Sec-Fetch-Site, Origin, Refererthat fragmented shared-cache entries — declared reads are protected by same-origin policy, and caching becomes opt-in on the wire instead of just in prose (#3071). -
79b96cf: Address server function calls by path:
<endpoint>/<id>, with arguments staying in the query.The id travelled in
X-Server-Function-Id, with?id=as the fallback for requests the client runtime did not make. Both are gone; it moves into the path — what per-function edge rules, cache policies andhttp.routelabels key on — leaving one place in the request that carries it, so a cache in front of the app cannot be made to store one function's response under another's key (#3070). POST addresses move too, andendpointnow gates dispatch on both halves: a request whose path does not start with it is not a call.serverFunctionUrl(id, boundArgs?)andparseServerFunctionUrl(url)ship on both entries for integrations composing action urls. A GET call whose url would exceed 2000 characters dispatches over POST instead, marked as a read — a cache miss rather than a 414.A read whose query is not an argument encoding hands that query to the function as a lone
URLSearchParams, the read-side mirror of a no-JS form post decoding to a loneFormData, so amethod="get"submit reaches the function it addresses. Which reading applies is decided by the url alone, never by a header;argsstays reserved on the query, and a value under it that is not an argument array answers 400. -
82b4e14: Add
fetchtoconfigureServerFunctionsClient: the function the transport sends every server-function request with, typed and called as(address, init)— the address relative to the document, as the global one receives it — so an ordinary fetch wrapper drops in, a hand-written one needs no casts, andparseServerFunctionUrlreads the id back out for telemetry.nullrestores the global.An app-shaped url is what makes it worth a seam: the handler takes a web
Request, so a route that rewrites into the canonical address dispatches like any other call, and nothing downstream — the router's action-url interception, the plugin's dev middleware, the generated dispatch gate — has to learn a second address format. A wrapper forwardsinit— the call'ssignalrides on it — keeps the call same-origin, and hands back what the peer answered, unread. The seam is the client transport's exit only: a server-side call runs in process and never reaches a fetch.Also tidies the
endpointdocumentation on both entries, which the path-addressing change left saying the same thing twice. -
c07edcb: Fail a server function call on a response the runtime did not write, instead of resolving it to
undefined(#3087).Only the protocol's error header and a 5xx counted as failure, so every other non-2xx was decoded as a result — and decoding a login page, or an empty 405, yields nothing. A response at 400 or above carrying no body format now fails the call with the status on the error, undecoded, and before the passthrough control flow uses: a refusal can carry a
Locationof its own, and the passthrough would have handed it back as control flow. Redirects are left alone —fetchfollows them, so an interstitial arrives as its page at 200, and a 3xx only reaches the transport where something opted out of following one.BodyFormat.Voidmarks the one response the runtime encodes without a format to carry — a function that returned nothing — sorespond(undefined, { status: 400 })stays a result alongsidenew Response(null, { status: 404 })andrespond(value, { status: 400 }). A client that predates the tag decodes it the same way; a client that has it, talking to a server that does not, reads an untagged void 4xx as a refusal.A 2xx is not judged at all: a login page served at 200 is indistinguishable from a void result by header alone. One runtime-produced shape is caught with the foreign ones — a verbatim
X-Content-Rawresponse at a non-2xx status, which an integration'sresponseHandlerclaims before the check. -
Updated dependencies [8d249c7]
-
Updated dependencies [f0c3692]
-
Updated dependencies [f3da41e]
-
Updated dependencies [a10cf1a]
-
Updated dependencies [8d249c7]
-
Updated dependencies [8d249c7]
-
Updated dependencies [8d249c7]
-
Updated dependencies [8d249c7]
- solid-js@2.0.0-rc.4