@solidjs/signals
Advanced tools
@@ -7,5 +7,5 @@ import { globalQueue, activeTransition, currentTransition, setActiveTransition, schedule, flush } from "./scheduler.js"; | ||
| function restoreTransition(e, n) { | ||
| function restoreTransition(e, r) { | ||
| globalQueue.initTransition(e); | ||
| const t = n(); | ||
| const t = r(); | ||
| flush(); | ||
@@ -16,2 +16,18 @@ return t; | ||
| /** | ||
| * The primitive for mutations: imperative async workflows whose *writes span | ||
| * an async gap* — optimistic write, server round-trip, reconciling write — | ||
| * where intermediate state must not leak and failure must revert cleanly | ||
| * (pair with `createOptimistic` / `createOptimisticStore`). | ||
| * | ||
| * Navigation-shaped updates do not need an action. A plain setter call is | ||
| * enough: reads pull the async, and downstream async computeds hold their | ||
| * previous values per-node until the new ones are ready (`isPending` / | ||
| * `latest` expose the in-flight state). Reach for `action` only when writes | ||
| * happen *after* async work, not merely upstream of it. | ||
| * | ||
| * Framework-level actions (router form actions, server actions) are | ||
| * specializations of this primitive: they are actions in exactly this sense — | ||
| * the same transactional semantics — with form binding, serialization, and | ||
| * submission tracking layered on top. The shared name is deliberate. | ||
| * | ||
| * Wraps a generator function so each invocation runs as a single transaction | ||
@@ -63,8 +79,8 @@ * (a "transition") that batches every signal/store write between yields. The | ||
| */ function action(e) { | ||
| return (...n) => new Promise((t, r) => { | ||
| const i = e(...n); | ||
| return (...r) => new Promise((t, n) => { | ||
| const i = e(...r); | ||
| globalQueue.initTransition(); | ||
| let o = activeTransition; | ||
| o.Te.push(i); | ||
| const done = (e, n, s = false) => { | ||
| const done = (e, r, s = false) => { | ||
| o = currentTransition(o); | ||
@@ -75,8 +91,8 @@ const u = o.Te.indexOf(i); | ||
| schedule(); | ||
| s ? r(n) : t(e); | ||
| s ? n(r) : t(e); | ||
| }; | ||
| const step = (e, n) => { | ||
| const step = (e, r) => { | ||
| let t; | ||
| try { | ||
| t = n ? i.throw(e) : i.next(e); | ||
| t = r ? i.throw(e) : i.next(e); | ||
| } catch (e) { | ||
@@ -93,3 +109,25 @@ return done(undefined, e, true); | ||
| if (e.done) return done(e.value); | ||
| if (isThenable(e.value)) return void e.value.then(e => restoreTransition(o, () => step(e)), e => restoreTransition(o, () => step(e, true))); | ||
| // Thenable assimilation can itself throw synchronously (a `then` | ||
| // getter, or a `then()` method that throws — #2918). Match `await` | ||
| // semantics: the failure is thrown back into the generator at the | ||
| // yield point (catchable there); if uncaught, step()'s guard settles | ||
| // the action so its iterator never leaks in the transition. The | ||
| // settled flag implements A+ 2.3.3.3.4.1: a throw after the thenable | ||
| // already called a callback is ignored. | ||
| let r = false; | ||
| try { | ||
| if (isThenable(e.value)) return void e.value.then(e => { | ||
| if (r) return; | ||
| r = true; | ||
| restoreTransition(o, () => step(e)); | ||
| }, e => { | ||
| if (r) return; | ||
| r = true; | ||
| restoreTransition(o, () => step(e, true)); | ||
| }); | ||
| } catch (e) { | ||
| if (r) return; | ||
| r = true; | ||
| return void restoreTransition(o, () => step(e, true)); | ||
| } | ||
| restoreTransition(o, () => step(e.value)); | ||
@@ -96,0 +134,0 @@ }; |
| import { handleAsync, clearStatus, notifyStatus } from "./async.js"; | ||
| import { EFFECT_TRACKED, REACTIVE_OPTIMISTIC_DIRTY, NOT_PENDING, STATUS_UNINITIALIZED, REACTIVE_REASK, REACTIVE_RECOMPUTING_DEPS, CONFIG_SYNC, STATUS_PENDING, STATUS_ERROR, REACTIVE_NONE, REACTIVE_SNAPSHOT_STALE, unwrapOverride, OVERRIDE_UNDEFINED, CONFIG_AUTO_DISPOSE, REACTIVE_LAZY, REACTIVE_DISPOSED, REACTIVE_CHECK, REACTIVE_DIRTY, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, defaultContext, CONFIG_IN_SNAPSHOT_SCOPE, CONFIG_TRANSPARENT, CONFIG_OWNED_WRITE, CONFIG_NO_SNAPSHOT, $REFRESH, REACTIVE_MANUAL_WRITE, NO_SNAPSHOT, STORE_SNAPSHOT_PROPS, EFFECT_USER } from "./constants.js"; | ||
| import { EFFECT_TRACKED, REACTIVE_OPTIMISTIC_DIRTY, NOT_PENDING, STATUS_UNINITIALIZED, REACTIVE_REASK, REACTIVE_RECOMPUTING_DEPS, CONFIG_SYNC, STATUS_PENDING, STATUS_ERROR, REACTIVE_NONE, REACTIVE_SNAPSHOT_STALE, unwrapOverride, OVERRIDE_UNDEFINED, REACTIVE_LAZY, REACTIVE_DISPOSED, CONFIG_AUTO_DISPOSE, REACTIVE_CHECK, REACTIVE_DIRTY, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, defaultContext, CONFIG_IN_SNAPSHOT_SCOPE, CONFIG_TRANSPARENT, CONFIG_OWNED_WRITE, CONFIG_NO_SNAPSHOT, $REFRESH, REACTIVE_MANUAL_WRITE, NO_SNAPSHOT, STORE_SNAPSHOT_PROPS, EFFECT_USER } from "./constants.js"; | ||
@@ -5,0 +5,0 @@ import { NotReadyError } from "./error.js"; |
@@ -46,3 +46,11 @@ import { REACTIVE_IN_HEAP, EFFECT_TRACKED, EFFECT_USER, REACTIVE_IN_HEAP_HEIGHT, REACTIVE_RECOMPUTING_DEPS, REACTIVE_MANUAL_WRITE, REACTIVE_ZOMBIE, REACTIVE_CHECK, REACTIVE_DIRTY } from "./constants.js"; | ||
| e.u = t & -4 | REACTIVE_DIRTY | REACTIVE_IN_HEAP; | ||
| } else e.u = t | REACTIVE_IN_HEAP; | ||
| } else { | ||
| e.u = t | REACTIVE_IN_HEAP; | ||
| // An unmarked node entering a marked heap invalidates the markHeap memo: | ||
| // `_marked` is only reset by runHeap, so a write between two mid-tick | ||
| // pulls (read-time markHeap + updateIfNecessary) would otherwise leave | ||
| // this node unmarked and every downstream pull stale until the next | ||
| // flush (#2922: the second `latest()` returned the first write's value). | ||
| if (E.tE && !(t & REACTIVE_DIRTY)) E.tE = false; | ||
| } | ||
| if (!(t & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(e, E); | ||
@@ -49,0 +57,0 @@ } |
@@ -326,4 +326,8 @@ import { EFFECT_RENDER, EFFECT_USER, STATUS_PENDING, REACTIVE_DISPOSED, REACTIVE_ZOMBIE, NOT_PENDING, EFFECT_TRACKED, CONFIG_IN_SNAPSHOT_SCOPE, REACTIVE_SNAPSHOT_STALE, REACTIVE_OPTIMISTIC_DIRTY, REACTIVE_REASK, REACTIVE_MANUAL_WRITE, STATUS_UNINITIALIZED } from "./constants.js"; | ||
| // Detach: the stashed transition keeps its batch; ambient work that | ||
| // follows lands in a fresh one. | ||
| currentBatch = this.N = createBatch(); | ||
| // follows lands in a fresh one. If the batch is already a separate | ||
| // ambient one — action done() restored activeTransition without | ||
| // adopting the batch, and an ordinary write landed there before | ||
| // the scheduled flush (#2916) — keep it: replacing it would strand | ||
| // its queued pending nodes with held _pendingValues forever. | ||
| if (this.N === e) currentBatch = this.N = createBatch(); | ||
| // Run lane effects immediately (before stashing) - lanes with no pending async | ||
@@ -336,3 +340,6 @@ if (activeLanes.size) { | ||
| clock++; | ||
| scheduled = dirtyQueue.EE >= dirtyQueue.Le; | ||
| // A kept ambient batch may hold pending nodes (#2916): stay | ||
| // scheduled so the outer drain loop commits them via the plain | ||
| // flush path instead of leaving them until the next natural flush. | ||
| scheduled = dirtyQueue.EE >= dirtyQueue.Le || this.N.yt.length > 0; | ||
| reassignPendingTransition(e.yt); | ||
@@ -339,0 +346,0 @@ activeTransition = null; |
@@ -1,4 +0,4 @@ | ||
| import { NOT_PENDING, REACTIVE_DISPOSED, REACTIVE_DIRTY, REACTIVE_CHECK, unwrapOverride, STATUS_UNINITIALIZED, STATUS_PENDING, REACTIVE_MANUAL_WRITE } from "./constants.js"; | ||
| import { NOT_PENDING, REACTIVE_DISPOSED, REACTIVE_DIRTY, REACTIVE_CHECK, unwrapOverride, REACTIVE_ZOMBIE, STATUS_UNINITIALIZED, STATUS_PENDING, REACTIVE_MANUAL_WRITE } from "./constants.js"; | ||
| import { setSignal, read, context, stale, currentOptimisticLane, prepareComputed, tracking, setLatestReadActive, setContextInternal, optimisticComputed, setPendingCheckActive, latestReadActive, optimisticSignal, pendingCheckActive } from "./core.js"; | ||
| import { setSignal, prepareComputed, read, context, stale, currentOptimisticLane, tracking, setLatestReadActive, setContextInternal, optimisticComputed, setPendingCheckActive, latestReadActive, optimisticSignal, pendingCheckActive } from "./core.js"; | ||
@@ -9,3 +9,3 @@ import { NotReadyError } from "./error.js"; | ||
| import { insertIntoHeap, queueFor } from "./heap.js"; | ||
| import { insertIntoHeap, queueFor, markHeap } from "./heap.js"; | ||
@@ -18,3 +18,3 @@ import "./invariants.js"; | ||
| import { GlobalQueue, clock, insertSubs, schedule } from "./scheduler.js"; | ||
| import { GlobalQueue, clock, insertSubs, schedule, activeTransition } from "./scheduler.js"; | ||
@@ -180,2 +180,12 @@ /** | ||
| try { | ||
| // An untracked latest() read has no reading context, so read() never | ||
| // performs its mid-tick pull — a plain write queued between two latest() | ||
| // calls left a still-subscribed shadow at its previous speculative value | ||
| // until the flush (#2922). Mirror the tracked-read pull here: mark the | ||
| // queued staleness through the graph, then bring the shadow up to date. | ||
| const e = queueFor(t); | ||
| if (t.qe >= e.Le && !(t.u & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) { | ||
| markHeap(e); | ||
| prepareComputed(t, true); | ||
| } | ||
| r = read(t); | ||
@@ -196,2 +206,7 @@ } catch (t) { | ||
| } | ||
| // A shadow recomputed by the pull above (not at creation) holds its fresh | ||
| // speculative value in _pendingValue; a contextless read() only surfaces | ||
| // _value. Overrides stay authoritative (A17), and stale readers keep the | ||
| // other transition's committed view, matching read()'s own selection. | ||
| if (t.ge !== NOT_PENDING && !hasActiveOverride(t) && !(stale && t.ve && activeTransition !== t.ve)) return t.ge; | ||
| return r; | ||
@@ -198,0 +213,0 @@ } |
| /** | ||
| * The primitive for mutations: imperative async workflows whose *writes span | ||
| * an async gap* — optimistic write, server round-trip, reconciling write — | ||
| * where intermediate state must not leak and failure must revert cleanly | ||
| * (pair with `createOptimistic` / `createOptimisticStore`). | ||
| * | ||
| * Navigation-shaped updates do not need an action. A plain setter call is | ||
| * enough: reads pull the async, and downstream async computeds hold their | ||
| * previous values per-node until the new ones are ready (`isPending` / | ||
| * `latest` expose the in-flight state). Reach for `action` only when writes | ||
| * happen *after* async work, not merely upstream of it. | ||
| * | ||
| * Framework-level actions (router form actions, server actions) are | ||
| * specializations of this primitive: they are actions in exactly this sense — | ||
| * the same transactional semantics — with form binding, serialization, and | ||
| * submission tracking layered on top. The shared name is deliberate. | ||
| * | ||
| * Wraps a generator function so each invocation runs as a single transaction | ||
@@ -3,0 +19,0 @@ * (a "transition") that batches every signal/store write between yields. The |
| /** | ||
| * The primitive for mutations: imperative async workflows whose *writes span | ||
| * an async gap* — optimistic write, server round-trip, reconciling write — | ||
| * where intermediate state must not leak and failure must revert cleanly | ||
| * (pair with `createOptimistic` / `createOptimisticStore`). | ||
| * | ||
| * Navigation-shaped updates do not need an action. A plain setter call is | ||
| * enough: reads pull the async, and downstream async computeds hold their | ||
| * previous values per-node until the new ones are ready (`isPending` / | ||
| * `latest` expose the in-flight state). Reach for `action` only when writes | ||
| * happen *after* async work, not merely upstream of it. | ||
| * | ||
| * Framework-level actions (router form actions, server actions) are | ||
| * specializations of this primitive: they are actions in exactly this sense — | ||
| * the same transactional semantics — with form binding, serialization, and | ||
| * submission tracking layered on top. The shared name is deliberate. | ||
| * | ||
| * Wraps a generator function so each invocation runs as a single transaction | ||
@@ -3,0 +19,0 @@ * (a "transition") that batches every signal/store write between yields. The |
+1
-1
| { | ||
| "name": "@solidjs/signals", | ||
| "version": "2.0.0-beta.20", | ||
| "version": "2.0.0-beta.21", | ||
| "description": "Solid's reactive primitives: signals, memos, effects, stores, and async-aware computations.", | ||
@@ -5,0 +5,0 @@ "author": "Ryan Carniato", |
+5
-1
@@ -115,3 +115,3 @@ # @solidjs/signals | ||
| Use `action()` to coordinate async workflows with the reactive graph: | ||
| Use `action()` for mutations — imperative async workflows whose writes span an async gap. Each invocation runs as a single transaction: every write between yields batches into one atomic update, and nothing commits until the action completes or the next `yield` resolves. | ||
@@ -124,2 +124,6 @@ ```typescript | ||
| Navigation-shaped updates don't need an action. A plain setter call is enough — reads pull the async, downstream async computeds hold their previous values until new ones are ready, and `isPending`/`latest` expose the in-flight state. Reach for `action` when writes happen *after* async work (pairing with optimistic primitives for tentative state that reverts on failure), not when the async is merely downstream of a synchronous write. | ||
| Framework-level actions (router form actions, server actions) are specializations of this primitive: the same transactional semantics with form binding, serialization, and submission tracking layered on top. | ||
| ## Optimistic Updates | ||
@@ -126,0 +130,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
1070549
1.44%23660
0.97%261
1.56%