Patch Changes
-
#3310: Preserve queued effects when pending actions merge into another transaction, preventing stale DOM output after signal values commit. Thanks @DerpyCrabs!
-
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. -
6c8c956: Attribution:
feedback()gains the fact tables that have no verdict of their own.flightscounts, per async source, flights started, landed, and abandoned (superseded by a newer flight before landing — the re-ask-on-every-keystroke signature) with landed wall time.fallbacksmeasures, per loading boundary (named by owner path), how many times and for how long its fallback was shown and how many shows were sub-150ms flashes — the other end of the SILENT_HOLD spectrum.sourcesrows gainlate/lateMs: acknowledged holds that still ran pastholds.infoMs, where the affordance is not the whole answer. NewAttributionHooks.boundaryFallbackhook point at the boundary's source-set transitions.@solidjs/diagnosticsexports theFlightStatsandFallbackStatstypes; artifacts and the bridge carry the new tables through the existingfeedbackfield. -
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. -
ef2b02c: Internal cleanup with no behavior change: inline four single-use helpers (
hasContext/isUndefined,markCovered,shallowWithSymbols), delete two dead ones (isNextProxy,ownEnumerableKeysPlain), and collapsespread()'s nullish-source handling into one accessor closure. A few dozen bytes off the app 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). -
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
-
ae46c92: Comment-only: update doc paths in source comments after relocating the signals internals docs (INTERNALS-*, SPEC-ASYNC-SEMANTICS, rules-mining) from the package root into
packages/signals/docs/. No behavior change. -
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. -
f98bd77: Hold conditional reveals that first observe async work started in an earlier flush. The revealing signal now stays pending until the details can commit with it, including when the reveal creates a new child reader. Preserve fresh/reset loading-boundary fallbacks and avoid opening a second transition for readers already waiting in one.
-
fc7e626: Merge clone-path folds onto a container privatized mid-batch (#3271). Family and array drafts fold by swapping their pending backing in and re-slotting the parent with a CAS against the pre-batch old. When a descendant of the same node was written earlier in the draft, the descendant's fold path-copies THROUGH the ancestor first — privatizeCommitted clones the ancestor's committed backing and re-points the parent slot at the clone — so the ancestor's own fold swapped in a stale ensurePB-time clone, failed the parent CAS, and its writes were silently discarded (writable projections; plain object stores fold through the overlay path and were immune). Such folds now merge the batch's written keys onto the privatized container in place — the trap's written-keys bound is authoritative, with a value-diff fallback when an array length write poisoned it — composing both folds instead of losing one.
-
d50e855: rc.6 P1 store sweep — three fold-machinery gaps reported by @brenelz, all predating the #3271 fix:
- #3282 — an array move (
reverse/unshift/splice) plus an edit of a moved row corrupted sibling rows: the row target's parent-key is stamped at wrap time and never followed the move, so the fold's parent-slot re-point wrote the edited row's clone over whichever sibling now occupied the old index ([1,2]became[1,1]). Fold-time slot writes (privatization stitch, drainFolds path-copy, and the eager-fold twin) now resolve the slot by raw identity when the stamped key is stale — arrays only, fold-time only, no read-path cost. - #3283 —
deep()silently unsubscribed from every untouched child after a parent-field edit: the walk bypasses the proxy traps, and a bareReflect.ownKeyson a plain-object overlay pending backing (own keys = this batch's writes) hid inherited committed keys from the mid-flush re-walk, dropping those records from the effect's refreshed dependency set. The walk now merges committed keys minus deletes, mirroring the ownKeys trap's #3044 overlay merge. - #3284 — in derived stores, a descendant write disconnected ancestor observers and broke proxy identity:
privatizeCommittedregistered its clone only in the global lookup, but family targets resolve children throughfam.map, so the next parent read wrapped a fresh target and orphaned the original's nodes. The clone now registers in the target's own map.
- #3282 — an array move (
-
8f9f369: Suspend uninitialized async values across optimistic lanes so
latest()-conditioned branches wait for their first value instead of renderingundefined. -
c531e2a: Fix
refresh()of an optimistic value throwingGlobalQueue._notifyAuthoritativeObservers is not a functionand halting the graph in apps that never calluntil()(#3303). The refresh waiter reads authoritatively — the override is not the answer it waits for — which marks the node as authoritatively observed; when the re-ask then landed equal to the override, the wakeup went through a late-bound hook onlyuntil()installed.refresh()now installs it too. -
aed21ac: Fix a same-batch store reset leaving subscribers on the cancelled value (#3296). A draft write (
setStore(s => { s.count = 1 })) notifies its node at setter exit; an adoption in the same batch (setStore(reconcile(...))or a returned replacement) discards the draft and diffs the incoming object against the committed backing, so a key the draft changed and the adoption restored never re-notified — untracked reads showed the reset while memos and effects committed the draft value. Adoptions now diff against the view the nodes were last told — the draft's pending backing when one preceded them — exactly as a second draft write would; the last write wins for every setter form. -
b3c94be: Fix
createTrackedEffectmissing a signal written during a render-effect callback (#3291). Tracked effects read with committed visibility, but their wake was pushed straight into the user queue at notify time, so a write staged during a flush's render phase re-ran the effect in the same pass — before the value committed — and nothing re-notified it afterwards. Wakes (and the first run) now ride the heap like every other subscriber, so the run always lands after the commit; the tracked-effect special case in the scheduler is removed. -
0653673: Route errors thrown while applying asynchronous computed setters through the
node's error state. This prevents user callbacks invoked during asynchronous
projection reconciliation, such as key selectors, from escaping as unhandled
promise rejections. -
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. -
94fe5b4: Drop the lane source's redundant
isPendingcompanion refresh on derived pending/settle; the verdict never depended on it and the source's own paths keep it current. -
23477ae: Optimistic and
latest()lanes now hold under the same rule as transactions: async derived from the lane holds the lane's reveal only when a render effect observes it pending and noLoadingboundary catches it. An async memo nobody renders, or one inside a boundary that shows its fallback, no longer blocks the lane (#3289). -
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.
-
067e3bc: Optimistic increments keep stacking after a sibling landing. With several
optimistic-store actions in flight (optimisticvotes++, server confirm,
refresh(store)), the first vote's truth landing is staged into the
transaction that still retains the second vote, and that vote's increment
replays over it. A third click's draft then read the staged truth WITHOUT
the replayed override: draft reads composed live overrides only while no
pending backing existed, andvotes++reads before the first write triggers
the view-reseed hand-off. It read base, wrote base + 1, and its override
landed on the value already on screen — the click was invisible and the
count stuck (or fell back) until truth caught up. Draft reads now compose
overrides whenever the pending backing is not the draft's own view-seeded
clone. -
8a65e5e: An optimistic store's first flight suspends into its Loading boundary again.
The flight-owned transaction (#3146) was declared for the uninitialized
first ask too, so every transition-riding consumer —render()'s scheduled
root insert included — was held until the initial fetch landed: the page
stayed blank (content outside the boundary included) and the boundary's
fallback never showed, whilecreateStore(fn, seed)and
createOptimistic(fn, seed)in the same spot showed it. Nothing has
committed on a first flight, so there is no truth to keep on screen and no
optimistic state to protect: it now declares nothing, like the loading
window (#2933). Refetch flights declare exactly as before, so bare
optimistic writes during an in-flight refetch still ride the flight's
transaction (#2951) and content stays put until the new truth lands. -
f24e53d:
refresh()after a held manual write stays a quiet re-ask. When an action
wrote to a derived store and later calledrefresh(store), the lift of the
manual-write mask (#3026) dropped the re-ask classification, so the refetch
was treated as a brand-new question and pended every leaf — every sibling
row lit upisPending, and a row-scopedaffects()could not narrow it.
The lift now keeps the classification: same-question motion stays silent,
and only the written slot and any declaredaffects()mark read pending
until the truth lands. Same-tick precedence (#2692) is unchanged. -
fa568d3: Relocate #3277's uninitialized cross-lane suspension check from core
read()into the optimistic module'slaneSuspends. No behavior change — the check is only reachable under a lane, which implies the engine is installed — but the inline placement taxed every bundle including storeless floors (27-66 B across five size scenarios); inlaneSuspendsonly bundles that retain the optimistic module pay. -
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. -
c07a044: Add a development CJS build,
dist/node.dev.cjs, selected by thedevelopmentcondition on therequirebranch ofexports. Previouslyrequirealways resolved to the productiondist/node.cjs(__DEV__false), so a CJS host that resolvedsolid-js's dev server artifact would getDEV === undefinedfrom its@solidjs/signalsdependency — a dev server runtime whose diagnostics channel was silently absent. Dev CJS is the unmangled twin ofdist/dev.js; the production CJS is unchanged. -
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. -
e346e61: Store leaf nodes ride
slotSignal— one pre-shaped literal with_host/_keybackrefs replacing the per-node options object, equals closure, unobserved closure, and the NodeExtension that held it; the unobserved sweep dispatches CONFIG_SLOT_NODE nodes to one shared hook. getNode self-time −23% on dbmon warm mounts. -
713a910: Internal: the store fold's descriptor-preserving property copy is one
copyOwnhelper instead of three inline repeats. No behavior change. -
e346e61: Store first-read diet: the get trap's accessor probe verdict threads through to node creation (one descriptor scan per first read, not two), the trap's duplicate node-map lookup is hoisted, and the first tracked read populates the wrap cache so the second read skips wrapNext. dbmon mount min −4%, get-trap self-time −19%.
-
0255729: Stop the store proxy's dev strict-read check from firing on the engine's
thenable probe. Resolving a promise with a store proxy —refresh(store)'s
waiter delivers the store,Promise.resolve(store),return storefrom an
async function — makes the engine readstore.thensynchronously in the
caller's scope. When that scope carries a strict-read label (an effect
callback, a component body) the read produced a spurious
STRICT_READ_UNTRACKEDwarning, and against a refetching derived store it
could escalate to thePENDING_ASYNC_UNTRACKED_READthrow, rejecting the
promise being resolved.await refresh(list)inside an action logged the
warning on every call. Thethenprobe is not a read the user wrote and is
now exempt from both. -
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.