Patch Changes
-
8ff4803: A write becomes visible at flush — to every channel (A28). Between
set(x)and the flush that carries it, the write is not the committed value, not the staged valuelatest()/isPending()serve, and not an input to any derivation created meanwhile:latest(x)answers the pre-write value,isPending(x)is false,until()'s predicate evaluated in the carrying flush sees it. Optimistic writes are writes too (A28 (5), "match React"):setOptimistic(v)becomes the active override at the flush that carries it — plain reads,snapshot(),in, keys,lengthandisPending()see nothing before — while the writer's own channels (a functional updater, the store draft, theaffects()declaration walk) compose on it. A rewrite of a node a transaction holds keeps the staged value the last flush left forlatest()/verdicts until the next flush. Companions and store keys first materialized under a hold are born as the holding transaction's (#3336).Landed as a read-side rule rather than #3337's deferred subscriber walk: "unflushed" is structural (an ambient staged value outside a flush), the plain write path is untouched, and readers served the flushed value are latched for the carrying flush. Supersedes the #2922 mid-tick
latest()pull (flush()first to read your own write) and re-pins the pre-A28 expectations accordingly. -
e9c464b: Docs: inside an
action, a bareyieldis required after anawaitbefore anything that creates a reader —until(),latest(), a memo or effect, a mount — not only before writes. Theuntil()docstring's own example hadawaitstraight intoyield until(...); theuntil(...)expression is evaluated in the post-awaitcontinuation, outside the transaction, and its predicate reader is born held there (#3482). Example corrected. -
0da94f9: An async memo's held landing keeps the committed frame's dependencies (#3461).
selected = createMemo(async () => (b() ? b() : a()))withbheld by a slow flight: selected's held pass read onlyb, and its landing trimmedaat once, before the write staged the landed value under the hold. A later mainlineawrite no longer reached selected, soA: 1committed besideSelected: 0whileBstill read 0. The landing now trims only when it published (A30, the landing twin of a staged sync pass); a transition-held landing leaves the tail for its commit, so theawrite reaches selected and joins the hold, and the four values reveal as one frame.
-
8bf04ea: New dev diagnostic
ASYNC_STORE_SETTER: a store setter callback that returns a Promise now throws in dev (through the diagnostics channel) instead of being silently ignored. A store setter is a synchronous transaction — the draft closes when the callback returns — sosetStore(async d => …)committed only the writes before its firstawaitand lost the rest. Covers every store family with a user setter (createStore,createOptimisticStore, the derived store's manual setter); a derived store's own async compute is not affected. Store-specific: a signal may legitimately hold a promise, sosetSignalhas no such rule. Production is unchanged.Docs: the
OBSERVE.excludeguidance in RFC 08 no longer suggestsrunWithOwner(panelRoot, …)for panel writes (that is a write in an owned scope, see #3512); the store's own nodes carry the excluded owner. -
eaa7e33: The attribution engine's folds, queries and formatters are named exports of
@solidjs/signals/attribution(andsolid-js/attribution), not methods ofattribution.costs(),feedback(),why(target),subscriptions(target),formatRerun(event),formatOrigin(origin)— import them by name.attributionkeepsenable,disable,subscribe,markFlightand the record ring buffers (history,holds,interactions,navigations,waterfalls).attribution.formatisformatRerun.- Why: the fold tables (
costs,feedback) are the dev/agent view of the records and the part of the engine that grows; a production adapter consumes records and never calls them. Each fold's module registers its accounting with the engine's fold seam when it is imported, so under the package'ssideEffects: falsea consumer that only subscribes to records ships neither the tables nor the work of filling them — importingcostsorfeedbackis what turns them on. Measured: −1,150 B brotli for a records consumer (the size scenario's cap ratcheted to 27.25 KB); tree-shake tests pin it from source and from the built observe artifact. - New types
AttributionCostTables;ScopeCost/WriteCostand the feedback types move with their modules and are still exported from the entry. The prod tier's inert twin exports the same named surface, typed against the real one. @solidjs/diagnostics:AttributionCosts/AttributionFeedbackare aliases ofAttributionCostTables/AttributionFeedbackTables; the capture and the browser bridge import the folds by name. Artifact shape unchanged.
-
ebedb44: Attribution re-run records are serializable as emitted, and the observe tier's idle cost is a cap.
RerunEventno longer carries the livenode. It names its scope bynodeId— the engine's per-node id, stable across the scope's runs in the process and distinct between scopes (so runs of unnamed effects still fold to one scope after the record has left the process). In-process consumers that want the node askOBSERVE.subjectOf(event), which now answers for re-run records as it did for diagnostic events, for as long as the caller holds the record object.attribution.why(target)andsubscriptions(target)are unchanged.@solidjs/diagnosticsartifact format v7: re-runs are stored verbatim (RerunRecordis now an alias ofRerunEvent), and the artifact gainstimeOrigin— the capturing process'sperformance.timeOrigin— so every relativeatin it (re-runs, holds, records, diagnosticdata) is convertible to absolute time after the fact, and a server capture lines up with the browser session it served. The JSONL meta line carries it too; the browser bridge payload includes it.- New tripwire in the signals suite: the built observe artifact runs a graph-heavy workload within 1.25× of the built prod artifact with no hooks installed (measured 1.03–1.09). The idle wiring cost was informational before; it is capped now.
-
a8a8949: The window after an action body ends and its override is superseded by the committed truth (#3427) now reads like a landing supersession for every reader: a stale reader re-run by an unrelated write keeps displaying the override, a memo created mainline during the window is held with the transaction, and
isPending()reads true while the truth differs from the override (latest()already answered the truth). The node carries no transaction stamp in that window — an override written inside an action never passes the adoption loop that stamps one — sosupersededReadand the verdict now resolve the owning transaction through_overrideOwner. Also: alatest()/isPending()pull from mainline never enters a transaction (it is an observation); one did through the supersession path and captured the caller's synchronous block. -
d80cd1f: A memo or effect created from mainline code while a transaction holds a value it reads is now "born held" (A29, creation-time form): its creation pass derives from the transaction's staged world and is staged into that transaction — committed with it, and for an effect first run by its commit — instead of committing the held value into the mainline frame beside readers that show the committed one. The mainline block that created it is untouched:
enterStagedReadno longer enters the transaction ambiently from creation code, so an unrelated write made after such a mount (a click that opens a panel while an action is in flight, then writes something else) is a mainline write again rather than being swallowed into the action. An untracked read of a born-held memo throwsNotReadyErroruntil the commit — it has no committed value to serve. -
d826cd3: The client error hook —
configureClientErrors({ onError })fromsolid-js, andrender/hydrate'sonErroroption in@solidjs/web: the prod-tier seam through which an app, or an error monitor, hears the one failure nothing else can see — an<Errored>/createErrorBoundarycollected it and renders its fallback. The twin of the server'sconfigureServerErrors. An uncaught error is not this hook's: the halt (REACTIVITY_HALTED) hands its cause to the platform'sreportError, the channel every monitor already listens on.Once per error object (a
reset()re-collecting the same failure says nothing new; a primitive is reported per sight);ownerPathcarries the component labels where the runtime keeps owner names; no return — the client has no wire to map for; a throwing hook is reported and ignored. A root's own hook wins over the ambient one for failures under it.Pay-for-use: the hook machinery (
core/error-hooks.ts) is retained bycreateErrorBoundaryor the app's ownconfigureClientErrorsimport; a root's hook is parked on the root owner under the registeredROOT_ERROR_HOOKsymbol (defined in the scheduler), sorenderretains nothing for an app that passes none. Core floor unchanged; apps with a boundary +~200 B.The server surface consolidates on the same name.
onErroronrenderToStream/renderToStringand onhandleServerFunctionRequestis the server error hook ((error, context) => wire | void);onServerErroris removed. A one-argumentonErrorwritten for the old shape keeps working and now hears every handled failure — filter oncontext.handling === "failed"for the request-failing ones alone. Newhandling: "serialize"for a hydration value that would not serialize (what seroval'sonErrorreported, for a render that passed one). With no hook anywhere, a failure that fails the request still reachesconsole.error. -
cc0396b:
_parentjoins_nameas a field signals' property mangling reserves — the two cross-package owner fields.Signals' prod and observe artifacts rename every
_-prefixed property except a reserved list; the dev artifact (which the test suites run against) is unmangled. Two things read_parentacross the package boundary and only worked in dev:solid-js's client hydration walksowner._parentto the root to mark the hydration snapshot scope. In the built prod and observe artifacts the walk found nothing and marked the current owner instead, so computations created outside that owner's subtree during hydration read live values rather than the server snapshot.- The core's owner walks —
ownerPathandOBSERVE.exclude/isExcluded— oversolid-js's server owners.ownerPathhad a server-side shim (located(), now removed);OBSERVE.excludewas a silent no-op for a server owner outside dev.
Cost: ~40 B brotli on the prod app scenarios; the observe scenarios did not grow. Pinned from both ends:
packages/solid/test/cross-package-fields.spec.tschecks the reserved fields survive in the mangled artifacts and scans the built client artifacts ofsolid-js,@solidjs/weband@solidjs/universalfor any signals_field that is not reserved;packages/web/test/server/server-owner-walks.spec.tsxrunsownerPathandOBSERVE.excludeover server owners against the built observe and development artifacts.Also: RFC 08 gains "Values in records — the PII surface", the complete list of record and finding fields that carry user data (value previews, interaction target text, navigation paths/params,
data.erroron the server error findings) for exporters that leave the process. -
d2a36f5:
dynamic(source, { static })andisStatic(o, key)dynamic()pays for a factory memo plus a per-instance memo so the source can
change. A great many call sites never change: a runtimestyled()that always
renders"li", and — the case this exists for — a polymorphic component whose
asarrived as a literal. The compiler encodesas="button"at a call site as
a data property andas={isLink() ? "a" : "button"}as a getter, so which one
the caller wrote is readable at runtime.isStatic(o, key)reads it: one descriptor lookup, no read of the value,
nothing tracked, looking throughmerge()/omit()views to the leaf that owns
the key. Data property or absent-from-a-fixed-key-set is static; a getter, a
store key, or a memo-backedmerge()source is not.dynamic(source, { static: true })then says the source cannot change: it is
called once, untracked, atdynamic()time, and each instance renders the
result with no computation of its own — a tag goes to the compiled element path
(create or claim, spread), a component is called directly. No owner is created
on either side, so hydration ids stay aligned between server and client. A
static source may not return a promise.function Polymorphic(props) { const Tag = dynamic(() => props.as, { static: isStatic(props, "as") }); return <Tag {...omit(props, "as")} />; }
asstays public and reactive; the literal case stops paying for it. Note the
two paths produce DIFFERENT hydration ids (the memo path's element sits one
owner deeper), which is fine becauseisStaticreads the same descriptors on
both sides — but it is why the classification must be per instance rather than
per component. -
50323b4: Keep a mainline-computed effect value out of a parked transaction. When an effect stamped by a held transaction recomputed on an unrelated write and no longer read the held source, the forced re-run inside that transaction re-claimed ownership of the value it had just published. A finalize-time re-entry (a
Loadingboundary'sonreset flipping its fallback state) then parked the effect with the transaction, leaving ashow() ? details() : "hidden"reader stale until the unrelated async settled (#3412). -
53280e7: The error hooks and
SSR_RENDER_ERROR_CONTAINEDtell where an error was thrown apart from where it was met.ownerPathonClientErrorContextandServerErrorContextis now where the error was thrown: the labels root-first up the owner chain of the computation that threw — the component that broke — falling back to the boundary's chain when the throw crossed nothing the runtime could name. A newboundaryPathis where it was met: the same labels up the chain of the<Errored>that rendered its fallback (client and server) or the<Loading>that shipped the rejection (server,handling: "client"). Before,ownerPathwas the boundary's on both sides, so every component under one boundary grouped into one path. On the client the engine's status wrapper already named the thrower (StatusError.source); on the server the owner scopes stamp it as the error escapes. TheSSR_RENDER_ERROR_CONTAINEDfinding follows:ownerPathlocates the throw,data.boundary/data.boundaryPaththe boundary. -
d7cb456: Fix a second write arriving while an async chain is still in flight (#3373, #3374, #3375, #3376).
- A flight's landing now retires only the node's own pending entry. When an input was re-asked mid-flight (
arestarted whileb's first flight was up),bstays pending ona; the stale landing no longer let the transaction commit the newer signal beside the older derived value (2 / 1, #3373) or blipisPendingtofalse(#3376). A fresh flight drops pending entries its inputs propagated earlier — the run read them, so a masked input (an active override, A17) does not hold it. - A transaction now tests whether a source's own flight is still up by its self entry rather than
_error.source, which a later-pending input overwrites; the held write no longer commits ahead of its answer once the load is re-asked under anon-scoped boundary (#3375). - A collecting
Loadingboundary records every source the notifying effect is pending on, not only the one the notification carries — anonreset no longer reveals content when the boundary's one collected source settles while the effect is still pending on a flight it already carried (#3375). - A render effect served a pending node's committed value (the A15 reveal carve-out) joins the transaction's reporters for that node, so a keyed remount that disposes the original reader no longer lets a same-value rewrite commit the held write while the derivation is in flight (
Count: 1besideDetails: 0, #3374). - A
Loadingboundary'sonreset ends the hold on writes that only its readers observed: a reader registered while the boundary showed content stops blocking once the boundary flips to its fallback, and the parked transaction is woken and re-judged in the same drain. A reader outside the boundary that also observes the flight still holds it (#3375).
- A flight's landing now retires only the node's own pending entry. When an input was re-asked mid-flight (
-
a0d6dd2: Conditional JSX across a held branch change stays coherent with its inputs (#3438).
An effect's dependencies are the committed frame's until its run applies — the effect twin of the memo rule from #3410. A pass that direct-committed its value but whose run was stashed with a hold (the same flush pended an async memo) used to trim the dependencies its previous run still displayed, so a later mainline write to one of them never reached the effect:
{show() ? count() : "hidden"}held onshow → falseshowedCount: 1besidePanel: 0whileShowstill readtrue. The trim now waits for the run to apply, so the write re-derives the effect against the committed inputs (Panel: 1besideCount: 1), and the hold's landing revealshiddenwithShow: false. -
c3ae310: Conditional memos across a held branch change stay coherent with their inputs (#3408, #3410).
- A tracked computation served a live transaction's staged value now enters that transaction, so its result is held with it (#3408). A memo whose branch flipped mainline and started reading a held signal previously published the staged value beside the signal's committed one (
Panel: 1next toCount: 0). The read entry is the twin of the existing write-side (setSignal) and stamped-recompute entries. - A memo's dependencies are the committed frame's until the frame is replaced (#3410): a pass that stages its value leaves the previous pass's dependency tail linked, and the commit trims it. A write to a dependency the committed value still derives from reaches the memo and joins its hold — as an unconditional read would — instead of revealing the write beside a stale committed derivation (
Count: 1next toSelected: 0whileFixed: false).
- A tracked computation served a live transaction's staged value now enters that transaction, so its result is held with it (#3408). A memo whose branch flipped mainline and started reading a held signal previously published the staged value beside the signal's committed one (
-
6095955: Two ways a held write stayed staged past the point it should have revealed:
- A transaction whose only reporter is disposed by ambient work (a
<Show>unmounting the reader of a pending memo) was never re-judged — the flush only evaluates the active transaction, and nothing re-entered a parked one. The writes held with that reader (a signal set while it was pending) stayed staged forever. Disposing a pending reader parked in a transaction now wakes it; the flush re-enters a woken transaction on an otherwise idle pass, prunes the dead reporter and commits (#3372). - An effect that reads
latest()(or otherwise adopts an optimistic lane through its deps) direct-commits on a lane pass, but a hold it had staged on an earlier, lane-free pass of the same transaction was left in place and the transaction's commit published that older frame over the fresh value —Pair: 0 / 0for good. A lane recompute now drops the hold it supersedes, override or not (#3377).
- A transaction whose only reporter is disposed by ambient work (a
-
e80f241: A node recomputed under a held transaction no longer tears down the committed frame's children immediately. Status propagation stamps a parked dependent with the transaction without recomputing it, so its owned children (nested render effects, memos,
onCleanupregistrations) still belong to what is on screen; when the pending source landed, the recompute disposed them on the spot and their cleanups ran mid-hold, before the transaction's atomic reveal (#3404). Those children are now deferred as zombies until the node commits, matching the plain-flush path. Children built by a recompute that never committed (a staged value, a pending window, a run under the transaction) are still disposed immediately on the next re-run — no frame ever showed them. -
84adf0b: Fix a Loading boundary whose
onkey consultsisPending()through a memo freezing the page (#3528). The key is evaluated fromnotify, inside the pending memo's own pass; reading the pendingisPendingmemo there recorded an untracked-pending re-run dependency on that memo, making the async memo depend on a memo that depends on it — every pending mark re-derived it and it never converged. Theonaccessor is now evaluated as a spectator (spectate): untracked, and never recorded as a dependency of whichever node's pass it happens to run in. -
5c1f01f: A
Loadingboundary whoseonresets keeps its fallback until every reader under it has settled, not only the ones that notified after the reset (#3459).- The reset clears the boundary's collected sources and re-collects from the pending notifications that follow. A reader already pending from an earlier write never re-notifies (status propagation dedupes on its
_pendingSources), so a sibling reader's fresh flight was the only source collected, and its landing revealed the still-flying one stale:B: 1 | Fast: 1 | Slow: 0for two seconds. The reset now also harvests what its forwarded readers still wait on from the live transactions'_asyncReporters(INV-3, the one record of a forwarded reader), so the hold the #3375 ruling takes off the lane lands on the boundary instead:B: 1 | Loading, thenFast: 1 | Slow: 1together.
- The reset clears the boundary's collected sources and re-collects from the pending notifications that follow. A reader already pending from an earlier write never re-notifies (status propagation dedupes on its
-
a5d8eae: A memo computes under its own lane posture, never its puller's (#3442).
A combined
isPending(() => [fast(), copy()])over two async memos, withcopya sync memo wrapping the slow one, released the hold as soon as the fast flight landed:Fast: 1besideSlow: 0withPending: false, thenSlow: 1a second later. The probe effect carries the companion lane of the pending signals it reads, and its pull ofcopyran under that lane — where a pending node on no lane serves its committed value instead of throwing — socopypublished a stale settled value, dropped its pending status, and its readers stopped holding the slow flight.recomputenow runs a memo plain unless the memo itself is lane-dirty or adopts a lane through its dependencies; effects keep the ambient lane, since their runs are the lane's own view. Both values now reveal together, with the probe reporting pending until they do. -
14ded24:
merge()no longer flattens through a plain-object merge result. That form is a real object callers may copy (Reflect.ownKeysdescriptor copies,{...props}) or mutate afterwards (@solidjs/htmlassigns props and achildrengetter after spreading), and a latermergetunnelled back to the original sources through the$SOURCESsymbol — resurrecting removed keys and dropping added or overwritten ones (#3384). The plain result now records no sources and is read like any other object; only merge proxies, whose writes are no-ops, are still flattened. A side effect:merge(defaults, props)wherepropsis itself a plain merge result that covers every default now returnspropsdirectly instead of always allocating. -
25c5064: Optimistic frames release when nothing authoritative is left to wait on, and a shared render effect no longer entangles unrelated updates (#3426, #3427, #3407).
- The last async reader of an optimistic value unmounting mid-action releases the frame (#3426): the lane's hold check prunes dead reporters itself (
sourceObserved, shared with the settle verdict), instead of waiting for a flight nobody observes to land. - The action body ending starts the correction (#3427): with the bodies over and no authoritative work in flight — no override node's own source, no held plain load the action asked for — each override's truth supersedes it now and the graph re-derives from it as the transaction's held work, settling when that lands. Before, the settle waited for the obsolete lane-derived flight, flashed the obsolete optimistic frame, then reverted and re-asked. A co-written flag stays through a plain load; optimistic store edits keep the settle-then-revert order.
- A render effect's pass belongs to whatever dirtied it (#3407): a sync
actionwrite to a signal that shared a hole with a held async ({b()}:{detailsA()}) merged into the async's transaction and waited (0:0 → 2:1) while the same plain write passed through.recomputere-enters a stamped node's transaction for memos only; the landing of a flight enters every transaction waiting on it (enterWaiting), which is where the effect re-entry's one legitimate job — completing the reveals that discovered the flight — now lives. Two independent flights read in one hole land at their own times.
- The last async reader of an optimistic value unmounting mid-action releases the frame (#3426): the lane's hold check prunes dead reporters itself (
-
1ce0f85: Optimistic settle verdicts (#3409, #3411).
isPending(() => [a(), b()])over two async siblings of an optimistic value reports pending as soon as either read is (#3409).assignOrMergeLanenow follows a merged lane to its root and runs the parent/child check on it; the old "merged lane is stale, take the source lane" shortcut moved the combined probe's effect onto the held parent lane, where its verdict waited on the async it reports.- An unowned
onSettledcallback (event handler, action body) reads a settled world (#3411). The settle only enqueues the reverted subscribers for the next pass, so a callback fired in the commit pass saw the optimistic source reverted beside a sync memo of it still holding the optimistic value. The fire now waits for the heap to drain.
-
05c7e21: Two flights through one memo settle as one unit, and a branch a held
Showis removing keeps followinglatest()(#3443, #3444).- A memo another live transaction holds, made pending by a second flight, entangles the two at the propagation (#3443). The second flight only propagated pending onto the memo — no recompute, its inputs' values were unchanged — so the memo's stamped re-entry never ran and the first transaction never learned it was waiting: it revealed
A: 1beside the committedSum: 0, andSum: 2arrived withB: 1. Now one reveal when both have landed (A15). A render effect reading both plainly stays parallel, as before; a write whose async work flows into a held memo is held with it. - A zombie dirtied through the lane channel runs instead of being cancelled (#3444). When the parking batch is the transaction, queued zombie recomputes are cancelled as a world the zombie never displays — but overrides and
latest()companions are the mainline frame, and the still-visible branch showedlatest(count)at 0 beside the same read outside at 1.
- A memo another live transaction holds, made pending by a second flight, entangles the two at the propagation (#3443). The second flight only propagated pending onto the memo — no recompute, its inputs' values were unchanged — so the memo's stamped re-entry never ran and the first transaction never learned it was waiting: it revealed
-
9da7f0a:
isPending(details)reports the load of an optimistic value whendetailsderives it through an async memo (#3379).notifyStatusnow assigns the node's optimistic lane before poking its companions, so a companion lane created by the poke is parented to the node's lane: the indicator effect flushes on the companion's child lane immediately instead of merging it into the held lane and waiting on the async it reports. -
62b0a22: A render effect that stops reading a pending async memo no longer keeps the memo's source held. A pending reporter that recovers without its flight landing — its pass no longer reads the source, e.g. a
show()gate closed — is a completion event for the transaction it reported to:recomputenow wakes that parked transaction (wokenTransitions, the third site after disposal #3372 and boundary reset #3375) so it is re-judged and its held writes commit. Before,reporterBlocksSourcealready judged the effect dead but nothing re-asked the parked transaction, and an ordinary write to the source stayed staged for as long as the flight stayed up — forever when it never landed. Found by the semantic fuzzer (#3446, law P1, 21/1000 cases) and reproduced as the posture matrix'seffect × gatedAwaycell. -
27b24aa: Five hold-consistency fixes (#3456, #3458, #3460, #3463, #3469)
- #3456: a pass that re-parks on a new pending source set retires the sources it stopped carrying from its dependents, so a conditional whose async branch was cancelled no longer stays pending forever on a flight it has no path to.
- #3458: a stale render reader that is a flight's first observer registers the flight with the transaction it reveals a reader of (INV-3, via the reader's queue chain), so the transaction waits for it instead of revealing its other inputs beside the reader's pre-flight value.
- #3460: lanes mirror transitions from the outside — a render effect off a held lane (mounted mid-hold, or re-run by an unrelated sync write) is served the committed value, publishes at once, entangles nothing, and re-runs at the lane's release;
latest()andcreateOptimisticsources alike. Only direct reads return the override while the lane holds. Off the lane is provenance, not membership: a pass under the lane's own transaction (its async's landing) is the lane's work. - Lanes stage (#3479 review): an optimistic derivation is an override. A lane pass on a memo publishes its speculative result as a derived override instead of direct-committing
_value, so the committed view an outsider sees is a whole frame — the held source and its derivations together, never a committed shadow beside a speculative memo. The revert promotes a derived override the truth confirmed (no re-ask waterfall) and re-derives one it superseded. - #3463: a reader whose removal is staged in a live transaction (a zombie) is still on screen and keeps holding until the commit that disposes it; it is moot only for that transaction's own verdict.
- #3469 (A30): a pass that changed nothing replaced nothing either — its dependency trim waits on the flush's verdict (
heldTrims), so a same-value branch switch under a hold still follows its committed inputs.
-
549f482: Trim per-node work on the hydration claim path.
gatherHydratableasks once whether the root contains frame regions and tests containment against that list, instead of walking every keyed node's ancestor chain withclosest("[data-fid]");insert()builds a parent's claim array in one indexed pass overchildNodesthat drops separators as it copies, instead of an iterator spread followed by a compacting pass; andclearSnapshotsassignsundefinedto the extension's_snapshotValuerather thandelete-ing it, which pushed every hydrated source's extension object into dictionary mode. -
d80cd1f:
isPending()on an optimistic node whose own source landed a value differing from the displayed override now readstrueeven when the node has never committed (its first landing was held by a reveal that never landed). The verdict's uninitialized suppression — A19 exception (1), "no observable value exists" — no longer applies under a displayed override, which is an observable value (A18 d). -
76230f9: Report
isPending()true for a synchronous memo held by a downstream async memo. The memo's staged recompute only refreshed its verdict companion inside an active transition, but a plain flush can become a hold after that recompute (an async memo pends and the batch is adopted into a transaction), so the memo read not-pending while the signal it derived from read pending (#3413). -
347a5ca: A memo wrapping
isPending(x)agrees with a directisPending(x)read while a sibling async memo holds the write (#3457).- The fresh-read pairing rule (A10) only mutes a verdict for a LANDED answer awaiting reveal; while the transaction still has an async source computing, pending is the verdict for every reader. That carve-out was gated on the node's
_transitionstamp, but a sync memo staged AFTER the transaction opened is pushed straight into the transaction's batch and is not stamped until the flush stashes the hold. A wrapper memo recomputing on the companion flip read the memo's fresh staged value mid-flush, was told "not pending", and cachedfalsefor the whole hold, while the direct render-effect probe (which reads the committed value under the companion lane) reportedtrue. The scan now runs for an unstamped node too: the transaction it resolves to is the one that owns its staged write.
- The fresh-read pairing rule (A10) only mutes a verdict for a LANDED answer awaiting reveal; while the transaction still has an async source computing, pending is the verdict for every reader. That carve-out was gated on the node's
-
a8a8949:
latest(() => store.key)on a derived store (projection) that has not yet resolved threw for tracked and untracked reads but returned the seed throughlatest():read()routes alatest()read to the companion before its firewall/status logic, and the leaf's own_valueis the seed.latest()now judges "uninitialized" on the leaf's owner — the projection's firewall — and throwsNotReadyErrorlike every other read (A25: the seed is a draft, never a value; A7). -
632e45c: Give the latest() shadow companion the
ownedWriteflag its isPending companion already carries (#3378). A companion sync is internal plumbing that can run from inside a computation — a transition-held memo recompute pulled mid-tick by a reader creating or refreshing its latest() shadow — and the dev owned-scope write guard halted the app on the shadow write. Toggling a JSX branch that readslatest(memo)off while an action is pending and restoring it as the action resumes threw REACTIVE_WRITE_IN_OWNED_SCOPE. -
75c5113:
latest()of an uninitialized async source now throwsNotReadyErrorin every scope. Unowned callers (event handlers, imperative code) used to receiveundefined— a value the accessor's type excludes — because the uninitialized case shared the pending-shadow fallback's condition inlatestRead.isPending()is unchanged: an unowned probe still answersfalse(A16), whichbooleanadmits. Spec: A7 amended, A16 wording corrected (the boundary is ownership, not tracking), A17 authoritative-reader carve-out ruled, A32 added (children-forbidden readers see the frame). -
899c2c4:
merge()andomit()are always lazy views, and props consumers read their leavesomit(props, ...keys)returns a live view ofpropsfor every input — a plain object included — instead of copying it with agetOwnPropertyDescriptor+definePropertyper prop. A predicate form hides keys by rule without enumerating first:omit(props, k => k[0] === "$").merge()no longer builds an eager copy when its sources are plain objects: underProxyit always returns an O(1) view over the flattened sources (a single non-function source is returned as is).The two compose flat. An
omit()over amerge()carries one filtered view per flattened merge source, amerge()over anomit()takes the view record as a leaf, and nested omits fold their filters into one record. A component chain ofmerge(defaults) → omit(consumed) → merge(statics) → omit("as")— the shape headless-UI libraries render every element through — collapses to leaf views over the original objects, each with its accumulated filter, with no proxy layer left between the outermost spread and the author's props.merge()keeps the omitted keys hidden by construction (#3014) rather than by treating the omit as opaque. Construction cost drops 3–7× at depth 1–7; the SSR polymorphic-chain bench (#3448) runs ~2.4× faster.Reads stay cheap: a view over plain objects resolves a key → owning-leaf table once, on first read, and every
get/has/descriptor is one lookup after that.spread()(DOM and universal) andssrElement()read the leaves directly — never through the proxies' traps — and walk that table when there is one, so an effect rerun costs one read per key, as it did over the copy. Both proxies use a class target and one shared handler (no per-instance closures).A view over a store asks the store nothing but the read. Each source's kind (plain object, omit record, proxy, memo) is decided once, when the view is built, and carried beside it — every brand check on a Proxy is a trap (
instanceofis agetPrototypeOftrap, as expensive as a store read), and store detection goes through$TARGET, a symbol the store'sgettrap answers on its fast path, never its generic tracked-read path.merge(defaults, store)constructs ~30% faster than the copy did and reads ~15% faster;omit(store)reads at parity.The views tell the truth:
Object.getOwnPropertyDescriptor(view, key)reports a data descriptor only when the key is a data property of a plain leaf (the compiler's encoding of a static prop) and an accessor for a getter, a store key, or a memo source. Together with the new internalhasStaticKeys(),spread()now skips the children effect for static children behindomit/mergelayers (#3388 through views).Behavior changes:
- Writes to a
merge()oromit()result are no-ops (they already were for the proxy forms). A caller that needs its own object copies it ({ ...merged }), and the copy carries no sources (#3384).@solidjs/htmlnow collects its own props and spreads into onemerge()at the end instead of assigning onto the result. - A data property on a source is read live through the view rather than snapshotted at
merge()/omit()time. - Key order of a merged view is the merged order — every key at the position of the last source that carries it — matching
ssrElement's array form. - Sources are treated as own-keyed; a key added to a plain source after merging is not seen (the copy did not see it either).
- Enumerating a view through its traps (
for…in,Object.keys,{ ...view }) costs a trap per key, as any proxy does; the internal consumers avoid it. Environments withoutProxykeep the copy paths.
Internal helpers for consumers, exported from
solid-js:viewOf(o),mergeView(o),omitView(o),sourceKeys(entry, kind),sourceHas(entry, kind, key),sourceGet(entry, kind, key),hasStaticKeys(o),resolvedTable(o), theSOURCE_*kinds. - Writes to a
-
ca05917:
OBSERVE.excludenow covers writes: a root write to an excluded subject (the observer's own store or signal) no longer counts toward the interaction that made it, and an interaction whose writes all went to excluded subjects with none of the app's work run — a click on a devtools panel's own button — is not recorded. Store nodes carry the owner their store was created under, so an excluded panel's store is an excluded subject like its signals. -
3ae9e92:
OBSERVE.records— one records channel on both platforms (observe/dev tiers); frame records from both ends; the client"call"record;observeServerFunctionCallsremoved@solidjs/signals:OBSERVE.records—subscribe(type, listener),observed(type),emit(type, event, live)— the channel every runtime record rides, created once per process and registered onglobalThisunderSymbol.for("@solidjs/signals/observe/records")so a second copy of the core (a bundled server build instrumented through--import) and wire layers bundled without a framework import reach the same listener sets. Listeners are snapshotted per emit; a throwing listener is reported and the rest run. Types:Records,RecordTypes(extendsHostRecordTypes; both declared empty, for the runtimes to augment — one augmenter per interface),RecordType,RecordEvent,RecordLive,RecordListener. Folds out of prod. NewOBSERVE.attribution.currentOrigin()(and thecurrentOriginhook onAttributionHooks): the provenance a root write performed now would be stamped with — the interaction whose handler is running, the navigation/effect/action frame open, or inside a recompute the origin of the change that caused it — as the engine's ownChangeOriginobject,undefinedwhen external or with no engine; for a runtime stamping a record of its own. The installed hooks are also registered onglobalThisunderSymbol.for("@solidjs/signals/observe/attribution"), the same reach-without-an-import the channel has.solid-js: the"boundary"record moves fromOBSERVE.server.recordstoOBSERVE.records(augmenting the core'sRecordTypes).OBSERVE.serverkeeps only thetraceslot;ServerRecordsis gone.@solidjs/web: the"invocation"and"frame"records move toOBSERVE.records(augmentingHostRecordTypesthroughsolid-js). New"call"record (CallEvent,CallLive,CallListener): one per server-function call made from the browser, at the caller's settle —{ id, at, durationMs, method: "GET" | "POST", outcome, status?, origin?, deferred? }with{ args, response?, result? | error? }beside it; joins the server's"invocation"byid, and — throughorigin, the engine's own interaction/navigation object read at dispatch viacurrentOrigin()— the attribution engine'sInteractionEvent/NavigationEvent/HoldEventby identity, so an observer files the call under the click that made it without a time join. The"frame"record now has a client half:FrameEventisFrameProducedEvent | FrameAppliedEvent, discriminated byside, same census on both; the client half (applyFrameResponse, one per stream in a response) addsaddress(theasremap) andoutcome: "truncated"for a body that ended beforecomplete, withlive.response. Server census fix:regionscountshtmlchunks addressed to a child frame id (the former count read a chunk type that does not exist), andshellMsis set by the stream's own shell only. The emitters and their wrappers fold out of the prod client artifacts behind the observe literal (prodapplyFrameResponseand the server-function dispatch are the pre-existing functions, no extra frame or promise hop). The server-functions and frames client entries gainobserveanddevelopmentbuilds and export conditions (server-functions/dist/client.{observe,dev}.js,frames/dist/client.observe.js); the server-functions client is now built with its flags replaced in every tier (before,_SOLID_DEV_there was an unreplaced truthy string).- Removed:
observeServerFunctionCallsand theServerFunctionCall/ServerFunctionRequestCall/ServerFunctionResponseCalltypes, from both server-function entries. Subscribe toOBSERVE.records"call"(client) or"invocation"(server) instead. @solidjs/diagnostics(format v6):artifact.serveris replaced byartifact.records: { boundary, invocation, frame, call }— always present, captured on both platforms including the browser bridge; typesBoundaryRecord,InvocationRecord,FrameRecord(FrameProducedRecord | FrameAppliedRecord),CallRecord(withorigin?: ChangeOrigin),ArtifactRecordsreplace theServer*Record/ArtifactServernames. JSONL: one line per record withtypenaming its table; the meta line'sboundaryCount/invocationCount/frameCountbecomerecordCounts: { boundary, invocation, frame, call }.
-
328580f:
omit()over amerge()is one record that holds the merge's record, not one leaf view per merge sourceAn omit over a merge used to flatten at construction: one
OmitViewplus one combined hidden-key list per flattened leaf, and the nextmerge()copied those entries into its own arrays. On a component chain of defaults + omit + spread (Kobalte-shaped:merge(omit(merge(omit(props)))), four layers) that was ~19 records and as many list copies per element — the largest allocation of the render. The omit now holds theMergeViewrecord itself (a new source kind,SOURCE_MERGE) and is one record however many leaves the merge has; a latermerge()carries it as one entry, and a lateromit()folds into it. Nothing is read through a proxy trap along the way: the entry helpers (sourceKeys/sourceHas/sourceGet,hasStaticKeys, descriptors, the resolved table) recurse into the record by function call.- Reads through the nesting are one walk per read (a nested entry answers presence and value together), and a nested record counts no reads of its own toward the table threshold — the view that was asked decides for the whole tree, and its table is collected in one pass over the leaves rather than one table per layer.
- New
@internalsourceOwners(source, keys, owners): every key of a props source — a plain object, a store, or a merge/omit view — appended in merged order with the object that owns it, in one pass, later sources moving a key to the end.ssrElementcollects any spread that is not plain objects only (a view, a store, the array form with one among them) this way, so each attribute is one direct read of its owner — noinwalk per key through the layers, no key list per leaf, no table, and no per-entry classification (pushEntryis gone). - An omit's
$SOURCESnever answers anything now (previously its filtered leaf views); consumers reach the record throughviewOfand walk it as one filtered entry.
Measured against
next(interleaved, min of N, quiet machine): the tier-1 polymorphic-chain SSR harness allocates 12% less per row (14.6 → 12.8 KB) and is 2–6% faster across the interpreter, Sparkplug, Maglev and TurboFan tiers; the props-chain microbench builds 6–65% faster and builds+consumes 7–31% faster by depth and tier; the omit/merge micro-suite is flat or better in every shape. On the yak-bench SSR lanes with every yak piece on Solid primitives: +7% geomean, +20–36% on the component-composition cases (polymorphic-chain,tabs,multifile-composition), which brings those to parity with yak's hand-rolled runtime. -
0bffee2: One definition of "unflushed" for signals and store leaves. A staging adopted by a transaction before any flush carried it (
set(x, 1); action(...)in one tick — same-tick adoption is by design) is still unflushed whatever its stamp:latest(x)answers the pre-write value andisPending(x)false inside the adopting action's body, as store leaves already did through their own selection while signals answered the staged value andtrue.initTransitionmarks such nodes at adoption (CONFIG_ADOPTED_UNFLUSHED); the carrying flush clears the mark and the hold takes over. -
f329a26: One ownership relation,
ownsHold, answers "is this hold part of the running pass's world" for the stale-reader clause, the lane arm and the store's backing holds — a refactor with no behavior change, recording the ruling that a lane is a transaction with an override whose world includes the transition that owns it. -
c827758: A projection's leaf companions die with the projection. Disposing a store whose async source was mid-refetch left the
latest()shadow of a leaf orphaned: never derived (its compute read through the projection in flight, the backfilled override stood in), its override dropped at the settle, NotReady forever against a leaf whose committed value differed — the__TEST__quiescence invariant INV-4 at the next flush. Externally coherent, but a companion outliving its source. The firewall's teardown now snaps the companions of its companion-bearing leaves (the shadow is retired; a later read recreates it from the committed view), andlatest()of a leaf whose firewall is already disposed serves the committed value without creating a shadow — a boundary's content re-running after the teardown had recreated one that nothing would retire. Surfaced once #3495 stopped leaking parked transactions, which had masked every quiescence check in the posture matrix; all 621 matrix cells now run with zero invariant violations. -
6e9243c: fix(signals): a projection leaf released by the unobserved sweep also leaves its firewall's companion set, so an obsolete value read only through
latest()/isPending()becomes collectable once every reader is disposed (#3503) -
7a09cd9: One slow value selection,
serve, for signal reads and store property nodes (the fast paths keep their inline ternary). Fixes a derivation's untracked read of a derived optimistic store's key after its own truth landed differently from the optimistic edit: the store served the memo the superseded override and let it publish, where a signal serves the landed truth and holds the memo with the action (A18). -
61a114c: Server boundary records on
OBSERVE.server.records(observe/dev tiers)- New
"boundary"record: one per<Loading>boundary that waited during a server render, delivered when it settles —{ id, at, durationMs, heldMs, passes, outcome: "settled" | "fallback" | "client" | "error", streamed, revealGroup?, ownerPath? }, with the thrown error beside it.idpairs it withSSR_RENDER_ERROR_CONTAINED;passescounts render passes (a sequential chain reads as3+); under a<Reveal>group the record waits for the group's swap soheldMsmeasures how long finished content was held for its siblings. No clock is read without a listener, and the emitter folds out of prod. - The server records channel is
OBSERVE.server.records.subscribe(type, listener)— the server twin ofOBSERVE.attribution.subscribe(type, …).OBSERVE.server.invocations(unreleased) is renamed onto it:subscribe("invocation", …). TheInvocationChanneltype is gone;ServerRecordsis the channel's interface. - Types now layer one augmenter per interface:
@solidjs/signalsdeclaresServerObserveempty;solid-jsaugments it withrecords: ServerRecordsandtrace: ServerTrace, declaring both;@solidjs/webaugments those two through"solid-js".TraceSlotin@solidjs/webis now an alias ofsolid-js'sServerTrace. (Two augmentations of one re-exported interface through different module aliases merge order-dependently in TypeScript — one set was silently lost.) @solidjs/signalsexportsownerPath(subject)— the root-first component-label walk its diagnostics already make — so a record'sownerPathand the finding it pairs with come from the one walk.
- New
-
0d8347a: Server records reach the diagnostics artifact and the dev checks (server-dev-build-plan P4)
@solidjs/diagnosticsartifact format v5:artifact.server: { boundaries, invocations } | nullfoldsOBSERVE.server.recordswhen the scenario runs under the server runtime —captureArtifact(() => renderToStream(…))— one row per<Loading>boundary that waited and per server-function execution;nullfor client captures and the browser bridge. New exported typesArtifactServer,ServerBoundaryRecord,ServerInvocationRecord(mirrors of the runtime'sBoundaryEvent/InvocationEvent; the package still depends on@solidjs/signalsalone). JSONL egress addsboundaryandinvocationlines and the header counts.InvocationEvent.boundary: a direct server-function call made during a<Loading>boundary's render pass carries that boundary's hydration id, the"boundary"record'sid— the join between a boundary's wait and the calls under it.- Two dev checks derived from the boundary facts in
ssrLoadingBoundary:ASYNC_WATERFALLwithdata.side: "server"(passes - 1sequential flights; 2 →info, structured only; 3+ → consolewarn) and a new codeSSR_CLIENT_CONTENT_MASKED(warn,ssr) for client-only content that surfaced only after a real server wait — the server's work discarded, the fallback shown for the wait. Dev tier only; the boundary clock now runs in dev without a listener. solid-js's serveremitFindingkeepsinfofindings off the console (structured channel only), matching the core.
-
7623ce1: Server diagnostics on
OBSERVE.diagnostics;OBSERVE.serverowned bysolid-js's server entryThe server runtime now reports on the same structured channel as the client. Findings — facts about a render, present in observe and dev builds —
SSR_RENDER_ERROR_CONTAINED(a render error a boundary routed;data.handlingisfallback,client, orfailed— the structured face of whatrenderToStream'sonErrorreceives, on the process-wide channel),SSR_SUBTREE_ABANDONED(a failed fragment's pending descendants discarded),SSR_STREAM_ABANDONED(consumer cancelled or sink failed mid-render),LATE_HEADER_WRITE(recorded beside the existing dev throw / prod log),SERVER_FN_ERROR_SANITIZED(the original error the production wire replaced), andFRAME_MARKER_CORRUPTEDfrom the frames client. Checks — dev-only guidance — convert every serverconsole.warnto a code:SERVER_WRITE,REVEAL_IN_RENDER_TO_STRING,LAZY_ASSET_UNMAPPED,PRELOAD_DESCRIPTOR_INVALID,HEAD_TAG_INVALID,BEHAVIOR_CLAIM_DROPPED, andUNRECOGNIZED_INSERT_VALUE(now one code and arenderkind on both platforms);ASYNC_OUTSIDE_LOADING_BOUNDARYon the server records withdata.side: "server"before it throws. Server components are labelled forownerPath(createComponentruns the body under a transparent<Name>owner in observe/dev — no hydration id consumed), soin <App> › <Page>reads the same on both sides, and the server entry installs the same repair-guide console footer as the client. Prod artifacts carry none of it;DiagnosticKindgainsssr,head,render.OBSERVE.server's objects (the invocation listener set, the trace-provider slot) are now created bysolid-js's server entry, once per process underSymbol.for("solid-js/observe/server")onglobalThis, rather than by@solidjs/web's module init: an observer'sinit()that imports onlysolid-jscan subscribe andprovidebefore the web runtime has loaded, and a host that bundles the runtime into its server build and instruments through a--imported module finds one listener set and one provider across both copies. The core keepsserver: {}; the client pays nothing. -
af94f67: Server observe surface:
OBSERVE.serverand the invocation channelOBSERVEgains aserverslot — an augmentableServerObserveinterface declared empty in@solidjs/signals(re-exported bysolid-js), typed and emitted into by@solidjs/web's server runtime, so server-side observability consumers subscribe on the oneOBSERVEobject they already know from the client. The first channel isOBSERVE.server.invocations:subscribe("invocation", (event, live) => …)delivers one{ id, direct, at, durationMs, outcome, deferred? }record per server-function execution — HTTP dispatch and direct SSR calls alike — when it settles, with the request event,request,args, and the result or the error as thrown beside it. Observers, not policy: any number of listeners, none able to alter the call;wrapInvocationremains the single policy hook.@solidjs/webnow publishes observe-tier server artifacts (dist/server.observe.js,server-functions/dist/server.observe.js,frames/dist/server.observe.js) under theobserveexport condition, alongside the existing dev/prod pairs. The surface and every emit site fold out of the prod artifacts. -
e87d694:
{ shallow: true }computed stores keep their leaves raw on every path (#3498). The projection draft no longer wraps a nested value in a draft proxy — leaf identity holds and a frozen leaf can no longer trip a Proxy invariant — and the loading shadow, its commit copy, the SSR draft and snapshots, and the hydration replay shadow copy only the root container instead of JSON-cloning the tree, which turnedDateinto a string,NaNintonull, droppedundefinedproperties, and could not represent BigInt or cycles. Deep stores are unchanged. -
dd19e9e: Consolidate value-selection predicates:
readerSeesCommitted(the full committed-vs-staged arm read()'s slow tail used to inline) andvisibleOverride/hasActiveOverride(one definition each, previously duplicated between the core, lanes, verdict channels and the store) — a zero-semantic-change refactor toward one implementation per rule (DESIGN-CONSOLIDATION, move 3b). -
c245532: Spec: A33 records the fallback-hold ruling — a flight observed only behind a
<Loading>fallback holds no transaction (#3375), and anonreset moves the hold onto the boundary rather than ending it (#3459). Comment-only citations at the two sites; no runtime change. -
34287d8: SSR render failures reach the client sanitized (#3468): the wire policy the server-function handler has applied since #3113/#3116 now covers every SSR road a failure takes — the error an
<Errored>serializes for hydration, a rejected async source serialized into the stream, a<Loading>fragment's_frrejection, a frame stream's error chunks (the fragment's, a live hole's, the root's). Outside the dev build a plain thrown value is replaced with a genericError("Internal Server Error");message,causeand own properties stay on the server. A"use server"function called in-process during SSR never touches the RPC wire, so before this the production page load shipped what that wire withholds.solid-js(server):<Errored>sanitizes before rendering its fallback and serializes the same replacement, so fallback markup and the hydration record agree. A fallback printingerr().messageshows the generic message in production, as for a server-function failure.markSafeError(Symbol.for("solid.SafeError")) passes through with own properties; an Error reached as a value is data and passes as written (#3113's ruling). One replacement per original, however many roads it takes. New findingSSR_ERROR_SANITIZED(info, observe + dev;data.errorthe original), besideSSR_RENDER_ERROR_CONTAINEDwhich carries the failure itself.ssrSanitizeErroris exposed to the runtimes throughsolid-js/internal. Server findings now carry theirownerPathin the observe artifact too — the core's walk reads_parentunder its own build's property mangling and found nothing on a server owner there, so the server entry locates its findings itself.@solidjs/web(server): the hydration serialize funnel guards every channel it writes (a promise's rejection, an async iterable's thrown step); a fragment's terminal error reaches the_frrejection and a transport sink's error chunk sanitized while the abandonment ledger keeps the original; the frame sink's root error chunk and the live-hole/live-attribute error chunks carry the replacement's message.@solidjs/signals: theSSR_ERROR_SANITIZEDcode.
The dev/prod line is the build variant: the
developmentserver artifacts keep full fidelity; production and observe sanitize. -
64f9266: A render effect that stops reading a pending memo no longer keeps the memo's source held, in every ordering. Reporter liveness now reads this pass's deps —
reporterBlocksSource's scan stops at_depsTailinstead of walking the committed frame's deps that A30 keeps linked until the commit trims them (a staged pass that had stopped reading the memo still looked live through its kept dep, and the hold it kept was the commit that would have trimmed it). A reporter retires when its pass drops a dep — not only when it recovers from pending, which a reporter registered by the stale-reader carve-out never was — and the retirement wakes every parked transaction rather than the reporter's stamp, since the transaction waiting on it registered it without stamping it. Semantic fuzzer (#3446), same campaign: 994 pass / 0 fail / 6 policy, from 984 / 4 / 12. -
0148d58: A render effect's untracked read of a store key held by a foreign action — a key with no node, or a
reconcileadoption held by the action — is now recorded for replay at the action's commit, as the signal path always was. Previously the store's backing-level selection served the committed value but skipped the registration, so the effect stayed on the pre-action value after the action settled. One registration (recordStaleReplay) is shared by the node path and the store's backing paths. -
765a656:
Object.getOwnPropertyDescriptoron a store is now reactive: the descriptor trap subscribes to the key's presence node and witnessesisPending()/affects()asindoes. Previously a render effect that inspected a key through a descriptor never re-ran for an optimistic add or delete, and anisPending()probe over it saw nothing. -
9db33cf: The store's backing-level visibility (which container — committed or staged — a reader of a held store sees, for property reads,
in,Object.keysand descriptors) is oneholdVisibleon the core's shared predicates for both hold kinds (a setter's fold, an adoption under a transaction), replacing six store-local helpers. No behavior change; −313 B minified in the store. -
bfd6f6c: Store node reads select committed-vs-staged by the core's
readerSeesCommitted(one Rule 1 implementation for signals and store nodes). Fixes a render effect's untracked read of a store key held by a foreign action never replaying at that action's commit — the store's hand-restated stale-of-foreign clause served the committed value but skipped the replay registration the signal path performs, leaving the effect on the pre-action value permanently. -
2054045: An optimistic store override now survives its key becoming unobserved. The property node was released with the override on it the moment its last reader left, so an untracked read of the key (
s.n) returned the committed value while the action was still live; the release now waits for the flush that resolves the override, as an optimistic signal keeps its override whether or not anything reads it. -
c410709: An optimistic add or delete on a store now survives its only structural observer leaving: the key's presence node was released with the membership override on it, so
in,Object.keysand property descriptors fell back to the committed structure while the action was still live. The release now waits for the flush that resolves the override, as the value slot's does. -
f555ec2: The store setter's owned-scope write guard no longer exempts roots (#3500). A
createRootbody is tree construction — every dev component body, every context Provider, the top ofrender(), and the whole SSR pass run directly under one — so a store write there is a write in an owned scope, exactly assetSignalhas always treated it. Dev/test builds now throwREACTIVE_WRITE_IN_OWNED_SCOPEfor store writes in root and component bodies that previously passed silently; production is unaffected (the guard is dev-only).OBSERVE.exclude: theIMMUTABLE_UPDATE_IN_STOREcensus now names the store's own owner as its subject rather than the writer's context, so an excluded panel's store stays silent however its writes arrive. The docs no longer suggestrunWithOwner(panelRoot, …)for panel writes — that is a write in an owned scope. -
d8e35a3: A memo or user effect created on mainline whose untracked read (
untrack(() => s.n),deep(s)) is of a store key held by a live action is now born held (A29), as the same read of a signal is: the pass enters the action's transaction and publishes nothing until the action commits. Previously the store's untracked paths served the held value without entering, so a mainline memo published the action's unrevealed write to the screen. Covers keys with and without a node andreconcileadoptions held by an action. -
5f7da9d: A transaction blocked on a memo's flight stays blocked while an upstream re-ask supersedes that flight (#3462).
transitionCompletejudged a reporter's source by its own flight alone (the self entry in_pendingSources). A re-ask upstream retires that entry and leaves the source pending on the new flight instead, so the verdict flipped to "complete" while the source's reader still could not render. The transaction was parked, so nothing re-judged it until a re-entry: repeatingsetShow(true)while the first write was held re-entered it, and the flush committedShow: truebesidePanel: hidden, with the panel catching up seconds later at the chain's landing. The source now blocks while it is pending on anything; the landing folds the transaction in as before (A15), and without the repeated write the frames are unchanged. -
7f5f902:
merge()/omit()views build their resolved key table on enumeration or once reads have paid for it, not on the first read;ssrElementwalks a view's entries instead of asking for its tableA
merge()/omit()view over plain objects keeps a resolved table — every key mapped to the leaf that owns it — so a clientspreadrerunning its effect, orObject.keys/{...props}, is one lookup per key. Since the views became lazy (#3454) that table was built by the first per-key trap read as well. On the server that is the wrong trade: a component reads its merged props a few times, the element serializes them once, and the view is discarded, so a Kobalte-shaped component chain (Dialog.Trigger→Button.Root→Polymorphic) paid for a table per layer per element. Profiled underrenderToString, a third of the time was in the table code (mergeTable,tableSet,omitTable) and the garbage it produced, plusArray.prototype.concatcombining omit filters.- Signals: a per-key
get/has/getOwnPropertyDescriptoranswers by a source walk (last source first, oneineach) until the view has been read 16 times — the break-even between a build (~60 ns per key of every leaf) and a walk (~20 ns per source) — and then builds the table as before, so a long-lived client view read on every reactive rerun is one lookup per read from its first few updates on. Enumeration (ownKeys, or a consumer askingresolvedTable) builds it outright, unchanged. An omit over one object never builds one. The read count lives in the table slot until it is decided, so a view carries no extra field. Combined omit filters are copied withslice+pushinstead ofconcat(2–3× cheaper once optimized, and — unlike a hand-written loop — no more expensive than the builtin in the interpreter and baseline tiers, which is what an instruction-count benchmark under Valgrind mostly runs). - Web (SSR):
ssrElementno longer prefers a view's resolved table; it walks the view's entries the way it already walked the array form — an omit over a merge as its filtered leaf entries — so serializing an element builds no table at all. Attribute order is the same merged order (a key at the position of the last source that carries it).
Same-process A/B on the tier-1
polymorphic-chainSSR bench (200 rows,renderToString): the chain form goes from 8.2× the compiled floor to 5.8×, chain-static from 7.7× to 5.6× (−27% wall-clock). At the signals layer the props-chain bench improves 5–17% on build, 6–14% on build+consume, and steady-state per-key reads on a prebuilt view are unchanged (the table is in use). The only slower band is a view read 16–20 times and never enumerated (+4–17% at depth 1–3, faster at depth 7), which is the transition the threshold is designed around.Tests: the table is undecided after a handful of reads on both a merge and an omit-over-merge, built after the 16th with identical answers before and after; enumeration builds it on a fresh view; a plain omit never builds one; a store-leaf view settles to "none" and keeps walking. SSR: a spread over an omit-over-merge and over a bare merge serializes the merged order and builds no table.
- Signals: a per-key
-
31adfce: A write is a proposal (A34, #3494). A mainline write to a node a transaction holds — the same value again or another — is a second proposal for the same slot: the writer's tick joins the hold at the next flush's start and reveals with it (
setA(1); setB(1)withb=1held holds A with B again; #3473 had dropped the entry, and with it the grouping, while fixing itsactiveTransitionleak — the entry is now deferred instead). A tick whose writes net to the committed value proposed nothing: the node is not staged, not stamped, and not pending (setShow(false); setShow(true)beside a heldsetCount(1)no longer readsisPending(show)true nor captures the latersetShow(false)into the hold — the hide that used to be lost). Fixes the torn[1, 0, 1]effect input from #3473's review. Also: a stale reader served a held memo's committed value counts as observing the memo's flight (a reveal in the flush that retires the flight's last reader holds the transaction —Count: 1no longer publishes beside a visibleCopy: 0), and alatest()shadow backfilled under a transaction never blocks its settle (removing the last reader releases the held write at once). From differential fuzzing of the ruling and review: a source going pending behind a memo's kept dependency tail now re-derives the memo instead of marking it pending (A30 amendment) — a reader that stopped reading a memo is no longer registered on the memo's next flight (which held an action's truth on a fetch nobody displayed), and a memo whose committed frame still derives from the source no longer publishes stale beside the source's new inputs (1 0withselectedderived fromremote(0)); the no-proposal drop applies to unstamped nodes only — a held proposal rewritten to the committed value stays its transaction's, so the authoritative value that follows commits instead of being skipped as another transaction's — and covers writable memos (createSignal(fn)) as it covers signals; a same-value write to a held node schedules its own flush, so the join never leaks into a later, unrelated tick; and the join drain runs inside the flush's guard, so a throwing comparator cannot wedge the scheduler.