@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 +22 -1
- package/README.md +101 -32
- package/dist/all-keyed.d.mts +18 -0
- package/dist/all-keyed.mjs +19 -0
- package/dist/client.d.mts +94 -0
- package/dist/client.mjs +143 -0
- package/dist/devtools-panel.d.mts +1 -1
- package/dist/devtools-panel.mjs +5 -4
- package/dist/devtools-store.mjs +6 -5
- package/dist/hydrate.mjs +10 -16
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +3 -1
- package/dist/next.d.mts +2 -1
- package/dist/next.mjs +12 -10
- package/dist/provider.d.mts +3 -3
- package/dist/provider.mjs +4 -0
- package/dist/rsc.d.mts +2 -1
- package/dist/rsc.mjs +4 -12
- package/dist/sent.mjs +18 -0
- package/dist/tanstack-router.d.mts +2 -1
- package/dist/tanstack-router.mjs +13 -58
- package/dist/use-query.d.mts +4 -3
- package/dist/use-query.mjs +36 -9
- package/dist/use-waterfall.mjs +2 -1
- package/package.json +2 -2
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
|
-
|
|
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: `
|
|
18
|
-
| `@raf-query/react` |
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
480
|
-
yield*
|
|
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
|
-
`
|
|
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
|
|
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
|
|
566
|
-
|
|
|
567
|
-
| `
|
|
568
|
-
| `
|
|
569
|
-
| `
|
|
570
|
-
| `
|
|
571
|
-
| `
|
|
572
|
-
| `hashKey(key)`, `matchesKey(key, prefix)`
|
|
573
|
-
| `QueryObserver`
|
|
574
|
-
| `CancelledError`
|
|
575
|
-
| `
|
|
576
|
-
| `
|
|
577
|
-
| `
|
|
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
|
|
582
|
-
|
|
|
583
|
-
| `
|
|
584
|
-
| `
|
|
585
|
-
| `
|
|
586
|
-
| `
|
|
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
|
package/dist/client.mjs
ADDED
|
@@ -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 };
|
package/dist/devtools-panel.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
360
|
+
onClick: () => void Effect.runPromise(Query.reset(query))
|
|
360
361
|
}),
|
|
361
362
|
/* @__PURE__ */ jsx(ActionButton, {
|
|
362
363
|
color: statusColors.error,
|
package/dist/devtools-store.mjs
CHANGED
|
@@ -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 =
|
|
18
|
+
const state = Query.getUnsafe(query);
|
|
18
19
|
if (state.isFetching) return "fetching";
|
|
19
20
|
if (state.status === "error") return "error";
|
|
20
|
-
if (query
|
|
21
|
-
return
|
|
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
|
|
27
|
+
observers: Query.observerCountUnsafe(query),
|
|
27
28
|
query,
|
|
28
|
-
state:
|
|
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 {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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
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 {
|
|
4
|
-
import {
|
|
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
|
|
28
|
-
|
|
29
|
-
sent
|
|
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(
|
|
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
|
}
|
package/dist/provider.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { QueryClient } from "
|
|
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
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
|
-
|
|
32
|
-
|
|
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 {
|
|
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> {
|
package/dist/tanstack-router.mjs
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
|
30
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
/**
|
package/dist/use-query.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
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 =
|
|
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:
|
|
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
|
/**
|
package/dist/use-query.mjs
CHANGED
|
@@ -1,25 +1,52 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useQueryClient } from "./provider.mjs";
|
|
3
2
|
import { unsafeCast } from "./cast.mjs";
|
|
4
|
-
import {
|
|
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
|
-
|
|
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 [
|
|
19
|
-
const
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
const controls =
|
|
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
|
}
|
package/dist/use-waterfall.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
43
|
+
"@raf-query/core": "0.1.0",
|
|
44
44
|
"effect": "^4.0.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|