(
rt: AtomClientRuntime,
self: {
readonly id: string
readonly handler: (i: I) => Effect.Effect<A, E, any>
readonly options?: ClientForOptions
readonly queryKeyProjectionHash?: string
}
)
| 414 | if (opts.refetchInterval) atom = Atom.withRefresh(Duration.millis(opts.refetchInterval))(atom) |
| 415 | if (opts.structuralSharing ?? true) atom = structuralShare(atom) |
| 416 | return atom |
| 417 | } |
| 418 | |
| 419 | /** Constant atom for disabled / `mode:"optional"`-None queries: stays Initial, never fetches. */ |
| 420 | export const disabledQueryAtom: Atom.Atom<AsyncResult.AsyncResult<any, any>> = Atom.readable(() => |
| 421 | AsyncResult.initial(false) |
| 422 | ) |
| 423 | |
| 424 | /** |
| 425 | * Bumps when the browser regains connectivity (the `online` event) — the tanstack |
| 426 | * `refetchOnReconnect` trigger. One shared listener (module-level). SSR-guarded. |
| 427 | */ |
| 428 | const onlineSignal: Atom.Atom<number> = Atom.readable((get) => { |
| 429 | let count = 0 |
| 430 | if (typeof window === "undefined") return count |
| 431 | const update = () => { |
| 432 | if (navigator.onLine) get.setSelf(++count) |
| 433 | } |
| 434 | window.addEventListener("online", update) |
| 435 | get.addFinalizer(() => window.removeEventListener("online", update)) |
| 436 | return count |
| 437 | }) |
| 438 | |
| 439 | /** |
| 440 | * Focus OR reconnect, as a single signal for `swr` — both should stale-revalidate a query. |
| 441 | * swr takes one `focusSignal`, so we fold window-focus + reconnect into one derived atom; |
| 442 | * a bump from either triggers swr's stale check. |
| 443 | */ |
| 444 | const focusOrReconnectSignal: Atom.Atom<number> = Atom.make((get) => get(Atom.windowFocusSignal) + get(onlineSignal)) |
| 445 | |
| 446 | /** |
| 447 | * Build the per-input atom family for a request handler — the query CACHE IDENTITY. |
| 448 | * |
| 449 | * This is the TanStack `queryKey = [handler, input]` equivalent and the piece that makes |
| 450 | * caching cross-component: `Atom.family` memoizes one atom per structurally-distinct input |
| 451 | * (v4 hashes the input via Hash/Equal), so every component querying the same handler+input |
| 452 | * reads the SAME atom instance => one fetch, one shared result in the global registry, |
| 453 | * ref-counted and GC'd on idle ttl. (The registry + ttl give lifetime; reactivity keys give |
| 454 | * invalidation; the family gives identity/sharing — all three are needed.) |
| 455 | * |
| 456 | * The family is created once per handler (see query.ts's per-handler cache), so it is shared |
| 457 | * process-wide via the registry. |
| 458 | * |
| 459 | * Invalidation is hierarchical: each atom registers under EVERY prefix of its full key |
| 460 | * `[...makeQueryKey(self), input]`. Since reactivity matches keys by exact hash, registering |
| 461 | * all prefixes means `invalidate(P)` refreshes every atom whose key starts with `P` — e.g. |
| 462 | * `["$X"]` refreshes all inputs, `["$X","$List",input]` only that input. (`makeQueryKey`'s |
| 463 | * collapsed form `getQueryKey` — what mutations invalidate by default — is one of the prefixes.) |
| 464 | */ |
| 465 | export const buildQueryFamily = <I, A, E>( |
| 466 | rt: AtomClientRuntime, |
| 467 | self: { |
| 468 | readonly id: string |
| 469 | readonly handler: (i: I) => Effect.Effect<A, E, any> |
| 470 | readonly options?: ClientForOptions |
| 471 | readonly queryKeyProjectionHash?: string |
| 472 | } |
| 473 | ) => { |
searching dependent graphs…