Skip to content

Release RC packages (rc) - #3697

Open
github-actions[bot] wants to merge 1 commit into
nextfrom
changeset-release/next
Open

github-actions[bot] wants to merge 1 commit into
nextfrom
changeset-release/next

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to next, this PR will be updated.

⚠️⚠️⚠️⚠️⚠️⚠️

next is currently in pre mode so this branch has prereleases rather than normal releases. If you want to exit prereleases, run changeset pre exit on next.

⚠️⚠️⚠️⚠️⚠️⚠️

Releases

@solidjs/[email protected]

Patch Changes

  • ad1ecc5: serverComponents (SSR): keep attribute-slot positions bindable on intrinsic elements. A dynamic class or style now compiles to a whole-attribute _$ssrElementAttribute hole instead of a value inside the template's quotes, so a server component's class={{ selected: filters.all }} can mark the class name a client attribute slot owns; ref/on* positions compile, as before, to one guarded _$ssrClaim hole per element, which now emits _s:on:*/_s:ref markers for slot reads (the _bnd behavior-claim marker is gone). A spread element compiles its named ref/on* (<button {...rest} onClick={row.go} ref={row.el} />, wherever they sit relative to the spreads) to the same claim map — { click: expr, ref: [a, b] }, duplicate refs merged, a duplicate handler last-wins as on the template path — keyed by the index of the source each attribute sits before (<b {...a} onClick={go} {...b}> → { 1: { click: go } }) and handed to _$ssrElement as a seventh argument, a thunk the runtime reads only inside a server component's render, so plain SSR never evaluates the expressions (they used to drop with no marker and no finding); the runtime settles each handler position in source order, as the client's spread/merge() do (the last source that has the key owns it, undefined included). A ref beside a lone spread goes through the sources loop under the option (a dynamic spread call is thunked there, as it is beside any other attribute). The event name is always the lowercased on* remainder (onClick → click); on:/oncapture: are not 2.0 syntaxes and are no longer special-cased. Both compilers share the attributeSlots server-components fixture; plain SSR output is unchanged.
  • 077c787: A <textarea> with a dynamic value/defaultValue no longer desyncs hydration ids for the siblings after it (Solid 2.0 - Textarea hydration bug #3691). The server folds the value into the textarea's text content, and that fold was taking the _$scope hydration-id reservation a dynamic child hole gets — but the client writes the value as a plain property effect that never allocates an id, so every component after the textarea hydrated one id off and its updates targeted detached DOM. The fold is now treated like the innerHTML/textContent redirects (innerHTML from a call expression consumes an extra hydration id — the next element is left unclaimed #3015): opaque content, no id reservation, in both the Babel plugin and the native compiler.
  • c71486d: DOM output: an element's ref now runs after its attributes and spread are applied, and still before its children. A ref's own attribute writes now survive client creation the way they already survived hydration ([2.0] A ref's attribute write survives hydration but is overwritten on client creation #3685).

@solidjs/[email protected]

Patch Changes

  • ad1ecc5: serverComponents (SSR): keep attribute-slot positions bindable on intrinsic elements. A dynamic class or style now compiles to a whole-attribute _$ssrElementAttribute hole instead of a value inside the template's quotes, so a server component's class={{ selected: filters.all }} can mark the class name a client attribute slot owns; ref/on* positions compile, as before, to one guarded _$ssrClaim hole per element, which now emits _s:on:*/_s:ref markers for slot reads (the _bnd behavior-claim marker is gone). A spread element compiles its named ref/on* (<button {...rest} onClick={row.go} ref={row.el} />, wherever they sit relative to the spreads) to the same claim map — { click: expr, ref: [a, b] }, duplicate refs merged, a duplicate handler last-wins as on the template path — keyed by the index of the source each attribute sits before (<b {...a} onClick={go} {...b}> → { 1: { click: go } }) and handed to _$ssrElement as a seventh argument, a thunk the runtime reads only inside a server component's render, so plain SSR never evaluates the expressions (they used to drop with no marker and no finding); the runtime settles each handler position in source order, as the client's spread/merge() do (the last source that has the key owns it, undefined included). A ref beside a lone spread goes through the sources loop under the option (a dynamic spread call is thunked there, as it is beside any other attribute). The event name is always the lowercased on* remainder (onClick → click); on:/oncapture: are not 2.0 syntaxes and are no longer special-cased. Both compilers share the attributeSlots server-components fixture; plain SSR output is unchanged.
  • ad1ecc5: Native compiler: $key on an intrinsic element with a spread (<li $key={id} class="todo" {...attrs}>) now follows the same rule as on a template element — SSR compiles it to the _key attribute the frame morph matches keyed elements by, and a DOM compile strips it. It previously passed through the spread path unrenamed, so server markup carried a literal $key attribute and keyed morphs lost identity. Babel already behaved this way; the shared keyedElements fixtures pin the spread case for both.
  • 077c787: A <textarea> with a dynamic value/defaultValue no longer desyncs hydration ids for the siblings after it (Solid 2.0 - Textarea hydration bug #3691). The server folds the value into the textarea's text content, and that fold was taking the _$scope hydration-id reservation a dynamic child hole gets — but the client writes the value as a plain property effect that never allocates an id, so every component after the textarea hydrated one id off and its updates targeted detached DOM. The fold is now treated like the innerHTML/textContent redirects (innerHTML from a call expression consumes an extra hydration id — the next element is left unclaimed #3015): opaque content, no id reservation, in both the Babel plugin and the native compiler.
  • c71486d: DOM output: an element's ref now runs after its attributes and spread are applied, and still before its children. A ref's own attribute writes now survive client creation the way they already survived hydration ([2.0] A ref's attribute write survives hydration but is overwritten on client creation #3685).

@solidjs/[email protected]

Patch Changes

@solidjs/[email protected]

Patch Changes

@solidjs/[email protected]

Patch Changes

  • 829f981: Fix class object values rejecting undefined at the type level. ClassValue's record form was Record<string, boolean>, so class={{ active: props.active }} with an optional prop did not type-check, which 1.x classList allowed. The runtime already treats undefined as off.
  • ad1ecc5: JSX typings: $key?: string | number is declared on intrinsic elements (JSX.CustomAttributes), the entity identity the frame morph matches keyed server elements by. Both compilers already handled it (SSR → _key, DOM strips it); TypeScript rejected it.
  • Updated dependencies [5bef430]
  • Updated dependencies [829f981]
  • Updated dependencies [ad1ecc5]
  • Updated dependencies [1ad0be0]
  • Updated dependencies [ad1ecc5]
  • Updated dependencies [ad1ecc5]
  • Updated dependencies [fe1eb68]
  • Updated dependencies [ad1ecc5]
  • Updated dependencies [0bf5758]
  • Updated dependencies [644eaf3]
  • Updated dependencies [ad1ecc5]

@solidjs/[email protected]

Patch Changes

@solidjs/[email protected]

Patch Changes

  • 656f0df: Fix createOptimisticStore writes not reverting after a failed action while the attribution engine is enabled (createOptimisticStore overlay is not reverted after a failed action while attribution is enabled (rc.10 regression) #3687). The OPTIMISTIC_REVERTED check called the node's comparator detached from the node; store slot nodes share one comparator that reads this._host, so the check threw inside the settle before the drop notified subscribers. The comparator is now invoked as the node's method.

  • 6958367: A tracked read served an active optimistic override is lane work, not a transaction entry (Reading a visible optimistic value in Show holds an independent signal update #3698). The reader is a lane member — its pass publishes a derived override, displays at once, and is promoted or reverted with the action — and lane membership never holds a synchronous write (the Existing and newly revealed latest readers display different values #3460 ruling: "a held lane is basically a micro transition from the outside… we wouldn't hold a sync write on a transition. Lanes are the same"). So an unrelated synchronous write that re-runs such a reader publishes at once, isPending() on it is false, and the action stays open; entry remains for reads served held or superseded truth (A29, A18 (c)).

    A <Show> over an optimistic value broke this through its children: the compiler emits a memo inside the when getter, so Show's condition memo owns a child, and a lane pass over a memo that owns children parked the previous children as a transaction zombie (2.0.0-rc nested render effect cleanup run too early #3404) — which queued the memo as the action's pending node "for the zombies alone" and stamped it, so its next mainline recompute re-entered the hold and adopted the unrelated write (drag() stayed at the committed value while latest(drag) moved and isPending(drag) was true, for the action's lifetime). The same mechanism Card disappears from keyed <For> during overlapping optimistic actions with an empty <Show> #3662 fixed for effects, now for memos: a memo's lane pass parks a lane frame (CONFIG_LANE_FRAME), retired when the lane's queue applies the pass — in the pass's flush for a lane that is not held, at the release for one that is — and the memo is neither queued nor stamped for the action's commit.

  • d21c2be: Wide store folds that delete keys (or add many) no longer mutate the committed backing in place. A backing that served as a prototype overlay is a V8 prototype object, and below V8's descriptor limit every in-place add or delete on it costs O(keys) — a 400-key keyed record churning 100 keys per commit paid 4.7 ms per step. The commit now rebuilds the backing for such folds and swaps it in like a clone-path draft (0.22 ms per step); pure rewrites keep the in-place overlay flatten, and containers past the descriptor limit stay in place where V8 already makes the ops O(1). (2.0.0-rc.10 (performance, V8 only): a keyed projection record of a few hundred keys pays O(keys) for every key a derive adds or deletes — the overlay commit mutates an object V8 has made a fast-mode prototype #3689)

  • f1b0776: Reading a store key an open action did not write no longer holds the reader with that action (Reading an unchanged store property holds an independent synchronous update #3688). The store's container-level hold gate entered a deriving pass into the transaction for any key of a held container; a memo reading an unchanged store.stable beside an independent signal was held until the action settled, and the signal's isPending went true. The gate now consults the fold's written-key record (wk) for get, has and descriptor reads: a key the fold never wrote or deleted reads the same from either backing and is served committed with no entry — the same per-key precision node reads already had. ownKeys, $TRACK and deep() stay container-wide; optimistic families, chained backings, folds with no trap record and array-length writes keep the whole-container hold.

  • d0e487b: STRICT_READ_UNTRACKED now fires in two places it was skipped (STRICT_READ_UNTRACKED is skipped during hydration and in lazy() component bodies #3675). In @solidjs/signals, read() served a snapshot-scope reader the captured value and returned before the strict-read check, so a component body's direct read stayed silent during the hydration pass and only warned after client navigation; the check now runs first. In solid-js, lazy() rendered the loaded component through a bare untrack() instead of createComponent, so its body had no component label and its direct reads were never checked — and, in dev/observe builds, its owner carried no component name for diagnostics. It now renders through createComponent; production output is unchanged. The warning's console line also names the value that was read when it has a name — a signal's name option or the store key: Reactive value "count" read directly in <Child> will not update. (the nodeName field already carried it).

  • ad1ecc5: Server components: attribute slots are rebuilt as one slot per data context, bound per position; the spread shape and behavior claims are gone. A slot renders one of two things — markup, placed as <props.x />, or attribute values: a called slot's result is a plain object the server template reads by property at positions (class={row.rowClass}, hidden={row.removed}, checked={row.done}, onInput={row.toggle}, ref={row.el}). Each read marks the element (_s:class="row#0001:rowClass", _s:on:input="row#0001:toggle", _s:class="list:allDone=completed" for a class-name read) and the client binds exactly those positions: it runs the fill once per occurrence, writes the values that change per position, dispatches events to the current handler, fires ref once, and a morph skips the positions a fill owns. One call is one occurrence, grouped by data context, consumable across many elements; a call repeated within a render (a component prop getter re-evaluated per read) is one occurrence and one record — by $key when given, by structural args otherwise, so $key is optional and names an entity for state that must follow it across responses. On the document face the fill runs at t=0 so values are in the HTML before JavaScript and hydration binds the same nodes; on the stream face reads yield stand-ins and the record carries the args.

    The rule, in one sentence: a slot property is a JSX attribute value, whole, and nothing else. A stand-in that is stringified, used in an expression (Symbol.toPrimitive; reason coerced), placed as text, or reached inside template quotes renders nothing on either face and reports ATTRIBUTE_SLOT_POSITION (dev), so a misuse shows on the first render rather than the first refetch. A stand-in passed in another slot's argument — at the top or nested in plain objects and arrays ({ nested: { x: row.done } }, [row.done]; a Map, Set or class instance is not walked) — is not data the server has: the arg carries undefined at that path on both faces (the record, and the document face's t=0 fill reads the same), with a finding (reason arg, data.path when nested). A cyclic arg crosses the border as a cycle (three walks used to overflow the stack on it; the border walk toBorderForm — every document-face serialization — is copy-on-write with no per-node bookkeeping on ordinary data, measured at or below its previous cost, and falls back to a memoized clone once a cycle is met past depth 16, with replacements reused so a shared generator is seated once). A value reachable at two arg paths crosses as one (one seat for one generator, the identity the input had); a stand-in shared between two paths is reported once, at its first. On the runtime spread path, a stand-in at a prop:* key is a finding (reason prop; the compiled form drops prop:* as SSR always has). Fill output keys are the fill's, save an explicit reserved set ($-prefixed, digit-leading, length, slice, t/h/p, then, constructor/toString/valueOf/toJSON): a key that shadows an Array.prototype method (filter, at, map) binds as data on both faces. The frame client reports the otherwise-silent failures — an element whose markers can never bind because no fill resolves for the prop, or a called occurrence's args record never arrived (reason orphan, data.why fill/record, once per occurrence), and a fill that is not a function or returns a DOM node or a non-object where data was read (reason fill-shape). Handler positions get one listener per element and event, dispatching to the current fill's handlers; a rebind that releases a key clears the attribute it wrote; an occurrence's end detaches the listeners it attached, so a kept un-keyed element carries one listener (not one per occurrence that bound it, firing twice) and a dropped occurrence's handler never runs through its disposed fill. On the document face claims is armed on a render context derived from the page's for the component's subtree, so the page's elements after a server component keep the pre-slot spread walk. Named ref/on* on a spread element (<button {...rest} onClick={row.go} />) bind like every other handler position: ssrElement takes the compiled claim map as an optional seventh argument (a thunk keyed by source index, read only under an armed render context) and reads it — and the spread's own handler keys — through ssrClaim's logic (arrays of refs, merged duplicates, handler tuples, the server-local finding). A handler position settles in source order, as the client's spread/merge() do: the last source that has the key — a spread or a named attribute at its position, whatever the value, undefined included — owns it, and a nullish owner binds nothing (<b {...rest} onClick={cond ? row.go : undefined}> binds nothing on both sides when cond is false); refs merge whatever their order; one marker per position (see the compiler changeset). A merged array ref (ref={[a, b]} ref={c}) binds every entry on the template path too. The lazy-asset resolution cache is created on the root render context, so a lazy() under a derived context (a Loading boundary, a server-owned frame) and one outside it share one cache — one resolver call per module per request. Under the serverComponents compiler option a dynamic class/style on an intrinsic element compiles to ssrElementAttribute, so class={undefined} writes nothing where template quotes wrote class="" (nullish is "not set", as for every other attribute). @solidjs/web now ships skills/server-components/SKILL.md (the package's files) with the rule, the fill idiom and the diagnostic's reasons.

    Surface added: AttributeSlot<P, J> from @solidjs/web/frames — (props: P & { $key? }) => J, props optional when P is empty; the _s:* markers, SLOT_VALUE/slotValue/isSlotValue, SLOT_MARKER, SLOT_FACE_* from @solidjs/web's server entry as the runtime surface; the ATTRIBUTE_SLOT_POSITION diagnostic code (reasons spread, stringified, coerced, inline, text, markup, server-local, reserved-key, arg, prop; client: orphan, fill-shape). Removed: the _bnd behavior-claim marker, CLAIM_PROP, BEHAVIOR_CLAIM_DROPPED (a server-local function at a handler position is now ATTRIBUTE_SLOT_POSITION), the frame props option and FrameHostOptions.delegate/FrameOptions.delegate. The client frame's binding reads slot values in the render effect's compute phase, so a fill of getters tracks each position's own reads.

  • fe1eb68: The route the document arrived on, declared by the router with the call it already uses. NavigationRef.initial on OBSERVE.attribution.withOrigin: a router wraps the work that establishes its initial match (building its context) instead of a location write, on both sides. On the client the attribution engine opens the frame at the time origin (at defaults to 0, the document's own navigation start; a router mounted late passes its own), takes no from, and settles it committed with writes: 0 when the frame closes, so the first "navigation" record (NavigationEvent.initial: true) names the route the page loaded as — the pageload's route pattern, which every navigation but the first already had. It is a declaration, not a timing: kept out of feedback().navigations. On the server, where there is no engine, the server entry's withOrigin files the ref on the render, and the request's "render" record carries it as RenderEvent.route ({ name, to, params }, read from the ref when the render settles) — the name a consumer gives the request (http.route) where the URL would scatter one page across as many names as it has parameters. New type RenderRoute from @solidjs/web.

    NavigationRef.interaction: a router that awaits between the request and the write (guards or loaders resolved in its core before it publishes the location) captures OBSERVE.attribution.currentOrigin() in the request and hands it back on the ref, and the write it publishes later joins the click as if it had been synchronous. Declared beats ambient: the key's presence is the declaration, and an interaction on the stack at write time is used only when the key is absent.

    formatOrigin renders the initial declaration initial navigation to /users/:id (/users/42). Prod artifacts byte-identical; the observe tier's client artifacts byte-identical.

  • 0bf5758: Observe tier: OBSERVE.include(owner) and a body viewer's "call" record.

    • OBSERVE.include(owner) re-admits an owner subtree under an excluded one; isExcluded answers by the nearest marked ancestor, so an observer that renders the app inside its own shell (<DevToolbar><App /></DevToolbar>) can hide its chrome and still see the app. One mark per owner — a later exclude/include on the same owner replaces it — and a mark belongs at the owner's creation (the engine caches each node's verdict on first read).
    • OBSERVE.records.subscribe(type, listener, { bodies?: boolean }) — a listener that reads the live handles an emitter pays per record to take asks for them; OBSERVE.records.observed(type, "bodies") is that emitter's second gate, true only while such a listener of the type is installed (RecordSubscribeOptions exported from @solidjs/signals and solid-js). Accepted for any type; meaningful today for "call".
    • The "call" record's bodies are taken only under that opt-in — a plain listener (an APM adapter, the performance tracks) gets no live.request and the transport's own live.response, as before. With it, live.request is the request as dispatched (final url and RequestInit, prepareRequest applied), built into a Request of the listener's own — with the body only when it is a string, URLSearchParams, FormData, Blob, ArrayBuffer or a view of one, and without it for a ReadableStream or an async iterable (a streaming upload); live.response is an unread clone() taken before the transport's decode. The clone is not taken for an event-stream response, is cancelled at settle for a deferred result (a generator's stream — the branch would buffer every chunk for nobody), and falls back to the transport's object when clone() refuses (an already-read response a configured fetch handed over); the request is absent when the address is relative and there is no location to resolve it against, and when the Request constructor rejects what the configured fetch accepted — nothing taken for the record can fail the call. CallEvent.name carries the reference's metadata name (the compiler's dev-only seed, or an explicit label, which survives to production).

[email protected]

Patch Changes

  • 5bef430: Move createErrorBoundary, createLoadingBoundary and createRevealOrder types to solid-js/internal; they remain runtime exports of solid-js but are no longer part of its public types (solid-js types: index.d.ts re-exports createErrorBoundary, createLoadingBoundary and createRevealOrder, which hydration.d.ts strips as @internal #3709)

  • d0e487b: STRICT_READ_UNTRACKED now fires in two places it was skipped (STRICT_READ_UNTRACKED is skipped during hydration and in lazy() component bodies #3675). In @solidjs/signals, read() served a snapshot-scope reader the captured value and returned before the strict-read check, so a component body's direct read stayed silent during the hydration pass and only warned after client navigation; the check now runs first. In solid-js, lazy() rendered the loaded component through a bare untrack() instead of createComponent, so its body had no component label and its direct reads were never checked — and, in dev/observe builds, its owner carried no component name for diagnostics. It now renders through createComponent; production output is unchanged. The warning's console line also names the value that was read when it has a name — a signal's name option or the store key: Reactive value "count" read directly in <Child> will not update. (the nodeName field already carried it).

  • fe1eb68: The route the document arrived on, declared by the router with the call it already uses. NavigationRef.initial on OBSERVE.attribution.withOrigin: a router wraps the work that establishes its initial match (building its context) instead of a location write, on both sides. On the client the attribution engine opens the frame at the time origin (at defaults to 0, the document's own navigation start; a router mounted late passes its own), takes no from, and settles it committed with writes: 0 when the frame closes, so the first "navigation" record (NavigationEvent.initial: true) names the route the page loaded as — the pageload's route pattern, which every navigation but the first already had. It is a declaration, not a timing: kept out of feedback().navigations. On the server, where there is no engine, the server entry's withOrigin files the ref on the render, and the request's "render" record carries it as RenderEvent.route ({ name, to, params }, read from the ref when the render settles) — the name a consumer gives the request (http.route) where the URL would scatter one page across as many names as it has parameters. New type RenderRoute from @solidjs/web.

    NavigationRef.interaction: a router that awaits between the request and the write (guards or loaders resolved in its core before it publishes the location) captures OBSERVE.attribution.currentOrigin() in the request and hands it back on the ref, and the write it publishes later joins the click as if it had been synchronous. Declared beats ambient: the key's presence is the declaration, and an interaction on the stack at write time is used only when the key is absent.

    formatOrigin renders the initial declaration initial navigation to /users/:id (/users/42). Prod artifacts byte-identical; the observe tier's client artifacts byte-identical.

  • 0bf5758: Observe tier: OBSERVE.include(owner) and a body viewer's "call" record.

    • OBSERVE.include(owner) re-admits an owner subtree under an excluded one; isExcluded answers by the nearest marked ancestor, so an observer that renders the app inside its own shell (<DevToolbar><App /></DevToolbar>) can hide its chrome and still see the app. One mark per owner — a later exclude/include on the same owner replaces it — and a mark belongs at the owner's creation (the engine caches each node's verdict on first read).
    • OBSERVE.records.subscribe(type, listener, { bodies?: boolean }) — a listener that reads the live handles an emitter pays per record to take asks for them; OBSERVE.records.observed(type, "bodies") is that emitter's second gate, true only while such a listener of the type is installed (RecordSubscribeOptions exported from @solidjs/signals and solid-js). Accepted for any type; meaningful today for "call".
    • The "call" record's bodies are taken only under that opt-in — a plain listener (an APM adapter, the performance tracks) gets no live.request and the transport's own live.response, as before. With it, live.request is the request as dispatched (final url and RequestInit, prepareRequest applied), built into a Request of the listener's own — with the body only when it is a string, URLSearchParams, FormData, Blob, ArrayBuffer or a view of one, and without it for a ReadableStream or an async iterable (a streaming upload); live.response is an unread clone() taken before the transport's decode. The clone is not taken for an event-stream response, is cancelled at settle for a deferred result (a generator's stream — the branch would buffer every chunk for nobody), and falls back to the transport's object when clone() refuses (an already-read response a configured fetch handed over); the request is absent when the address is relative and there is no location to resolve it against, and when the Request constructor rejects what the configured fetch accepted — nothing taken for the record can fail the call. CallEvent.name carries the reference's metadata name (the compiler's dev-only seed, or an explicit label, which survives to production).
  • Updated dependencies [656f0df]

  • Updated dependencies [6958367]

  • Updated dependencies [d21c2be]

  • Updated dependencies [f1b0776]

  • Updated dependencies [d0e487b]

  • Updated dependencies [ad1ecc5]

  • Updated dependencies [fe1eb68]

  • Updated dependencies [0bf5758]

@solidjs/[email protected]

Patch Changes

@solidjs/[email protected]

Patch Changes

  • 5bef430: Move createErrorBoundary, createLoadingBoundary and createRevealOrder types to solid-js/internal; they remain runtime exports of solid-js but are no longer part of its public types (solid-js types: index.d.ts re-exports createErrorBoundary, createLoadingBoundary and createRevealOrder, which hydration.d.ts strips as @internal #3709)

  • 829f981: Fix class object values rejecting undefined at the type level. ClassValue's record form was Record<string, boolean>, so class={{ active: props.active }} with an optional prop did not type-check, which 1.x classList allowed. The runtime already treats undefined as off.

  • ad1ecc5: dynamic: a kept resolution's address delivery no longer trips the dev owned-scope write guard when the source is a memo that already settled the server-component call (todos = createMemo(() => getTodos()), dynamic(() => todos()), refresh(todos) in an action — the multi-flight shape; the hydrated document's first refetch takes the same path). The delivery then runs inside dynamic's own compute rather than a promise microtask; the per-site address signal is now created with ownedWrite, since nothing in that compute reads it back. Before, dev builds threw REACTIVE_WRITE_IN_OWNED_SCOPE into the nearest error boundary on the first refetch.

  • 1ad0be0: Fix SSR class and style objects rendering a leading separator when their first entry is skipped (class=" on", style=";top:1px"). ssrClassName and ssrStyle added the separator by loop index; it now depends on what was already written.

  • ad1ecc5: Server components: attribute slots are rebuilt as one slot per data context, bound per position; the spread shape and behavior claims are gone. A slot renders one of two things — markup, placed as <props.x />, or attribute values: a called slot's result is a plain object the server template reads by property at positions (class={row.rowClass}, hidden={row.removed}, checked={row.done}, onInput={row.toggle}, ref={row.el}). Each read marks the element (_s:class="row#0001:rowClass", _s:on:input="row#0001:toggle", _s:class="list:allDone=completed" for a class-name read) and the client binds exactly those positions: it runs the fill once per occurrence, writes the values that change per position, dispatches events to the current handler, fires ref once, and a morph skips the positions a fill owns. One call is one occurrence, grouped by data context, consumable across many elements; a call repeated within a render (a component prop getter re-evaluated per read) is one occurrence and one record — by $key when given, by structural args otherwise, so $key is optional and names an entity for state that must follow it across responses. On the document face the fill runs at t=0 so values are in the HTML before JavaScript and hydration binds the same nodes; on the stream face reads yield stand-ins and the record carries the args.

    The rule, in one sentence: a slot property is a JSX attribute value, whole, and nothing else. A stand-in that is stringified, used in an expression (Symbol.toPrimitive; reason coerced), placed as text, or reached inside template quotes renders nothing on either face and reports ATTRIBUTE_SLOT_POSITION (dev), so a misuse shows on the first render rather than the first refetch. A stand-in passed in another slot's argument — at the top or nested in plain objects and arrays ({ nested: { x: row.done } }, [row.done]; a Map, Set or class instance is not walked) — is not data the server has: the arg carries undefined at that path on both faces (the record, and the document face's t=0 fill reads the same), with a finding (reason arg, data.path when nested). A cyclic arg crosses the border as a cycle (three walks used to overflow the stack on it; the border walk toBorderForm — every document-face serialization — is copy-on-write with no per-node bookkeeping on ordinary data, measured at or below its previous cost, and falls back to a memoized clone once a cycle is met past depth 16, with replacements reused so a shared generator is seated once). A value reachable at two arg paths crosses as one (one seat for one generator, the identity the input had); a stand-in shared between two paths is reported once, at its first. On the runtime spread path, a stand-in at a prop:* key is a finding (reason prop; the compiled form drops prop:* as SSR always has). Fill output keys are the fill's, save an explicit reserved set ($-prefixed, digit-leading, length, slice, t/h/p, then, constructor/toString/valueOf/toJSON): a key that shadows an Array.prototype method (filter, at, map) binds as data on both faces. The frame client reports the otherwise-silent failures — an element whose markers can never bind because no fill resolves for the prop, or a called occurrence's args record never arrived (reason orphan, data.why fill/record, once per occurrence), and a fill that is not a function or returns a DOM node or a non-object where data was read (reason fill-shape). Handler positions get one listener per element and event, dispatching to the current fill's handlers; a rebind that releases a key clears the attribute it wrote; an occurrence's end detaches the listeners it attached, so a kept un-keyed element carries one listener (not one per occurrence that bound it, firing twice) and a dropped occurrence's handler never runs through its disposed fill. On the document face claims is armed on a render context derived from the page's for the component's subtree, so the page's elements after a server component keep the pre-slot spread walk. Named ref/on* on a spread element (<button {...rest} onClick={row.go} />) bind like every other handler position: ssrElement takes the compiled claim map as an optional seventh argument (a thunk keyed by source index, read only under an armed render context) and reads it — and the spread's own handler keys — through ssrClaim's logic (arrays of refs, merged duplicates, handler tuples, the server-local finding). A handler position settles in source order, as the client's spread/merge() do: the last source that has the key — a spread or a named attribute at its position, whatever the value, undefined included — owns it, and a nullish owner binds nothing (<b {...rest} onClick={cond ? row.go : undefined}> binds nothing on both sides when cond is false); refs merge whatever their order; one marker per position (see the compiler changeset). A merged array ref (ref={[a, b]} ref={c}) binds every entry on the template path too. The lazy-asset resolution cache is created on the root render context, so a lazy() under a derived context (a Loading boundary, a server-owned frame) and one outside it share one cache — one resolver call per module per request. Under the serverComponents compiler option a dynamic class/style on an intrinsic element compiles to ssrElementAttribute, so class={undefined} writes nothing where template quotes wrote class="" (nullish is "not set", as for every other attribute). @solidjs/web now ships skills/server-components/SKILL.md (the package's files) with the rule, the fill idiom and the diagnostic's reasons.

    Surface added: AttributeSlot<P, J> from @solidjs/web/frames — (props: P & { $key? }) => J, props optional when P is empty; the _s:* markers, SLOT_VALUE/slotValue/isSlotValue, SLOT_MARKER, SLOT_FACE_* from @solidjs/web's server entry as the runtime surface; the ATTRIBUTE_SLOT_POSITION diagnostic code (reasons spread, stringified, coerced, inline, text, markup, server-local, reserved-key, arg, prop; client: orphan, fill-shape). Removed: the _bnd behavior-claim marker, CLAIM_PROP, BEHAVIOR_CLAIM_DROPPED (a server-local function at a handler position is now ATTRIBUTE_SLOT_POSITION), the frame props option and FrameHostOptions.delegate/FrameOptions.delegate. The client frame's binding reads slot values in the render effect's compute phase, so a fill of getters tracks each position's own reads.

  • ad1ecc5: Frames: a refetch of a server-component call that a boundary is already showing now settles when its response has applied, not at the response header. The header is not an answer for a showing call — until the new content lands the boundary still shows the previous render — so isPending(source) stays true through the refetch and a yield refresh(source) inside an action holds its transaction (and any optimistic write in it) until the refetched slot args are on screen, matching what a single-flight mutation already does. Cold mounts and switches to an address nothing shows keep header-time resolution.

  • fe1eb68: The route the document arrived on, declared by the router with the call it already uses. NavigationRef.initial on OBSERVE.attribution.withOrigin: a router wraps the work that establishes its initial match (building its context) instead of a location write, on both sides. On the client the attribution engine opens the frame at the time origin (at defaults to 0, the document's own navigation start; a router mounted late passes its own), takes no from, and settles it committed with writes: 0 when the frame closes, so the first "navigation" record (NavigationEvent.initial: true) names the route the page loaded as — the pageload's route pattern, which every navigation but the first already had. It is a declaration, not a timing: kept out of feedback().navigations. On the server, where there is no engine, the server entry's withOrigin files the ref on the render, and the request's "render" record carries it as RenderEvent.route ({ name, to, params }, read from the ref when the render settles) — the name a consumer gives the request (http.route) where the URL would scatter one page across as many names as it has parameters. New type RenderRoute from @solidjs/web.

    NavigationRef.interaction: a router that awaits between the request and the write (guards or loaders resolved in its core before it publishes the location) captures OBSERVE.attribution.currentOrigin() in the request and hands it back on the ref, and the write it publishes later joins the click as if it had been synchronous. Declared beats ambient: the key's presence is the declaration, and an interaction on the stack at write time is used only when the key is absent.

    formatOrigin renders the initial declaration initial navigation to /users/:id (/users/42). Prod artifacts byte-identical; the observe tier's client artifacts byte-identical.

  • ad1ecc5: JSX typings: $key?: string | number is declared on intrinsic elements (JSX.CustomAttributes), the entity identity the frame morph matches keyed server elements by. Both compilers already handled it (SSR → _key, DOM strips it); TypeScript rejected it.

  • 0bf5758: Observe tier: OBSERVE.include(owner) and a body viewer's "call" record.

    • OBSERVE.include(owner) re-admits an owner subtree under an excluded one; isExcluded answers by the nearest marked ancestor, so an observer that renders the app inside its own shell (<DevToolbar><App /></DevToolbar>) can hide its chrome and still see the app. One mark per owner — a later exclude/include on the same owner replaces it — and a mark belongs at the owner's creation (the engine caches each node's verdict on first read).
    • OBSERVE.records.subscribe(type, listener, { bodies?: boolean }) — a listener that reads the live handles an emitter pays per record to take asks for them; OBSERVE.records.observed(type, "bodies") is that emitter's second gate, true only while such a listener of the type is installed (RecordSubscribeOptions exported from @solidjs/signals and solid-js). Accepted for any type; meaningful today for "call".
    • The "call" record's bodies are taken only under that opt-in — a plain listener (an APM adapter, the performance tracks) gets no live.request and the transport's own live.response, as before. With it, live.request is the request as dispatched (final url and RequestInit, prepareRequest applied), built into a Request of the listener's own — with the body only when it is a string, URLSearchParams, FormData, Blob, ArrayBuffer or a view of one, and without it for a ReadableStream or an async iterable (a streaming upload); live.response is an unread clone() taken before the transport's decode. The clone is not taken for an event-stream response, is cancelled at settle for a deferred result (a generator's stream — the branch would buffer every chunk for nobody), and falls back to the transport's object when clone() refuses (an already-read response a configured fetch handed over); the request is absent when the address is relative and there is no location to resolve it against, and when the Request constructor rejects what the configured fetch accepted — nothing taken for the record can fail the call. CallEvent.name carries the reference's metadata name (the compiler's dev-only seed, or an explicit label, which survives to production).
  • 644eaf3: New "request" record on OBSERVE.records (CallRequestEvent, CallRequestListener; augmenting HostRecordTypes through solid-js beside "call"): one per server-function request the browser sent, delivered at the send — after argument serialization and prepareRequest, immediately before the transport's fetch receives the request — so a network panel can show a call that is still in flight, or one that never settles, which the "call" record (delivered at settle) cannot. { side: "client", id, name?, at, method: "GET" | "POST", origin? }: id, name, method and origin as on the call's CallEvent; at is performance.now() at the send (not the call's dispatch — CallEvent.at is when the call was made, and the gap is what building the request cost); side is the client's today, the server half (the request arrived, ahead of its "invocation") additive later, the way "frame" has two halves.

    • CallLive is one object across a call's records — documented and pinned: the live handed to the "request" listener is, by identity, the live handed to the "call" listener at settle, filled in as the call proceeds (args from the start, request at the send, response and result/error at settle — on every settle, whether or not a "call" record goes out, so a "request" listener alone reads the outcome off the object it holds). An in-process consumer joins a call's request to its settle by identity — the way origin joins a record to the interaction's — with no id-plus-time join and no sequence number. It is one mutable object and one Request shared by both records: treat it as read-only, and read the request's body through live.request.clone() so the other record's listener still can.
    • Gated per call, delivered per emission: every gate is read once at the call's start — the observation exists when either "call" or "request" is observed; a "request" record is built only if observed("request") and a "call" record only if observed("call") were true then (explicit now — a "request" listener alone gets no settle record). Each record is delivered to the listeners installed when it fires, so a listener subscribing mid-call can receive a "request" and never that call's "call", or a "call" without its "request". bodies on a "request" subscription governs live.request alone: the request is reconstructed when observed("call", "bodies") || observed("request", "bodies"), set on live before the "request" record is delivered; the response clone stays the "call" listener's opt-in. at is read before the reconstruction, so its cost is not in the send's timestamp.
    • What emits nothing: a call that failed before its request was built (argument serialization or prepareRequest threw) emits no "request" — nothing was handed to fetch — only its "call" settle; a call whose fetch never settles emits "request" only; a call an integration answered locally (intercept) emits neither. A fetch that throws synchronously still emits "request" ("left" means handed to fetch). A deferred/streaming result emits "request" at the send and "call" at handoff, as today. A throwing "request" listener is reported and cannot break the call; listeners run synchronously before the send, so a slow one delays the fetch — hand work off to a microtask.
    • Production artifacts are byte-identical: everything lives behind IS_OBSERVE. Not in the diagnostics capture's RECORD_TYPES (capture artifacts keep settled summaries only); no performance-tracks change.
  • ad1ecc5: SSR: ssrElement's spread walk keeps its pre-slot shape on the hot path. Attribute-slot handling — a class/style object, a stand-in at an attribute or behavior key, a slot's return among the sources — moves into helpers reached only for object values and non-literal sources; a string class/style, a plain attribute and a plain source cost what they did before slots (spread-static-tail bench: the two-source forms had regressed 7–28%).

  • Updated dependencies [5bef430]

  • Updated dependencies [d0e487b]

  • Updated dependencies [fe1eb68]

  • Updated dependencies [0bf5758]

[email protected]

Patch Changes

[email protected]

Patch Changes

@github-actions
github-actions Bot force-pushed the changeset-release/next branch 14 times, most recently from dd06243 to f4654b0 Compare September 29, 2026 15:23
@github-actions
github-actions Bot force-pushed the changeset-release/next branch from f4654b0 to 6eaa70e Compare September 29, 2026 15:45
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.

0 participants