| 331 | return Date.now() - ts >= staleTimeMs |
| 332 | } |
| 333 | |
| 334 | export const staleTimeMsOf = (opts: AtomQueryOptions): number => |
| 335 | Duration.toMillis(Duration.fromInputUnsafe(opts.staleTime ?? defaults.staleTime)) |
| 336 | |
| 337 | // A query fetch that is interrupted (a subscriber lost interest / a refresh superseded it / the |
| 338 | // component navigated) leaves the family atom `waiting=true` with NO fiber running — effect-core |
| 339 | // `makeEffect` removes its result-observer before interrupting, so the interrupt is never written |
| 340 | // back; it stays `waitingFrom(previous)`. `swr`/`isStaleResult` both short-circuit |
| 341 | // `if (waiting) return false`, so that stuck `waiting` is neither completing nor considered stale — |
| 342 | // mount/focus/staleness all skip it. Left alone the interrupt is TERMINAL: any current or future |
| 343 | // subscriber inherits the `waiting` forever (cold → `latestDefined` throws → white screen; warm → |
| 344 | // the Mako Bauhaus PickList stale wedge). |
| 345 | // |
| 346 | // The invariant we restore in this layer (no effect-core change): an interrupt must never be a |
| 347 | // terminal state. We do NOT auto-retry the interrupted fetch — an interrupt is intentional (that |
| 348 | // caller lost interest), and the first observer recovers through its normal refresh/staleness rules. |
| 349 | // We only guarantee that a *genuine* new/parallel/future observer is never stuck on a departed |
| 350 | // observer's interrupt: per family atom we count live computes (`inFlight`), so "stuck" = a `waiting` |
| 351 | // result with `inFlight === 0` (nothing is actually fetching). On mount a stuck atom triggers one |
| 352 | // fresh fetch instead of adopting the dangling `waiting`; a genuinely in-flight fetch (`inFlight > 0`) |