Patch Changes
-
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: 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. -
43fae6e:
onCleanupcallbacks on one owner now run in reverse registration order (unwind), restoring the 1.x #1562 semantics; in production, component bodies share the enclosing owner, so a parent that registers cleanup before rendering its children now tears down after them, matching dev (#3572). -
21b9784: A
"recovery"record onOBSERVE.records(RecoveryEvent): the client rendering a<Loading>boundary the server handed over — its fragment rejected or the stream was cut — with how long the fallback stood (waitedMs) and what the fresh render cost (renderMs), joined to the server's"boundary"record byid. -
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: 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. -
c74365d:
createRootJSDoc now states that a root created inside an owner is disposed with it (detach withrunWithOwner(null, …)); MIGRATION note added. -
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.
-
75ed5e9:
<Errored>builds a function-valuedfallbackinside its own scope, whatever its arity — like<Show>resolving a function child. A zero-arity thunk (fallback={() => <F />}, type-reachable since() => Xis assignable to(err, reset) => X) used to be handed back unresolved for the consuming hole to build on the enclosing owner's counter, which permuted hydration keys whenever a scoped hole followed the boundary in the same element (surfaced by #3620). Zero-arity and two-arity fallbacks now allocate identically on server and client; reset, error narrowing, fallback reactivity and the devconsole.errorfor a fallback that cannot see the error are unchanged. A rest-parameter fallback ((...args) => …,length0) now receiveserrandresettoo. -
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. -
2d64742: Fix a per-request SSR memory leak for bare
ssrSource: "client"sources (#3657). A derived read of a client-only source (<Show when={client().length}>, adynamic()source memo, an<Errored>aggregate) subscribed its retry to the shared never-settling client-hole promise; those subscriptions could never fire but were never released either, pinning each request's computation, props and data for the life of the process. The client hole is now an inert thenable rather than a native promise —thendrops its callbacks — so no subscription site can accumulate anything on it. Rendered output is unchanged. -
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). -
a10d33b: Hybrid memo/signal handoff waits for the server answer to land and opens no pending window. An
ssrSource: "hybrid"createMemoor function-formcreateSignalover an async generator re-ran the client generator at creation — a fresh flight ahead of the still-pending server answer, superseding it and reading pending from creation until the client's first yield — so a streamed<Loading>resuming to claim its fragment after the answer landed selected its fallback against resolved server content: a "Hydration key miss" warning and a flash of the fallback over the streamed content (the #3574 failure, on the value-shaped takeover). The handoff now follows the store's rules (#3551, #3593): it waits for the adopted answer to land, lands that answer as the handoff stream's synchronous first step, discards the client's duplicate first yield, adopts a rejected server answer, and is superseded by a dependency change before the landing. The node reads settled through the handoff, andisPendingis false for it, until the client generator produces something new. Sync and promise-shaped hybrid computes are unchanged. -
ae2bc9f: Hybrid store hydration waits for the server's answer before handing off to the client (#3498)
Root cause: the hybrid store gate flipped synchronously at the end of the claim pass. With
loadingValue/seedLoadingValuethe server serializes a pending placeholder whose real answer arrives later over the stream; flipping before it landed let the client takeover supersede the server flight, so the engine dropped the server's answer and the store never showed it.The handoff now follows four rules:
- It waits for the first server answer to land. Synchronous when the serialized value is already settled, as before. When it is pending, the handoff happens when that answer lands — resolve or reject — by adopting it as a one-yield stream whose next pull (the engine's own continuation after the landing commits) is the handoff. It never waits on hydration end and never holds hydration open.
- Only the handoff run's first yield is the duplicate. Later runs (a dependency change,
refresh()) run the client source against the real draft and commit their first yield normally; previously every run kept discarding it. - A rejected server answer is the adopted answer. The store surfaces the error until a
refresh()(a non-handoff run) replaces it; the client's handoff run does not paper over the rejection. - A dependency change before the pending answer lands supersedes it. Like any new pending change, it cancels the incoming server answer: the store goes live on that run — genuinely new work, not a handoff, so its first yield commits — and the abandoned server flight's landing or rejection is dropped without applying to the store or running it again.
-
cf61b8e: Hybrid store handoff no longer opens a pending window (#3574). An
ssrSource: "hybrid"store's client takeover run — the re-run that continues from the adopted server answer — made the store read pending until its duplicate first yield landed, so a streamed<Loading>resuming to claim its fragment after the answer landed selected its fallback against resolved server content: a "Hydration key miss" warning and a flash of the fallback over the streamed content. The handoff now lands the adopted answer as a synchronous first step (generator and promise-shaped sources alike); the store reads settled through the handoff, andisPendingis false for it, until the client source produces something new. -
7742b28: Fix hydration adoption trace pulling an async iterable the compute did not construct. The trace-run (
subFetch) primed async-generator computes by pulling their first step under mockedfetch/Promise; it now does so only when the returned value is its own iterator (a generator object), leaving live-call iterables and deserialized codec streams untouched. Pulling a foreign adapter minted its resolver through the mockedPromise, corrupting its queue — the next streamed value threwtemp.s is not a functionfrom the document's inline runtime. Surfaces with a nested-async live answer ({ progress: asyncIterable }) read by a child memo during hydration, and as #3647: a foreign iterable whose[Symbol.asyncIterator]()is the subscription itself (the router'sliveQuery) was opened against afetchthat never settles, so it never connected after hydration. -
7742b28: Fix hydration of
<Loading on={...}>: the server's boundary now accounts for the client'sondependency node when it fakes the boundary's nesting depth, so content and thunk-fallback element keys match. Previously every element under a boundary withonmissed its key on hydration (detached duplicates, nothing interactive). The serverLoadingalso passesonthrough tocreateLoadingBoundary. -
ed60f05: A cleanup that disposes its own root (or throws) runs exactly once: the disposal list is detached before it runs (#3601).
-
f41c6a4: Fix
renderToStringexiting the process on a late async rejection (#3570). Every server async flight settles an internal deferred; underrenderToString(no serialization channel, the sync<Loading>path never awaits the pending source), in a<NoHydration>zone, or for an unread source, nothing observed it, so an async memo/store/projection that rejected after the HTML was returned became anunhandledRejection. The deferred is now observed at creation; consumers see exactly what they saw before. -
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: Server
createProjection/createStoreover an async iterable pump in frame scope like a memo does (Stage 8 B5): inside a server-owned frame render every yield lands in the store, commits the binding ledger (live holes reading the store re-emit) and holds the response until the source ends; reads follow the live state. A live-branded source there stays connected, and under a live component's document render a projection takes its first value and closes the source, so the document completes. The slot-border trace's snapshot now waits only for undrained writes, not for a pull in flight. -
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: 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. -
3218b7a: A held derivation is not a proposal (#3612). A mainline write to a writable memo (
createSignal(fn)) or function-form store (createStore(fn)) whose staging is a pass result held by another transaction no longer suppresses that transaction's re-derivation: the write still joins the transaction (A34), and the derivation re-runs under the hold with the written value asprev. Same-frame "manual write wins" (#2692) and last-write-wins for writes made inside the transaction are unchanged. -
b149cd2: - Hydration facades (
createMemo, function-formcreateSignal/createOptimistic, function-formcreateStore/createOptimisticStore,createProjection,createErrorBoundary,createLoadingBoundary, effects) no longer throw when created with no owner (runWithOwner(null, …)) or under a root without anidwhile hydrating. A node with no id counter to consume has nothing to hydrate positionally, so it takes the same non-hydrating path astransparent: true. Function-formcreateSignalnow honorstransparentlikecreateMemodoes (#3609).ssrSource: "hybrid"on a sync or promise-shaped compute is now identical to"server"for function-form stores,createOptimisticStoreandcreateProjection, matching memos and function-form signals: the serialized value is adopted and the compute does not re-run on the client (no refetch) until a dependency changes orrefresh(). The handoff — the client continuing the server's stream from the adopted answer — only arms when the compute returns an async iterable. The creation-time gate flip that memos and signals made for non-iterable shapes is gone too: its write was held by the hydration snapshot scope and replayed after hydration completed, which re-ran the compute live.
-
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 (
-
c9e1954: Loading
onfollows the frame (#3540). When a dependency ofonchanges, the boundary still stops waiting on its current content immediately — the frame no longer waits for it — but its fallback swap now lands with the same frame as the change that caused it, instead of in the current frame beside content the change is still holding. Navigating product A → B inside an action (or by a write whose async is in flight) with the shell readingproduct(id)outside a<Loading on={id()}>that readscomments(id)goes[A] → [B + spinner] → [B + comments], not[A] → [A + spinner] → [B + spinner] → [B + comments]. If the comments land before the shell, no fallback is ever shown. Nothing else holding the frame, the fallback and the committed change land together in the same pass, as before.Read
latest()inon(or any display-ahead state:isPending(), an optimistic signal) to keep the previous behavior — the fallback shows now, beside the still-held frame.If the same data the boundary is waiting on is also read outside it, the frame waits on that read and no fallback appears; DEV warns
LOADING_ON_OUTSIDE_HOLDwith the fix (move the outside read under the boundary). A frame held by the write's action or by other data past the content's landing shows no fallback either — a race, not a warning.Erroredno longer acceptson(norcreateErrorBoundaryanonoption). It was added in #3556 and never released in a stable — rc-only. Retry through theresetthe fallback receives (fallback={(err, reset) => ...}), or re-mount the boundary on the dependency (<Show keyed when={id()}>). -
55779c0:
Loading'sonprop is a dependency list, not a key (#3540). The expression is tracked and its value is never compared: a write to anything it reads — plain, optimistic, or a source going pending — re-arms the boundary. A re-armed boundary that has something pending under it shows its fallback again; one with nothing pending does nothing (no fallback flash).latest()insideonis redundant.The re-arm lands in the current frame. A write that makes content pending is held by the readers still showing the old content, and its batch commits when the data lands — but the boundary's swap to its fallback is not part of that batch: it is applied at the flush's finalize, mainline, past any transaction park, so the fallback shows now beside whatever the write is still holding elsewhere on the page. Previously the swap was staged into the pending write's transaction and landed with its commit, by which point the data had arrived and the fallback never showed whenever any other reader of the same data existed (#3524, #3529). The children are not re-created; they stay alive behind the fallback.
Erroredaccepts the sameon: while it shows its error fallback, a change to a dependency clears the caught error and retries the children (reset keys).createErrorBoundarytakes{ on }as its third argument.Boundaries are exempt from A29 born-held: a
Loadingmounted while a transaction holds what it reads shows its fallback now (and reveals the staged content at the commit) instead of being born held with the transaction. Born held stays right for a plain memo or effect — published, its value would tear the frame — but a boundary that has not revealed is the exception by definition: its job is to catch what is not ready under it rather than let it hold. This also closes the static-vs-function-child<Show keyed>inconsistency from the issue. -
84562fc:
LOADING_ON_OUTSIDE_HOLDnow recommends the structural fix (one hold owns the data, orisPending()for the wait) and mentionslatest()inononly as a capability. Docs and JSDoc forLoading onupdated to match. -
28fcc9b: The DEV
LOADING_ON_OUTSIDE_HOLDdiagnostic now reports only the deterministic shape:onre-armed a Loading boundary while the very async source it is waiting on is also read by a live reader outside it, so the frame is held on that source and the fallback can never be seen (data.sourcenames it; the fix is to move the outside read under the boundary). The after-the-fact report — the frame held by the write's action or by other pending data past the content's landing, so the staged fallback was cleared before display — is removed: that is a race the developer does not control, a fallback that loses it is a legitimate outcome, and the engine cannot tell an action that awaited exactly this data from one that awaited something slower. -
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. -
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:
@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
-
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. -
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. -
974506c: The
solid-js/refreshHMR memo is now framework plumbing to the observe tiers: an internal_plumbingmemo option (CONFIG_PLUMBING) leaves it unnamed, out of every owner path, and unrecorded by the attribution engine — no creation or re-run record of its own — while the component body it runs stays fully observed. Replaces the empty-name workaround from #3629, which still produced a blank-namedcreaterecord per component mount. -
e77087a: The
solid-js/refreshHMR wrapper no longer appears in owner paths. Its memo is nameless, so a component reads as<App> › <Router>in performance-track labels, diagnostic findings and captures rather than<App> › [solid-refresh]App › <Router>; the proxy also carries the component's own name, so an unlabelledcreateComponentopens a<App>root rather than<[solid-refresh]App>. -
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. -
20204ec: Server
scopereserves the hydration slot without formatting an id string until a child needs one; recovers most of the renderToString cost added by #3599. -
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. -
eb3d699: Dev: warn with
UNTRACKED_READ_AFTER_AWAITwhen an async computation first reads a signal, memo, or store property after anawait. Such reads are not dependencies, so the computation silently keeps its old result when they change. The check attributes the read to its computation through V8 async stack traces (Chromium browsers, Node, Deno, Bun; silent on other engines), runs only in dev builds, keeps dev settle timing identical to production, warns once per computation per signal/memo and once per store, and does not blame a continuation for reads made by effect callbacks, cleanups, oraction()bodies it triggered. -
Updated dependencies [fd36d37]
-
Updated dependencies [fd36d37]
-
Updated dependencies [fd36d37]
-
Updated dependencies [43fae6e]
-
Updated dependencies [fd36d37]
-
Updated dependencies [c74365d]
-
Updated dependencies [7599885]
-
Updated dependencies [6717d35]
-
Updated dependencies [f884589]
-
Updated dependencies [1d3ec5d]
-
Updated dependencies [fd36d37]
-
Updated dependencies [873187b]
-
Updated dependencies [a124577]
-
Updated dependencies [3af4696]
-
Updated dependencies [3613c3a]
-
Updated dependencies [ed60f05]
-
Updated dependencies [739404d]
-
Updated dependencies [ab254b0]
-
Updated dependencies [ebc1b03]
-
Updated dependencies [1d3ec5d]
-
Updated dependencies [886850b]
-
Updated dependencies [3218b7a]
-
Updated dependencies [d2d7bd4]
-
Updated dependencies [7742b28]
-
Updated dependencies [40c65cd]
-
Updated dependencies [9c6c5cd]
-
Updated dependencies [5f28e7d]
-
Updated dependencies [c9e1954]
-
Updated dependencies [55779c0]
-
Updated dependencies [84562fc]
-
Updated dependencies [28fcc9b]
-
Updated dependencies [235173e]
-
Updated dependencies [f2bd662]
-
Updated dependencies [756b1b3]
-
Updated dependencies [384a631]
-
Updated dependencies [658eecd]
-
Updated dependencies [1a7d14f]
-
Updated dependencies [5351a3e]
-
Updated dependencies [cb2fa7b]
-
Updated dependencies [53498dd]
-
Updated dependencies [c8a9d23]
-
Updated dependencies [fd36d37]
-
Updated dependencies [fd36d37]
-
Updated dependencies [fd36d37]
-
Updated dependencies [310116a]
-
Updated dependencies [4b62bc2]
-
Updated dependencies [974506c]
-
Updated dependencies [dd53561]
-
Updated dependencies [e10a4ba]
-
Updated dependencies [1f40560]
-
Updated dependencies [c1b68d9]
-
Updated dependencies [eb3d699]
-
Updated dependencies [709c02b]
-
Updated dependencies [c25d69f]
-
Updated dependencies [27bb3fa]
-
Updated dependencies [9e65de2]
- @solidjs/signals@2.0.0-rc.10