Patch Changes
-
ce17c29: Type the anchor's scroll opt-out as
noscroll, notnoScroll(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"));noScrollwas the one camelCase outlier, so the spelling the router README shows was a type error. Runtime is unchanged:setAttributeand 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, likesubscribe), 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,historyLimitis the largest), so a hold adds to what the engine does and never takes away what another asked for — a track enabled withlog: falsebeside 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 callingdisable()once cannot strand a hold.enablePerformanceTracksreleases through the token. Dev builds create a component'sconsole.createTasktask 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, likesubscribe). 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,historyLimitis 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(defaulttrue).falseturns 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. isSilentHoldandisLongHoldare exported from@solidjs/signals/attribution(inert in prod), so consumers apply the engine's own hold verdicts instead of thresholds of their own.InteractionEvent.atis now the browser event's owntimeStampwhen@solidjs/webdispatches the handler (guarded against epoch-clock stamps), so it equalsPerformanceEventTiming.startTimefor the same interaction — a direct join to INP. NewInteractionEvent.inputDelayMsreports event creation → handler entry;handlerMsis now entry → return, andsettledMscontinues to measure fromat.
-
fd36d37: Attribution engine: timeline records, and a
flushStarthookFive 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) andfallback(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.subjectOfanswers for the node-bearing ones.Core: a
flushStarthook besideflushEnd(one drain, never nested), andeffectRunStartnow fires in observe builds like itseffectRunEndtwin, so writes inside effect callbacks carry theireffectorigin 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-trackspaints the new records: creation runs and effect callbacks on theEffects/Memostracks, drains on thePropagationtrack (one wave per drain), flights and fallbacks on a newAsynctrack. -
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'squery()-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-declaresGET(); aGET()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
renderToStreamconsumer cancels the readable or apipesink throws, the render is disposed, but the tapped iterator handed to the serializer kept delegatingnext()to the source, so an iterable memo, an async memo that resolves to an iterable, and a generator projection kept pulling (or never ran theirfinally) for as long as the source lived. The tapped iterator now answersdoneand calls the source'sreturn()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 pendingnext()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 effectdiv.spreadand its children insertdiv.children. The names travel as a trailing options argument oneffect/insert({ name }) and a trailing string onspread;@solidjs/web'seffect,insert, andspreadaccept 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 readsspan.textContent ← countinstead ofeffect ← count. Production ignores the names; output with the option off is unchanged. DOM output only;sourceNames: trueturns it on withcomponents. -
fd36d37: Rename the compilers'
componentNamesoption tosourceNames, nowboolean | { components?: boolean }.sourceNames: true(or{ components: true }) is whatcomponentNames: truewas — the tag as written in source ascreateComponent'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/compilerrejectscomponentNamesas an unknown option, so a stale@solidjs/vite-pluginfails 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'sinsertat the statement, the server'sssr()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.Elementexcludes 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 raiseUNSCOPED_HOLE_ALLOCATED_IDS(warn, kindrender, 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.afterafter 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-arityfallback={() => <F />}thunk built by the consuming hole is that shape).data.nameis 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 bothsolid-jsfacades (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 ofsolid-jsand@solidjs/webare byte-identical to before. -
6717d35: Diagnostic code consolidation. Codes are public API (the Sentry fingerprint roots); three renames.
WIDE_WRITEis folded intoHUGE_FAN_OUT— one code, one threshold story. The core's always-on check warns at 2000 subscribers; the attribution engine'sfanOutthreshold (default 250, waswideWrites) warns earlier while it is enabled and reports through the same emitter, sodata.countis the subscriber count and, from the engine,data.writesays which write reached it ("write","refresh","async"). One WeakMap dedupes both reporters (re-warn after another 500).AttributionOptions.wideWrites→fanOut.SERVER_FN_ERROR_SANITIZEDandSSR_ERROR_SANITIZEDare one code,SERVER_ERROR_SANITIZED— the same fact from two roads.data.sourceis"server-function"(severityerror, from@solidjs/web/server-functions) or"ssr"(severityinfo, from the SSR<Errored>/rejection path);data.erroris the original anddata.wirethe replacement on both.ASYNC_WATERFALLis client-only again: the server's boundary-passes verdict is its own code,SSR_BOUNDARY_WATERFALL(kindssr;infofor two sequential waits,warnfor three or more;data: { boundary, passes, sequentialMs }).data.sideis 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'sReadableStreamis 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). AReadableStreamnow 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 admitsAsyncIterable<T>— the answer of aliveserver component reference. Type-only:dynamicis 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 deliversnext's address when it is not the one showing.Signals: the lane landing in
asyncWritenow calls a userequalscomparator 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, anddynamic({ 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 +$$froute as one found at discovery, rather than rejecting the fragment over an empty region. -
bfc320c: Let the
csrf.originallowlist 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 refusedSec-Fetch-Site: cross-site(andsame-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-setOriginis decided by the matcher: refused when none is configured (today's default) or when it does not answertrue;nonestays refused. An admitted cross-origin caller getsAccess-Control-Allow-Originechoing its exactOrigin(withVary: Origin), the protocol's response headers exposed, and theOPTIONSpreflight answered for the transport's methods and headers — on every response, the labelled unknown-id 404 included.Access-Control-Allow-Credentials: trueis sent only with the newcsrf.allowCredentialsoption, 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, everyGET-declared read carriesVary: Origin— the read stays ungated andAllow-Originappears 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 noVary, as before. The client'sendpointoption now documents an absolute URL, withcsrf.originas 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 wrappedquery()orasyncarrow 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>/onErrorin every tier). Move the async upstream (createAsync/createMemoread synchronously by the source) or uselazy().Wire shape: each async
dynamic()instance now writes one hydration record, and everydynamic()instance allocates one more hydration owner id (keys under adynamic()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"— butframeTransformFlightResultscanned only the top level ofdatafor 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'screateFlightDataCollectorwith 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: truethat 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.consumerand.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 oneframeTransformFlightResultread — 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/serverand 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.jsdrops from ~155 KB to ~44 KB.ChunkReader,createChunk,frameAddressandserializeStreamare now re-exported from@solidjs/web/server-functions/server(internal transport building blocks the frames artifact imports). -
495db9c: Remove a sole text child of
0orNaNwhen 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
renderToStreamhanging or throwingTypeError: 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()/readableclose the writable — and the serializer's completion no longer assembles a shell on the disposed render.
-
fd8b3df: Fix
hydrate()halting withTypeError: Cannot read properties of null (reading '_config')when auseHead({ 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. ThewaitAssetgate memo is created without an owner, and the hydratingcreateMemotried to peek a hydration id from that null owner; the gate is nowtransparent, so hydration never sees it.useHeadalso 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_frrecord 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 aholesmap;fragment/hole/attr: their own), the mount keeps a ledger of applied content (Frame.have()), and theliveloop asks the frames handler per connect (responseHandler.resume) so a reconnect sends the address's version ordinal asLast-Event-IDand the ledger asX-Frame-Have. A frame render given that list (FrameStreamOptions.resume.have, read byframeTransformResultat 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.FrameChunkgainsholeandattrmembers;FRAME_HAVE_HEADER/FRAME_HAVE_BUDGETare exported from the frames client and server entries. -
1d3ec5d: Frame renders tear down when their reader is gone.
serverComponentResponse's bodycancel()and the request's abort (frameTransformResult/frameTransformFlightResultpassevent.request.signal) now dispose the render throughrenderToStream's disconnect path — previouslycancel()only dropped writes and the render ran on until its sources happened to end.renderToStreamgains asignal?: AbortSignaloption for this (theSSR_STREAM_ABANDONEDfinding reports it asdata.reason: "signal");renderToFrameStream,renderServerComponentandserverComponentResponseaccept it through their options. A frame flight response stops at the frame in progress and skips the rest. Insolid-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 throughlive(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),completeis a completion, supersession from another response cancels the connection so the loop reconnects, and one live connection is held per address.getServerFunctionInvocation()reportslive;serverComponentResponsetakeslive. Without a loop, a frame response ending before a started frame'scompleteis now that frame's error. -
1d3ec5d: Live server components on the document face (Stage 8 B3)
Server: a component a
liveserver 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 checkSSR_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 hydratingdynamic()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 adynamic(() => 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:
livecalls move to a live address,<endpoint>/live/<id>, answered in event-stream framing.A
liveloop 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 perdata: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'sid:; the loop echoes the last one back asLast-Event-IDon 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:
liveclaims 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 anopen()accessor besideabort()). 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'sLast-Event-ID; a takeover that finds the same value on the server yields nothing.
- A live connection now lives as long as its response: an answer holding nested promises or async iterables (
-
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 atonSettled, 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-IDand the wire skip are unchanged. -
235173e: Prune the observability surface:
OBSERVE.attribution.install, theAttributionHookstype,DEV.setConsoleFooter, theTraceSlotandOriginRefaliases,PerformanceTracksOptions.group, and the untestedsolid-js/refreshruntime modes (esm,webpack5,rspack-esm) are gone —@solidjs/compiler'stransformRefresh({ bundler })accepts only"vite" | "standard"to match.ownerPath()is nowOBSERVE.ownerPath(subject)anddiagnosticGuideUrl()isDEV.guideUrl(code);why()also accepts a scope name. Engine record types (RerunEvent,HoldEvent, …) live onsolid-js/attributiononly, andInteractionRef/NavigationRefon the main entries only.@solidjs/diagnosticstypes its record tables off the runtimes' own catalogue (RecordEvent<K>), adds therecoverytable, and bumps the artifactformatVersionto 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 routerquerywhose 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
registerFragmentparks 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_frpromise 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) areRecordTypesentries delivered onOBSERVE.records.subscribe(type, (event, live) => …), with the live node beside each record. Removedattribution.subscribe(both overloads),OBSERVE.subjectOf, theAttributionRecords/AttributionRecordTypetypes, and theisSilentHold/isLongHoldhelpers —HoldEventnow carriessilentandlong, computed at settle.attribution.history(),waterfalls(),holds(),navigations()andinteractions()collapse intoattribution.history(type).DiagnosticListenerreceives the subject as its second argument. The channel allocates nothing per emit (copy-on-write listener lists), and aRerunEventis built only while a listener, a fold or the log wants it. Record listeners belong to the channel and are no longer dropped byattribution.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 webspreadlabels ride a module-levelspreadNamethateffectandinsertread while the spread body runs (no options argument at the call sites), and the boundary node names gate the call or set_nameafter it. The minified prod bundles are structurally identical tonext(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 panelenablePerformanceTracks(options?)paints the attribution engine's records — re-runs (Effects/Memos, coloured by self time,warningfor a provably wasted run), interactions (input delay, handler, settle by outcome), holds (warningfor a silent hold,errorfor a long one — the engine's own verdicts), navigations (named by route) — and the web runtime's server-functioncallandframerecords (Server) as custom tracks in the groupSolid, through the panel's extensibility API. Every span is emitted retroactively from the record's ownperformance.now()stamps; labels come from the shared formatters (formatOrigin,formatRerun,ownerPath), so the timeline agrees with the diagnostics artifact by construction. Dev builds emitperformance.measureentries withdetail.devtools(why-chain tooltips, cause/deps/blocker properties; entries cleared in batches); observe builds emitconsole.timeStampspans with a0.05msfloor 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: falseby 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-jsnow exportsownerPath(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
DiagnosticEventdelivered whileenablePerformanceTracks()is on becomes a marker on the Performance panel's Timings track (SILENT_HOLD — <App> › <Search>), coloured by severity; atwarnor 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 idand the root write'sOriginto every node span. solid-jsexportsdiagnosticGuideUrl(code)— the repair guide URL the console footer already prints.
- Every
-
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 toEffects/Memos/Propagationspans,Asyncflights and fallbacks, Timings markers for findings (SILENT_HOLD — <Search>) and theServertrack'ssolid-boundaryspans; wherever the label is shorter than the path, the full runtime owner path is the span'sOwner pathproperty. Presentation only: the records the engine delivers are unchanged. -
fd36d37:
@solidjs/web/performance-tracks: aPropagationtrack (replacingScheduler)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
Propagationtrack 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 awarningnode with nothing after it the equality cutoff at work; a wave that mostly re-ran unchanged nodes is itself awarning. The wave span ignoresminMs, 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>'scondition value/condition/value, a boundary'schildren/boundary/value,<Switch>'sconditions,<Reveal>'sreveal order) present as the tag, and aprimitive.localname (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'sNodeproperty. Presentation only: the records are what the engine delivered.Engine:
ChangeRecordcarriesnodeId(the written signal's, or the changed memo's — the same id space asRerunEvent.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 anonymouscomputeds in owner paths before. -
fd36d37:
@solidjs/web/performance-tracks: one instance per page — a secondenablePerformanceTracks()joins the running one and returns its own release (HMR re-evaluation no longer paints every span twice or strands a hold); the./performance-tracksexport resolves to the inert artifact under thenode/worker/denoconditions so an SSR pass takes no engine hold and emits nothing; hostperformance/consolecalls are guarded so a throw drops the entry instead of propagating into the engine's record loop;rich: falsefalls back toperformance.measurewhereconsole.timeStampis 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-Timingmetrics beside the trace entries:solid-invocation;dur;desc="<id>"on a server-function response, andsolid-shell;durplus onesolid-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 theinvocation/boundaryrecords (no wire change without an observer); prod builds carry no code.descvalues are sanitized to printable ASCII (a non-ASCII header value throws fromHeaders.append, which used to be able to hang a response whose first write carried it).@solidjs/web/performance-tracksreads the metrics back: acallrecord's response headers become<id> · serverspans beneath the call, placed against the resource'sresponseStart(viaPerformanceObserverfor late-arriving entries, centred in the call when none arrives within 30 s), and the document'sPerformanceNavigationTiming.serverTimingpaintsshell · server/boundary … · serverwhen the tracks enable. -
dd53561:
"render"record andAttributionOptions.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 —renderToStringorrenderToStream— is now a record onOBSERVE.records:RenderEvent { mode: "string" | "stream", at, shellMs?, durationMs, boundaries, outcome: "complete" | "abandoned" | "error" }, delivered when the render ends, withRenderLive { event?: RequestEvent, trace: TraceContext }beside it.shellMsis render start → the shell complete (the stream's shell handed to the sink; the string's document assembled);boundariescounts the<Loading>boundaries the shell waited on. TypesRenderEvent,RenderLive,RenderListenerare exported from@solidjs/web.The response's
Server-Timingmetrics are now strictly projections of records, one gate each (observed(type) || dev):solid-invocationfrom the"invocation"record,solid-shellfrom the"render"record'sshellMs,solid-boundaryfrom 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-shellnow rides the"render"listener, not the"boundary"listener; an observe deployment that subscribed to"boundary"alone keeps itssolid-boundarymetrics and needs a"render"subscription forsolid-shell. Dev builds still write all three always.AttributionOptions.values: "full" | "labels" | "none"(@solidjs/signals, re-exported bysolid-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 soInteractionEvent.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 abuttonor ana;"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.scruband the adapter's scrub helpers.@solidjs/web/performance-trackspaints 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 isenablePerformanceTracks({ attribution: { values: "labels" } }). Behavior change: a finding's marker always carriesevent.message.Internal:
solid-js's server render context seam_timingbecame_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
undefinedargument intonull.search(1, undefined, "milk")took the bound form-post path, which moves the leading arguments into?args=as JSON, so the function ran withlimit = nulland its default parameter never applied. A trailing string now goes through the codec like any other argument list that JSON cannot carry: withenableRichArguments()the function receivesundefined, and without it the call throws the same "sent as JSON by default" error assearch(1, undefined). Bound form actions (action.with(id)posting FormData, URLSearchParams or a File) keep the?args=shape and still sendundefinedasnull, which matches the no-JS action url. -
1d3ec5d:
serverFunctionUrlon alivereference 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 innerGET(fn)'s.Frame attr holes: an
attrre-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 noremovedlist (a conditional reconnect's cannot).data-lhaand a<details>/<dialog>openare kept, as the morph keeps them. -
e10a4ba: SSR element and props-view fast paths for the per-element hot path.
ssrElementtakes an optional trailingattrs— 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,ssrElementremembers one record — the<tagand</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 asprops[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,ssrElementruns 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 itsgettrap walks plain sources directly, reading before theincheck. 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 answersundefinedon its symbol fast path, a proxy that does not know the key forwards it to a target without it — where$TARGETand then each view kind was two to four trap hops per source at everymerge(),omit(),isStaticandssrElement(a merge over an omit view: 81 → 50 ns; an omit over a merge: 63 → 36 ns;ssrElementon a view: −8%). The private$SOURCES/$OMIT/$VIEWsymbols are retired for it; a store-shaped proxy that implements the brand protocol should answer$RECORDwithundefinedto 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 readinganswer().progresssplit 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, formerlyenvelopeContainerTraces) atcontext.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.jsonfiles no longer declarename/exports, which shadowed the root exports map for@solidjs/web/server-functions/clientunder prefix-matching resolvers (Vite 8 / Rolldown, Node CJS).@solidjs/web/server-functions/rich-argsnow builds on Vite 8 without aresolve.dedupeworkaround. -
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