@raf-query/core 0.0.19 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,10 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.0](https://github.com/KATT/raf-query/compare/0.0.19...0.1.0) (2026-10-12)
4
+
5
+ ### ⚠ BREAKING CHANGES
6
+
7
+ * Effect-native engine; core exposes only Effect APIs ([#23](https://github.com/KATT/raf-query/issues/23))
8
+ * **core:** make QueryObserver an Effect-style data type ([#29](https://github.com/KATT/raf-query/issues/29))
9
+
10
+ * **core:** make QueryObserver an Effect-style data type ([#29](https://github.com/KATT/raf-query/issues/29)) ([50fbb45](https://github.com/KATT/raf-query/commit/50fbb45bf3edcaa83673e2177894a76bdf76560b))
11
+ * Effect-native engine; core exposes only Effect APIs ([#23](https://github.com/KATT/raf-query/issues/23)) ([8e0bb09](https://github.com/KATT/raf-query/commit/8e0bb09a63d644ede053f443406a521ff2613a6f))
12
+
13
+ ### Features
14
+
15
+ * **core:** stream SSR dehydration as Effect APIs; integrations become thin edges ([#30](https://github.com/KATT/raf-query/issues/30)) ([6b2b570](https://github.com/KATT/raf-query/commit/6b2b5703f5463c0f3805c233cf4ead90df9efb76)), references [#29](https://github.com/KATT/raf-query/issues/29)
16
+ * typed services for Effect queries; QueryClient layer and dispose() ([#31](https://github.com/KATT/raf-query/issues/31)) ([6aeaa3d](https://github.com/KATT/raf-query/commit/6aeaa3d356f6483654729620ac26762508eb8fd8))
17
+
3
18
  ## [0.0.19](https://github.com/KATT/raf-query/compare/0.0.18...0.0.19) (2026-10-12)
4
19
 
5
20
  ### Features
6
21
 
7
- * **core:** type the cache's promises as Thenable ([#21](https://github.com/KATT/raf-query/issues/21)) ([c0541b0](https://github.com/KATT/raf-query/commit/c0541b0c14e687f25d47f25fe457e49b135d8c06))
22
+ - **core:** type the cache's promises as Thenable ([#21](https://github.com/KATT/raf-query/issues/21)) ([c0541b0](https://github.com/KATT/raf-query/commit/c0541b0c14e687f25d47f25fe457e49b135d8c06))
8
23
 
9
24
  ## [0.0.18](https://github.com/KATT/raf-query/compare/0.0.17...0.0.18) (2026-10-12)
10
25
 
package/README.md CHANGED
@@ -6,16 +6,18 @@ Queries start in route loaders and React Server Components, travel to components
6
6
 
7
7
  The cache's mechanics (dedupe, staleness, retries, infinite paging, SSR hydration) follow [TanStack Query](https://tanstack.com/query); the public API is built around loaders, RSC, and `use()` instead of `useQuery`-in-render.
8
8
 
9
+ The cache is written in [Effect](https://effect.website), but using raf-query doesn't take any Effect: `queryFn`s return promises, and `QueryClient`, the hooks and the integrations speak promises. Apps that do use Effect get the same cache as Effects, with typed errors and services ([Effect](#effect)).
10
+
9
11
  ## Status
10
12
 
11
13
  Early. Both packages are on npm at `0.0.x`, and the API can still change between patch releases. [`examples/`](examples/) has the same issue tracker built on TanStack Start and on Next.js App Router.
12
14
 
13
15
  ## Packages
14
16
 
15
- | Package | Contents |
16
- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
17
- | `@raf-query/core` | Framework-agnostic: `QueryClient`, cache, `queryOptions` / `infiniteQueryOptions`, handles, dehydrate / hydrate stream, `allKeyed` |
18
- | `@raf-query/react` | Hooks and provider. Re-exports everything from `@raf-query/core`, so apps install and import only this package |
17
+ | Package | Contents |
18
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
19
+ | `@raf-query/core` | Framework-agnostic and Effect-only: `QueryCache`, `queryOptions` / `infiniteQueryOptions`, handles, dehydrate / hydrate |
20
+ | `@raf-query/react` | `QueryClient` (the cache with Promise methods), hooks, provider, `allKeyed`. Re-exports `@raf-query/core`, so apps import only this package |
19
21
 
20
22
  `@raf-query/react` subpath entries for framework integrations and tooling:
21
23
 
@@ -363,7 +365,7 @@ function EditTitle(props: { post: Post }) {
363
365
  | `qc.setData(factory, input, updater)` | Write data directly |
364
366
  | `qc.getData(factory, input)` | Read cached data, or `undefined` |
365
367
 
366
- Data can be `undefined` when the `queryFn`'s type allows it (`Promise<User | undefined>`); it's cached and reused like any other value, unlike in TanStack Query, where an `undefined` result is an error. So `getData` and an updater's `previous` return `undefined` both with no data and with `undefined` data; `qc.getQuery(factory, input).getState().hasData` tells them apart.
368
+ Data can be `undefined` when the `queryFn`'s type allows it (`Promise<User | undefined>`); it's cached and reused like any other value, unlike in TanStack Query, where an `undefined` result is an error. So `getData` and an updater's `previous` return `undefined` both with no data and with `undefined` data; `Query.getUnsafe(qc.getQuery(factory, input)).hasData` tells them apart.
367
369
 
368
370
  ## SSR and hydration
369
371
 
@@ -392,7 +394,7 @@ export function getRouter() {
392
394
  }
393
395
  ```
394
396
 
395
- `setupRouterIntegration` streams queries during SSR, clears the request's cache when the response finishes, and turns `redirect()` thrown from a `queryFn` into a router navigation. `getRouter` runs per request on the server, so each request gets its own client. Options: `dehydrateOptions` (`filter`, `keepErrors`, `serializeData`), `hydrateOptions` (`deserializeData`), and `handleRedirects: false` to handle redirects yourself. By default a server error crosses to the browser as a generic error, so its message doesn't leak into the page.
397
+ `setupRouterIntegration` streams queries during SSR, clears the request's cache when the response finishes, and turns `redirect()` thrown from a `queryFn` into a router navigation. `getRouter` runs per request on the server, so each request gets its own client. Options: `dehydrateOptions` (`filter`, `keepErrors`, `serializeData`), `hydrateOptions` (`deserializeData`), and `handleRedirects: false` to handle redirects yourself. By default a server error crosses to the browser as a `RedactedError` naming the query, so its message doesn't leak into the page.
396
398
 
397
399
  ### Next.js App Router
398
400
 
@@ -458,13 +460,19 @@ A factory that imports server-only code (a database client) can't be imported by
458
460
 
459
461
  ## Effect
460
462
 
461
- raf-query runs requests on [Effect](https://effect.website), and Effect code can use it without wrapping:
463
+ raf-query's cache is written in [Effect](https://effect.website): `QueryClient` is a Promise wrapper around a `QueryCache` service from `@raf-query/core`, available as `qc.effect`. Effect code uses the cache directly, or builds its own with `QueryCache.make(config)` (scoped: closing the scope interrupts every request and timer) or `QueryCache.layer(config)`:
462
464
 
463
465
  - A `queryFn` can return an `Effect`. Cancelling the request interrupts it.
466
+ - An Effect `queryFn`'s error type is the query's: `qc.effect.fetch(issueQuery, input)` fails with `IssueNotFound | Query.CancelledError`. `ErrorOf<TFactory>` names it; it's `unknown` for promises. A `queryFn` that throws instead of returning is a defect, and isn't retried.
464
467
  - `retry` accepts a `Schedule` as well as a count or predicate.
465
- - `qc.effect` has Effect versions of `ensure`, `fetch` and `invalidate`, plus `watch`, a `Stream` of the entry's state.
468
+ - `qc.effect` has every verb as an Effect: `prefetch`, `ensure`, `fetch`, `invalidate`, `reset`, `remove`, `setData`, `getData` (an `Option`), `dehydrate`, `hydrate`. `watch` is a `Stream` of an entry's state, and `events` a `Stream` of cache events.
469
+ - SSR streaming is Effect too, and the integrations are thin wrappers over it. `dehydrateStream(options)` (scoped) returns the cache's `initial` state and a `Stream` of `updates`: entries that start or settle later, each sent once, batched per microtask. The stream ends when the scope closes. `hydrateStream(stream)` hydrates each state as it arrives. `Stream.toReadableStream` / `Stream.fromReadableStream` carry it over a web stream. The `sent` option (a `Set` of hashes) makes separate `dehydrate` calls of one request skip entries already sent.
470
+ - Entries are a data type with module functions, as in Effect's own modules: `Query.fetch(query)`, `Query.refetch(query)`, `Query.invalidate(query)`, `Query.changes(query)`, and `Query.getUnsafe(query)` / `Query.subscribeUnsafe(query, listener)` for synchronous reads.
471
+ - `QueryObserver` is a data type too: one reader's view of a query, as `useQuery` sees it (the controls and the promise, with `whilePending`, `refetchInterval` and the focus / reconnect options). `QueryObserver.make(query, options)` creates one, and `QueryObserver.changes(observer)` streams its results; while it runs, the query counts as read and polls on `refetchInterval`.
472
+ - Time comes from the `Clock` and the page from the `Platform` service (focus, visibility, `navigator.onLine`; the default reads the globals). Provide `TestClock` or another `Platform` to test staleness, gc and offline retries without a browser.
466
473
 
467
474
  ```ts
475
+ import { QueryCache, queryOptions } from "@raf-query/core";
468
476
  import { Effect, Schedule, Stream } from "effect";
469
477
 
470
478
  export function issueQuery(input: { id: number }) {
@@ -476,15 +484,69 @@ export function issueQuery(input: { id: number }) {
476
484
  }
477
485
 
478
486
  const program = Effect.gen(function* () {
479
- const issue = yield* qc.effect.fetch(issueQuery, { id: 1 });
480
- yield* qc.effect.watch(issueQuery, { id: 1 }).pipe(
487
+ const cache = yield* QueryCache.QueryCache;
488
+ const issue = yield* cache.fetch(issueQuery, { id: 1 });
489
+ yield* cache.watch(issueQuery, { id: 1 }).pipe(
481
490
  Stream.map((state) => state.data?.title),
482
491
  Stream.runForEach((title) => Effect.log(title)),
483
492
  );
484
493
  });
494
+
495
+ Effect.runFork(program.pipe(Effect.provide(QueryCache.layer())));
496
+ ```
497
+
498
+ `watch` counts as a reader while it runs, so invalidation and window focus refetch the entry. Outside React, `cache.mount` (`Effect<void, never, Scope>`) refetches read, stale entries on window focus and reconnect while its scope is open.
499
+
500
+ ### Services
501
+
502
+ A `queryFn`'s Effect can require services. They're part of the factory's type, `ServicesOf<TFactory>`, and every verb that can start the query requires them:
503
+
504
+ ```ts
505
+ import { QueryCache, queryOptions } from "@raf-query/core";
506
+ import { Context, Effect, Layer } from "effect";
507
+
508
+ class IssueApi extends Context.Service<
509
+ IssueApi,
510
+ { readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound> }
511
+ >()("app/IssueApi") {}
512
+
513
+ const IssueApiLive = Layer.succeed(IssueApi, { get: IssueRepo.get });
514
+
515
+ export function issueQuery(input: { id: number }) {
516
+ return queryOptions({
517
+ queryKey: ["issue", input.id],
518
+ queryFn: () =>
519
+ Effect.gen(function* () {
520
+ const api = yield* IssueApi;
521
+ return yield* api.get(input.id);
522
+ }),
523
+ });
524
+ }
525
+
526
+ // Effect<Issue, IssueNotFound | Query.CancelledError, QueryCache | IssueApi>
527
+ const issue = QueryCache.QueryCache.use((cache) =>
528
+ cache.fetch(issueQuery, { id: 1 }),
529
+ );
530
+
531
+ Effect.runFork(
532
+ issue.pipe(Effect.provide(QueryCache.layer()), Effect.provide(IssueApiLive)),
533
+ );
534
+ ```
535
+
536
+ - On `QueryCache`, `ensure`, `fetch`, `prefetch`, `getQuery` and `watch` require the factory's services, and so does `Query.fetch`. A request runs with the services the cache was built with, under those of the latest of these calls for its entry. Requests that start without a caller (a render's read, `refetch`, polling, focus and reconnect, `invalidate`) reuse the entry's last services.
537
+ - The caller's `Scope` isn't passed on: a request outlives the call that started it. Scope a `queryFn`'s resources with `Effect.scoped`.
538
+ - Each request runs in a `raf-query.request` span with a `raf_query.hash` attribute, under the latest caller's span if it had one. Defects are logged with `Effect.logError`; typed failures are the query's error.
539
+
540
+ `QueryClient` gets its services from a `layer`. It's built into the client's scope with the cache, so it must build synchronously. The client's type is `QueryClient<TServices>`, and its verbs accept only factories whose services the layer provides; any other factory is a type error. `dispose()` interrupts every request and runs the layer's finalizers:
541
+
542
+ ```ts
543
+ const queryClient = new QueryClient({ layer: IssueApiLive }); // QueryClient<IssueApi>
544
+ const handle = queryClient.prefetch(issueQuery, { id: 1 });
545
+
546
+ await queryClient.dispose();
485
547
  ```
486
548
 
487
- `watch` counts as a reader while it runs, so invalidation and window focus refetch the entry.
549
+ `QueryClientProvider` takes a client of any services. A hook can't know which ones, so `useQueryClient()` returns a `QueryClient<unknown>`, whose verbs accept any factory, and `useQuery` reads any factory: a request then fails with a defect if the client's layer lacks a service it needs.
488
550
 
489
551
  ## Devtools
490
552
 
@@ -533,17 +595,18 @@ Both render nothing on the server, since the server's cache isn't the browser's.
533
595
 
534
596
  | Member | Use |
535
597
  | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
536
- | `new QueryClient(config?)` | `config.defaults` (query options below), `config.onError`, `config.isServer` |
598
+ | `new QueryClient(config?)` | `config.defaults` (query options below), `config.onError`, `config.isServer`, `config.layer` ([services](#services)) |
537
599
  | `prefetch`, `ensure`, `fetch` | [Start queries](#starting-queries) |
538
600
  | `invalidate`, `setData`, `getData` | [Write and read the cache](#mutations) |
539
601
  | `reset(factory, input)` / `reset(filter)` | Drop data and errors; read queries suspend and refetch. Resolves when they settle |
540
602
  | `remove(factory, input)` / `remove(filter)` | Remove entries and abort their requests |
541
- | `effect` | [Effect versions](#effect) of `ensure`, `fetch`, `invalidate`, plus `watch` |
603
+ | `effect` | The [Effect](#effect) `QueryCache` behind the client |
542
604
  | `subscribe(listener)` | Cache events: `added`, `updated`, `observers` (a reader subscribed or left), `removed`, and `failed` (with the `error`) |
543
605
  | `getAll()`, `getQuery(factory, input?)` | Cache entries, for devtools and integrations |
544
606
  | `clear()` | Drop every entry and abort in-flight requests |
545
607
  | `mount()` | Start window-focus and reconnect refetching. The providers call it |
546
608
  | `isServer` | Detected from `window`. Server clients don't retry, never gc, skip stale timers, and never revalidate cached data |
609
+ | `dispose()` | Interrupt every request and release the layer's services. Resolves when its finalizers have run |
547
610
 
548
611
  Query options, accepted by `queryOptions`, `infiniteQueryOptions` and `config.defaults`:
549
612
 
@@ -562,29 +625,34 @@ In the browser, retries pause while `navigator.onLine` is `false`: after the ret
562
625
 
563
626
  `@raf-query/core`:
564
627
 
565
- | Export | Use |
566
- | ---------------------------------------------- | ---------------------------------------------------------------- |
567
- | `QueryClient` | The cache |
568
- | `queryOptions(options)` | Typed options for a query factory |
569
- | `infiniteQueryOptions(options)` | Typed options for a paged query factory |
570
- | `allKeyed(record)` | Await a record of promises, keeping keys |
571
- | `dehydrate(client)` / `hydrate(client, state)` | Low-level cache transfer used by the integrations |
572
- | `hashKey(key)`, `matchesKey(key, prefix)` | Key helpers |
573
- | `QueryObserver` | One reader's view of a query, with tracked results; for bindings |
574
- | `CancelledError` | Rejection of a request cancelled by `clear()` or a restart |
575
- | `QueryHandle<TFactory>` | Handle type for props |
576
- | `EnsuredQueryHandle<TFactory>` | Handle type for props fed by `ensure` |
577
- | `Thenable<T>` | Promise with `status` / `value` / `reason`, read by `use()` |
578
- | `DataOf<TFactory>`, `InputOf<TFactory>` | A factory's data and input types |
628
+ | Export | Use |
629
+ | ------------------------------------------------------------ | -------------------------------------------------------------------------- |
630
+ | `QueryCache` | The cache service (`qc.effect`): `make`, `layer`, and the verbs as Effects |
631
+ | `Query` | Cache entries: `Query.fetch`, `Query.changes`, `Query.getUnsafe`, … |
632
+ | `Platform` | The page the cache watches (focus, network); swap it in tests |
633
+ | `queryOptions(options)` | Typed options for a query factory |
634
+ | `infiniteQueryOptions(options)` | Typed options for a paged query factory |
635
+ | `hashKey(key)`, `matchesKey(key, prefix)` | Key helpers |
636
+ | `QueryObserver` | One reader's view of a query, with tracked results; for bindings |
637
+ | `Query.CancelledError` | Failure of a request whose entry was removed or cleared before it settled |
638
+ | `RedactedError` | What a streamed pending query rejects with when its server request failed |
639
+ | `QueryHandle<TFactory>` | Handle type for props |
640
+ | `EnsuredQueryHandle<TFactory>` | Handle type for props fed by `ensure` |
641
+ | `Thenable<T>` | Promise with `status` / `value` / `reason`, read by `use()` |
642
+ | `DataOf<TFactory>`, `InputOf<TFactory>`, `ErrorOf<TFactory>` | A factory's data, input and error types |
643
+ | `ServicesOf<TFactory>` | The services a factory's `queryFn` Effect needs; `never` for promises |
579
644
 
580
645
  `@raf-query/react` (plus all of the above):
581
646
 
582
- | Export | Use |
583
- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
584
- | `QueryClientProvider` | Provides a client to the tree |
585
- | `useQueryClient()` | Read the client |
586
- | `useQuery(factory, handle, options?)` | `[Thenable<Data>, controls]`; for paged factories `Data` is `{ pages, pageParams }` and controls include paging |
587
- | `useWaterfall(factory, input)` | Start a query during render and return its handle |
647
+ | Export | Use |
648
+ | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
649
+ | `QueryClient` | The cache with Promise methods ([above](#api-reference)) |
650
+ | `allKeyed(record)` | Await a record of promises, keeping keys |
651
+ | `dehydrate(client)` / `hydrate(client, state)` | Low-level cache transfer used by the integrations |
652
+ | `QueryClientProvider` | Provides a client to the tree |
653
+ | `useQueryClient()` | Read the client |
654
+ | `useQuery(factory, handle, options?)` | `[Thenable<Data>, controls]`; for paged factories `Data` is `{ pages, pageParams }` and controls include paging |
655
+ | `useWaterfall(factory, input)` | Start a query during render and return its handle |
588
656
 
589
657
  ## Development
590
658
 
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll };
@@ -0,0 +1,140 @@
1
+ import { QueryKey } from "./key.mjs";
2
+ import { DataOf, EnsuredQueryHandle, ErrorOf, InputArgs, InputOf, LoadOptions, QueryBehavior, QueryFactory, QueryHandle, ServicesOf } from "./options.mjs";
3
+ import { DehydrateOptions, DehydratedState, DehydratedStream, HydrateOptions } from "./hydration.mjs";
4
+ import { CancelledError, Of, Query, State, query_d_exports } from "./query.mjs";
5
+ import { Context, Effect, Layer, Option, Scope, Stream } from "effect";
6
+ declare namespace cache_d_exports {
7
+ export { Config, Event, Filter, LoadArgs, QueryCache, Target, TypeId, Updater, isQueryCache, layer, make };
8
+ }
9
+ export declare const TypeId = "~@raf-query/core/QueryCache";
10
+ export type TypeId = typeof TypeId;
11
+ export interface Config {
12
+ /** Defaults for every query; a factory's own options win. */
13
+ readonly defaults?: QueryBehavior;
14
+ /**
15
+ * Whether this cache runs on the server. Default: the {@link Platform}'s.
16
+ * Server caches don't retry, never garbage-collect, skip stale timers, and
17
+ * reuse cached data in `prefetch` / `ensure` / `fetch` instead of
18
+ * revalidating it, unless `invalidate` marked it.
19
+ */
20
+ readonly isServer?: boolean;
21
+ }
22
+ /** Emitted on {@link QueryCache.events} for every cache change. */
23
+ export type Event = {
24
+ readonly query: Query;
25
+ /** `observers`: a reader subscribed or unsubscribed. */
26
+ readonly type: "added" | "observers" | "removed" | "updated";
27
+ } | {
28
+ /** A request failed (after retries), with this error. */
29
+ readonly error: unknown;
30
+ readonly query: Query;
31
+ readonly type: "failed";
32
+ };
33
+ /** Matches cache entries by key prefix, or by exact key with `exact`. */
34
+ export interface Filter {
35
+ readonly exact?: boolean;
36
+ readonly queryKey: QueryKey;
37
+ }
38
+ /** `ensure` / `fetch` arguments: the input, then load options for paged factories. */
39
+ export type LoadArgs<TFactory extends QueryFactory> = InputArgs<TFactory> | [input: InputOf<TFactory>, options: LoadOptions<TFactory>];
40
+ /** The entries `invalidate`, `reset` and `remove` act on: one factory call, or a filter. */
41
+ export type Target<TFactory extends QueryFactory> = [factory: TFactory, ...args: InputArgs<TFactory>] | [filter: Filter];
42
+ /**
43
+ * A new value, or a function of the current one. `previous` is `undefined`
44
+ * both with no data and with `undefined` data.
45
+ */
46
+ export type Updater<TData> = TData | ((previous: TData | undefined) => TData);
47
+ export interface QueryCache {
48
+ /** Every entry. */
49
+ readonly all: Effect.Effect<ReadonlyArray<Query>>;
50
+ /** Drop every entry and interrupt their requests. */
51
+ readonly clear: Effect.Effect<void>;
52
+ /** Snapshot the cache for transfer to another cache (server → browser). */
53
+ dehydrate(options?: DehydrateOptions): Effect.Effect<DehydratedState>;
54
+ /**
55
+ * Snapshot the cache, then stream entries as they start or settle, each
56
+ * entry once. Entries that change in the same tick share a batch, which
57
+ * is sent a microtask later. `updates` ends when the scope closes, after
58
+ * sending what changed before; `hydrateStream` is the receiving end.
59
+ */
60
+ dehydrateStream(options?: DehydrateOptions): Effect.Effect<DehydratedStream, never, Scope.Scope>;
61
+ /**
62
+ * Start a query and wait for it on a cache miss. Fails if that request
63
+ * fails. Stale cached data succeeds immediately and revalidates in the
64
+ * browser. The handle is marked `ensured`, so `useQuery` returns its data
65
+ * directly.
66
+ */
67
+ ensure<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Effect.Effect<EnsuredQueryHandle<TFactory>, ErrorOf<TFactory> | CancelledError, ServicesOf<TFactory>>;
68
+ /** Cache events from the moment the stream starts. */
69
+ readonly events: Stream.Stream<Event>;
70
+ /** Like `ensure`, but succeeds with the data. */
71
+ fetch<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Effect.Effect<DataOf<TFactory>, ErrorOf<TFactory> | CancelledError, ServicesOf<TFactory>>;
72
+ /**
73
+ * Cached data for a factory call. `Some(undefined)` when the data itself
74
+ * is `undefined`.
75
+ */
76
+ getData<TFactory extends QueryFactory>(factory: TFactory, ...args: InputArgs<TFactory>): Effect.Effect<Option.Option<DataOf<TFactory>>>;
77
+ /**
78
+ * The cache entry for a factory call, created if missing. Doesn't fetch,
79
+ * but its requests run with this fiber's services from now on. For
80
+ * bindings; apps use the verbs.
81
+ */
82
+ getQuery<TFactory extends QueryFactory>(factory: TFactory, ...args: InputArgs<TFactory>): Effect.Effect<Of<TFactory>, never, ServicesOf<TFactory>>;
83
+ /**
84
+ * `getQuery`, synchronously, for renders. Leaves the entry's services as
85
+ * they are: the cache's, or the latest verb caller's.
86
+ */
87
+ getQueryUnsafe<TFactory extends QueryFactory>(factory: TFactory, ...args: InputArgs<TFactory>): Of<TFactory>;
88
+ /**
89
+ * Write dehydrated entries. Existing entries only take newer data; pending
90
+ * entries resume the sender's request.
91
+ */
92
+ hydrate(state: DehydratedState, options?: HydrateOptions): Effect.Effect<void>;
93
+ /** `hydrate` every state of a stream as it arrives (`dehydrateStream`'s `updates`). */
94
+ hydrateStream<E, R>(states: Stream.Stream<DehydratedState, E, R>, options?: HydrateOptions): Effect.Effect<void, E, R>;
95
+ /**
96
+ * Mark queries stale and refetch the ones being read. Succeeds once those
97
+ * refetches settle, so an action stays pending until fresh data is on
98
+ * screen.
99
+ */
100
+ invalidate<TFactory extends QueryFactory>(...target: Target<TFactory>): Effect.Effect<void>;
101
+ readonly isServer: boolean;
102
+ /**
103
+ * Refetch read, stale queries on window focus and reconnect while the
104
+ * scope is open. Mounts share one platform subscription. Does nothing on
105
+ * the server.
106
+ */
107
+ readonly mount: Effect.Effect<void, never, Scope.Scope>;
108
+ /**
109
+ * Start a query without waiting and return its handle. Fresh data is
110
+ * reused; stale data refetches in the background (in the browser only, see
111
+ * {@link Config.isServer}). Errors surface where the data is read, never
112
+ * here.
113
+ */
114
+ prefetch<TFactory extends QueryFactory>(factory: TFactory, ...args: InputArgs<TFactory>): Effect.Effect<QueryHandle<TFactory>, never, ServicesOf<TFactory>>;
115
+ /**
116
+ * Remove queries from the cache and interrupt their requests. Readers keep
117
+ * the removed entry until they render again.
118
+ */
119
+ remove<TFactory extends QueryFactory>(...target: Target<TFactory>): Effect.Effect<void>;
120
+ /**
121
+ * Drop queries' data and errors, as if they were never fetched. Read ones
122
+ * suspend and refetch; succeeds once those refetches settle.
123
+ */
124
+ reset<TFactory extends QueryFactory>(...target: Target<TFactory>): Effect.Effect<void>;
125
+ /** Write data for a factory call. Readers re-render without suspending. */
126
+ setData<TFactory extends QueryFactory>(factory: TFactory, input: InputOf<TFactory>, updater: Updater<DataOf<TFactory>>): Effect.Effect<void>;
127
+ /** Listen to cache events synchronously, as they happen. */
128
+ subscribeUnsafe(listener: (event: Event) => void): () => void;
129
+ readonly [TypeId]: TypeId;
130
+ /** A factory call's `Query.changes`. */
131
+ watch<TFactory extends QueryFactory>(factory: TFactory, ...args: InputArgs<TFactory>): Stream.Stream<State<DataOf<TFactory>>, never, ServicesOf<TFactory>>;
132
+ }
133
+ export declare const QueryCache: Context.Service<QueryCache, QueryCache>;
134
+ export declare function isQueryCache(value: unknown): value is QueryCache;
135
+ /** Build a cache in the current scope, with the current `Clock` and {@link Platform}. */
136
+ export declare const make: (config?: Config) => Effect.Effect<QueryCache, never, Scope.Scope>;
137
+ /** A layer that provides a {@link QueryCache} for the layer's lifetime. */
138
+ export declare const layer: (config?: Config) => Layer.Layer<QueryCache>;
139
+ //#endregion
140
+ export { cache_d_exports };
package/dist/cache.mjs ADDED
@@ -0,0 +1,176 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import { Platform } from "./platform.mjs";
3
+ import { unsafeCast } from "./cast.mjs";
4
+ import { listen } from "./listen.mjs";
5
+ import { hashKey, matchesKey } from "./key.mjs";
6
+ import { changes, emit, fetch, invalidate, isObserved, isStaleUnsafe, make as make$1, onPlatformEvent, provide, remove, reset, setData, setOptions, start } from "./entry.mjs";
7
+ import { dehydrateEntries, dehydrateStream, hydrateEntries } from "./hydration.mjs";
8
+ import { optionsFor } from "./options.mjs";
9
+ import { Clock, Context, Effect, Layer, Option, Predicate, RcRef, Stream } from "effect";
10
+ //#region src/cache.ts
11
+ /**
12
+ * The query cache, as an Effect service. `make` builds one in the current
13
+ * scope; closing the scope interrupts every request and timer it started.
14
+ * Everything that waits, or changes the cache, is an Effect; `getQueryUnsafe`
15
+ * and `subscribeUnsafe` are for renders and external stores.
16
+ *
17
+ * Requests run with the services the cache was built with, under those of
18
+ * the latest verb that called for the entry. So the verbs that can start a
19
+ * request (`ensure`, `fetch`, `prefetch`, `getQuery`, `watch`) require the
20
+ * factory's {@link ServicesOf}; requests a render, a refetch or `invalidate`
21
+ * starts reuse the entry's last services.
22
+ */
23
+ var cache_exports = /* @__PURE__ */ __exportAll({
24
+ QueryCache: () => QueryCache,
25
+ TypeId: () => TypeId,
26
+ isQueryCache: () => isQueryCache,
27
+ layer: () => layer,
28
+ make: () => make
29
+ });
30
+ const TypeId = "~@raf-query/core/QueryCache";
31
+ const QueryCache = Context.Service(TypeId);
32
+ function isQueryCache(value) {
33
+ return Predicate.hasProperty(value, TypeId);
34
+ }
35
+ function isUpdateFunction(updater) {
36
+ return Predicate.isFunction(updater);
37
+ }
38
+ function typed(entry) {
39
+ return unsafeCast(entry);
40
+ }
41
+ function handleOf(entry) {
42
+ return unsafeCast(entry.handle);
43
+ }
44
+ function dataOf(entry) {
45
+ return unsafeCast(entry.state.data);
46
+ }
47
+ /**
48
+ * Background refetch of cached data a verb reuses. A server cache renders
49
+ * once, so a refetch there could only duplicate a request whose result is
50
+ * never shown; it refetches only what `invalidate` marked.
51
+ */
52
+ function revalidate(entry) {
53
+ const state = entry.state;
54
+ const wanted = entry.cache.isServer ? state.isInvalidated : isStaleUnsafe(entry);
55
+ if (!state.isFetching && wanted) start(entry);
56
+ }
57
+ /**
58
+ * Waits for an entry's data on a cache miss. Cached data returns at once,
59
+ * and revalidates in the background (see `revalidate`).
60
+ */
61
+ function load(entry, pages) {
62
+ if (!entry.state.hasData) return unsafeCast(fetch(entry, { pages }));
63
+ revalidate(entry);
64
+ return Effect.succeed(dataOf(entry));
65
+ }
66
+ /** Settles every effect, started together. */
67
+ function settleAll(effects) {
68
+ return Effect.all(effects, {
69
+ concurrency: "unbounded",
70
+ discard: true
71
+ });
72
+ }
73
+ /** Build a cache in the current scope, with the current `Clock` and {@link Platform}. */
74
+ const make = Effect.fnUntraced(function* (config = {}) {
75
+ const platform = yield* Platform;
76
+ const state = {
77
+ clock: yield* Clock.Clock,
78
+ defaults: config.defaults ?? {},
79
+ entries: /* @__PURE__ */ new Map(),
80
+ isServer: config.isServer ?? platform.isServer,
81
+ listeners: /* @__PURE__ */ new Set(),
82
+ platform,
83
+ scope: yield* Effect.scope,
84
+ services: yield* Effect.context()
85
+ };
86
+ const entryFor = (key) => {
87
+ const hash = hashKey(key);
88
+ const existing = state.entries.get(hash);
89
+ if (existing) return existing;
90
+ const entry = make$1(state, key, hash);
91
+ state.entries.set(hash, entry);
92
+ emit(state, {
93
+ query: entry,
94
+ type: "added"
95
+ });
96
+ return entry;
97
+ };
98
+ const build = (factory, input) => {
99
+ const options = optionsFor(factory, input);
100
+ const entry = entryFor(options.queryKey);
101
+ setOptions(entry, options, input);
102
+ return entry;
103
+ };
104
+ /** `build`, for a verb: the entry's requests get the caller's services. */
105
+ const buildFor = (factory, input) => Effect.contextWith((caller) => {
106
+ const entry = build(factory, input);
107
+ provide(entry, caller);
108
+ return Effect.succeed(entry);
109
+ });
110
+ const match = (target) => {
111
+ const [first, input] = unsafeCast(target);
112
+ if (Predicate.isFunction(first)) {
113
+ const entry = state.entries.get(hashKey(optionsFor(first, input).queryKey));
114
+ return entry ? [entry] : [];
115
+ }
116
+ const hash = hashKey(first.queryKey);
117
+ return [...state.entries.values()].filter((entry) => first.exact ? entry.hash === hash : matchesKey(entry.key, first.queryKey));
118
+ };
119
+ const subscription = yield* RcRef.make({ acquire: Effect.acquireRelease(Effect.sync(() => platform.subscribeUnsafe((event) => {
120
+ for (const entry of state.entries.values()) if (isObserved(entry)) onPlatformEvent(entry, event);
121
+ })), (unsubscribe) => Effect.sync(unsubscribe)) });
122
+ const subscribeUnsafe = (listener) => {
123
+ state.listeners.add(listener);
124
+ return () => {
125
+ state.listeners.delete(listener);
126
+ };
127
+ };
128
+ const getQueryUnsafe = (factory, ...args) => typed(build(factory, args[0]));
129
+ return {
130
+ all: Effect.sync(() => [...state.entries.values()]),
131
+ clear: Effect.sync(() => {
132
+ for (const entry of state.entries.values()) remove(entry);
133
+ }),
134
+ dehydrate: (options = {}) => Effect.sync(() => dehydrateEntries(state.entries.values(), options)),
135
+ dehydrateStream: (options = {}) => dehydrateStream(() => state.entries.values(), subscribeUnsafe, options),
136
+ ensure: (factory, ...args) => Effect.flatMap(buildFor(factory, args[0]), (entry) => load(entry, args[1]?.pages).pipe(Effect.as({
137
+ ...handleOf(entry),
138
+ ensured: true
139
+ }))),
140
+ events: listen(subscribeUnsafe),
141
+ fetch: (factory, ...args) => Effect.flatMap(buildFor(factory, args[0]), (entry) => load(entry, args[1]?.pages)),
142
+ getData: (factory, ...args) => Effect.sync(() => {
143
+ const options = optionsFor(factory, args[0]);
144
+ const entry = state.entries.get(hashKey(options.queryKey));
145
+ return entry?.state.hasData ? Option.some(dataOf(entry)) : Option.none();
146
+ }),
147
+ getQuery: (factory, ...args) => Effect.map(buildFor(factory, args[0]), typed),
148
+ getQueryUnsafe,
149
+ hydrate: (dehydrated, options = {}) => Effect.sync(() => hydrateEntries(entryFor, dehydrated, options)),
150
+ hydrateStream: (states, options = {}) => Stream.runForEach(states, (dehydrated) => Effect.sync(() => hydrateEntries(entryFor, dehydrated, options))),
151
+ invalidate: (...target) => Effect.suspend(() => settleAll(match(target).map(invalidate))),
152
+ isServer: state.isServer,
153
+ mount: state.isServer ? Effect.void : Effect.asVoid(RcRef.get(subscription)),
154
+ prefetch: (factory, ...args) => Effect.map(buildFor(factory, args[0]), (entry) => {
155
+ if (entry.state.hasData) revalidate(entry);
156
+ else start(entry);
157
+ return handleOf(entry);
158
+ }),
159
+ remove: (...target) => Effect.sync(() => {
160
+ for (const entry of match(target)) remove(entry);
161
+ }),
162
+ reset: (...target) => Effect.suspend(() => settleAll(match(target).map(reset))),
163
+ setData: (factory, input, updater) => Effect.sync(() => {
164
+ const entry = build(factory, input);
165
+ const next = isUpdateFunction(updater) ? updater(entry.state.hasData ? dataOf(entry) : void 0) : updater;
166
+ setData(entry, next);
167
+ }),
168
+ subscribeUnsafe,
169
+ [TypeId]: TypeId,
170
+ watch: (factory, ...args) => Stream.unwrap(Effect.map(buildFor(factory, args[0]), (entry) => unsafeCast(changes(entry))))
171
+ };
172
+ });
173
+ /** A layer that provides a {@link QueryCache} for the layer's lifetime. */
174
+ const layer = (config) => Layer.effect(QueryCache, make(config));
175
+ //#endregion
176
+ export { QueryCache, TypeId, cache_exports, isQueryCache, layer, make };