github solidjs/solid @solidjs/signals@2.0.0-rc.9

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 value latest() / 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, length and isPending() see nothing before — while the writer's own channels (a functional updater, the store draft, the affects() declaration walk) compose on it. A rewrite of a node a transaction holds keeps the staged value the last flush left for latest()/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 bare yield is required after an await before anything that creates a reader — until(), latest(), a memo or effect, a mount — not only before writes. The until() docstring's own example had await straight into yield until(...); the until(...) expression is evaluated in the post-await continuation, 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())) with b held by a slow flight: selected's held pass read only b, and its landing trimmed a at once, before the write staged the landed value under the hold. A later mainline a write no longer reached selected, so A: 1 committed beside Selected: 0 while B still 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 the a write 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 — so setStore(async d => …) committed only the writes before its first await and 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, so setSignal has no such rule. Production is unchanged.

    Docs: the OBSERVE.exclude guidance in RFC 08 no longer suggests runWithOwner(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 (and solid-js/attribution), not methods of attribution.

    • costs(), feedback(), why(target), subscriptions(target), formatRerun(event), formatOrigin(origin) — import them by name. attribution keeps enable, disable, subscribe, markFlight and the record ring buffers (history, holds, interactions, navigations, waterfalls). attribution.format is formatRerun.
    • 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's sideEffects: false a consumer that only subscribes to records ships neither the tables nor the work of filling them — importing costs or feedback is 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/WriteCost and 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/AttributionFeedback are aliases of AttributionCostTables/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.

    • RerunEvent no longer carries the live node. It names its scope by nodeId — 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 ask OBSERVE.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) and subscriptions(target) are unchanged.
    • @solidjs/diagnostics artifact format v7: re-runs are stored verbatim (RerunRecord is now an alias of RerunEvent), and the artifact gains timeOrigin — the capturing process's performance.timeOrigin — so every relative at in it (re-runs, holds, records, diagnostic data) 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 — so supersededRead and the verdict now resolve the owning transaction through _overrideOwner. Also: a latest() / 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: enterStagedRead no 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 throws NotReadyError until the commit — it has no committed value to serve.

  • d826cd3: The client error hook — configureClientErrors({ onError }) from solid-js, and render/hydrate's onError option 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> / createErrorBoundary collected it and renders its fallback. The twin of the server's configureServerErrors. An uncaught error is not this hook's: the halt (REACTIVITY_HALTED) hands its cause to the platform's reportError, 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); ownerPath carries 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 by createErrorBoundary or the app's own configureClientErrors import; a root's hook is parked on the root owner under the registered ROOT_ERROR_HOOK symbol (defined in the scheduler), so render retains nothing for an app that passes none. Core floor unchanged; apps with a boundary +~200 B.

    The server surface consolidates on the same name. onError on renderToStream/renderToString and on handleServerFunctionRequest is the server error hook ((error, context) => wire | void); onServerError is removed. A one-argument onError written for the old shape keeps working and now hears every handled failure — filter on context.handling === "failed" for the request-failing ones alone. New handling: "serialize" for a hydration value that would not serialize (what seroval's onError reported, for a render that passed one). With no hook anywhere, a failure that fails the request still reaches console.error.

  • cc0396b: _parent joins _name as 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 _parent across the package boundary and only worked in dev:

    • solid-js's client hydration walks owner._parent to 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 — ownerPath and OBSERVE.exclude/isExcluded — over solid-js's server owners. ownerPath had a server-side shim (located(), now removed); OBSERVE.exclude was 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.ts checks the reserved fields survive in the mangled artifacts and scans the built client artifacts of solid-js, @solidjs/web and @solidjs/universal for any signals _ field that is not reserved; packages/web/test/server/server-owner-walks.spec.tsx runs ownerPath and OBSERVE.exclude over 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.error on the server error findings) for exporters that leave the process.

  • d2a36f5: dynamic(source, { static }) and isStatic(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 runtime styled() that always
    renders "li", and — the case this exists for — a polymorphic component whose
    as arrived as a literal. The compiler encodes as="button" at a call site as
    a data property and as={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 through merge()/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-backed merge() source is not.

    dynamic(source, { static: true }) then says the source cannot change: it is
    called once, untracked, at dynamic() 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")} />;
    }

    as stays 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 because isStatic reads 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 Loading boundary's on reset flipping its fallback state) then parked the effect with the transaction, leaving a show() ? details() : "hidden" reader stale until the unrelated async settled (#3412).

  • 53280e7: The error hooks and SSR_RENDER_ERROR_CONTAINED tell where an error was thrown apart from where it was met.

    ownerPath on ClientErrorContext and ServerErrorContext is 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 new boundaryPath is 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, ownerPath was 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. The SSR_RENDER_ERROR_CONTAINED finding follows: ownerPath locates the throw, data.boundary / data.boundaryPath the 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 (a restarted while b's first flight was up), b stays pending on a; the stale landing no longer let the transaction commit the newer signal beside the older derived value (2 / 1, #3373) or blip isPending to false (#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 an on-scoped boundary (#3375).
    • A collecting Loading boundary records every source the notifying effect is pending on, not only the one the notification carries — an on reset 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: 1 beside Details: 0, #3374).
    • A Loading boundary's on reset 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).
  • 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 on show → false showed Count: 1 beside Panel: 0 while Show still read true. The trim now waits for the run to apply, so the write re-derives the effect against the committed inputs (Panel: 1 beside Count: 1), and the hold's landing reveals hidden with Show: 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: 1 next to Count: 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: 1 next to Selected: 0 while Fixed: false).
  • 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 / 0 for good. A lane recompute now drops the hold it supersedes, override or not (#3377).
  • 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, onCleanup registrations) 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 on key consults isPending() through a memo freezing the page (#3528). The key is evaluated from notify, inside the pending memo's own pass; reading the pending isPending memo 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. The on accessor 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 Loading boundary whose on resets 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: 0 for 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, then Fast: 1 | Slow: 1 together.
  • a5d8eae: A memo computes under its own lane posture, never its puller's (#3442).

    A combined isPending(() => [fast(), copy()]) over two async memos, with copy a sync memo wrapping the slow one, released the hold as soon as the fast flight landed: Fast: 1 beside Slow: 0 with Pending: false, then Slow: 1 a second later. The probe effect carries the companion lane of the pending signals it reads, and its pull of copy ran under that lane — where a pending node on no lane serves its committed value instead of throwing — so copy published a stale settled value, dropped its pending status, and its readers stopped holding the slow flight. recompute now 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.ownKeys descriptor copies, {...props}) or mutate afterwards (@solidjs/html assigns props and a children getter after spreading), and a later merge tunnelled back to the original sources through the $SOURCES symbol — 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) where props is itself a plain merge result that covers every default now returns props directly 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 action write 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. recompute re-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.
  • 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). assignOrMergeLane now 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 onSettled callback (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 Show is removing keeps following latest() (#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: 1 beside the committed Sum: 0, and Sum: 2 arrived with B: 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 showed latest(count) at 0 beside the same read outside at 1.
  • 9da7f0a: isPending(details) reports the load of an optimistic value when details derives it through an async memo (#3379). notifyStatus now 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: recompute now 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, reporterBlocksSource already 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's effect × gatedAway cell.

  • 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() and createOptimistic sources 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. gatherHydratable asks once whether the root contains frame regions and tests containment against that list, instead of walking every keyed node's ancestor chain with closest("[data-fid]"); insert() builds a parent's claim array in one indexed pass over childNodes that drops separators as it copies, instead of an iterator spread followed by a compacting pass; and clearSnapshots assigns undefined to the extension's _snapshotValue rather than delete-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 reads true even 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 direct isPending(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 _transition stamp, 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 cached false for the whole hold, while the direct render-effect probe (which reads the committed value under the companion lane) reported true. The scan now runs for an unstamped node too: the transaction it resolves to is the one that owns its staged write.
  • a8a8949: latest(() => store.key) on a derived store (projection) that has not yet resolved threw for tracked and untracked reads but returned the seed through latest(): read() routes a latest() read to the companion before its firewall/status logic, and the leaf's own _value is the seed. latest() now judges "uninitialized" on the leaf's owner — the projection's firewall — and throws NotReadyError like every other read (A25: the seed is a draft, never a value; A7).

  • 632e45c: Give the latest() shadow companion the ownedWrite flag 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 reads latest(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 throws NotReadyError in every scope. Unowned callers (event handlers, imperative code) used to receive undefined — a value the accessor's type excludes — because the uninitialized case shared the pending-shadow fallback's condition in latestRead. isPending() is unchanged: an unowned probe still answers false (A16), which boolean admits. 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() and omit() are always lazy views, and props consumers read their leaves

    omit(props, ...keys) returns a live view of props for every input — a plain object included — instead of copying it with a getOwnPropertyDescriptor + defineProperty per 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: under Proxy it 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 a merge() carries one filtered view per flattened merge source, a merge() over an omit() takes the view record as a leaf, and nested omits fold their filters into one record. A component chain of merge(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) and ssrElement() 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 (instanceof is a getPrototypeOf trap, as expensive as a store read), and store detection goes through $TARGET, a symbol the store's get trap 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 internal hasStaticKeys(), spread() now skips the children effect for static children behind omit/merge layers (#3388 through views).

    Behavior changes:

    • Writes to a merge() or omit() 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/html now collects its own props and spreads into one merge() 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 without Proxy keep 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), the SOURCE_* kinds.

  • ca05917: OBSERVE.exclude now 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; observeServerFunctionCalls removed

    • @solidjs/signals: OBSERVE.recordssubscribe(type, listener), observed(type), emit(type, event, live) — the channel every runtime record rides, created once per process and registered on globalThis under Symbol.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 (extends HostRecordTypes; both declared empty, for the runtimes to augment — one augmenter per interface), RecordType, RecordEvent, RecordLive, RecordListener. Folds out of prod. New OBSERVE.attribution.currentOrigin() (and the currentOrigin hook on AttributionHooks): 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 own ChangeOrigin object, undefined when external or with no engine; for a runtime stamping a record of its own. The installed hooks are also registered on globalThis under Symbol.for("@solidjs/signals/observe/attribution"), the same reach-without-an-import the channel has.
    • solid-js: the "boundary" record moves from OBSERVE.server.records to OBSERVE.records (augmenting the core's RecordTypes). OBSERVE.server keeps only the trace slot; ServerRecords is gone.
    • @solidjs/web: the "invocation" and "frame" records move to OBSERVE.records (augmenting HostRecordTypes through solid-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" by id, and — through origin, the engine's own interaction/navigation object read at dispatch via currentOrigin() — the attribution engine's InteractionEvent / NavigationEvent / HoldEvent by 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: FrameEvent is FrameProducedEvent | FrameAppliedEvent, discriminated by side, same census on both; the client half (applyFrameResponse, one per stream in a response) adds address (the as remap) and outcome: "truncated" for a body that ended before complete, with live.response. Server census fix: regions counts html chunks addressed to a child frame id (the former count read a chunk type that does not exist), and shellMs is set by the stream's own shell only. The emitters and their wrappers fold out of the prod client artifacts behind the observe literal (prod applyFrameResponse and the server-function dispatch are the pre-existing functions, no extra frame or promise hop). The server-functions and frames client entries gain observe and development builds 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: observeServerFunctionCalls and the ServerFunctionCall / ServerFunctionRequestCall / ServerFunctionResponseCall types, from both server-function entries. Subscribe to OBSERVE.records "call" (client) or "invocation" (server) instead.
    • @solidjs/diagnostics (format v6): artifact.server is replaced by artifact.records: { boundary, invocation, frame, call } — always present, captured on both platforms including the browser bridge; types BoundaryRecord, InvocationRecord, FrameRecord (FrameProducedRecord | FrameAppliedRecord), CallRecord (with origin?: ChangeOrigin), ArtifactRecords replace the Server*Record / ArtifactServer names. JSONL: one line per record with type naming its table; the meta line's boundaryCount/invocationCount/frameCount become recordCounts: { boundary, invocation, frame, call }.
  • 328580f: omit() over a merge() is one record that holds the merge's record, not one leaf view per merge source

    An omit over a merge used to flatten at construction: one OmitView plus one combined hidden-key list per flattened leaf, and the next merge() 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 the MergeView record itself (a new source kind, SOURCE_MERGE) and is one record however many leaves the merge has; a later merge() carries it as one entry, and a later omit() 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 @internal sourceOwners(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. ssrElement collects 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 — no in walk per key through the layers, no key list per leaf, no table, and no per-entry classification (pushEntry is gone).
    • An omit's $SOURCES never answers anything now (previously its filtered leaf views); consumers reach the record through viewOf and 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 and isPending(x) false inside the adopting action's body, as store leaves already did through their own selection while signals answered the staged value and true. initTransition marks 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), and latest() 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. id pairs it with SSR_RENDER_ERROR_CONTAINED; passes counts render passes (a sequential chain reads as 3+); under a <Reveal> group the record waits for the group's swap so heldMs measures 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 of OBSERVE.attribution.subscribe(type, …). OBSERVE.server.invocations (unreleased) is renamed onto it: subscribe("invocation", …). The InvocationChannel type is gone; ServerRecords is the channel's interface.
    • Types now layer one augmenter per interface: @solidjs/signals declares ServerObserve empty; solid-js augments it with records: ServerRecords and trace: ServerTrace, declaring both; @solidjs/web augments those two through "solid-js". TraceSlot in @solidjs/web is now an alias of solid-js's ServerTrace. (Two augmentations of one re-exported interface through different module aliases merge order-dependently in TypeScript — one set was silently lost.)
    • @solidjs/signals exports ownerPath(subject) — the root-first component-label walk its diagnostics already make — so a record's ownerPath and the finding it pairs with come from the one walk.
  • 0d8347a: Server records reach the diagnostics artifact and the dev checks (server-dev-build-plan P4)

    • @solidjs/diagnostics artifact format v5: artifact.server: { boundaries, invocations } | null folds OBSERVE.server.records when the scenario runs under the server runtime — captureArtifact(() => renderToStream(…)) — one row per <Loading> boundary that waited and per server-function execution; null for client captures and the browser bridge. New exported types ArtifactServer, ServerBoundaryRecord, ServerInvocationRecord (mirrors of the runtime's BoundaryEvent/InvocationEvent; the package still depends on @solidjs/signals alone). JSONL egress adds boundary and invocation lines 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's id — the join between a boundary's wait and the calls under it.
    • Two dev checks derived from the boundary facts in ssrLoadingBoundary: ASYNC_WATERFALL with data.side: "server" (passes - 1 sequential flights; 2 → info, structured only; 3+ → console warn) and a new code SSR_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 server emitFinding keeps info findings off the console (structured channel only), matching the core.
  • 7623ce1: Server diagnostics on OBSERVE.diagnostics; OBSERVE.server owned by solid-js's server entry

    The 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.handling is fallback, client, or failed — the structured face of what renderToStream's onError receives, 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), and FRAME_MARKER_CORRUPTED from the frames client. Checks — dev-only guidance — convert every server console.warn to a code: SERVER_WRITE, REVEAL_IN_RENDER_TO_STRING, LAZY_ASSET_UNMAPPED, PRELOAD_DESCRIPTOR_INVALID, HEAD_TAG_INVALID, BEHAVIOR_CLAIM_DROPPED, and UNRECOGNIZED_INSERT_VALUE (now one code and a render kind on both platforms); ASYNC_OUTSIDE_LOADING_BOUNDARY on the server records with data.side: "server" before it throws. Server components are labelled for ownerPath (createComponent runs the body under a transparent <Name> owner in observe/dev — no hydration id consumed), so in <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; DiagnosticKind gains ssr, head, render.

    OBSERVE.server's objects (the invocation listener set, the trace-provider slot) are now created by solid-js's server entry, once per process under Symbol.for("solid-js/observe/server") on globalThis, rather than by @solidjs/web's module init: an observer's init() that imports only solid-js can subscribe and provide before 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 keeps server: {}; the client pays nothing.

  • af94f67: Server observe surface: OBSERVE.server and the invocation channel

    OBSERVE gains a server slot — an augmentable ServerObserve interface declared empty in @solidjs/signals (re-exported by solid-js), typed and emitted into by @solidjs/web's server runtime, so server-side observability consumers subscribe on the one OBSERVE object they already know from the client. The first channel is OBSERVE.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; wrapInvocation remains the single policy hook.

    @solidjs/web now publishes observe-tier server artifacts (dist/server.observe.js, server-functions/dist/server.observe.js, frames/dist/server.observe.js) under the observe export 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 turned Date into a string, NaN into null, dropped undefined properties, 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) and visibleOverride / 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 an on reset 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 _fr rejection, 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 generic Error ("Internal Server Error"); message, cause and 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 printing err().message shows 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 finding SSR_ERROR_SANITIZED (info, observe + dev; data.error the original), beside SSR_RENDER_ERROR_CONTAINED which carries the failure itself. ssrSanitizeError is exposed to the runtimes through solid-js/internal. Server findings now carry their ownerPath in the observe artifact too — the core's walk reads _parent under 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 _fr rejection 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: the SSR_ERROR_SANITIZED code.

    The dev/prod line is the build variant: the development server 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 _depsTail instead 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 reconcile adoption 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.getOwnPropertyDescriptor on a store is now reactive: the descriptor trap subscribes to the key's presence node and witnesses isPending() / affects() as in does. Previously a render effect that inspected a key through a descriptor never re-ran for an optimistic add or delete, and an isPending() 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.keys and descriptors) is one holdVisible on 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.keys and 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 createRoot body is tree construction — every dev component body, every context Provider, the top of render(), and the whole SSR pass run directly under one — so a store write there is a write in an owned scope, exactly as setSignal has always treated it. Dev/test builds now throw REACTIVE_WRITE_IN_OWNED_SCOPE for store writes in root and component bodies that previously passed silently; production is unaffected (the guard is dev-only).

    OBSERVE.exclude: the IMMUTABLE_UPDATE_IN_STORE census 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 suggest runWithOwner(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 and reconcile adoptions held by an action.

  • 5f7da9d: A transaction blocked on a memo's flight stays blocked while an upstream re-ask supersedes that flight (#3462).

    transitionComplete judged 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: repeating setShow(true) while the first write was held re-entered it, and the flush committed Show: true beside Panel: 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; ssrElement walks a view's entries instead of asking for its table

    A merge()/omit() view over plain objects keeps a resolved table — every key mapped to the leaf that owns it — so a client spread rerunning its effect, or Object.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.TriggerButton.RootPolymorphic) paid for a table per layer per element. Profiled under renderToString, a third of the time was in the table code (mergeTable, tableSet, omitTable) and the garbage it produced, plus Array.prototype.concat combining omit filters.

    • Signals: a per-key get/has/getOwnPropertyDescriptor answers by a source walk (last source first, one in each) 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 asking resolvedTable) 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 with slice + push instead of concat (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): ssrElement no 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-chain SSR 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.

  • 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) with b=1 held holds A with B again; #3473 had dropped the entry, and with it the grouping, while fixing its activeTransition leak — 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 held setCount(1) no longer reads isPending(show) true nor captures the later setShow(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: 1 no longer publishes beside a visible Copy: 0), and a latest() 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 0 with selected derived from remote(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.

Don't miss a new solid release

NewReleases is sending notifications on new releases.