Patch Changes
-
215de3b: Align store overloads across the signals, client, and server entry points. Plain stores share
StoreOptions, projection forms shareProjectionOptions, plain optimistic stores expose their existing options argument, and derived optimistic stores are typed as refreshable. -
1a1e2f2: Attribution:
feedback()— what the user waited on, as ranked tables.DEV.attribution.feedback()is a pure fold over the records the engine already keeps —holds()and the interaction on each re-run — with no measurement or hook sites of its own, the waycosts()folds re-runs into scope and write tables.sourcesranks each set of async sources that held writes by the silent time spent behind them, withholds/heldMs/worstMs,silent/silentMs,acknowledgedBy(which affordance answered and in how many holds — a source acknowledged on one screen and silent on another reads as exactly that),latestOnly(answered only by alatest()shadow), theinteractionsthat were held, the distinctwrites, andactions.interactionsranks user events (type + target; repeated dispatches fold together) by total cost, pairing the synchronous re-run work one dispatch caused (runs,selfMs,worstDispatchMs— the long-flush hazard) with the time its writes spent held (holds,heldMs,silentMs,worstHoldMs— the silent-hold hazard): the two INP failure modes as columns of one row. Every hold counts at any duration;SILENT_HOLDremains the thresholded verdict over the same records. New exported typesFeedbackSourceandFeedbackInteraction; the reactivity-diagnostics skill gains a "where to start" entry, andsolid-js's console footer names the surface. -
7c14e23: Attribution: write provenance — who performed a change.
Every root
ChangeRecordnow carriesorigin: the imperative frame that made the write.interaction(a user event — type, described target such asbutton#next "Next →", and dispatch time),effect(the callback's name),action(the generator's name),async(the landing's node), orexternal(timers, sockets, promise callbacks — including writes after anawaitrather than ayieldinside an action, the documented transaction escape). Frames nested under an interaction carry it: an action a click started (every step, including post-yieldresumptions), an effect whose run a click's write caused, an async flight a click's write launched. Why-chains print the origin after the write;RerunEvent.interactionandHoldEvent.interactionexpose the interaction a run or hold traces back to, andSILENT_HOLDnow opens with what the user did and measures the wait from the event, not from the first parked flush.@solidjs/webdeclares the interaction around its two dispatch sites — delegated events (onClick,onInput,onKeyDown, pointer events: every INP-relevant type) and runtime-attached direct handlers (spreads, non-literal handler expressions) — via the newDEV.attribution.withInteraction(ref, fn), which custom renderers and test harnesses can call themselves. New core dev hookseffectRunStart/effectRunEnd(replacingeffectRun) andactionStepStart/actionStepEnd; all sites fold out of prod, verified byte-identical against the size scenarios. -
c6c415b: Mark
createTrackedEffectas@deprecated. It is retained to ease 1.x migration, but it should not appear in new code: usecreateEffect(compute, effect)for side effects that follow reactive state (it separates tracking from the side effect, knows its dependencies before it runs, and participates in async and transitions) andonSettledfor one-time DOM work after render.onSettledis unaffected (it uses the internal tracked-effect node directly). -
bd22ac8: Rename the three legacy client dev artifacts to the
<entry>.dev.{js,cjs}convention every other dev build already uses:solid-js/dist/dev.*→dist/solid.dev.*,@solidjs/web/dist/dev.*→dist/web.dev.*,@solidjs/universal/dist/dev.*→dist/universal.dev.*. With server dev builds now shipping asdist/server.dev.*, a baredev.jsno longer says which entry it is the dev build of. Theexportsmaps are updated; only code deep-importingdist/dev.jsdirectly (bypassingexports) is affected. -
6c8c956: Diagnostics console addressability: compiled JSX binding effects (attribute, class, style, property, spread, insert) are tagged in dev with the element they write, and a console diagnostic about such an effect prints that element as a second argument — hover highlights it on the page, click jumps to it in the Elements panel. Why-chains (
DEV.attribution.enable()logging) print as collapsed console groups, one headline per run with the causes inside. The once-per-code footer now pairs the installed skill path with the file's stable GitHub URL, anchored to the code's section. -
d5aba4b:
WIDE_WRITEandHOT_SCOPE_FANOUTdiagnostics, and the reactivity-diagnostics and agent-loops skills, now prescribe a projection (createProjection, or acreateStore(fn)keyed by id) as the fan-out repair. They previously named an API that is not part of 2.0 (#3304). -
3ae0ca0: Diagnostics locate themselves and report once.
- Every
DiagnosticEventnow carriesownerPath— the root-first chain of named owners enclosing the subject (["<App>", "<TodoRow>", "effect"]). Component roots are labeled<Name>bysolid-js's dev component wrapper, so the path reads as the component tree down to the scope; owned-scope write errors in a component body now say(in <TodoRow>). - Console reports are a single entry per finding: message, an
in <App> › <TodoRow> › effectline, and the once-per-code repair footer as trailing lines — the footer no longer lands as a separate, duplicate-looking[CODE]line. Advisory (info) events emit no footer at all. ASYNC_OUTSIDE_LOADING_BOUNDARYfires once perrender()instead of once per pending render effect (N async siblings at mount produced N copies).- New dev-only helpers on the signals core:
reportDiagnostic(entry)(the console face) andownerPath(subject);emitDiagnostictakes an optional subject (defaulting to the ambient reactive context).
- Every
-
1a1e2f2: Diagnostics: the responsiveness gate — holds and feedback in the artifact,
expectNoSilentHolds, and Loop 4.The artifact (format v2) now carries
attribution.holds— every transition hold the scenario caused, with the held writes, the blockers, the wait measured from the interaction, and which affordances acknowledged it — andattribution.feedback, the rankedsources/interactionstables folded from them. JSONL egress emitsholdandfeedbackrecords; the browser bridge and the/__solid/diagnosticsprotocol gainholds()andfeedback()live queries.New gates:
expectNoSilentHolds(artifact, { maxSilentMs })fails on any hold the screen never acknowledged (noisPending()/latest()reader, no optimistic value, noaffects()mark, nothing painted) with the interaction, held write, blocker, and duration as evidence;expectHoldBudget(artifact, ms, { source })bounds hold latency regardless of acknowledgment.ScenarioBudgetgainsmaxSilentHoldMsandmaxHoldMs; Vitest gainstoHaveNoSilentHolds()andtoStayWithinHoldBudget(ms). The agent-loops skill gains "Loop 4 — Responsiveness": readfeedback.sourcesfirst, repair by shape (isPending→latest→createOptimistic), and the explicit anti-repair — never make the gate pass by moving the write off the async path. TypesHoldEvent,ChangeOrigin,AttributionFeedback,FeedbackSource,FeedbackInteractionare exported. -
6c8c956: Attribution:
EFFECT_RELAY_TEARdiagnostic — derived state kept in sync by an effect (createEffect(() => f(a()), v => setS(v))). "Should have been a memo" is a claim about intent the runtime cannot see; what it can see is the harm: every scope that reads bothaandSruns twice for one write ofa— once in the flush whereachanged (against the staleS), once after the effect's write lands — and the first frame was inconsistent. The engine proves that from the cause chain (a re-run whose root writes all came from effects, one of whose runs shares a root write with the victim's previous run) and reports it once per relay, with intent heuristics as message modifiers rather than gates:copy(the written value is the effect's compute output — by contract a pure function of its tracked reads, so derivable; warns immediately, and on its own after two runs even with no double-running reader, since everything reading the copy paints a flush behind the source),passthrough(the compute output is one of the effect's sources — the prop-to-state port: read the source directly), andsoleWriter(nothing else writes the signal). A tear whose write is none of these isinfo(a DOM-measurement effect tears legitimately — the cost of measuring) until the same relay has torn three times. Thereactivity-diagnosticsskill documents the code and repairs. -
1a1e2f2: Attribution:
EFFECT_WRITES_OWN_SOURCEdiagnostic. An effect whose callback writes a value its own inputs depend on converges (the second run finds nothing to change) rather than looping, so the flush guard never fires — yet the flush settled in two passes and the screen rendered the pre-write value in between. The engine now walks each effect re-run's cause chain (root writes, through any depth of memos) and, when a root write's effect origin resolves to the effect that is re-running, reports the cycle once:warnfor a single effect (the written value is a function of what the effect reads — make it a memo, or normalize where the source is written),infofor a cycle relayed across several effects (each effect-origin write is joined to the run that made it, so the walk continues hop by hop). Effect-originChangeOriginframes gainrun, theRerunEvent.runwhose effect phase performed the write. Thereactivity-diagnosticsskill documents the code and repair. -
b6a90f9: Fix
deferStreambeing a silent no-op inside a code-splitlazy()component (#3299). A module load is code, not data: the shell's "no new async discovered during the sync render" rule cannot be evaluated for a segment whose code has not run, so the shell now waits for the chunk even under a<Loading>(the boundary still owns the data the loaded code discovers — plain async streams behind the fallback as before, and adeferStreamread inside the chunk holds the shell exactly like one in an eagerly imported component). Only the first render that reaches an un-preloaded chunk pays; a lazy mounted by a post-shell fragment streams as before.Also closes a gap in the flush loop where a shell blocker registered while a boundary resumed during the drain — after the awaited set had settled but before the flush attempt snapshotted it — was never re-awaited.
dynamic()keeps streaming its source by default (a source is data of unknown cost) and gains adeferStreamoption to opt into holding the shell on it, with the same meaning ascreateMemo's. -
3424f9a: Consume the first value of live-derived stores during SSR and reconnect their live sources after hydration.
-
6c8c956: Attribution:
IMMUTABLE_UPDATE_IN_STOREdiagnostic. A store setter that replaces a container with a fresh object or array whose leaves are mostly the same values —draft.user = { ...draft.user, name },draft.items = [...draft.items, x],draft.items = draft.items.filter(…)— is the React habit the store does not need: it tracks leaves, so a fresh container makes every reader of the container's path re-run for the one leaf that moved. The store's write-channel notify now announces replaced containers to the attribution engine with a leaf census (identity on unwrapped values; object keys by key, array items by membership; containers over 64 leaves are skipped), and the engine warns once per store path when at least half the leaves carried over unchanged, naming the draft mutation that touches only the changed key or index andreconcile()for data arriving from outside. Genuinely new data (nothing carried over), draft mutation, andreconcile()do not report. NewAttributionHooks.storeReplacedhook point. -
f4d3c87: Responsiveness thresholds and the LONG_HOLD diagnostic.
SILENT_HOLDdefaults tighten toholds: { infoMs: 100, warnMs: 200 }(from 300/500): RAIL's "feels instant" ceiling and the INP "good" ceiling. The engine measures to the commit, not the paint, so every number is a floor on what the user saw; the console's thresholds now sit at the strict end of the band.- New
LONG_HOLD(responsivenesskind): an acknowledged hold whose quiescent tail — from the last write to join it to the commit — reachedlongHolds.infoMs(default 500ms),warnfromlongHolds.warnMs(1000ms). Measured from the last join so a hold that keeps taking input is judged by each wait, not its lifetime. The repair is a fallback: aLoadingboundary keyed withon(a revealed boundary withoutonkeeps the old content — that is the hold), a fresh boundary, or making the data fast. A silent long hold stays oneSILENT_HOLDwith the same repair appended anddata.long: true. HoldEvent.tailMsadded;holdMsnow runs from the interaction dispatch or the first parked flush, whichever is earlier (a node rewritten mid-hold keeps only its latest record, so the flush clock keeps the first wait from being forgotten).ChangeRecord.atstamps root writes.feedback().sources[].late/lateMsreplaced bylong/longMs: holds whose tail reached the long-hold threshold, acknowledged or not.@solidjs/diagnosticsartifact format version 3 (tailMson holds,long/longMson sources); hold evidence in assertion failures includestailMs.RerunEvent.phasevalue"transition"renamed to"held"("plain" | "held" | "optimistic") — the dev surface uses one word for the state.
-
d601119: Remove the experimental patch channel and patch-mode list driver (always opt-in, never default). Graph-native regions own value delivery and the unified-For design owns list structure, so the channel's parallel delivery machinery is retired:
patch.ts/patch-driver.tsdeleted, the compiler-contract exports (registerPatch/registerRowOps/registerSlotPatch/patchableRaw,patchDriver/rowProof/driveList) removed, thepatchDrivercompiler option dropped from both compilers, the insert$llseam stripped, and the write-side channel struct dieted to the single written-keys bound (t.wk) the core fold/notify paths actually use. Store-family app bundles reclaim up to ~900 B brotli; every measured tier shrinks. -
ac5159a: Preserve the supplied type in
Store<T>instead of adding a shallow readonly mapping. -
de1c8b5: Revert the complete-seed requirement on derived store forms (#3258). Derived
createStore,createProjection, and derivedcreateOptimisticStoreacceptPartial<T>seeds again, on maintainer review: requiring a fullTforces callers to fabricate a throwaway complete object in the common async case — any object store reconciling on a non-idkey needs the options slot, hence the seed slot — while the seed is never observable there (reads pend until the first resolution). The type-honesty concern it addressed is real only for sync draft-reading callbacks and is better served by the seedless-callback direction discussed in #3194. Since #3258 never shipped in a release, its pending changeset is dropped rather than superseded; the API is unchanged from 2.0.0-rc.6. The #3260 overload alignment (slot order,shallowin options,Refreshablederived returns) is unaffected. -
80ff52e: Add development server builds —
dist/server.dev.*forsolid-jsand@solidjs/web, andframes/dist/server.dev.*— selected by thedevelopmentexport condition nested undernode/worker/deno(nesting is required: those conditions precede the top-leveldevelopmentkey, so a top-level entry never matched on a server). Until now SSR had no dev build: the only server artifact was built with_SOLID_DEV_stripped, so the server runtime's dev checks (head/preload descriptor validation,useHeadwarnings, the committed-response header guard) never ran outside the test suite.The server entries now gate their public dev flags on the same
_SOLID_DEV_replace as their internals instead of hard-coding them:solid-js's serverDEVis@solidjs/signals'DEVobject in the dev artifact (soDEV.diagnostics.subscribe/capturework server-side) andundefinedin prod;@solidjs/web's serverisDevistruein the dev artifact andfalsein prod.Behavior change for dev SSR hosts that pass the
developmentcondition (Vite dev does by default): a header write after the response has committed now throws with the offending header named, where the production artifact continues toconsole.errorand drop the write.Also runs
replaceDev(false)onsolid-js's production server build so a future_SOLID_DEV_gate insrc/server/cannot constant-fold into the dev branch in production. -
01e3a57: Attribution: transition holds and the
SILENT_HOLDdiagnostic.When a write lands on async work the runtime holds it until the data settles — correct, but from the user's side the click did nothing until then. The attribution engine now records every such hold that staged a root write (
DEV.attribution.holds(): duration, parked flushes, the held writes with their values, the async blockers, and which affordances answered it), and emitsSILENT_HOLDwhen the screen provably rendered no acknowledgment: noisPending()/latest()reader anywhere downstream of the held writes or their blockers, no optimistic value, noaffects()mark, and no effect ran inside the parked flushes. The verdict is tiered byholds: { infoMs, warnMs }(default 300/500ms): advisory on the structured channel, then a consolewarnnaming the write, the blocker, and the concrete repair —isPending(() => blocker()),latest(source), orcreateOptimisticfor actions. Holds with no root write (initial loads, barerefresh()) are never judged.New dev hook points on the core (
effectRun,holdStart/holdEnd,transitionSettled,transitionMerged) sit outside everytryand fold out of prod — verified byte-identical against the size scenarios.DiagnosticKindgains"responsiveness";solid-js's console footer teaches the attribution surface for it, and the reactivity-diagnostics skill documents the repair. -
6c8c956: Attribution:
UNSTABLE_LIST_IDENTITYdiagnostic. When amapArray/<For>update disposes and recreates most rows while the entering items are field-for-field equivalent to the ones they replaced (a re-fetch handed back fresh objects for the same records under identity keying, or a key function returned unstable keys), every row's DOM and state was thrown away and rebuilt for data that did not change.mapArraynow hands the exited and entered items to the attribution engine after a churning commit (AttributionHooks.listChurn); the engine pairs them (byid/key/_idwhen present, else by position), samples shallow equivalence, and warns once per list naming the repair — key by a stable field or merge withreconcile(data, "id"), or, when a key function is already in use, return a stable field from it.mapArraynodes now carry thenameoption as their node name in dev so the list is named in the report. -
Updated dependencies [215de3b]
-
Updated dependencies [6c8c956]
-
Updated dependencies [1a1e2f2]
-
Updated dependencies [7c14e23]
-
Updated dependencies [ef2b02c]
-
Updated dependencies [c6c415b]
-
Updated dependencies [6c8c956]
-
Updated dependencies [d5aba4b]
-
Updated dependencies [3ae0ca0]
-
Updated dependencies [ae46c92]
-
Updated dependencies [6c8c956]
-
Updated dependencies [1a1e2f2]
-
Updated dependencies [f98bd77]
-
Updated dependencies [fc7e626]
-
Updated dependencies [d50e855]
-
Updated dependencies [8f9f369]
-
Updated dependencies [c531e2a]
-
Updated dependencies [aed21ac]
-
Updated dependencies [b3c94be]
-
Updated dependencies [0653673]
-
Updated dependencies [6c8c956]
-
Updated dependencies [94fe5b4]
-
Updated dependencies [23477ae]
-
Updated dependencies [f4d3c87]
-
Updated dependencies [067e3bc]
-
Updated dependencies [8a65e5e]
-
Updated dependencies [f24e53d]
-
Updated dependencies [fa568d3]
-
Updated dependencies [d601119]
-
Updated dependencies [ac5159a]
-
Updated dependencies [de1c8b5]
-
Updated dependencies [c07a044]
-
Updated dependencies [01e3a57]
-
Updated dependencies [e346e61]
-
Updated dependencies [713a910]
-
Updated dependencies [e346e61]
-
Updated dependencies [0255729]
-
Updated dependencies [6c8c956]
- @solidjs/signals@2.0.0-rc.7