@raf-query/core 0.11.1 → 0.12.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +28 -18
  3. package/dist/client.d.mts +132 -121
  4. package/dist/client.mjs +59 -162
  5. package/dist/entry.d.mts +67 -148
  6. package/dist/entry.mjs +303 -474
  7. package/dist/features/focus-refetch/focus-refetch.d.mts +15 -0
  8. package/dist/features/focus-refetch/focus-refetch.mjs +18 -0
  9. package/dist/features/polling/polling.d.mts +12 -0
  10. package/dist/features/polling/polling.mjs +35 -0
  11. package/dist/features/reconnect-refetch/reconnect-refetch.d.mts +15 -0
  12. package/dist/features/reconnect-refetch/reconnect-refetch.mjs +14 -0
  13. package/dist/features/shared.mjs +13 -0
  14. package/dist/guards.mjs +23 -0
  15. package/dist/hydration/hydration.d.mts +73 -0
  16. package/dist/hydration/hydration.mjs +128 -0
  17. package/dist/index.d.mts +18 -13
  18. package/dist/index.mjs +15 -13
  19. package/dist/infinite/infinite.d.mts +71 -0
  20. package/dist/infinite/infinite.mjs +88 -0
  21. package/dist/internal.d.mts +13 -0
  22. package/dist/internal.mjs +7 -0
  23. package/dist/keys/keys.d.mts +46 -0
  24. package/dist/keys/keys.mjs +53 -0
  25. package/dist/keys/targets.d.mts +56 -0
  26. package/dist/keys/targets.mjs +85 -0
  27. package/dist/links/retry/retry.d.mts +26 -0
  28. package/dist/links/retry/retry.mjs +51 -0
  29. package/dist/links/timeout/timeout.d.mts +9 -0
  30. package/dist/links/timeout/timeout.mjs +27 -0
  31. package/dist/load.d.mts +124 -0
  32. package/dist/load.mjs +165 -0
  33. package/dist/options.d.mts +112 -351
  34. package/dist/options.mjs +25 -41
  35. package/dist/presets.d.mts +29 -0
  36. package/dist/presets.mjs +25 -0
  37. package/dist/root.d.mts +82 -0
  38. package/dist/structural-sharing/structural-sharing.mjs +32 -0
  39. package/dist/subscribe.d.mts +15 -0
  40. package/dist/subscribe.mjs +29 -0
  41. package/dist/thenable.d.mts +13 -17
  42. package/dist/thenable.mjs +25 -6
  43. package/dist/verbs.d.mts +109 -0
  44. package/dist/verbs.mjs +87 -0
  45. package/package.json +13 -9
  46. package/dist/_virtual/_rolldown/runtime.mjs +0 -13
  47. package/dist/all-keyed.d.mts +0 -18
  48. package/dist/all-keyed.mjs +0 -19
  49. package/dist/binding.d.mts +0 -92
  50. package/dist/binding.mjs +0 -186
  51. package/dist/cache.d.mts +0 -164
  52. package/dist/cache.mjs +0 -184
  53. package/dist/controls.d.mts +0 -95
  54. package/dist/controls.mjs +0 -70
  55. package/dist/hydration.d.mts +0 -84
  56. package/dist/hydration.mjs +0 -148
  57. package/dist/infinite.d.mts +0 -5
  58. package/dist/infinite.mjs +0 -103
  59. package/dist/key.d.mts +0 -18
  60. package/dist/key.mjs +0 -36
  61. package/dist/listen.mjs +0 -19
  62. package/dist/notify.mjs +0 -21
  63. package/dist/observer.d.mts +0 -110
  64. package/dist/observer.mjs +0 -300
  65. package/dist/platform.d.mts +0 -27
  66. package/dist/platform.mjs +0 -44
  67. package/dist/query.d.mts +0 -137
  68. package/dist/query.mjs +0 -147
  69. package/dist/request.mjs +0 -47
  70. package/dist/retry.mjs +0 -25
  71. package/dist/share.mjs +0 -40
package/CHANGELOG.md CHANGED
@@ -1,10 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.12.0](https://github.com/KATT/raf-query/compare/0.11.1...0.12.0) (2026-11-08)
4
+
5
+ ### ⚠ BREAKING CHANGES
6
+
7
+ * greenfield rewrite on a promise kernel, with load() handles and opt-in behaviors ([#230](https://github.com/KATT/raf-query/issues/230))
8
+
9
+ ### Features
10
+
11
+ * greenfield rewrite on a promise kernel, with load() handles and opt-in behaviors ([#230](https://github.com/KATT/raf-query/issues/230)) ([e2a6f85](https://github.com/KATT/raf-query/commit/e2a6f85297992dc8a5cdd3f871a54452393ae978))
12
+
3
13
  ## [0.11.1](https://github.com/KATT/raf-query/compare/0.11.0...0.11.1) (2026-10-17)
4
14
 
5
15
  ### Features
6
16
 
7
- * **core:** take throwOnError from the query's options (2/2) ([#214](https://github.com/KATT/raf-query/issues/214)) ([ef1f0fd](https://github.com/KATT/raf-query/commit/ef1f0fd8eda2a73f4c4c268e8a3a5cf91a0ba0d8)), references [#448](https://github.com/KATT/raf-query/issues/448)
17
+ - **core:** take throwOnError from the query's options (2/2) ([#214](https://github.com/KATT/raf-query/issues/214)) ([ef1f0fd](https://github.com/KATT/raf-query/commit/ef1f0fd8eda2a73f4c4c268e8a3a5cf91a0ba0d8)), references [#448](https://github.com/KATT/raf-query/issues/448)
8
18
 
9
19
  ## [0.11.0](https://github.com/KATT/raf-query/compare/0.10.4...0.11.0) (2026-10-16)
10
20
 
package/README.md CHANGED
@@ -1,47 +1,57 @@
1
1
  # @raf-query/core
2
2
 
3
- The framework-agnostic cache behind [raf-query](https://raf-query.katt.dev), written in and for [Effect](https://effect.website): every verb is an Effect with typed errors and services, and entries can be watched as Streams. It also holds `QueryClient`, the same cache with Promise methods, which the framework bindings re-export.
3
+ The framework-agnostic query client behind [raf-query](https://raf-query.katt.dev): a small cache that dedupes requests, keeps entries fresh, and hands out serializable handles. Retries, timeouts, refetch on focus or reconnect, polling and hydration are opt-in links, features and exports, so a client bundles only what it turns on.
4
4
 
5
- **[Documentation](https://raf-query.katt.dev/docs/effect)** · [API reference](https://raf-query.katt.dev/docs/api-reference#raf-querycore) · [GitHub](https://github.com/KATT/raf-query)
5
+ **[Documentation](https://raf-query.katt.dev)** · [API reference](https://raf-query.katt.dev/docs/api-reference#raf-querycore) · [GitHub](https://github.com/KATT/raf-query)
6
6
 
7
7
  > [!NOTE]
8
8
  > raf-query is early. The API can still change between minor releases, and you use it at your own risk.
9
9
 
10
- React apps install [`@raf-query/react`](https://www.npmjs.com/package/@raf-query/react) instead: it re-exports this package, so you never import core directly. Install core on its own for Effect programs and for bindings to other frameworks.
10
+ React apps install [`@raf-query/react`](https://www.npmjs.com/package/@raf-query/react) instead: it re-exports this package, so you never import core directly. Install core on its own for scripts, tests and bindings to other frameworks.
11
11
 
12
12
  ## Install
13
13
 
14
14
  ```bash
15
- npm install @raf-query/core effect
15
+ npm install @raf-query/core
16
16
  ```
17
17
 
18
18
  ## Usage
19
19
 
20
20
  ```ts
21
- import { QueryCache, queryOptions } from "@raf-query/core";
22
- import { Effect, Schedule, Stream } from "effect";
21
+ import {
22
+ createQueryClient,
23
+ group,
24
+ invalidateOne,
25
+ load,
26
+ queryOptions,
27
+ retry,
28
+ revalidate,
29
+ } from "@raf-query/core";
30
+
31
+ const issues = group("issues");
23
32
 
24
33
  export function issueQuery(input: { id: number }) {
25
34
  return queryOptions({
26
- queryKey: ["issue", input.id],
27
- queryFn: () => IssueRepo.get(input.id), // Effect<Issue, IssueNotFound>
28
- retry: Schedule.exponential("200 millis").pipe(Schedule.upTo({ times: 3 })),
35
+ queryKey: issues("byId", input.id),
36
+ queryFn: () => getIssue(input.id),
29
37
  });
30
38
  }
31
39
 
32
- const program = Effect.gen(function* () {
33
- const cache = yield* QueryCache.QueryCache;
34
- yield* cache.fetch(issueQuery, { id: 1 }); // fails with IssueNotFound
35
- yield* cache.watch(issueQuery, { id: 1 }).pipe(
36
- Stream.map((state) => state.data?.title),
37
- Stream.runForEach((title) => Effect.log(title)),
38
- );
40
+ const queryClient = createQueryClient({ links: [retry()], staleTime: 5000 });
41
+
42
+ const [, data] = await load(queryClient, {
43
+ issue: revalidate(issueQuery, { id: 1 }),
39
44
  });
45
+ console.log(data.issue.title);
40
46
 
41
- Effect.runFork(program.pipe(Effect.provide(QueryCache.layer())));
47
+ await invalidateOne(queryClient, issueQuery, { id: 1 });
42
48
  ```
43
49
 
44
- [Effect](https://raf-query.katt.dev/docs/effect) and [services](https://raf-query.katt.dev/docs/effect-services) in the docs cover the rest.
50
+ ## Entry points
51
+
52
+ | Import | Use |
53
+ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
54
+ | `@raf-query/core` | `createQueryClient`, query factories, `load`, the verbs, the built-in links and features, `group`, `dehydrate` and `hydrate` |
45
55
 
46
56
  ## License
47
57
 
package/dist/client.d.mts CHANGED
@@ -1,130 +1,141 @@
1
- import { DataOf, InputArgs, InputOf, LoadOptions, PagedQueryFactory, QueryFactory, QueryHandle, QueryHandleWithData } from "./options.mjs";
2
- import { Query, query_d_exports } from "./query.mjs";
3
- import { DehydrateOptions, DehydratedState, HydrateOptions } from "./hydration.mjs";
4
- import { Config, Event, Filter, QueryCache, Target, Updater, cache_d_exports } from "./cache.mjs";
5
- import { Effect, Layer } from "effect";
1
+ import { QueryKey } from "./keys/keys.mjs";
2
+ import { AnyPart, AnyRoot, Decl, Declares, MetaOf, MetaOfDecl, RootTypes } from "./root.mjs";
3
+ import { Stamp } from "./keys/targets.mjs";
4
+ import { QueryOptions } from "./options.mjs";
5
+ import { Listeners, SubscribeOptions } from "./subscribe.mjs";
6
+ import { Entry } from "./entry.mjs";
6
7
  //#region src/client.d.ts
7
- export interface QueryClientConfig<TServices = never> extends Config {
8
- /**
9
- * Services for queries whose `queryFn` returns an Effect. Built with the
10
- * client, so it must build synchronously; `dispose()` runs its
11
- * finalizers.
12
- */
13
- readonly layer?: Layer.Layer<TServices>;
14
- /** Called with every failed request, including failed prefetches. */
15
- onError?(error: unknown, query: Query): void;
16
- /**
17
- * When `false`, `useQuery` for an Effect `queryFn` (a typed `ErrorOf`)
18
- * returns a `Result` in the first tuple slot instead of a promise that
19
- * rejects. Defaults to `false` when `layer` is set, `true` otherwise.
20
- * A query's own `throwOnError` (in `queryOptions`) and a read's win over
21
- * it; types only see those two. Promise `queryFn`s always throw, whatever
22
- * the flags say: reads tell them apart by what the `queryFn` returned
23
- * ({@link Query.returnsEffectUnsafe}).
24
- */
25
- readonly throwOnError?: boolean;
8
+ type LinkFunction = (op: Operation, next: (op: Operation) => Promise<unknown>) => Promise<unknown>;
9
+ type FeatureFunction = (client: QueryClient, signal: AbortSignal) => void;
10
+ /** Any link, whatever `meta` it reads. */
11
+ export type AnyLink = LinkFunction & AnyPart;
12
+ /** Any feature, whatever `meta` it reads. */
13
+ export type AnyFeature = FeatureFunction & AnyPart;
14
+ /** Time and timers, injectable so tests control them without patching globals. */
15
+ export interface Clock {
16
+ clearTimeout(handle: unknown): void;
17
+ now(): number;
18
+ setTimeout(callback: () => void, ms: number): unknown;
19
+ }
20
+ export declare const systemClock: Clock;
21
+ /** One request, as links see it. `TMeta` is what the link declares. */
22
+ export interface Operation<TMeta extends object = object> {
23
+ readonly client: QueryClient;
24
+ /** The entry's data when the request started (paged queries extend it). */
25
+ readonly data: unknown;
26
+ /** For paged queries: which page to add. */
27
+ readonly direction: "next" | "previous" | undefined;
28
+ readonly hasData: boolean;
29
+ readonly hash: string;
30
+ readonly input: unknown;
31
+ /** The query's `meta`, `{}` when it has none. */
32
+ readonly meta: TMeta;
33
+ readonly options: QueryOptions;
34
+ /** For paged queries: how many pages a first load fetches. */
35
+ readonly pages: number | undefined;
36
+ readonly queryKey: QueryKey;
37
+ readonly signal: AbortSignal;
38
+ }
39
+ /**
40
+ * Wraps every request, like a tRPC link. Call `next(op)` to run the rest of
41
+ * the chain; the last link is the query's own `queryFn`. `TDecl` is the
42
+ * `meta` it reads: see {@link link}.
43
+ */
44
+ export type Link<TDecl extends Decl = Decl> = LinkFunction & Declares<TDecl>;
45
+ /**
46
+ * A cache-wide behavior, started when a provider mounts the client and
47
+ * stopped by `signal`. `TDecl` is the `meta` it reads: see {@link feature}.
48
+ */
49
+ export type Feature<TDecl extends Decl = Decl> = FeatureFunction & Declares<TDecl>;
50
+ /**
51
+ * A link that declares the `meta` fields it reads. A root's queries take
52
+ * those fields only when the root has the link.
53
+ *
54
+ * ```ts
55
+ * const traced = link<{ meta: { traceId?: string } }>((op, next) =>
56
+ * withTrace(op.meta.traceId, () => next(op)),
57
+ * );
58
+ * ```
59
+ */
60
+ export declare function link<TDecl extends Decl = Decl>(run: (op: Operation<MetaOfDecl<TDecl> & object>, next: (op: Operation) => Promise<unknown>) => Promise<unknown>): Link<TDecl>;
61
+ /**
62
+ * A feature that declares the `meta` fields it reads. Without `setup`, it
63
+ * only declares them: a root's own fields, such as an `owner` its targets
64
+ * match on.
65
+ *
66
+ * ```ts
67
+ * const ownership = feature<{ meta: { owner?: "posts-team" | "users-team" } }>();
68
+ * ```
69
+ */
70
+ export declare function feature<TDecl extends Decl = Decl>(setup?: FeatureFunction): Feature<TDecl>;
71
+ export type ClientEvent = {
72
+ readonly entry: Entry;
73
+ readonly type: "added" | "removed" | "updated";
74
+ } | {
75
+ readonly entry: Entry;
76
+ readonly type: "readers";
77
+ };
78
+ export interface QueryClientConfig {
79
+ readonly clock?: Clock;
80
+ /** Behaviors started while a provider has the client mounted. */
81
+ readonly features?: ReadonlyArray<AnyFeature>;
82
+ /** Default `gcTime`: 5 minutes. Server clients never gc. */
83
+ readonly gcTime?: number;
84
+ /** Default: no `window`. Server clients never gc and never revalidate cached data. */
85
+ readonly isServer?: boolean;
86
+ /** Wrap every request, outermost first. */
87
+ readonly links?: ReadonlyArray<AnyLink>;
88
+ /** Default `staleTime`: `0`. */
89
+ readonly staleTime?: number;
90
+ /** Default `structuralSharing`: `true`. */
91
+ readonly structuralSharing?: boolean;
26
92
  }
27
93
  /**
28
- * The query cache, with Promise methods. Create one per request on the
29
- * server and one per browser tab. `effect` is the same cache's Effect API.
30
- * Its verbs take factories whose `queryFn` needs at most `TServices`, the
31
- * services of `config.layer`.
94
+ * A query cache. `TTypes` is its root's types, which factories built with
95
+ * `bindQueryOptions<typeof makeQueryClient>()` are checked against. The bare
96
+ * `QueryClient` type takes a client of any root.
32
97
  */
33
- export declare class QueryClient<TServices = never> {
34
- #private;
35
- /** The Effect-native cache behind this client. */
36
- readonly effect: QueryCache;
98
+ export interface QueryClient<TTypes extends RootTypes = AnyRoot> {
99
+ /** Entries changed since the last notification. */
100
+ readonly "~changed": Set<Entry>;
101
+ readonly "~entries": Map<string, Entry>;
102
+ readonly "~events": Listeners<ClientEvent>;
103
+ /** Orders requests and invalidations. */
104
+ "~seq": number;
105
+ /** Invalidations not yet older than every entry's data. */
106
+ "~stamps": Array<Stamp>;
107
+ /** Phantom: the root's types, invariant. `undefined` at runtime. */
108
+ readonly "~types"?: {
109
+ readonly root: (types: TTypes) => TTypes;
110
+ };
111
+ readonly clock: Clock;
112
+ readonly config: QueryClientConfig;
37
113
  readonly isServer: boolean;
38
- /**
39
- * Default for Effect `useQuery` reads. See
40
- * {@link QueryClientConfig.throwOnError}.
41
- */
42
- readonly throwOnError: boolean;
43
- constructor(config?: QueryClientConfig<TServices>);
44
- /**
45
- * The cache entry for a factory call, created if missing. Doesn't fetch.
46
- * For bindings; apps use the verbs.
47
- */
48
- getQuery<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): Query<TFactory>;
49
- /**
50
- * Start a query without waiting and return its handle. Fresh data is
51
- * reused; stale data refetches in the background (in the browser only).
52
- * Errors surface where the data is read, never here.
53
- */
54
- prefetch<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): QueryHandle<TFactory>;
55
- /**
56
- * {@link fetch} without waiting on stale data: start a query and wait for
57
- * it on a cache miss. Stale cached data resolves immediately and
58
- * revalidates in the browser. Rejects if that request fails.
59
- */
60
- ensure<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): Promise<QueryHandleWithData<TFactory>>;
61
- /** {@link ensure}, loading `options.pages` pages of a paged query. */
62
- ensure<TFactory extends PagedQueryFactory<TServices>>(factory: TFactory, input: InputOf<TFactory>, options: LoadOptions): Promise<QueryHandleWithData<TFactory>>;
63
- /**
64
- * Start a query and resolve to a handle holding its data. Waits on a cache
65
- * miss and on stale data (on the server, only invalidated data), so the
66
- * data is fresh. Rejects if that request fails.
67
- */
68
- fetch<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): Promise<QueryHandleWithData<TFactory>>;
69
- /** {@link fetch}, loading `options.pages` pages of a paged query. */
70
- fetch<TFactory extends PagedQueryFactory<TServices>>(factory: TFactory, input: InputOf<TFactory>, options: LoadOptions): Promise<QueryHandleWithData<TFactory>>;
71
- /**
72
- * Mark queries stale and refetch the ones being read. Resolves once those
73
- * refetches settle, so an action stays pending until fresh data is on
74
- * screen. Takes a factory and its input, or a {@link QueryCache.Filter}.
75
- */
76
- invalidate<TFactory extends QueryFactory<TServices>>(...target: Target<TFactory>): Promise<void>;
77
- /**
78
- * Drop queries' data and errors, as if they were never fetched. Read ones
79
- * suspend and refetch; resolves once those refetches settle.
80
- */
81
- reset<TFactory extends QueryFactory<TServices>>(...target: Target<TFactory>): Promise<void>;
82
- /**
83
- * Remove queries from the cache and abort their requests. Readers keep the
84
- * removed entry until they render again.
85
- */
86
- remove<TFactory extends QueryFactory<TServices>>(...target: Target<TFactory>): void;
87
- /** Write data for a factory call. Readers re-render without suspending. */
88
- setData<TFactory extends QueryFactory<TServices>>(factory: TFactory, input: InputOf<TFactory>, updater: Updater<DataOf<TFactory>>): void;
89
- /**
90
- * Cached data for a factory call, or `undefined` if there is none. Data can
91
- * itself be `undefined`; `Query.getUnsafe(getQuery(...)).hasData` tells
92
- * them apart.
93
- */
94
- getData<TFactory extends QueryFactory<TServices>>(factory: TFactory, ...args: InputArgs<TFactory>): DataOf<TFactory> | undefined;
95
- /** Every entry, for devtools and integrations. */
96
- getAll(): ReadonlyArray<Query>;
97
- /**
98
- * How many entries have a request in flight: all of them, or those
99
- * matching `filter`. For global loading indicators.
100
- */
101
- isFetching(filter?: Filter): number;
102
- /** Listen to entries being added, updated, and removed. */
103
- subscribe(listener: (event: Event) => void): () => void;
104
- /** Drop every entry and abort in-flight requests. */
105
- clear(): void;
106
- /**
107
- * Start refetching read, stale queries on window focus and reconnect.
108
- * Returns the matching unmount. Providers call this; mounts are counted.
109
- */
110
- mount(): () => void;
111
- /**
112
- * Run an Effect with the cache (`QueryCache`) and the services of
113
- * `config.layer`, as `Effect.runPromise` does: it rejects with the
114
- * Effect's failure, or its defect. For an app's loaders and actions.
115
- */
116
- runPromise<A, E>(effect: Effect.Effect<A, E, QueryCache | TServices>): Promise<A>;
117
- /**
118
- * Close the client: interrupt every request and timer, and release the
119
- * layer's services. Resolves once the layer's finalizers have run.
120
- */
121
- dispose(): Promise<void>;
114
+ /** Cache events: entries added, updated, removed, or gaining and losing readers. */
115
+ subscribe(listener: (event: ClientEvent) => void, options?: SubscribeOptions): () => void;
122
116
  }
123
- /** Snapshot the cache for transfer to another client (server → browser). */
124
- export declare function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedState;
125
117
  /**
126
- * Write dehydrated entries into a client. Existing entries only take newer
127
- * data; pending entries resume the sender's request.
118
+ * Create a query client. Behaviors are opt-in: pass `links` and `features`,
119
+ * or spread `recommended()`. The client's `meta` type is what they declare.
120
+ *
121
+ * ```ts
122
+ * const ownership = feature<{ meta: { owner?: "posts-team" | "users-team" } }>();
123
+ * export const makeQueryClient = () =>
124
+ * createQueryClient({ links: [retry()], features: [ownership], staleTime: 30_000 });
125
+ * ```
126
+ *
127
+ * Make one per request on the server (in a loader or a request-scoped
128
+ * cache), never at module level.
129
+ */
130
+ export declare function createQueryClient<const TLinks extends ReadonlyArray<AnyLink> = [], const TFeatures extends ReadonlyArray<AnyFeature> = []>(config?: QueryClientConfig & {
131
+ readonly features?: TFeatures;
132
+ readonly links?: TLinks;
133
+ }): QueryClient<{
134
+ readonly meta: MetaOf<[...TLinks, ...TFeatures]>;
135
+ }>;
136
+ /**
137
+ * Start the client's features. Providers call it while mounted; the
138
+ * returned function stops them.
128
139
  */
129
- export declare function hydrate(client: QueryClient, state: DehydratedState, options?: HydrateOptions): void;
140
+ export declare function mount(client: QueryClient): () => void;
130
141
  //#endregion
package/dist/client.mjs CHANGED
@@ -1,176 +1,73 @@
1
- import { QueryCache, layer } from "./cache.mjs";
2
- import { Context, Effect, Exit, Layer, Option, Predicate, Scope } from "effect";
1
+ import { listeners } from "./subscribe.mjs";
3
2
  //#region src/client.ts
3
+ const systemClock = {
4
+ clearTimeout: (handle) => {
5
+ clearTimeout(handle);
6
+ },
7
+ now: () => Date.now(),
8
+ setTimeout: (callback, ms) => setTimeout(callback, ms)
9
+ };
4
10
  /**
5
- * A verb's Effect, run by a client. Its requirement is the factory's
6
- * `ServicesOf`, which the client's verbs only accept when its `layer`
7
- * provides them.
11
+ * A link that declares the `meta` fields it reads. A root's queries take
12
+ * those fields only when the root has the link.
13
+ *
14
+ * ```ts
15
+ * const traced = link<{ meta: { traceId?: string } }>((op, next) =>
16
+ * withTrace(op.meta.traceId, () => next(op)),
17
+ * );
18
+ * ```
8
19
  */
9
- function provided(effect) {
10
- return effect;
20
+ function link(run) {
21
+ return run;
11
22
  }
23
+ const noop = () => {};
12
24
  /**
13
- * Run a verb that waits on a request. A cancelled request rejects with an
14
- * `AbortError`, as an aborted `fetch` does, not with the Effect API's
15
- * `Query.CancelledError`.
25
+ * A feature that declares the `meta` fields it reads. Without `setup`, it
26
+ * only declares them: a root's own fields, such as an `owner` its targets
27
+ * match on.
28
+ *
29
+ * ```ts
30
+ * const ownership = feature<{ meta: { owner?: "posts-team" | "users-team" } }>();
31
+ * ```
16
32
  */
17
- function runLoad(effect) {
18
- return Effect.runPromise(effect).catch((error) => {
19
- throw Predicate.isTagged(error, "CancelledError") ? new DOMException("The query was cancelled", "AbortError") : error;
20
- });
21
- }
22
- /** `ensure` / `fetch` arguments, forwarded to the cache's same verb. */
23
- function loadArgs(input, options) {
24
- return [input, options];
33
+ function feature(setup = noop) {
34
+ return setup;
25
35
  }
26
36
  /**
27
- * The query cache, with Promise methods. Create one per request on the
28
- * server and one per browser tab. `effect` is the same cache's Effect API.
29
- * Its verbs take factories whose `queryFn` needs at most `TServices`, the
30
- * services of `config.layer`.
37
+ * Create a query client. Behaviors are opt-in: pass `links` and `features`,
38
+ * or spread `recommended()`. The client's `meta` type is what they declare.
39
+ *
40
+ * ```ts
41
+ * const ownership = feature<{ meta: { owner?: "posts-team" | "users-team" } }>();
42
+ * export const makeQueryClient = () =>
43
+ * createQueryClient({ links: [retry()], features: [ownership], staleTime: 30_000 });
44
+ * ```
45
+ *
46
+ * Make one per request on the server (in a loader or a request-scoped
47
+ * cache), never at module level.
31
48
  */
32
- var QueryClient = class {
33
- /** The Effect-native cache behind this client. */
34
- effect;
35
- isServer;
36
- /**
37
- * Default for Effect `useQuery` reads. See
38
- * {@link QueryClientConfig.throwOnError}.
39
- */
40
- throwOnError;
41
- /**
42
- * The cache and the layer's services. Typed `never` so clients of any
43
- * `TServices` stay assignable to each other; `runPromise` restores it.
44
- */
45
- #context;
46
- /**
47
- * Owns the cache's fibers, mounts and the layer's services; lives until
48
- * `dispose()`.
49
- */
50
- #scope = Scope.makeUnsafe();
51
- constructor(config = {}) {
52
- const cache = layer(config);
53
- const services = Effect.runSync(Layer.buildWithScope(config.layer ? Layer.provideMerge(cache, config.layer) : cache, this.#scope));
54
- this.#context = services;
55
- this.effect = Context.get(services, QueryCache);
56
- this.isServer = this.effect.isServer;
57
- this.throwOnError = config.throwOnError ?? config.layer === void 0;
58
- const onError = config.onError;
59
- if (onError) this.effect.subscribeUnsafe((event) => {
60
- if (event.type === "failed") onError(event.error, event.query);
61
- });
62
- }
63
- /**
64
- * The cache entry for a factory call, created if missing. Doesn't fetch.
65
- * For bindings; apps use the verbs.
66
- */
67
- getQuery(factory, ...args) {
68
- return this.effect.getQueryUnsafe(factory, ...args);
69
- }
70
- /**
71
- * Start a query without waiting and return its handle. Fresh data is
72
- * reused; stale data refetches in the background (in the browser only).
73
- * Errors surface where the data is read, never here.
74
- */
75
- prefetch(factory, ...args) {
76
- return Effect.runSync(provided(this.effect.prefetch(factory, ...args)));
77
- }
78
- ensure(factory, input, options) {
79
- return runLoad(provided(this.effect.ensure(factory, ...loadArgs(input, options))));
80
- }
81
- fetch(factory, input, options) {
82
- return runLoad(provided(this.effect.fetch(factory, ...loadArgs(input, options))));
83
- }
84
- /**
85
- * Mark queries stale and refetch the ones being read. Resolves once those
86
- * refetches settle, so an action stays pending until fresh data is on
87
- * screen. Takes a factory and its input, or a {@link QueryCache.Filter}.
88
- */
89
- invalidate(...target) {
90
- return Effect.runPromise(this.effect.invalidate(...target));
91
- }
92
- /**
93
- * Drop queries' data and errors, as if they were never fetched. Read ones
94
- * suspend and refetch; resolves once those refetches settle.
95
- */
96
- reset(...target) {
97
- return Effect.runPromise(this.effect.reset(...target));
98
- }
99
- /**
100
- * Remove queries from the cache and abort their requests. Readers keep the
101
- * removed entry until they render again.
102
- */
103
- remove(...target) {
104
- Effect.runSync(this.effect.remove(...target));
105
- }
106
- /** Write data for a factory call. Readers re-render without suspending. */
107
- setData(factory, input, updater) {
108
- Effect.runSync(this.effect.setData(factory, input, updater));
109
- }
110
- /**
111
- * Cached data for a factory call, or `undefined` if there is none. Data can
112
- * itself be `undefined`; `Query.getUnsafe(getQuery(...)).hasData` tells
113
- * them apart.
114
- */
115
- getData(factory, ...args) {
116
- return Option.getOrUndefined(Effect.runSync(this.effect.getData(factory, ...args)));
117
- }
118
- /** Every entry, for devtools and integrations. */
119
- getAll() {
120
- return Effect.runSync(this.effect.all);
121
- }
122
- /**
123
- * How many entries have a request in flight: all of them, or those
124
- * matching `filter`. For global loading indicators.
125
- */
126
- isFetching(filter) {
127
- return this.effect.isFetchingUnsafe(filter);
128
- }
129
- /** Listen to entries being added, updated, and removed. */
130
- subscribe(listener) {
131
- return this.effect.subscribeUnsafe(listener);
132
- }
133
- /** Drop every entry and abort in-flight requests. */
134
- clear() {
135
- Effect.runSync(this.effect.clear);
136
- }
137
- /**
138
- * Start refetching read, stale queries on window focus and reconnect.
139
- * Returns the matching unmount. Providers call this; mounts are counted.
140
- */
141
- mount() {
142
- const scope = Scope.forkUnsafe(this.#scope);
143
- Effect.runSync(this.effect.mount.pipe(Scope.provide(scope)));
144
- return () => {
145
- Effect.runSync(Scope.close(scope, Exit.void));
146
- };
147
- }
148
- /**
149
- * Run an Effect with the cache (`QueryCache`) and the services of
150
- * `config.layer`, as `Effect.runPromise` does: it rejects with the
151
- * Effect's failure, or its defect. For an app's loaders and actions.
152
- */
153
- runPromise(effect) {
154
- return Effect.runPromiseWith(this.#context)(effect);
155
- }
156
- /**
157
- * Close the client: interrupt every request and timer, and release the
158
- * layer's services. Resolves once the layer's finalizers have run.
159
- */
160
- dispose() {
161
- return Effect.runPromise(Scope.close(this.#scope, Exit.void));
162
- }
163
- };
164
- /** Snapshot the cache for transfer to another client (server → browser). */
165
- function dehydrate(client, options) {
166
- return Effect.runSync(client.effect.dehydrate(options));
49
+ function createQueryClient(config = {}) {
50
+ const events = listeners();
51
+ return {
52
+ "~changed": /* @__PURE__ */ new Set(),
53
+ "~entries": /* @__PURE__ */ new Map(),
54
+ "~events": events,
55
+ "~seq": 0,
56
+ "~stamps": [],
57
+ clock: config.clock ?? systemClock,
58
+ config,
59
+ isServer: config.isServer ?? !("window" in globalThis),
60
+ subscribe: (listener, options) => events.subscribe(listener, options)
61
+ };
167
62
  }
168
63
  /**
169
- * Write dehydrated entries into a client. Existing entries only take newer
170
- * data; pending entries resume the sender's request.
64
+ * Start the client's features. Providers call it while mounted; the
65
+ * returned function stops them.
171
66
  */
172
- function hydrate(client, state, options) {
173
- Effect.runSync(client.effect.hydrate(state, options));
67
+ function mount(client) {
68
+ const controller = new AbortController();
69
+ for (const start of client.config.features ?? []) start(client, controller.signal);
70
+ return () => controller.abort();
174
71
  }
175
72
  //#endregion
176
- export { QueryClient, dehydrate, hydrate };
73
+ export { createQueryClient, feature, link, mount, systemClock };