Skip to content

@solidjs/web@2.0.0-rc.10

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 27 Sep 06:30
· 32 commits to main since this release
c3adc28

Patch Changes

  • ce17c29: Type the anchor's scroll opt-out as noscroll, not noScroll (solidjs/solid-router#605). The client-navigation contract on plain <a> elements is spelled lowercase — link, state, replace, preload — like every other HTML attribute in these types (novalidate, autofocus, crossorigin), and the router documents and reads the lowercase form (a.hasAttribute("noscroll")); noScroll was the one camelCase outlier, so the spelling the router README shows was a type error. Runtime is unchanged: setAttribute and the HTML parser already lowercase the name, so existing <a noScroll> markup keeps working and only needs the spelling updated to type-check.

  • fd36d37: attribution.enable(opts) now returns the release of the hold it takes (idempotent, like subscribe), and options combine across holds by the most demanding request per key (the log prints while any holder wants it, a check runs while any holder wants it at the most sensitive threshold asked for, historyLimit is the largest), so a hold adds to what the engine does and never takes away what another asked for — a track enabled with log: false beside a console session leaves its log alone, in either order — instead of every call rebuilding the options from defaults. disable() is the full teardown whatever holds are outstanding (the console's reset), so re-enabling to reopen a window and calling disable() once cannot strand a hold. enablePerformanceTracks releases through the token. Dev builds create a component's console.createTask task only for components rendered while an attribution engine is installed — the stack capture per call roughly doubled dev mount for a session with nothing enabled; the tracks enabled at bootstrap still see every component's site.

  • fd36d37: Attribution engine: shared-consumer foundation

    • attribution.enable(opts) is a hold on the engine and returns its release (idempotent, like subscribe). The engine is one per page and shared by every consumer (a profiler track, an APM adapter, a diagnostics capture); it stays installed while any hold remains, and the last release uninstalls and clears everything. Options combine across holds by the most demanding request per key — the log prints while any holder wants it, a check runs while any holder wants it at the most sensitive threshold asked for, historyLimit is the largest — so a hold adds to what the engine does and never takes away what another asked for, whatever order the holds were taken in; releasing a hold withdraws its requests. A hold taken while already enabled opens a fresh window over the ring buffers and folds without disturbing live tracking state or other consumers' subscriptions. disable() is the full teardown whatever holds are outstanding (the console's and a test harness's reset) — a consumer sharing the page releases its own hold instead.
    • New AttributionOptions.checks (default true). false turns off all five cost checks — hotRuns, hotTime, wideDeps, unstableMemos, wideWrites — at once, so a records-only consumer pays for none of their bookkeeping. Hold/long-hold/waterfall tracking are unaffected.
    • isSilentHold and isLongHold are exported from @solidjs/signals/attribution (inert in prod), so consumers apply the engine's own hold verdicts instead of thresholds of their own.
    • InteractionEvent.at is now the browser event's own timeStamp when @solidjs/web dispatches the handler (guarded against epoch-clock stamps), so it equals PerformanceEventTiming.startTime for the same interaction — a direct join to INP. New InteractionEvent.inputDelayMs reports event creation → handler entry; handlerMs is now entry → return, and settledMs continues to measure from at.
  • fd36d37: Attribution engine: timeline records, and a flushStart hook

    Five new listener-gated records on attribution.subscribe: create (a computation's creation run — the mount flame), effect (an effect callback, timed and joined to its compute run), flush (one scheduler drain: runs, creations, whether it parked a transition, the interaction it served), flight (an async flight from origin to landing or abandonment, with the async node's owner path) and fallback (a loading boundary's fallback from show to hide). None is built, logged or folded unless something is subscribed to its type, so the console/agent readers pay nothing for records only a timeline wants. OBSERVE.subjectOf answers for the node-bearing ones.

    Core: a flushStart hook beside flushEnd (one drain, never nested), and effectRunStart now fires in observe builds like its effectRunEnd twin, so writes inside effect callbacks carry their effect origin in observe too, not only in dev. The observe core grows by 67 bytes minified; prod is byte-identical.

    The engine's effect-frame → node map is now filled by the first write inside a callback rather than by every callback (every reader resolves it through a write's origin, and most effect callbacks never write), which removes a WeakMap write per effect callback from the enabled engine's hot path.

    @solidjs/web/performance-tracks paints the new records: creation runs and effect callbacks on the Effects/Memos tracks, drains on the Propagation track (one wave per drain), flights and fallbacks on a new Async track.

  • 5e46732: In the dev build, a live GET() grant now survives an SSR program reload that re-evaluates the server module without the module that declared the read: a router's query()-declared reads no longer answer 405 until the next document render (#3564). The carried grant is provisional and dispatch-only — the origin gate stays on for that id, so a cross-site GET still answers 403 — until the live binding re-declares GET(); a GET() on a reference from the earlier evaluation grants nothing and no longer throws. Production builds are unchanged and still revoke the grant on every rebind (#3129).

  • 56918c6: Close a serialized async iterator once the response that carries it is abandoned. When a renderToStream consumer cancels the readable or a pipe sink throws, the render is disposed, but the tapped iterator handed to the serializer kept delegating next() to the source, so an iterable memo, an async memo that resolves to an iterable, and a generator projection kept pulling (or never ran their finally) for as long as the source lived. The tapped iterator now answers done and calls the source's return() once its computation is disposed, and the projection pump closes its source when it stops for disposal. The render's wind-down also closes the serializer, which returns every async iterator it is still pulling — including a source whose pending next() never settles, which the disposed check on the next pull could never reach.

  • fd36d37: sourceNames.bindings: compiled binding effects are named by what they write. An attribute effect gets <tag>.<attribute> as written (span.textContent, div.class:active, div.style:color; a template's merged effect lists all of its bindings), a hole's insert is named for the parent it fills (div.children), and a spread passes its tag so the runtime labels its attribute effect div.spread and its children insert div.children. The names travel as a trailing options argument on effect/insert ({ name }) and a trailing string on spread; @solidjs/web's effect, insert, and spread accept them and put them on the render effect nodes, where the dev and observe tiers show them in owner paths, attribution chains, and the Propagation track — a binding effect reads span.textContent ← count instead of effect ← count. Production ignores the names; output with the option off is unchanged. DOM output only; sourceNames: true turns it on with components.

  • fd36d37: Rename the compilers' componentNames option to sourceNames, now boolean | { components?: boolean }. sourceNames: true (or { components: true }) is what componentNames: true was — the tag as written in source as createComponent's third argument, on DOM and SSR output. The option is now the home for every kind of source name the compilers can carry into output for the dev and observe runtimes to label the reactive graph with (binding effects and primitives follow); the object form picks kinds. @solidjs/compiler rejects componentNames as an unknown option, so a stale @solidjs/vite-plugin fails loudly rather than compiling without labels.

  • 7599885: Dev diagnostic UNSCOPED_HOLE_ALLOCATED_IDS: an unscoped hole that took hydration ids at a position the other side does not share (#3567 follow-up)

    The one hydration-key gap left after #3599 is a bare identifier bound to a function — const renderHead = () => props.header; <div>{renderHead}</div>. The compiler sees a value and scopes nothing; both runtimes unwrap the function, but not at the same point (the client's insert at the statement, the server's ssr() inside the walk after every scoped sibling reserved its slot), so the keys of the hole's content and of the holes after it permute. JSX.Element excludes functions in 2.0, so type-checked code cannot write this hole; by ruling it is not scoped (no production cost for a shape the types reject). Instead the dev builds detect the permutation and raise UNSCOPED_HOLE_ALLOCATED_IDS (warn, kind render, once per site) on both server render and client hydrate. The server reports structurally — the counter's next id when the hole was registered (data.registered) differs from the one it was evaluated at (data.before, data.after after it ran); the client, which always builds in place, reports when the content it built inside an unscoped function hole moved the counter and missed a server-rendered key. A function hole with nothing scoped after it lands on the same ids on both sides and stays silent (a boundary's zero-arity fallback={() => <F />} thunk built by the consuming hole is that shape). data.name is the function; the message names the fix: call the function at the hole ({renderHead()}) or pass the built value. Scoped holes, memo and component accessors, children(), <For> rows and the runtime's own children inserts never raise it.

    Plumbing: a dev-only sharedConfig.devPeekNextContextId() on both solid-js facades (the next child id of the current owner, read without consuming — on the server without materializing a pending hole slot), installed under the dev gate so the prod and observe artifacts of solid-js and @solidjs/web are byte-identical to before.

  • 6717d35: Diagnostic code consolidation. Codes are public API (the Sentry fingerprint roots); three renames.

    • WIDE_WRITE is folded into HUGE_FAN_OUT — one code, one threshold story. The core's always-on check warns at 2000 subscribers; the attribution engine's fanOut threshold (default 250, was wideWrites) warns earlier while it is enabled and reports through the same emitter, so data.count is the subscriber count and, from the engine, data.write says which write reached it ("write", "refresh", "async"). One WeakMap dedupes both reporters (re-warn after another 500). AttributionOptions.wideWrites → fanOut.
    • SERVER_FN_ERROR_SANITIZED and SSR_ERROR_SANITIZED are one code, SERVER_ERROR_SANITIZED — the same fact from two roads. data.source is "server-function" (severity error, from @solidjs/web/server-functions) or "ssr" (severity info, from the SSR <Errored>/rejection path); data.error is the original and data.wire the replacement on both.
    • ASYNC_WATERFALL is client-only again: the server's boundary-passes verdict is its own code, SSR_BOUNDARY_WATERFALL (kind ssr; info for two sequential waits, warn for three or more; data: { boundary, passes, sequentialMs }). data.side is gone with it.
  • 663031d: Fix the document live channel (sc:live) crossing as an async iterator instead of a ReadableStream. The hydration serializer's channel guard (#3468) treated every async iterable as a source to wrap, and Node's ReadableStream is one — so the frame sink's hole/attr op channel reached the client in a shape its pump (getReader) never reads, and a server component streaming into the initial document froze at its first value (the chat welcome stopped after its first paragraph). A ReadableStream now keeps its type through the guard: a stream over the source's chunks whose failure crosses sanitized, like a rejection.

  • 1d3ec5d: dynamic's source type admits AsyncIterable<T> — the answer of a live server component reference. Type-only: dynamic is a memo and already pumped the iterable.

  • 1d3ec5d: dynamic: a live server component's reconnect after its source switched arguments keeps the mounted instance at the standing address. The binding equals-gate read "the address that is not the delivered one" as incoming, but the memo holds its first (kept) resolution forever, so the reconnect's re-yield of the standing binding swung the frame back to the document's address and the reconnect's render landed where nothing was bound. The gate now reads its arguments as (prev, next) and delivers next's address when it is not the one showing.

    Signals: the lane landing in asyncWrite now calls a user equals comparator as (prev, next), the order every other commit path uses (it passed (next, prev)).

  • a360ad0: Fix SSR hanging when a derived async computation reads a bare ssrSource: "client" hole inside <Loading>. createMemo(async …), createProjection, and dynamic({ deferStream: true }) whose compute throws the client-hole NotReady now classify FINAL — the boundary hands the subtree off to the client exactly as a direct read does — instead of subscribing a retry to a source that never settles and leaving the response open. A client hole that surfaces before the shell has flushed now takes the same fallback + $$f route as one found at discovery, rather than rejecting the fragment over an empty region.

  • bfc320c: Let the csrf.origin allowlist admit a cross-origin caller, and answer it with CORS (#3538). configureServerFunctionsServer({ csrf: { origin } }) accepted a string, a list or a matcher, but the handler refused Sec-Fetch-Site: cross-site (and same-site) before consulting it, so an explicitly listed origin was refused by every current browser and WebView — a client-only build in a Capacitor WebView could not call server functions on another host at all. Now a cross-site request carrying a browser-set Origin is decided by the matcher: refused when none is configured (today's default) or when it does not answer true; none stays refused. An admitted cross-origin caller gets Access-Control-Allow-Origin echoing its exact Origin (with Vary: Origin), the protocol's response headers exposed, and the OPTIONS preflight answered for the transport's methods and headers — on every response, the labelled unknown-id 404 included. Access-Control-Allow-Credentials: true is sent only with the new csrf.allowCredentials option, so listing an origin never silently turns on cookie sharing. The same-origin path is byte-identical to before, with one exception: once a matcher is configured, every GET-declared read carries Vary: Origin — the read stays ungated and Allow-Origin appears only for a listed origin, but a declared read is cacheable and its answer now depends on who asked, so a shared cache must not serve a same-origin page's header-less variant to the listed origin. Without a matcher, reads carry no Vary, as before. The client's endpoint option now documents an absolute URL, with csrf.origin as the server-side allowlist that makes it work.

  • 3af4696: fix(web): an async dynamic() instance serializes its landing and the client adopts it (#3666)

    dynamic() no longer opts its memo out of hydration serialization. The per-instance value memo is an ordinary async memo: when the source introduced async (returned a thenable during SSR) its landing is serialized under the instance's id and the client memo adopts the record during hydration instead of re-running the source and waiting on it — the pending beat that committed the enclosing <Loading> to a fallback the server never rendered, then missed the SSR'd nodes. A server component lands as a flight reference (_$SC.r(id, address) now resolves to the call's binding, so a wrapped query() or async arrow around a server reference hydrates the same as a direct call), a tag name as its string, and a sync source lands nothing — unchanged.

    A source that resolves to a client component function cannot serialize and is now refused on the server at the point the memo would serialize it — new diagnostic DYNAMIC_ASYNC_COMPONENT (recorded on the observe channel; the memo rejects into the nearest <Errored> / onError in every tier). Move the async upstream (createAsync/createMemo read synchronously by the source) or use lazy().

    Wire shape: each async dynamic() instance now writes one hydration record, and every dynamic() instance allocates one more hydration owner id (keys under a dynamic() element shift by one slot).

  • cf22713: Single-flight mutations that revalidate a server component answer with a frame stream again, and single-flight delivery is one shared path (#3638).

    The fold keys flight data by source — { [source]: slice }, the unnamed collector's slice under "true" — but frameTransformFlightResult scanned only the top level of data for components, framed nothing, and the handler fell back to the plain codec, which cannot encode a function (Server function result could not be encoded … "function"). Every single-flight mutation revalidating a server component (the router's createFlightDataCollector with server-component routes) failed. The transform now scans each source's slice, frames component-valued entries as regions addressed by their call (a bare function entry is addressed by its key), and keeps the source keys in the serialized envelope.

    Two more halves of the same protocol drift are fixed alongside: the frames client delivered the whole keyed envelope to the unnamed consumer only, and the frame policy stamped a bare X-Single-Flight: true that won the header merge and dropped named sources. Delivery is now one implementation in @solidjs/web/server-functions (deliverFlightData, internal) that both the plain client and the frames client call — each registered consumer receives its own slice, in registration order, with identical metadata handling — and the fold owns the single-flight header on every body shape.

    ServerComponentHandlerOptions.consumer and .codec (@experimental) are removed: they existed for a bundling condition (two copies of the server-function client) that the frames build rules out by construction.

  • 78523bf: Fix X-Frame-Stream: "" on single-flight server-component responses: the frames server artifacts (@solidjs/web/frames/server, all three tiers) bundled their own copy of the server-function runtime, so the invocation the handler recorded was never the one frameTransformFlightResult read — every POST server-component call with a flight source registered answered with an empty frame id and the client rendered nothing (#3641). The frames server build now resolves the server-function runtime and its wire layer to @solidjs/web/server-functions/server and the SSR runtime to @solidjs/web (externals, one instance per app by construction), which also fixes <select value> inside a server component streaming unresolved: compiled output armed the select-value gate through @solidjs/web, and the bundled renderer copy never saw it. frames/dist/server.js drops from ~155 KB to ~44 KB. ChunkReader, createChunk, frameAddress and serializeStream are now re-exported from @solidjs/web/server-functions/server (internal transport building blocks the frames artifact imports).

  • 495db9c: Remove a sole text child of 0 or NaN when that hole is replaced with an element, an array, or other content. Those values stay tracked as the raw primitive, and the old truthiness checks skipped cleanup, so the text node was left in place and zeros accumulated on later toggles.

  • f41c6a4: Fix renderToStream hanging or throwing TypeError: Cannot read properties of undefined (reading 'emit') when an async read that rejects is the direct child of <Loading> (#3569).

    • solid-js: a bare child's throw (<Loading>{data()}</Loading>, or a component whose return is the read) now routes through the boundary's error handler exactly like a template hole's does — the fragment rejects and the client re-renders the subtree (handling: "client"), instead of escalating to a request failure pre-flush.
    • @solidjs/web: a render failure (failRender) now completes the consumer — the awaited promise resolves with the HTML produced so far, pipe() ends its sink, pipeTo()/readable close the writable — and the serializer's completion no longer assembles a shell on the disposed render.
  • fd8b3df: Fix hydrate() halting with TypeError: Cannot read properties of null (reading '_config') when a useHead({ tag: "link", props: { rel: "stylesheet", href } }) sheet is still loading as hydration reaches it (the default with an async entry script), including on a late streamed boundary resume. The waitAsset gate memo is created without an owner, and the hydrating createMemo tried to peek a hydration id from that null owner; the gate is now transparent, so hydration never sees it. useHead also no longer gates stylesheets while hydrating: that content is already visible, and a pending read inside a boundary's claim window made the boundary render fresh DOM beside the server's.

    A streamed <Loading> boundary whose fragment swap the server holds on a stylesheet ($dfs) now resumes when the swap lands, not when its _fr record settles. Resuming on the settle claimed against a document that did not have the content yet, then the delayed swap inserted a second copy.

  • 1d3ec5d: Live server components reconnect conditionally (frame face). Every frame chunk carries a server-minted digest (html: skeleton digest plus a holes map; fragment/hole/attr: their own), the mount keeps a ledger of applied content (Frame.have()), and the live loop asks the frames handler per connect (responseHandler.resume) so a reconnect sends the address's version ordinal as Last-Event-ID and the ledger as X-Frame-Have. A frame render given that list (FrameStreamOptions.resume.have, read by frameTransformResult at the live address) skips the root when the skeleton matches and emits only the settled holes and attrs whose digest differs — never a fragment or a fallback reveal over content the client names. FrameChunk gains hole and attr members; FRAME_HAVE_HEADER / FRAME_HAVE_BUDGET are exported from the frames client and server entries.

  • 1d3ec5d: Frame renders tear down when their reader is gone. serverComponentResponse's body cancel() and the request's abort (frameTransformResult / frameTransformFlightResult pass event.request.signal) now dispose the render through renderToStream's disconnect path — previously cancel() only dropped writes and the render ran on until its sources happened to end. renderToStream gains a signal?: AbortSignal option for this (the SSR_STREAM_ABANDONED finding reports it as data.reason: "signal"); renderToFrameStream, renderServerComponent and serverComponentResponse accept it through their options. A frame flight response stops at the frame in progress and skips the rest. In solid-js, the server pump over an async iterable in frame scope closes its source from the compute's disposal — return() now, not at the source's next yield — so a standing source (a change feed, a subscription) does not hold what it subscribed to until an event nobody would see.

  • 1d3ec5d: Frames consume live: a server component called through live(fn) streams as an event-stream frame response (live headers, heartbeat, dev chaos knob) and the reference's loop owns the connection's lifetime — a body ending with the frame open is a death (backoff, reconnect, one morph, no fallback, no remount), complete is a completion, supersession from another response cancels the connection so the loop reconnects, and one live connection is held per address. getServerFunctionInvocation() reports live; serverComponentResponse takes live. Without a loop, a frame response ending before a started frame's complete is now that frame's error.

  • 1d3ec5d: Live server components on the document face (Stage 8 B3)

    Server: a component a live server function answers with renders into the document under a live scope — every async source it reads takes its first value into the markup and is closed, so the document completes; nested server components inherit the scope. The scope is judged from the memo's owner, which also fixes an unbranded thenable-resolved stream in server-component scope serializing instead of pumping. New dev-only check SSR_UNDECLARED_LIVE_SOURCE: an undeclared async iterable still pumping five seconds into a document render is named (frame-stream renders are never judged).

    Client: the frames intercept is consulted synchronously by live() and its answer rides on the iterable (LIVE_LOCAL); a hydrating dynamic() adopts it as its value at t=0 — no request, no pending beat — and takes over at its hydration scope's release, re-yielding the adopted binding and connecting once at the live address. A boundary the page is still delivering is answered with a promise that lands at its reveal, so a dynamic(() => call()) over a streaming boundary waits for the document instead of fetching. dynamic's memo treats the same server-component instance (same component and address) as equal, so placeholder and per-address binding never remount.

  • 7742b28: live calls move to a live address, <endpoint>/live/<id>, answered in event-stream framing.

    A live loop is a third caller kind receiving a third answer shape, so it gets a third path beside /data/<id> (caches key on the url, and a read carries no transport header). The server frames what it answers there as server-sent events — text/event-stream, Cache-Control: no-store, X-Accel-Buffering: no, one codec payload per data: event, a comment heartbeat every 20s — so buffering middleboxes pass live responses through. Each value-shaped yield carries a digest of its JSON form as the event's id:; the loop echoes the last one back as Last-Event-ID on reconnect, and a reconnect whose position equals the first yield's digest gets that yield skipped (fewer yields than before on a digest-equal reconnect — flagged). A cursor source reads the header off the request. Streamed answers at the data address are byte-identical to before.

    A client and server versioned apart miss each other on live calls until both are current. Development builds warn once when a page holds more than five live connections and the document came over HTTP/1.1.

  • 7742b28: Dev chaos-reconnect knob for live: configureServerFunctionsServer({ chaosReconnectEvery: ms }) ends every live response N ms after it opens, the way a dying connection ends it (the body breaks off with the stream still open), so the client loop's reconnect path — backoff, Last-Event-ID, the digest-equal skip, onstatus — runs continuously without a network to break. Applies to every event-stream response the live address answers; inert outside the dev build.

  • 7742b28: live claims the whole response and its post-hydration takeover fires per hydration scope.

    • A live connection now lives as long as its response: an answer holding nested promises or async iterables ({ meta, progress: gen }) keeps the connection open until every one has settled. A body ending with deferreds still open is a death — the loop reconnects and re-yields the whole answer, fresh; one whose deferreds all settled is a completion. Nothing is added to the wire: the decoder counts what the codec's own close records leave open (createJSONDeserializer's returned function gains an open() accessor beside abort()). Deferreds a reconnected-from death left open stay pending until the iteration ends for good, then fail.
    • In process, live() brands the answer that is the source — the top-level iterable, or a function-valued answer (a component) as a whole. Sources nested in a value answer are not branded: they are bounded and end on their own, and a document render pumps them to their end as it does any streamed answer (their sharing between the serializer and a reading memo is the SSR fix in the companion changeset).
    • The hydration takeover gate is keyed per snapshot scope: a live node in the shell reconnects when the root pass ends, a live node under a boundary when that boundary hydrates — no live node waits on another boundary or on page-wide hydration end. A later hydration pass (islands) arms its own gate.
    • The takeover run tells the live answer the value the page was served with (Symbol.for("solid.LiveResumeFrom")), which the loop names as its first connection's Last-Event-ID; a takeover that finds the same value on the server yields nothing.
  • 7742b28: live: a consumer ending a live iteration (return() — e.g. a memo re-invoking with new arguments) now COMPLETES the nested streams still open in its answer and leaves nested promises pending, instead of failing them with the transport's own AbortError. A child memo still reading a nested stream saw that error as an uncaught failure and halted the page on a route change. Ending by error (a 4xx, the caller's signal) still fails what is nested.

  • 7742b28: live: an iteration stamped with the value it resumes from (hydration's takeover of a server-rendered value) now yields that value first, before it connects. The server's digest-equal skip means the wire may carry no first emission, and the node that re-ran its compute for the takeover had no other way to land — left pending, it held every write of the tick that released it (the identity minted at onSettled, for one) until the source changed. Landing the adopted value is equality-quiet for a memo and a no-op reconcile for a projection; Last-Event-ID and the wire skip are unchanged.

  • 235173e: Prune the observability surface: OBSERVE.attribution.install, the AttributionHooks type, DEV.setConsoleFooter, the TraceSlot and OriginRef aliases, PerformanceTracksOptions.group, and the untested solid-js/refresh runtime modes (esm, webpack5, rspack-esm) are gone — @solidjs/compiler's transformRefresh({ bundler }) accepts only "vite" | "standard" to match. ownerPath() is now OBSERVE.ownerPath(subject) and diagnosticGuideUrl() is DEV.guideUrl(code); why() also accepts a scope name. Engine record types (RerunEvent, HoldEvent, …) live on solid-js/attribution only, and InteractionRef/NavigationRef on the main entries only. @solidjs/diagnostics types its record tables off the runtimes' own catalogue (RecordEvent<K>), adds the recovery table, and bumps the artifact formatVersion to 8.

  • b8ac688: renderToStream: a serialized source that rejects before the shell completes no longer exits the process. Before the shell, trackSerialized's race waits in the stub batch, and seroval subscribes to it only when the batch is flushed. While another fragment is still pending, that flush comes a macrotask later. A source rejecting in between rejected the race with no handler attached, so Node reported an unhandled rejection. Examples are an async memo that throws right away, or a router query whose guard refuses the request. The race is now observed where it is created, and seroval still receives the rejection when it subscribes.

    The fragment promise registerFragment parks in the same batch (<key>_fr) had the same exposure: a <Loading> whose content throws on a retry pass while the shell is still held — on a pending root hole, or a no-progress timer — rejects its _fr promise before anything has subscribed to it, and the process exited the same way. An <Errored> outside the boundary does not help pre-shell (the fragment channel owns the error once registered). That promise is now owned where it is created as well; the rejection still reaches the client, and the server error hook still hears it once.

  • 384a631: One records channel: the attribution engine's records (rerun, create, effect, flush, flight, fallback, interaction, hold, navigation, graph) are RecordTypes entries delivered on OBSERVE.records.subscribe(type, (event, live) => …), with the live node beside each record. Removed attribution.subscribe (both overloads), OBSERVE.subjectOf, the AttributionRecords/AttributionRecordType types, and the isSilentHold/isLongHold helpers — HoldEvent now carries silent and long, computed at settle. attribution.history(), waterfalls(), holds(), navigations() and interactions() collapse into attribution.history(type). DiagnosticListener receives the subject as its second argument. The channel allocates nothing per emit (copy-on-write listener lists), and a RerunEvent is built only while a listener, a fold or the log wants it. Record listeners belong to the channel and are no longer dropped by attribution.disable().

  • fd36d37: Fold the source-name plumbing out of the production artifacts: createStore's declared name is recorded in an observe-only branch of the public entry (no extra parameter on the store constructor), the web spread labels ride a module-level spreadName that effect and insert read while the spread body runs (no options argument at the call sites), and the boundary node names gate the call or set _name after it. The minified prod bundles are structurally identical to next (identifier-normalised diff empty); the observe-tier caps are ratcheted for the performance-tracks records with an audit note.

  • fd36d37: @solidjs/web/performance-tracks: Solid's records on the Chrome Performance panel

    enablePerformanceTracks(options?) paints the attribution engine's records — re-runs (Effects/Memos, coloured by self time, warning for a provably wasted run), interactions (input delay, handler, settle by outcome), holds (warning for a silent hold, error for a long one — the engine's own verdicts), navigations (named by route) — and the web runtime's server-function call and frame records (Server) as custom tracks in the group Solid, through the panel's extensibility API. Every span is emitted retroactively from the record's own performance.now() stamps; labels come from the shared formatters (formatOrigin, formatRerun, ownerPath), so the timeline agrees with the diagnostics artifact by construction. Dev builds emit performance.measure entries with detail.devtools (why-chain tooltips, cause/deps/blocker properties; entries cleared in batches); observe builds emit console.timeStamp spans with a 0.05ms floor and scrub value previews and non-button element text. Prod builds fold the module to a no-op. Options: attribution (the engine hold's options, log: false by default — asking for no console log, which quiets it only while no other holder wants it), minMs, rich, group, scrub. Returns the release of this adapter's hold — the engine's and its own.

    solid-js now exports ownerPath (already public on @solidjs/signals) — the root-first owner labels of a subject — on both the client and server entries, so in-process consumers of the records (this adapter) can label a re-run by its component path without importing signals directly.

  • fd36d37: Performance tracks: findings as markers, JSX-site stacks, richer properties

    • Every DiagnosticEvent delivered while enablePerformanceTracks() is on becomes a marker on the Performance panel's Timings track (SILENT_HOLD — <App> › <Search>), coloured by severity; at warn or worse it is annotated as a performance issue (detail.devtools.performanceIssue) for the Insights sidebar, linking the repair guide's section for the code. Under the scrub only the code, kind and owner travel.
    • In dev, the component wrapper stores a console.createTask(label) task on the component record (_component.task) for components rendered while an attribution engine is installed (a session with nothing enabled pays nothing), and every span and marker is emitted inside the nearest component's task, so the entry's stack in the panel points at the JSX site that rendered the component.
    • Rich mode adds Owner path (the unfolded runtime path), Node id and the root write's Origin to every node span.
    • solid-js exports diagnosticGuideUrl(code) — the repair guide URL the console footer already prints.
  • ed13427: @solidjs/web/performance-tracks: track-entry labels now start at the nearest component the developer wrote (<Search> › results) instead of the full folded owner path (<Document> › body › <App> › <Router> › … › <Search> › results), which the Performance panel elided in the middle — losing exactly the segment that says which component the node belongs to. Solid's own flow and platform components (<Show>, <For>, <Repeat>, <Switch>, <Match>, <Errored>, <Loading>, <Reveal>, <Portal>, <Dynamic>) are not anchors, so a node under <Card> › <Show> still labels <Card> › <Show> › effect. The same cut applies to Effects/Memos/Propagation spans, Async flights and fallbacks, Timings markers for findings (SILENT_HOLD — <Search>) and the Server track's solid-boundary spans; wherever the label is shorter than the path, the full runtime owner path is the span's Owner path property. Presentation only: the records the engine delivers are unchanged.

  • fd36d37: @solidjs/web/performance-tracks: a Propagation track (replacing Scheduler)

    Nothing re-renders in Solid, so React's component flame has no counterpart here; the picture a Solid developer wants is the graph a write travelled. The Propagation track paints each scheduler drain as a wave named by the writes that started it and what they reached (count 0 → 1 — click on button#next · 5 runs, 1 unchanged), and every run inside it — compute runs, creation runs, effect callbacks — at its own time, labelled by what made it run (<TodoRow> › effect ← doubled). The panel stacks the runs beneath their wave by time, so a wide flat wave is a coarse signal everyone depends on, a deep one a chain of memos, and a warning node with nothing after it the equality cutoff at work; a wave that mostly re-ran unchanged nodes is itself a warning. The wave span ignores minMs, so a fan-out of runs too small to paint still reads as its count.

    Node labels fold framework structure into what the developer wrote: a flow control's own nodes (<Show>'s condition value / condition / value, a boundary's children / boundary / value, <Switch>'s conditions, <Reveal>'s reveal order) present as the tag, and a primitive.local name (the store convention, and what the compiler will emit for a composed primitive's internals) as the primitive — with the runtime's name kept in the span's Node property. Presentation only: the records are what the engine delivered.

    Engine: ChangeRecord carries nodeId (the written signal's, or the changed memo's — the same id space as RerunEvent.nodeId), so a derived cause joins the run that produced it and repeated writes to one signal join each other after the record has left the process. @solidjs/signals' boundary nodes and <Switch>'s condition-builder memo are now named in observe builds (children, boundary, value, reveal order, conditions) where they read as anonymous computeds in owner paths before.

  • fd36d37: @solidjs/web/performance-tracks: one instance per page — a second enablePerformanceTracks() joins the running one and returns its own release (HMR re-evaluation no longer paints every span twice or strands a hold); the ./performance-tracks export resolves to the inert artifact under the node/worker/deno conditions so an SSR pass takes no engine hold and emits nothing; host performance/console calls are guarded so a throw drops the entry instead of propagating into the engine's record loop; rich: false falls back to performance.measure where console.timeStamp is missing; rich mode clears only the User Timing names it owns outright, leaving an app measure that shares a label alone. Engine: fallbacks staged on a transaction are held weakly, so a transaction dropped without settling releases them.

  • 757d1eb: Performance tracks: server spans in the browser panel (Stage 5)

    Dev and observe servers now write the request's timed work as Server-Timing metrics beside the trace entries: solid-invocation;dur;desc="<id>" on a server-function response, and solid-shell;dur plus one solid-boundary;dur;desc="<owner path>" per boundary that waited and settled before the shell on a document. Dev builds always write them; observe builds only while something on the server is subscribed to the invocation / boundary records (no wire change without an observer); prod builds carry no code. desc values are sanitized to printable ASCII (a non-ASCII header value throws from Headers.append, which used to be able to hang a response whose first write carried it).

    @solidjs/web/performance-tracks reads the metrics back: a call record's response headers become <id> · server spans beneath the call, placed against the resource's responseStart (via PerformanceObserver for late-arriving entries, centred in the call when none arrives within 30 s), and the document's PerformanceNavigationTiming.serverTiming paints shell · server / boundary … · server when the tracks enable.

  • dd53561: "render" record and AttributionOptions.values — the two remaining places where the observe surface duplicated a record or scrubbed one after the fact.

    "render" record (@solidjs/web, server). A server render — renderToString or renderToStream — is now a record on OBSERVE.records: RenderEvent { mode: "string" | "stream", at, shellMs?, durationMs, boundaries, outcome: "complete" | "abandoned" | "error" }, delivered when the render ends, with RenderLive { event?: RequestEvent, trace: TraceContext } beside it. shellMs is render start → the shell complete (the stream's shell handed to the sink; the string's document assembled); boundaries counts the <Loading> boundaries the shell waited on. Types RenderEvent, RenderLive, RenderListener are exported from @solidjs/web.

    The response's Server-Timing metrics are now strictly projections of records, one gate each (observed(type) || dev): solid-invocation from the "invocation" record, solid-shell from the "render" record's shellMs, solid-boundary from each "boundary" record the shell waited on — computed from the record objects at head commit, no second push. Wire format unchanged. Behavior change (observe tier): solid-shell now rides the "render" listener, not the "boundary" listener; an observe deployment that subscribed to "boundary" alone keeps its solid-boundary metrics and needs a "render" subscription for solid-shell. Dev builds still write all three always.

    AttributionOptions.values: "full" | "labels" | "none" (@solidjs/signals, re-exported by solid-js/attribution). One engine option governs the user-data fields of the engine's records at the source: ChangeRecord.prev/value, HeldWrite.prev/value, ChangeOrigin.target (and so InteractionEvent.target, HoldEvent.interaction.target), and every sentence built from them (formatRerun, formatOrigin, SILENT_HOLD/LONG_HOLD, OPTIMISTIC_REVERTED). "full" is today's dev output; "labels" drops value previews and keeps element text only on a button or an a; "none" drops both. The default is the build tier's: "full" in dev builds, "none" in observe builds (folded at build time — the observe engine ships "none" only). Across holds the least permissive level wins; a holder naming no level asks for the tier's default, so in an observe build it tightens to "none" beside anyone, while a single holder passing "full" there gets "full"; an explicit "full" never loosens what another holder demanded. Observe-tier consumers that export records should pass their level explicitly and treat it as their export contract.

    Removed: PerformanceTracksOptions.scrub and the adapter's scrub helpers. @solidjs/web/performance-tracks paints what the engine put on the record: an observe build's tracks inherit "none" (tighter than the old scrub — no element text on buttons/links either); the old observe posture is enablePerformanceTracks({ attribution: { values: "labels" } }). Behavior change: a finding's marker always carries event.message.

    Internal: solid-js's server render context seam _timing became _recordBoundary(event: BoundaryEvent) — the boundary files its record, the web runtime projects the header from it.

  • 777916a: Revert #3615: hydrate() no longer defers on _$HY.p; the ordering it guarded against does not occur in a correctly assembled document.

  • fb35efb: Server-function calls that end in a string no longer turn an earlier undefined argument into null. search(1, undefined, "milk") took the bound form-post path, which moves the leading arguments into ?args= as JSON, so the function ran with limit = null and its default parameter never applied. A trailing string now goes through the codec like any other argument list that JSON cannot carry: with enableRichArguments() the function receives undefined, and without it the call throws the same "sent as JSON by default" error as search(1, undefined). Bound form actions (action.with(id) posting FormData, URLSearchParams or a File) keep the ?args= shape and still send undefined as null, which matches the no-JS action url.

  • 1d3ec5d: serverFunctionUrl on a live reference returns the live address (<endpoint>/live/<id>[?args=...]) — the url that reference's own call requests, so a fetch of it is the call: a standing event stream to fetch by hand (curl -N), not to preload or prefetch. It used to return the data address, which a live call never requests. The one-shot url is the inner GET(fn)'s.

    Frame attr holes: an attr re-emission now matches the element to the tag's whole attribute text the way the root morph matches server output — attributes absent from it are removed even when the emission carries no removed list (a conditional reconnect's cannot). data-lha and a <details>/<dialog> open are kept, as the morph keeps them.

  • e10a4ba: SSR element and props-view fast paths for the per-element hot path. ssrElement takes an optional trailing attrs — attribute markup the caller already holds (a spread element's trailing attributes, a class a library computed and knows is clean), a string or a thunk called after the sources are walked — appended after the props' attributes in place of a { class, style } source that was built, keyed and precedence-walked on every render; ssrElementAttribute(key, value) (compiler primitive) serializes one attribute by the spread walk's rules for such a thunk. Per tag name, ssrElement remembers one record — the <tag and </tag> markup, void, textarea, raw-text — in place of building both strings and testing the name on every element; the attribute-name escape is remembered per name; and every source body is read as props[prop] (a plain read or the proxy's trap, which is all the source helper did for these kinds), with only the key list depending on the source's kind. On a button with four props, a skip predicate and a computed class, ssrElement runs within 9 ns/element of a hand-written writer that knows its tag and keys (from 22). merge() sizes its source arrays up front and its get trap walks plain sources directly, reading before the in check. A $PROXY-marked source is classified by ONE read of a new internal brand, $RECORD — a merge or omit view answers its record, a store answers undefined on its symbol fast path, a proxy that does not know the key forwards it to a target without it — where $TARGET and then each view kind was two to four trap hops per source at every merge(), omit(), isStatic and ssrElement (a merge over an omit view: 81 → 50 ns; an omit over a merge: 63 → 36 ns; ssrElement on a view: −8%). The private $SOURCES/$OMIT/$VIEW symbols are retired for it; a store-shaped proxy that implements the brand protocol should answer $RECORD with undefined to keep the read off its generic path. No output changes.

  • 7742b28: SSR: one async source, every reader. A generator yields to one reader, so under a server render the serializer pumping a memo's answer { meta, progress: gen } and a memo reading answer().progress split the generator's yields between them (the client received half the sequence; the reading memo could miss V1). Every async iterable the runtime reads on the server now goes through a seat on a shared multicast of the source (shareAsyncIterable, solid-js/internal): one pump, the whole sequence for every seat, a log trimmed to the slowest open seat, the last seat out closes the source. The serializer takes its seat through the border walk (toBorderForm, formerly envelopeContainerTraces) at context.serialize — for iterables nested anywhere in a memo's resolved value on the document face, and for frame slot args — and the frame sink's first-yield tap for document-face slot args uses the same seats, so a server component reading a source it also passes across no longer splits it.

  • b7701b5: Nested subpath package.json files no longer declare name/exports, which shadowed the root exports map for @solidjs/web/server-functions/client under prefix-matching resolvers (Vite 8 / Rolldown, Node CJS). @solidjs/web/server-functions/rich-args now builds on Vite 8 without a resolve.dedupe workaround.

  • Updated dependencies [fd36d37]

  • Updated dependencies [fd36d37]

  • Updated dependencies [43fae6e]

  • Updated dependencies [21b9784]

  • Updated dependencies [56918c6]

  • Updated dependencies [fd36d37]

  • Updated dependencies [c74365d]

  • Updated dependencies [7599885]

  • Updated dependencies [6717d35]

  • Updated dependencies [75ed5e9]

  • Updated dependencies [a360ad0]

  • Updated dependencies [2d64742]

  • Updated dependencies [3af4696]

  • Updated dependencies [a10d33b]

  • Updated dependencies [ae2bc9f]

  • Updated dependencies [cf61b8e]

  • Updated dependencies [7742b28]

  • Updated dependencies [7742b28]

  • Updated dependencies [ed60f05]

  • Updated dependencies [f41c6a4]

  • Updated dependencies [f41c6a4]

  • Updated dependencies [fd8b3df]

  • Updated dependencies [1d3ec5d]

  • Updated dependencies [1d3ec5d]

  • Updated dependencies [1d3ec5d]

  • Updated dependencies [3218b7a]

  • Updated dependencies [b149cd2]

  • Updated dependencies [7742b28]

  • Updated dependencies [c9e1954]

  • Updated dependencies [55779c0]

  • Updated dependencies [84562fc]

  • Updated dependencies [28fcc9b]

  • Updated dependencies [235173e]

  • Updated dependencies [384a631]

  • Updated dependencies [fd36d37]

  • Updated dependencies [fd36d37]

  • Updated dependencies [fd36d37]

  • Updated dependencies [757d1eb]

  • Updated dependencies [974506c]

  • Updated dependencies [e77087a]

  • Updated dependencies [dd53561]

  • Updated dependencies [20204ec]

  • Updated dependencies [7742b28]

  • Updated dependencies [eb3d699]

    • solid-js@2.0.0-rc.10