Skip to content

@solidjs/signals@2.0.0-rc.20

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 29 Oct 19:14
· 48 commits to main since this release
5e68925

Patch Changes

  • c9d1eeb: refresh(memo) inside an action no longer loops forever when a render
    effect reads isPending(memo) (#4349). The action's transaction landed
    beneath the re-ask's flight, so the reader observed the flight and held a
    frame of its own; the lane seam then re-derived that reader on the committed
    world every round, and each re-run re-entered the same transaction and parked
    it again until the dev guard threw "Potential Infinite Loop Detected". A
    reader a parked transaction holds that read none of its stagings now runs at
    the landing instead. The same loop hit [isPending(x), x()] in one render
    effect after an action's write to x's input. A bare refresh stays quiet
    (v41, then v42); affects() + refresh() shows pending, then v42.
    An action's affects() mark on a node whose own refetch is still in flight
    when the action's transaction lands now stays until that refetch lands, as a
    mark declared outside an action already did: an action that ends right after
    affects(x); refresh(x) shows pending whether the isPending read is inline
    or in its own memo, as JSX compiles a condition.

  • 178605c: isPending of a derived store's key no longer reads true for one pass at the store's first landing. A first answer is loading, not pending, as it is for the memo the store copies. Before, a <Show when={isPending(...)}> over such a key could open for that pass and mount content that was never meant to show.

  • 17ec571: A derived store whose derive reads an async memo through latest() no longer shows its seed for one pass after it first resolves: a memo (or a second derived store) reading it now sees what the derive wrote, so a render effect downstream commits only the real value, the same as one reading the store directly (#4355).

  • 178605c: A render effect reading a derived store that errored, then recovered through latest() into a new flight, now re-runs when that flight lands. Before, it never re-ran: an Errored around it went back to its content with the value missing, or a Loading around it stayed on its fallback.

  • 957f065: An <Errored> heals when the last reader that caught its error is disposed. The boundary waits on what threw only while a reader holding the error is alive, so a <Show when={isPending(m)}> that closes over m() once the action that made m throw settles no longer leaves the fallback up (#4373).

  • 3b7ea81: An <Errored> boundary no longer loops ("Potential Infinite Loop Detected") after a memo it caught is re-asked: a reader born in the flush that read a memo with a staged new error throws that error, not the committed one (D-41, #4404); a pass replacing an error the memo staged is a change even when it computes the committed value, so the latest() readers that threw the staged error re-derive (D-41, #4403); and at an action's landing an errored boundary tree re-derives, so it links the content the landing remounted and that content's recovery reaches it (D-3, D-39, #4405).

  • 2861ab8: An uninitialized async memo whose re-run throws from a dependency now reaches the wrapping <Errored> (#4364). The dependency's error was staged for the commit (D-41), but a reader threw it at once: a render effect reading a memo born staged, or the memo itself through latest(). The error reached the boundary naming that source, and the boundary's next pass took the source as settled because its error was only staged. It dropped the source and showed the content, so Loading revealed an empty hole that never recovered. An error staged on its source now keeps the boundary waiting until the commit publishes it.

  • 0f5b150: Smaller core for apps without isPending/latest or optimistic lanes: the verdict engine's async-memo bookkeeping, its re-derive checks and the pass-lane seat now sit behind the engines' hooks, so a bundle that never installs them folds that code away (hello world −92 B brotli). No behavior change.

  • e82efb0: A computed's error that halts the reactive system is rethrown as the error the user threw, not Solid's internal status wrapper. The wrapper carried the reactive node, so an error reporter (or a test runner) serializing the uncaught error walked the whole graph.

  • 2ba8a02: A memo that threw and then recovers under an open action now keeps its error on screen until the action commits. Before, a reader served the committed value, such as <Errored> content beside a latest() read ("X" + latest(s) + m()), read undefined in place of the error and showed X1undefined while the action was still open. The recovery's staging now keeps the error as the shown error until it commits (D-39, D-41). Frame and verdict readers are served that error, as an untracked read already was, and they re-derive when the action lands.

    The same holds for a memo the content created on its first pass and kept in a closure ((cached ||= createMemo(...))()). That memo is disposed with the pass that created it. The error boundary now waits on the reader when the source that threw is disposed. Before, it waited on the dead memo, which settled at once and looped the flush.

  • 910abc1: isPending(s) read through a memo (<Show when={isPending(s)}>, a compiled ternary's condition) no longer stays true after an action completes when s is also written outside the action (#4367). The outside write that matches the action's value ends the verdict, and the memo's pass leaves the verdict lane on a value equal to its committed one, so the equality gate notified nobody; the lane seam woke the reader that still showed the lane's true only when the lane's frame was not held, and the action held it. A pass outside any transaction's flush is the screen's: its readers now re-derive at the seam, as the inline isPending(s) does.

  • cddc7c3: A section a change mounts now commits once its async memos land when two boundaries read isPending() of the same memo and that memo lands last (#4338). The nested reader's pending held the isPending lane, so the sibling boundary's output became a lane node before the lane had shown; its render effect, reading that node from the frame, had no committed value to show and went pending on no flight — and was registered to re-derive at the lane's reveal only when it did not throw. It held the change, the lane ends only when the change lands, and the old content stayed on screen for good. The reader is now registered for the reveal before it waits, so it re-derives there and the change commits whole.

  • 81a12d8: A memo or render effect reading latest(a) beside b() no longer computes a torn pass on a batched write (#4365). The latest() read of a's staging opened a transaction for the flush, and the plain b() read after it took that transaction for a hold, so it paired a's new value with b's old one. Nothing held the flush, so that pass was shown: a render effect behind a memo ran [2,1] before [2,2], and a throw from the pass stuck at the error boundary. latest() no longer opens a transaction. Its reader's pass belongs to the frame, and if the flush parks it runs once more in the verdict lane, ahead of the hold.

  • 81a12d8: An async memo that reads latest() no longer asks the frame's question twice when its flush parks. Its first pass belongs to the frame and asks that frame's question. When the flush parks, the memo re-derives in the verdict lane for the display-ahead view, and that re-derive used to discard the frame's flight, so the transaction's landing asked the same question again. The landing now adopts the frame's flight, whether it has already landed or is still in flight.

  • 5f22701: latest(x) read inline and the same read through a memo (createMemo(() => latest(x))) show the same screen. A render effect re-run by a verdict memo's change now tears beside a held optimistic lane through the memo as it does inline (1 x1 / 5, #4284). A plain write beside an async memo shows the screen beside the proposal (v:0 l:1) in either read order and either form. The committed frame's effect beside one a held pass created tears during the hold inline as through a memo. latest() of an async derivation's first load still in flight in a verdict lane is loading, never undefined, including after the lane dissolves. The same holds through an identity memo or a derived store, and isPending() of such a load is loading too. A plain synchronous write beside latest() computes once. An <Errored> keeps its error on screen while the async memo the error came through re-asks after the source that threw recovers, instead of revealing content whose holes are still loading.

  • 81a12d8: Two latest() async readers under one async memo, with the second input written while the first one's flush is parked, ask each question once. A latest() reader whose frame lands at the seam no longer re-derives in the verdict lane, so its frame question isn't asked a second time. A verdict-lane pass that read a staging re-derives only while the transaction holding that staging stays parked, judged on that transaction rather than the lane's parent and not counting the pass's own render effect. Before, the memo asked a question twice and could show a value derived from the input's old answer next to its new one.

    A latest() reader's lane answer now reaches the screen whatever the grouping. An isPending() effect beside the reader no longer makes it wait for an unrelated hold, and an async memo over latest() that only its own flight holds shows the lane's answer before the final one. A [isPending(m), m()] reader of a sync latest() memo no longer shows a pending pair with the old value before the lane's.

  • 9baaba3: latest(), isPending() and plain reads of an uninitialized async memo now throw NotReady (A7) while the memo is re-asked as a verdict lane's work, for example when its compute reads latest(s) and s changes. Before, they returned undefined, and the lane's end committed that undefined as the memo's first answer, which tripped the flush loop guard (#4362, #4363). During hydration, a chain of memos over latest() adopts the streamed answers again instead of asking fetch(NaN) (#4376).

    A memo that reads latest() and throws over the committed value of an async input it was served while that input was in flight now re-derives when the input lands, so an <Errored> it tripped recovers (D-39). Before, the error stuck until some other input changed.

  • ffe0c1c: Dev: warn LOADING_ON_ACCESSOR when a Loading boundary's on receives a function (on={route} or on={() => route()}). The boundary never calls it, so it never re-arms; pass the value, on={route()}. Dev builds only.

  • b68942c: A <Loading on> boundary no longer shows its fallback when a held on change commits under an action's own refresh() (#4379). When an on change is held with an action (an input changed under its optimistic guess, #4336) and the content's new answer lands inside that hold a flush after the change, the boundary now folds the staged fallback back, as it already did when the content was ready at the change. The action's commit shows the content, and the quiet refresh() that follows keeps it on screen until it answers.

  • 7cacef2: A <Loading> armed by on={latest(id)} no longer stays on its fallback for good when another boundary's flight over the same id still holds the frame as the armed boundary's own flight lands. The boundary's output pass left the verdict lane inside the held transaction's flush with an answer equal to its committed value, so the equality gate notified nobody, and the lane seam wakes such a pass only when the frame is not held: when the frame landed, nothing re-ran the reader. A pass that leaves a lane the screen has shown while its frame is still held now hands the shown value to _value (what the screen shows stays the screen until the landing), so its answer is a change and lands with the frame.

  • ac28157: A per-key guess on createOptimisticStore stays on screen through a re-ask that lands while its action is pending (D-34, #4329). When the derive writes a keyed list through its draft (d.length = 0; d.push(...rows), or d.issues = rows) under a row that carries a guess, the new object at that row's slot is reconciled into the guessed row, as a returned landing is: the row keeps its proxy and the screen shows the guess until the action commits.

    On both landing paths, a guessed key is judged against its guess, so latest() answers the truth even when the truth equals the value the guess covered. A correction or confirmation no longer joins the whole landing to the guess's hold. When one re-ask answers guesses from two actions, they stay independent (D-33): each row reverts or commits when its own action settles, and rows with no guess show their new truth at the landing.

  • 1e115b8: createOptimisticStore: when a guess puts a different row object at an index, <For> (keyed and non-keyed) now picks up the new row at once and goes back at the settle, the same as direct reads. Before, <For> kept the old row if the new one had the same id, or, for rows without an id, the same contents or any contents in a reorder (#4344). An edit to that row then showed in direct reads but not on screen. A row the store already serves is now compared by the row it is, not by its key or contents. A server echo still confirms the guess.

  • 6b7e34a: An input changed outside an optimistic guess's action no longer tears the screen (#4336). When a createOptimistic or createOptimisticStore guess made in a pending action (a post's mine (sending) over sel() = 3) is re-asked because a plain write changed its input (setSel(1)), the write is held with that action (D-34). The screen keeps #3: dana | mine (sending) until the action commits, then shows #1: ... with its answer as one frame, whether the reads share one render effect or are split across several. isPending is true while the new question is in flight or held, including through the action's own refresh(), and latest() answers the newest truth. A refresh() of the same question still keeps its own frame. For a store, the hold applies to a frame that still reads a guess: a keyed store's other keys (comments[1] while the guess is on comments[3]) wait only for their own answer. An input set through its own optimistic write in another action now shows in its own lane once its answer lands, without the other action's guess beside it. Also fixed: a held write read in the same flush before the action's first lane read no longer shows before the commit. If that input's action ends without writing the truth, the other action's guess comes back in the revert's frame (#1: then #3: dana | mine (sending)) and lasts until its own action settles; the split-effect form waits with the input's lane instead of showing #1: beside the guess. A <Loading on> boundary whose fallback reads latest() no longer swaps to its fallback ahead of a held change: the swap lands with the change's frame (#4337).

  • 822fa01: A memo whose own error came from a pass over a held write no longer shows the Errored fallback for a moment when the hold lands. The landing re-derives the memo before the held effects run, as it already did for a render effect reading the same thing inline (#4407).

  • 2c63f41: A derived store whose derive peeks (untracked, catching NotReadyError) at a memo built from its own keys settles instead of re-running without bound (#4342). A derive's synchronous pass no longer re-runs the readers left linked to it from a flight that has already landed. A held frame keeps those links, and re-running them on every pass made a loop: the reader, then the peeked memo, then the derive, all in one flush, until queuePendingNode threw a RangeError. This is the query-hook shape: a keyed list of rows, each row with its own hook, and a parent key switched while the list's answer is in flight. Landings still re-run the readers that observed the flight (#4070).

  • 5f60713: A keyed createProjection whose function returned another store's proxy and then a plain array (for example a memo that returns issues or issues.filter(…)) keeps updating. Switching away from the proxy compared the new array against the proxy itself, which already showed the source store's new rows, so the projection's readers never heard the change and the frame stalled. The switch now tells every leaf and the container, and each node's own equality check decides whether its readers re-run (#4361).

  • 425a713: A render effect's first run goes through when its subtree mounts, whatever boundary above is still on its fallback in that flush (D-19). Under <Loading> > <Show when={ready()}> > <Loading>, a render effect in the inner content ran at once while the inner fallback showed when ready was a signal, but waited for the inner data when ready was an async memo settling: the outer boundary had not swapped to its content yet when the effect was created, and its first run was held like a queued one. Both gates now run it with the mount (#4343). User effects still wait for the reveal.

  • 38e0c49: <For> / mapArray over a shallow store diffs the store's backing array instead of reading every slot through the proxy. A one-row write on a 1,000-row shallow store spent ~1.3 ms in per-slot trap reads; the list's diff now costs what it does over a plain array (~0.05 ms). Optimistic shallow stores, accessor slots and reads inside a setter keep the per-slot path.

  • 56a982c: The first draft write to a plain store array clones it with slice() instead of copying one property descriptor per element. A push into a 10,000-row store drops from ~10.7 ms to ~0.15 ms per write. Arrays with accessors, named keys or a non-Array prototype keep the descriptor copy.

  • 1b32ff2: A reader created in the flush that clears an upstream error, under <Errored> and <Loading>, now shows the async memo's error once it rejects, instead of an empty hole. The rejection re-ran the error boundary outside the hold that had staged its content, and the boundary went back to the fallback it had already committed. Because that was no change, the boundary's reader did not re-run, and the hold's landing applied its stale run of the content. A held frame node that re-runs outside its hold now re-derives the readers of the staging it replaces (#4378).

  • 4d8195d: An unowned memo with an isPending() reader recomputes correctly (#4366). A module-level memo left with no reader goes dormant, and the next read wakes it with a recompute. When that read was an isPending(m) probe, the wake ran inside the probe's window, so the memo read its sources as the probe does, at their committed values: after an async source landed it derived from the old value (undefined + 1, so NaN), and after a source recovered from an error it kept the value from before the error. The wake is the memo's own pass (A31), the same as a pull, so it now runs outside the window.

  • 7ebd4a3: A memo of a verdict (createMemo(() => latest(s)) or createMemo(() => isPending(s))) no longer shows a held change early. Before, a pass that read an action's staged write and then read that memo moved into the verdict lane, and the lane showed what the pass had read while the action was still open. For example, <Errored> showed the branch that the action's write recovers in place of its error, and [s(), L()] in one render effect showed v:1 l:1. Now a derivation in that position belongs to the frame, as it does when it calls latest() inline, so its result appears when the action lands. The same holds for a pass created inside such a pass, such as the condition of a <Show> nested in a branch that read the staging; before, that case could leave the screen empty. A render effect in that position re-derives on the committed world when the flush parks, so it shows v:0 l:1, the same as the inline [s(), latest(s)] (D-26). The async-app generator found this through its E9 pair (seed 3289).

  • f94dcd5: An async memo that reads a memo using latest() or isPending() no longer asks the same question twice. Reading such a memo makes the async memo a verdict reader too (D-25). It was re-derived each time one of its verdict sources went pending, served a committed value it had already answered, and asked that question again; with two verdict sources it could ask a stale question after both had landed. Such a memo, one that calls neither latest() nor isPending() itself, now waits on a verdict source going pending like any derivation, and a re-run its hold makes reads plainly as the hold's work. It asks each question once. An async memo that calls latest() or isPending() itself is unchanged: it stays a verdict reader and shows the lane's answer ahead (F-6).

    A reader that groups isPending() with the value over a held write, such as [isPending(m1), m1()], now shows the lane's answer as a plain value reader does. A pass served a held memo's committed value no longer waits on that memo's flight, and a lane's reveal leaves the stale-reader mark the held transaction's landing needs to re-run it.

  • 409f893: A <Loading> that a verdict reader (<Show when={latest(x) ?? x()}>) mounts reveals its content when a component inside it writes a signal during its mount and a memo that stays pending with no reader reads it through other memos (gate = createMemo(() => version()), then a memo over gate), at any depth: that memo holds nothing and reaches no boundary. #4203's fix covered only a memo reading the written signal directly (#4340).

  • 85ad399: A memo or render effect born in a branch that a staged or held condition just opened no longer reads the screen when it calls latest() or isPending() (#4341). Its first pass runs inside the pass that opened the branch (or inside passes that branch is still creating, such as a <Show> or a component inside it), and the branch exists only in that world, so the pass reads that world like any derivation of it, and like bornInto does once the node is held. It becomes a verdict reader from its next pass. Before, the reader read the screen, where its branch was still closed: a <Match> callback's narrowed accessor saw its own branch as unmounted and threw "Attempting to access a stale value from ", which halted the reactive system. The order of reads inside the reader and whether a memo sits between the read and the binding no longer change this. Apps that do not use latest() or isPending() ship no new bytes.