@raf-query/react 0.0.18 → 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,31 @@
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
+
18
+ ## [0.0.19](https://github.com/KATT/raf-query/compare/0.0.18...0.0.19) (2026-10-12)
19
+
20
+ ### Features
21
+
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))
23
+
3
24
  ## [0.0.18](https://github.com/KATT/raf-query/compare/0.0.17...0.0.18) (2026-10-12)
4
25
 
5
26
  ### Bug Fixes
6
27
 
7
- * **core:** allow undefined query data ([#22](https://github.com/KATT/raf-query/issues/22)) ([9b85e57](https://github.com/KATT/raf-query/commit/9b85e57bf2fed4413cdbc1b49f925cc60dad695f)), references [#20](https://github.com/KATT/raf-query/issues/20)
28
+ - **core:** allow undefined query data ([#22](https://github.com/KATT/raf-query/issues/22)) ([9b85e57](https://github.com/KATT/raf-query/commit/9b85e57bf2fed4413cdbc1b49f925cc60dad695f)), references [#20](https://github.com/KATT/raf-query/issues/20)
8
29
 
9
30
  ## [0.0.17](https://github.com/KATT/raf-query/compare/0.0.16...0.0.17) (2026-10-12)
10
31
 
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,28 +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
- | `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 |
578
644
 
579
645
  `@raf-query/react` (plus all of the above):
580
646
 
581
- | Export | Use |
582
- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
583
- | `QueryClientProvider` | Provides a client to the tree |
584
- | `useQueryClient()` | Read the client |
585
- | `useQuery(factory, handle, options?)` | `[Promise<Data>, controls]`; for paged factories `Data` is `{ pages, pageParams }` and controls include paging |
586
- | `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 |
587
656
 
588
657
  ## Development
589
658
 
@@ -0,0 +1,18 @@
1
+ //#region src/all-keyed.d.ts
2
+ /** The resolved form of a record passed to {@link allKeyed}. */
3
+ export type Settled<TRecord> = { -readonly [TKey in keyof TRecord]: Awaited<TRecord[TKey]>; };
4
+ /**
5
+ * Ponyfill of the TC39 `Promise.allKeyed` proposal: a record of promises in,
6
+ * a promise of the same record out, rejecting on the first failure.
7
+ *
8
+ * ```ts
9
+ * const data = await allKeyed({
10
+ * post: qc.ensure(postQuery, { slug }),
11
+ * author: qc.ensure(authorQuery, { slug }),
12
+ * });
13
+ * ```
14
+ */
15
+ export declare function allKeyed<TRecord extends {
16
+ readonly [key: string]: unknown;
17
+ }>(record: TRecord): Promise<Settled<TRecord>>;
18
+ //#endregion
@@ -0,0 +1,19 @@
1
+ //#region src/all-keyed.ts
2
+ /**
3
+ * Ponyfill of the TC39 `Promise.allKeyed` proposal: a record of promises in,
4
+ * a promise of the same record out, rejecting on the first failure.
5
+ *
6
+ * ```ts
7
+ * const data = await allKeyed({
8
+ * post: qc.ensure(postQuery, { slug }),
9
+ * author: qc.ensure(authorQuery, { slug }),
10
+ * });
11
+ * ```
12
+ */
13
+ async function allKeyed(record) {
14
+ const keys = Object.keys(record);
15
+ const values = await Promise.all(keys.map((key) => record[key]));
16
+ return Object.fromEntries(keys.map((key, index) => [key, values[index]]));
17
+ }
18
+ //#endregion
19
+ export { allKeyed };
@@ -0,0 +1,94 @@
1
+ import { DataOf, DehydrateOptions, DehydratedState, EnsuredQueryHandle, HydrateOptions, InputArgs, InputOf, Query, QueryCache, QueryFactory, QueryHandle } from "@raf-query/core";
2
+ import { Layer } from "effect";
3
+ //#region src/client.d.ts
4
+ export interface QueryClientConfig<TServices = never> extends QueryCache.Config {
5
+ /**
6
+ * Services for queries whose `queryFn` returns an Effect. Built with the
7
+ * client, so it must build synchronously; `dispose()` runs its
8
+ * finalizers.
9
+ */
10
+ readonly layer?: Layer.Layer<TServices>;
11
+ /** Called with every failed request, including failed prefetches. */
12
+ onError?(error: unknown, query: Query.Query): void;
13
+ }
14
+ /**
15
+ * The query cache, with Promise methods. Create one per request on the
16
+ * server and one per browser tab. `effect` is the same cache's Effect API.
17
+ * Its verbs take factories whose `queryFn` needs at most `TServices`, the
18
+ * services of `config.layer`.
19
+ */
20
+ export declare class QueryClient<TServices = never> {
21
+ #private;
22
+ /** The Effect-native cache behind this client. */
23
+ readonly effect: QueryCache.QueryCache;
24
+ readonly isServer: boolean;
25
+ constructor(config?: QueryClientConfig<TServices>);
26
+ /**
27
+ * The cache entry for a factory call, created if missing. Doesn't fetch.
28
+ * For bindings; apps use the verbs.
29
+ */
30
+ getQuery<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): Query.Of<TFactory>;
31
+ /**
32
+ * Start a query without waiting and return its handle. Fresh data is
33
+ * reused; stale data refetches in the background (in the browser only).
34
+ * Errors surface where the data is read, never here.
35
+ */
36
+ prefetch<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): QueryHandle<TFactory>;
37
+ /**
38
+ * Start a query and wait for it on a cache miss. Rejects if that request
39
+ * fails. Stale cached data resolves immediately and revalidates in the
40
+ * browser. The handle is marked `ensured`, so `useQuery` returns its data
41
+ * directly.
42
+ */
43
+ ensure<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: QueryCache.LoadArgs<TFactory>): Promise<EnsuredQueryHandle<TFactory>>;
44
+ /** Like `ensure`, but resolves to the data. */
45
+ fetch<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: QueryCache.LoadArgs<TFactory>): Promise<DataOf<TFactory>>;
46
+ /**
47
+ * Mark queries stale and refetch the ones being read. Resolves once those
48
+ * refetches settle, so an action stays pending until fresh data is on
49
+ * screen. Takes a factory and its input, or a {@link QueryFilter}.
50
+ */
51
+ invalidate<TFactory extends QueryFactory<TServices>>(...target: QueryCache.Target<TFactory>): Promise<void>;
52
+ /**
53
+ * Drop queries' data and errors, as if they were never fetched. Read ones
54
+ * suspend and refetch; resolves once those refetches settle.
55
+ */
56
+ reset<TFactory extends QueryFactory<TServices>>(...target: QueryCache.Target<TFactory>): Promise<void>;
57
+ /**
58
+ * Remove queries from the cache and abort their requests. Readers keep the
59
+ * removed entry until they render again.
60
+ */
61
+ remove<TFactory extends QueryFactory<TServices>>(...target: QueryCache.Target<TFactory>): void;
62
+ /** Write data for a factory call. Readers re-render without suspending. */
63
+ setData<TFactory extends QueryFactory<TServices>>(factory: TFactory, input: InputOf<TFactory>, updater: QueryCache.Updater<DataOf<TFactory>>): void;
64
+ /**
65
+ * Cached data for a factory call, or `undefined` if there is none. Data can
66
+ * itself be `undefined`; `Query.getUnsafe(getQuery(...)).hasData` tells
67
+ * them apart.
68
+ */
69
+ getData<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): DataOf<TFactory> | undefined;
70
+ /** Every entry, for devtools and integrations. */
71
+ getAll(): ReadonlyArray<Query.Query>;
72
+ /** Listen to entries being added, updated, and removed. */
73
+ subscribe(listener: (event: QueryCache.Event) => void): () => void;
74
+ /** Drop every entry and abort in-flight requests. */
75
+ clear(): void;
76
+ /**
77
+ * Start refetching read, stale queries on window focus and reconnect.
78
+ * Returns the matching unmount. Providers call this; mounts are counted.
79
+ */
80
+ mount(): () => void;
81
+ /**
82
+ * Close the client: interrupt every request and timer, and release the
83
+ * layer's services. Resolves once the layer's finalizers have run.
84
+ */
85
+ dispose(): Promise<void>;
86
+ }
87
+ /** Snapshot the cache for transfer to another client (server → browser). */
88
+ export declare function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedState;
89
+ /**
90
+ * Write dehydrated entries into a client. Existing entries only take newer
91
+ * data; pending entries resume the sender's request.
92
+ */
93
+ export declare function hydrate(client: QueryClient, state: DehydratedState, options?: HydrateOptions): void;
94
+ //#endregion
@@ -0,0 +1,143 @@
1
+ import { unsafeCast } from "./cast.mjs";
2
+ import { QueryCache } from "@raf-query/core";
3
+ import { Context, Effect, Exit, Layer, Option, Scope } from "effect";
4
+ //#region src/client.ts
5
+ /**
6
+ * A verb's Effect, run by a client. Its requirement is the factory's
7
+ * `ServicesOf`, which the client's verbs only accept when its `layer`
8
+ * provides them.
9
+ */
10
+ function provided(effect) {
11
+ return unsafeCast(effect);
12
+ }
13
+ /**
14
+ * The query cache, with Promise methods. Create one per request on the
15
+ * server and one per browser tab. `effect` is the same cache's Effect API.
16
+ * Its verbs take factories whose `queryFn` needs at most `TServices`, the
17
+ * services of `config.layer`.
18
+ */
19
+ var QueryClient = class {
20
+ /** The Effect-native cache behind this client. */
21
+ effect;
22
+ isServer;
23
+ /**
24
+ * Owns the cache's fibers, mounts and the layer's services; lives until
25
+ * `dispose()`.
26
+ */
27
+ #scope = Scope.makeUnsafe();
28
+ constructor(config = {}) {
29
+ const cache = QueryCache.layer(config);
30
+ const services = Effect.runSync(Layer.buildWithScope(config.layer ? Layer.provide(cache, config.layer) : cache, this.#scope));
31
+ this.effect = Context.get(services, QueryCache.QueryCache);
32
+ this.isServer = this.effect.isServer;
33
+ const onError = config.onError;
34
+ if (onError) this.effect.subscribeUnsafe((event) => {
35
+ if (event.type === "failed") onError(event.error, event.query);
36
+ });
37
+ }
38
+ /**
39
+ * The cache entry for a factory call, created if missing. Doesn't fetch.
40
+ * For bindings; apps use the verbs.
41
+ */
42
+ getQuery(factory, ...args) {
43
+ return this.effect.getQueryUnsafe(factory, ...args);
44
+ }
45
+ /**
46
+ * Start a query without waiting and return its handle. Fresh data is
47
+ * reused; stale data refetches in the background (in the browser only).
48
+ * Errors surface where the data is read, never here.
49
+ */
50
+ prefetch(factory, ...args) {
51
+ return Effect.runSync(provided(this.effect.prefetch(factory, ...args)));
52
+ }
53
+ /**
54
+ * Start a query and wait for it on a cache miss. Rejects if that request
55
+ * fails. Stale cached data resolves immediately and revalidates in the
56
+ * browser. The handle is marked `ensured`, so `useQuery` returns its data
57
+ * directly.
58
+ */
59
+ ensure(factory, ...args) {
60
+ return Effect.runPromise(provided(this.effect.ensure(factory, ...args)));
61
+ }
62
+ /** Like `ensure`, but resolves to the data. */
63
+ fetch(factory, ...args) {
64
+ return Effect.runPromise(provided(this.effect.fetch(factory, ...args)));
65
+ }
66
+ /**
67
+ * Mark queries stale and refetch the ones being read. Resolves once those
68
+ * refetches settle, so an action stays pending until fresh data is on
69
+ * screen. Takes a factory and its input, or a {@link QueryFilter}.
70
+ */
71
+ invalidate(...target) {
72
+ return Effect.runPromise(this.effect.invalidate(...target));
73
+ }
74
+ /**
75
+ * Drop queries' data and errors, as if they were never fetched. Read ones
76
+ * suspend and refetch; resolves once those refetches settle.
77
+ */
78
+ reset(...target) {
79
+ return Effect.runPromise(this.effect.reset(...target));
80
+ }
81
+ /**
82
+ * Remove queries from the cache and abort their requests. Readers keep the
83
+ * removed entry until they render again.
84
+ */
85
+ remove(...target) {
86
+ Effect.runSync(this.effect.remove(...target));
87
+ }
88
+ /** Write data for a factory call. Readers re-render without suspending. */
89
+ setData(factory, input, updater) {
90
+ Effect.runSync(this.effect.setData(factory, input, updater));
91
+ }
92
+ /**
93
+ * Cached data for a factory call, or `undefined` if there is none. Data can
94
+ * itself be `undefined`; `Query.getUnsafe(getQuery(...)).hasData` tells
95
+ * them apart.
96
+ */
97
+ getData(factory, ...args) {
98
+ return Option.getOrUndefined(Effect.runSync(this.effect.getData(factory, ...args)));
99
+ }
100
+ /** Every entry, for devtools and integrations. */
101
+ getAll() {
102
+ return Effect.runSync(this.effect.all);
103
+ }
104
+ /** Listen to entries being added, updated, and removed. */
105
+ subscribe(listener) {
106
+ return this.effect.subscribeUnsafe(listener);
107
+ }
108
+ /** Drop every entry and abort in-flight requests. */
109
+ clear() {
110
+ Effect.runSync(this.effect.clear);
111
+ }
112
+ /**
113
+ * Start refetching read, stale queries on window focus and reconnect.
114
+ * Returns the matching unmount. Providers call this; mounts are counted.
115
+ */
116
+ mount() {
117
+ const scope = Scope.forkUnsafe(this.#scope);
118
+ Effect.runSync(this.effect.mount.pipe(Scope.provide(scope)));
119
+ return () => {
120
+ Effect.runSync(Scope.close(scope, Exit.void));
121
+ };
122
+ }
123
+ /**
124
+ * Close the client: interrupt every request and timer, and release the
125
+ * layer's services. Resolves once the layer's finalizers have run.
126
+ */
127
+ dispose() {
128
+ return Effect.runPromise(Scope.close(this.#scope, Exit.void));
129
+ }
130
+ };
131
+ /** Snapshot the cache for transfer to another client (server → browser). */
132
+ function dehydrate(client, options) {
133
+ return Effect.runSync(client.effect.dehydrate(options));
134
+ }
135
+ /**
136
+ * Write dehydrated entries into a client. Existing entries only take newer
137
+ * data; pending entries resume the sender's request.
138
+ */
139
+ function hydrate(client, state, options) {
140
+ Effect.runSync(client.effect.hydrate(state, options));
141
+ }
142
+ //#endregion
143
+ export { QueryClient, dehydrate, hydrate };
@@ -1,4 +1,4 @@
1
- import { QueryClient } from "@raf-query/core";
1
+ import { QueryClient } from "./client.mjs";
2
2
  import { CSSProperties } from "react";
3
3
  //#region src/devtools-panel.d.ts
4
4
  export type DevtoolsTheme = "dark" | "light" | "system";
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import { useOptionalQueryClient } from "./provider.mjs";
3
3
  import { countByStatus, sortOrders, statuses, useQueryRows, visibleRows } from "./devtools-store.mjs";
4
+ import { Query } from "@raf-query/core";
5
+ import { Effect, Predicate } from "effect";
4
6
  import { useState, useSyncExternalStore } from "react";
5
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
- import { Predicate } from "effect";
7
8
  //#region src/devtools-panel.tsx
8
9
  const palettes = {
9
10
  dark: {
@@ -346,17 +347,17 @@ function QueryDetails(props) {
346
347
  color: statusColors.fetching,
347
348
  disabled: options === void 0 || state.isFetching,
348
349
  label: "Refetch",
349
- onClick: () => void query.refetch()
350
+ onClick: () => void Effect.runPromise(Query.refetch(query))
350
351
  }),
351
352
  /* @__PURE__ */ jsx(ActionButton, {
352
353
  color: statusColors.stale,
353
354
  label: "Invalidate",
354
- onClick: () => void query.invalidate()
355
+ onClick: () => void Effect.runPromise(Query.invalidate(query))
355
356
  }),
356
357
  /* @__PURE__ */ jsx(ActionButton, {
357
358
  color: statusColors.inactive,
358
359
  label: "Reset",
359
- onClick: () => void query.reset()
360
+ onClick: () => void Effect.runPromise(Query.reset(query))
360
361
  }),
361
362
  /* @__PURE__ */ jsx(ActionButton, {
362
363
  color: statusColors.error,
@@ -1,3 +1,4 @@
1
+ import { Query } from "@raf-query/core";
1
2
  import { useSyncExternalStore } from "react";
2
3
  //#region src/devtools-store.ts
3
4
  const statuses = [
@@ -14,18 +15,18 @@ const sortOrders = [
14
15
  ];
15
16
  const stores = /* @__PURE__ */ new WeakMap();
16
17
  function statusOf(query) {
17
- const state = query.getState();
18
+ const state = Query.getUnsafe(query);
18
19
  if (state.isFetching) return "fetching";
19
20
  if (state.status === "error") return "error";
20
- if (query.observerCount === 0) return "inactive";
21
- return query.isStaleNow() ? "stale" : "fresh";
21
+ if (Query.observerCountUnsafe(query) === 0) return "inactive";
22
+ return Query.isStaleUnsafe(query) ? "stale" : "fresh";
22
23
  }
23
24
  function rowOf(query) {
24
25
  return {
25
26
  hash: query.hash,
26
- observers: query.observerCount,
27
+ observers: Query.observerCountUnsafe(query),
27
28
  query,
28
- state: query.getState(),
29
+ state: Query.getUnsafe(query),
29
30
  status: statusOf(query)
30
31
  };
31
32
  }
package/dist/hydrate.mjs CHANGED
@@ -1,27 +1,21 @@
1
1
  "use client";
2
+ import { hydrate } from "./client.mjs";
2
3
  import { useQueryClient } from "./provider.mjs";
3
- import { hashKey, hydrate } from "@raf-query/core";
4
+ import { sentHashes } from "./sent.mjs";
5
+ import { Query, hashKey } from "@raf-query/core";
4
6
  import { useEffect, useState } from "react";
5
7
  //#region src/hydrate.tsx
6
- const hydratedByClient = /* @__PURE__ */ new WeakMap();
7
- /** Hashes of entries a client received through {@link Hydrate}. */
8
- function hydratedHashes(client) {
9
- return hydratedByClient.get(client) ?? /* @__PURE__ */ new Set();
10
- }
11
- function markHydrated(client, state) {
12
- let hashes = hydratedByClient.get(client);
13
- if (!hashes) {
14
- hashes = /* @__PURE__ */ new Set();
15
- hydratedByClient.set(client, hashes);
16
- }
17
- for (const query of state.queries) hashes.add(hashKey(query.key));
8
+ /** In the SSR pass, entries from the RSC payload needn't be streamed again. */
9
+ function markSent(client, state) {
10
+ const sent = sentHashes(client);
11
+ for (const query of state.queries) sent.add(hashKey(query.key));
18
12
  }
19
13
  /**
20
14
  * Entries the client has no data for, which children need during this
21
15
  * render, and entries it already shows, which must wait for a commit.
22
16
  */
23
17
  function split(client, state) {
24
- const shown = new Set(client.getAll().filter((query) => query.getState().hasData).map((query) => query.hash));
18
+ const shown = new Set(client.getAll().filter((query) => Query.getUnsafe(query).hasData).map((query) => query.hash));
25
19
  const isShown = (query) => shown.has(hashKey(query.key));
26
20
  return {
27
21
  deferred: { queries: state.queries.filter(isShown) },
@@ -43,7 +37,7 @@ function Hydrate(props) {
43
37
  const [hydrated, setHydrated] = useState();
44
38
  if (hydrated?.state !== state) {
45
39
  const parts = split(client, state);
46
- markHydrated(client, state);
40
+ markSent(client, state);
47
41
  hydrate(client, parts.now, options);
48
42
  setHydrated({
49
43
  deferred: parts.deferred,
@@ -61,4 +55,4 @@ function Hydrate(props) {
61
55
  return props.children;
62
56
  }
63
57
  //#endregion
64
- export { Hydrate, hydratedHashes };
58
+ export { Hydrate };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,7 @@
1
+ import { QueryClient, QueryClientConfig, dehydrate, hydrate } from "./client.mjs";
2
+ import { Settled, allKeyed } from "./all-keyed.mjs";
1
3
  import { QueryClientProvider, useQueryClient } from "./provider.mjs";
2
4
  import { PagingControls, QueryControls, UseEnsuredQueryResult, UseQueryControls, UseQueryOptions, UseQueryResult, useQuery } from "./use-query.mjs";
3
5
  import { useWaterfall } from "./use-waterfall.mjs";
4
6
  export * from "@raf-query/core";
5
- export { type PagingControls, QueryClientProvider, type QueryControls, type UseEnsuredQueryResult, type UseQueryControls, type UseQueryOptions, type UseQueryResult, useQuery, useQueryClient, useWaterfall };
7
+ export { type PagingControls, QueryClient, type QueryClientConfig, QueryClientProvider, type QueryControls, type Settled, type UseEnsuredQueryResult, type UseQueryControls, type UseQueryOptions, type UseQueryResult, allKeyed, dehydrate, hydrate, useQuery, useQueryClient, useWaterfall };
package/dist/index.mjs CHANGED
@@ -1,5 +1,7 @@
1
+ import { allKeyed } from "./all-keyed.mjs";
2
+ import { QueryClient, dehydrate, hydrate } from "./client.mjs";
1
3
  import { QueryClientProvider, useQueryClient } from "./provider.mjs";
2
4
  import { useQuery } from "./use-query.mjs";
3
5
  import { useWaterfall } from "./use-waterfall.mjs";
4
6
  export * from "@raf-query/core";
5
- export { QueryClientProvider, useQuery, useQueryClient, useWaterfall };
7
+ export { QueryClient, QueryClientProvider, allKeyed, dehydrate, hydrate, useQuery, useQueryClient, useWaterfall };
package/dist/next.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { DehydratedState, QueryClientConfig } from "@raf-query/core";
1
+ import { QueryClientConfig } from "./client.mjs";
2
+ import { DehydratedState } from "@raf-query/core";
2
3
  import { ReactNode } from "react";
3
4
  //#region src/next.d.ts
4
5
  declare global {
package/dist/next.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  "use client";
2
+ import { QueryClient, dehydrate, hydrate } from "./client.mjs";
2
3
  import { QueryClientProvider } from "./provider.mjs";
3
- import { hydratedHashes } from "./hydrate.mjs";
4
- import { QueryClient, dehydrateQuery, hydrate } from "@raf-query/core";
4
+ import { sentHashes } from "./sent.mjs";
5
+ import { Query } from "@raf-query/core";
5
6
  import { useState } from "react";
6
7
  import { jsx } from "react/jsx-runtime";
7
8
  import { useServerInsertedHTML } from "next/navigation";
@@ -15,22 +16,23 @@ function receiveStream(client) {
15
16
  if (Array.isArray(queued)) for (const state of queued) hydrate(client, state);
16
17
  globalThis.RAF_QUERY_SSR = { push: (state) => hydrate(client, state) };
17
18
  }
19
+ function hasData(query) {
20
+ return Query.getUnsafe(query).hasData;
21
+ }
18
22
  /**
19
23
  * Streams entries that client components loaded during SSR (`useWaterfall`)
20
24
  * into the HTML, ahead of the markup that reads them. Entries that arrived
21
25
  * through `HydrationBoundary` are skipped: the RSC payload carries them.
22
- * Data must be JSON-serializable.
26
+ * Data must be JSON-serializable, so only settled entries are sent.
23
27
  */
24
28
  function useServerStream(client) {
25
- const [sent] = useState(() => /* @__PURE__ */ new Set());
26
29
  useServerInsertedHTML(() => {
27
- const hydrated = hydratedHashes(client);
28
- const queries = client.getAll().filter((query) => query.getState().hasData && !sent.has(query.hash) && !hydrated.has(query.hash)).map((query) => {
29
- sent.add(query.hash);
30
- return dehydrateQuery(query);
30
+ const state = dehydrate(client, {
31
+ filter: hasData,
32
+ sent: sentHashes(client)
31
33
  });
32
- if (queries.length === 0) return null;
33
- const json = escapeJson(JSON.stringify({ queries }));
34
+ if (state.queries.length === 0) return null;
35
+ const json = escapeJson(JSON.stringify(state));
34
36
  return /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: `(self.RAF_QUERY_SSR=self.RAF_QUERY_SSR||[]).push(${json})` } });
35
37
  });
36
38
  }
@@ -1,4 +1,4 @@
1
- import { QueryClient } from "@raf-query/core";
1
+ import { QueryClient } from "./client.mjs";
2
2
  import { ReactNode } from "react";
3
3
  //#region src/provider.d.ts
4
4
  /**
@@ -7,8 +7,8 @@ import { ReactNode } from "react";
7
7
  */
8
8
  export declare function QueryClientProvider(props: {
9
9
  readonly children?: ReactNode;
10
- readonly client: QueryClient;
10
+ readonly client: QueryClient<unknown>;
11
11
  }): import("react").JSX.Element;
12
12
  /** The client from the nearest {@link QueryClientProvider}. */
13
- export declare function useQueryClient(): QueryClient;
13
+ export declare function useQueryClient(): QueryClient<unknown>;
14
14
  //#endregion
package/dist/provider.mjs CHANGED
@@ -2,6 +2,10 @@
2
2
  import { createContext, use, useEffect } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  //#region src/provider.tsx
5
+ /**
6
+ * A hook can't know which services the provider's client has, so its verbs
7
+ * take any factory here.
8
+ */
5
9
  const QueryClientContext = createContext(null);
6
10
  /**
7
11
  * Provides a client to the tree, and refetches its read, stale queries on
package/dist/rsc.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { DehydrateOptions, QueryClient } from "@raf-query/core";
1
+ import { QueryClient } from "./client.mjs";
2
+ import { DehydrateOptions } from "@raf-query/core";
2
3
  import { ReactNode } from "react";
3
4
  //#region src/rsc.d.ts
4
5
  /**
package/dist/rsc.mjs CHANGED
@@ -1,8 +1,8 @@
1
+ import { dehydrate } from "./client.mjs";
2
+ import { sentHashes } from "./sent.mjs";
1
3
  import { Hydrate } from "./hydrate.mjs";
2
- import { canDehydrate, dehydrate, hashKey } from "@raf-query/core";
3
4
  import { jsx } from "react/jsx-runtime";
4
5
  //#region src/rsc.tsx
5
- const sentByClient = /* @__PURE__ */ new WeakMap();
6
6
  /**
7
7
  * Server component: sends the request client's entries to the browser cache.
8
8
  * Pending entries cross as promises and stream in as they settle. Entries an
@@ -20,18 +20,10 @@ const sentByClient = /* @__PURE__ */ new WeakMap();
20
20
  */
21
21
  function HydrationBoundary(props) {
22
22
  const client = props.client;
23
- let sent = sentByClient.get(client);
24
- if (!sent) {
25
- sent = /* @__PURE__ */ new Set();
26
- sentByClient.set(client, sent);
27
- }
28
- const alreadySent = sent;
29
- const filter = props.options?.filter ?? canDehydrate;
30
23
  const state = dehydrate(client, {
31
- ...props.options,
32
- filter: (query) => !alreadySent.has(query.hash) && filter(query)
24
+ sent: sentHashes(client),
25
+ ...props.options
33
26
  });
34
- for (const query of state.queries) alreadySent.add(hashKey(query.key));
35
27
  return /* @__PURE__ */ jsx(Hydrate, {
36
28
  state,
37
29
  children: props.children
package/dist/sent.mjs ADDED
@@ -0,0 +1,18 @@
1
+ //#region src/sent.ts
2
+ const sentByClient = /* @__PURE__ */ new WeakMap();
3
+ /**
4
+ * Hashes of a client's entries that already have a way to the browser
5
+ * cache: sent by an earlier `HydrationBoundary` of the request, or, in the
6
+ * SSR pass, hydrated from the RSC payload or streamed by `QueryProvider`.
7
+ * Integrations pass it as `sent`, so each entry crosses once.
8
+ */
9
+ function sentHashes(client) {
10
+ let sent = sentByClient.get(client);
11
+ if (!sent) {
12
+ sent = /* @__PURE__ */ new Set();
13
+ sentByClient.set(client, sent);
14
+ }
15
+ return sent;
16
+ }
17
+ //#endregion
18
+ export { sentHashes };
@@ -1,4 +1,5 @@
1
- import { DehydrateOptions, HydrateOptions, QueryClient } from "@raf-query/core";
1
+ import { QueryClient } from "./client.mjs";
2
+ import { DehydrateOptions, HydrateOptions } from "@raf-query/core";
2
3
  import { AnyRouter } from "@tanstack/react-router";
3
4
  //#region src/tanstack-router.d.ts
4
5
  export interface RouterIntegrationOptions<TRouter extends AnyRouter> {
@@ -1,13 +1,10 @@
1
- import { canDehydrate, dehydrateQuery, hydrate } from "@raf-query/core";
2
- import { Predicate } from "effect";
1
+ import { Effect, Exit, Predicate, Scope, Stream } from "effect";
3
2
  import { isRedirect } from "@tanstack/react-router";
4
3
  //#region src/tanstack-router.ts
5
4
  const noop = () => {};
6
5
  function setupServer(options) {
7
6
  const router = options.router;
8
7
  const queryClient = options.queryClient;
9
- const dehydrateOptions = options.dehydrateOptions;
10
- const shouldSend = dehydrateOptions?.filter ?? canDehydrate;
11
8
  const originalDehydrate = router.options.dehydrate;
12
9
  let finish = noop;
13
10
  let cleanedUp = false;
@@ -26,50 +23,18 @@ function setupServer(options) {
26
23
  queryClient.clear();
27
24
  return;
28
25
  }
29
- const sent = /* @__PURE__ */ new Set();
30
- const initial = [];
31
- for (const query of queryClient.getAll()) if (shouldSend(query)) {
32
- initial.push(dehydrateQuery(query, dehydrateOptions));
33
- sent.add(query.hash);
34
- }
35
- let controller;
36
- const stream = new ReadableStream({ start(streamController) {
37
- controller = streamController;
38
- } });
39
- let batch;
40
- const flush = () => {
41
- const queries = batch;
42
- batch = void 0;
43
- if (!queries || !controller) return;
44
- const chunk = [];
45
- for (const query of queries.values()) if (!sent.has(query.hash) && shouldSend(query)) {
46
- chunk.push(dehydrateQuery(query, dehydrateOptions));
47
- sent.add(query.hash);
48
- }
49
- if (chunk.length > 0) controller.enqueue(chunk);
50
- };
51
- const unsubscribe = queryClient.subscribe((event) => {
52
- if (event.type === "removed" || sent.has(event.query.hash)) return;
53
- if (!batch) {
54
- batch = /* @__PURE__ */ new Map();
55
- queueMicrotask(flush);
56
- }
57
- batch.set(event.query.hash, event.query);
58
- });
26
+ const scope = Scope.makeUnsafe();
27
+ const stream = Effect.runSync(queryClient.effect.dehydrateStream(options.dehydrateOptions).pipe(Scope.provide(scope)));
59
28
  finish = () => {
60
29
  finish = noop;
61
- flush();
62
- unsubscribe();
63
- try {
64
- controller?.close();
65
- } catch {}
30
+ Effect.runSync(Scope.close(scope, Exit.void));
66
31
  };
67
32
  router.serverSsr?.onRenderFinished(() => finish());
68
33
  return {
69
34
  ...dehydrated,
70
35
  raf: {
71
- initial,
72
- stream
36
+ initial: stream.initial,
37
+ stream: Stream.toReadableStream(stream.updates)
73
38
  }
74
39
  };
75
40
  };
@@ -79,28 +44,18 @@ function hasRafQueries(value) {
79
44
  }
80
45
  function setupClient(options) {
81
46
  const router = options.router;
82
- const queryClient = options.queryClient;
47
+ const cache = options.queryClient.effect;
83
48
  const hydrateOptions = options.hydrateOptions;
84
49
  const originalHydrate = router.options.hydrate;
85
50
  router.options.hydrate = async (dehydrated) => {
86
51
  await originalHydrate?.(dehydrated);
87
52
  if (!hasRafQueries(dehydrated)) return;
88
- hydrate(queryClient, { queries: dehydrated.raf.initial }, hydrateOptions);
89
- const reader = dehydrated.raf.stream.getReader();
90
- (async () => {
91
- try {
92
- for (;;) {
93
- const chunk = await reader.read();
94
- if (chunk.done) return;
95
- hydrate(queryClient, { queries: chunk.value }, hydrateOptions);
96
- }
97
- } catch (error) {
98
- console.error("raf-query: reading the query stream failed", error);
99
- reader.cancel(error).catch(() => {});
100
- } finally {
101
- reader.releaseLock();
102
- }
103
- })();
53
+ const raf = dehydrated.raf;
54
+ Effect.runSync(cache.hydrate(raf.initial, hydrateOptions));
55
+ Effect.runFork(cache.hydrateStream(Stream.fromReadableStream({
56
+ evaluate: () => raf.stream,
57
+ onError: (error) => error
58
+ }), hydrateOptions).pipe(Effect.catchCause((cause) => Effect.logError("raf-query: reading the query stream failed", cause))));
104
59
  };
105
60
  }
106
61
  /**
@@ -1,7 +1,8 @@
1
- import { DataOf, EnsuredQueryHandle, InputOf, IsPaged, QueryClient, QueryFactory, QueryHandle, QueryKey, QueryObserverOptions } from "@raf-query/core";
1
+ import "./client.mjs";
2
+ import { DataOf, EnsuredQueryHandle, InputOf, IsPaged, Query, QueryFactory, QueryHandle, QueryKey, QueryObserver, Thenable } from "@raf-query/core";
2
3
  //#region src/use-query.d.ts
3
4
  /** `refetchInterval`, `refetchOnWindowFocus`, `refetchOnReconnect`, `whilePending`. */
4
- export type UseQueryOptions = QueryObserverOptions;
5
+ export type UseQueryOptions = QueryObserver.Options;
5
6
  /** The second element of {@link useQuery}'s result. */
6
7
  export interface QueryControls<TFactory extends QueryFactory> {
7
8
  /** Error of the last failed request. Existing data stays readable. */
@@ -35,7 +36,7 @@ export interface PagingControls {
35
36
  readonly isFetchingPreviousPage: boolean;
36
37
  }
37
38
  export type UseQueryControls<TFactory extends QueryFactory> = IsPaged<TFactory> extends true ? QueryControls<TFactory> & PagingControls : QueryControls<TFactory>;
38
- export type UseQueryResult<TFactory extends QueryFactory> = readonly [promise: Promise<DataOf<TFactory>>, controls: UseQueryControls<TFactory>];
39
+ export type UseQueryResult<TFactory extends QueryFactory> = readonly [promise: Thenable<DataOf<TFactory>>, controls: UseQueryControls<TFactory>];
39
40
  /** {@link useQuery}'s result for a handle from `ensure`. */
40
41
  export type UseEnsuredQueryResult<TFactory extends QueryFactory> = readonly [data: DataOf<TFactory>, controls: UseQueryControls<TFactory>];
41
42
  /**
@@ -1,25 +1,52 @@
1
1
  "use client";
2
- import { useQueryClient } from "./provider.mjs";
3
2
  import { unsafeCast } from "./cast.mjs";
4
- import { QueryObserver } from "@raf-query/core";
3
+ import { useQueryClient } from "./provider.mjs";
4
+ import { Query, QueryObserver } from "@raf-query/core";
5
+ import { Effect } from "effect";
5
6
  import { use, useEffect, useState, useSyncExternalStore } from "react";
6
7
  //#region src/use-query.ts
7
8
  function queryFor(client, factory, input) {
8
9
  const args = [input];
9
10
  return client.getQuery(factory, ...args);
10
11
  }
12
+ const commands = /* @__PURE__ */ new WeakMap();
13
+ /** One stable Promise function per command Effect, so identities track the query's. */
14
+ function commandFor(effect) {
15
+ let command = commands.get(effect);
16
+ if (!command) {
17
+ command = () => Effect.runPromise(effect);
18
+ commands.set(effect, command);
19
+ }
20
+ return command;
21
+ }
22
+ /** `tracked`, with the observer's command Effects as Promise functions. */
23
+ function withPromiseCommands(tracked) {
24
+ return unsafeCast(new Proxy(tracked, { get: (target, name, receiver) => {
25
+ if (name === "fetchNextPage" || name === "fetchPreviousPage" || name === "refetch") return commandFor(target[name]);
26
+ return Reflect.get(target, name, receiver);
27
+ } }));
28
+ }
29
+ function storeFor(query, options) {
30
+ const observer = QueryObserver.makeUnsafe(query, options);
31
+ return {
32
+ getSnapshot: () => QueryObserver.getUnsafe(observer),
33
+ observer,
34
+ subscribe: (onChange) => QueryObserver.subscribeUnsafe(observer, onChange)
35
+ };
36
+ }
11
37
  function useQuery(factory, handle, options = {}) {
12
38
  const query = queryFor(useQueryClient(), factory, handle.input);
13
- query.read();
39
+ Query.readUnsafe(query);
14
40
  const refetchInterval = options.refetchInterval;
15
41
  const refetchOnReconnect = options.refetchOnReconnect;
16
42
  const refetchOnWindowFocus = options.refetchOnWindowFocus;
17
43
  const whilePending = options.whilePending;
18
- const [observer] = useState(() => new QueryObserver(query, options));
19
- const result = observer.getOptimisticResult(query, options);
20
- useSyncExternalStore(observer.subscribe, observer.getCurrentResult, observer.getCurrentResult);
44
+ const [store] = useState(() => storeFor(query, options));
45
+ const observer = store.observer;
46
+ const result = QueryObserver.getOptimisticResultUnsafe(observer, query, options);
47
+ useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
21
48
  useEffect(() => {
22
- observer.setOptions(query, {
49
+ QueryObserver.setOptionsUnsafe(observer, query, {
23
50
  refetchInterval,
24
51
  refetchOnReconnect,
25
52
  refetchOnWindowFocus,
@@ -33,8 +60,8 @@ function useQuery(factory, handle, options = {}) {
33
60
  refetchOnWindowFocus,
34
61
  whilePending
35
62
  ]);
36
- observer.trackProp("promise");
37
- const controls = observer.trackResult(result);
63
+ QueryObserver.trackProp(observer, "promise");
64
+ const controls = withPromiseCommands(QueryObserver.trackResult(observer, result));
38
65
  if (handle.ensured === true) return unsafeCast([use(result.promise), controls]);
39
66
  return unsafeCast([result.promise, controls]);
40
67
  }
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { useQueryClient } from "./provider.mjs";
3
+ import { Query } from "@raf-query/core";
3
4
  //#region src/use-waterfall.ts
4
5
  /**
5
6
  * Start a query during render and return its handle, for inputs only known
@@ -13,7 +14,7 @@ import { useQueryClient } from "./provider.mjs";
13
14
  */
14
15
  function useWaterfall(factory, ...args) {
15
16
  const query = useQueryClient().getQuery(factory, ...args);
16
- query.read();
17
+ Query.readUnsafe(query);
17
18
  return query.handle;
18
19
  }
19
20
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/react",
3
- "version": "0.0.18",
3
+ "version": "0.1.0",
4
4
  "description": "React bindings for raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",
@@ -40,7 +40,7 @@
40
40
  "./tanstack-router": "./dist/tanstack-router.mjs"
41
41
  },
42
42
  "dependencies": {
43
- "@raf-query/core": "0.0.18",
43
+ "@raf-query/core": "0.1.0",
44
44
  "effect": "^4.0.1"
45
45
  },
46
46
  "peerDependencies": {