@raf-query/core 0.0.3 → 0.0.4

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.
@@ -0,0 +1,50 @@
1
+ import { QueryKey } from "./key.mjs";
2
+ import { Query } from "./query.mjs";
3
+ import { QueryClient } from "./client.mjs";
4
+ //#region src/hydration.d.ts
5
+ /**
6
+ * One cache entry in transit. Settled entries carry `data`; entries still
7
+ * loading their first data carry the request's `promise`, so the receiving
8
+ * cache resumes it instead of fetching again.
9
+ */
10
+ export interface DehydratedQuery {
11
+ readonly data?: unknown;
12
+ readonly key: QueryKey;
13
+ readonly promise?: Promise<unknown>;
14
+ /** When `data` was fetched; `0` while pending. */
15
+ readonly updatedAt: number;
16
+ }
17
+ export interface DehydratedState {
18
+ readonly queries: ReadonlyArray<DehydratedQuery>;
19
+ }
20
+ export interface DehydrateOptions {
21
+ /**
22
+ * Which entries to send. Default: entries with data or a request in
23
+ * flight ({@link canDehydrate}).
24
+ */
25
+ readonly filter?: (query: Query) => boolean;
26
+ /**
27
+ * Send a pending query's rejection as-is. By default the receiving side
28
+ * gets a generic error, so server errors don't leak into the page; its
29
+ * error boundary can still retry on the client.
30
+ */
31
+ readonly keepErrors?: boolean;
32
+ /** Transform data before it is serialized. */
33
+ readonly serializeData?: (data: unknown) => unknown;
34
+ }
35
+ export interface HydrateOptions {
36
+ /** Reverse `serializeData`. */
37
+ readonly deserializeData?: (data: unknown) => unknown;
38
+ }
39
+ /** The default dehydrate filter: entries with data or a request in flight. */
40
+ export declare function canDehydrate(query: Query): boolean;
41
+ /** Snapshot one entry for transfer. */
42
+ export declare function dehydrateQuery(query: Query, options?: DehydrateOptions): DehydratedQuery;
43
+ /** Snapshot the cache for transfer to another client (server → browser). */
44
+ export declare function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedState;
45
+ /**
46
+ * Write dehydrated entries into a client. Existing entries only take newer
47
+ * data; pending entries resume the sender's request.
48
+ */
49
+ export declare function hydrate(client: QueryClient, state: DehydratedState, options?: HydrateOptions): void;
50
+ //#endregion
@@ -0,0 +1,50 @@
1
+ import { cacheOf } from "./client.mjs";
2
+ //#region src/hydration.ts
3
+ const noop = () => {};
4
+ const identity = (value) => value;
5
+ /** The default dehydrate filter: entries with data or a request in flight. */
6
+ function canDehydrate(query) {
7
+ const state = query.getState();
8
+ return state.hasData || state.isFetching;
9
+ }
10
+ /** Snapshot one entry for transfer. */
11
+ function dehydrateQuery(query, options = {}) {
12
+ const state = query.getState();
13
+ const serialize = options.serializeData ?? identity;
14
+ if (state.hasData) return {
15
+ data: serialize(state.data),
16
+ key: query.key,
17
+ updatedAt: state.updatedAt
18
+ };
19
+ const promise = state.promise.then(serialize, (error) => {
20
+ throw options.keepErrors ? error : /* @__PURE__ */ new Error(`Query ${query.hash} failed`);
21
+ });
22
+ promise.catch(noop);
23
+ return {
24
+ key: query.key,
25
+ promise,
26
+ updatedAt: 0
27
+ };
28
+ }
29
+ /** Snapshot the cache for transfer to another client (server → browser). */
30
+ function dehydrate(client, options = {}) {
31
+ const filter = options.filter ?? canDehydrate;
32
+ return { queries: client.getAll().filter(filter).map((query) => dehydrateQuery(query, options)) };
33
+ }
34
+ /**
35
+ * Write dehydrated entries into a client. Existing entries only take newer
36
+ * data; pending entries resume the sender's request.
37
+ */
38
+ function hydrate(client, state, options = {}) {
39
+ const deserialize = options.deserializeData ?? identity;
40
+ const cache = cacheOf(client);
41
+ for (const query of state.queries) {
42
+ const entry = cache.entryFor(query.key);
43
+ const current = entry.getState();
44
+ if (query.updatedAt > 0) {
45
+ if (!current.hasData || current.updatedAt < query.updatedAt) entry.setData(deserialize(query.data), query.updatedAt);
46
+ } else if (query.promise && !current.hasData) entry.resume(Promise.resolve(query.promise).then(deserialize));
47
+ }
48
+ }
49
+ //#endregion
50
+ export { canDehydrate, dehydrate, dehydrateQuery, hydrate };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,7 @@
1
- //#region src/index.d.ts
2
- /** Placeholder export until the core API lands. */
3
- export declare const name = "@raf-query/core";
4
- //#endregion
1
+ import { Settled, allKeyed } from "./all-keyed.mjs";
2
+ import { QueryKey, hashKey, matchesKey } from "./key.mjs";
3
+ import { AnyQueryOptions, DataOf, InfiniteData, InfiniteQueryOptions, InputArgs, InputOf, IsPaged, LoadOptions, PageFunctionContext, PageOf, QueryBehavior, QueryFactory, QueryFnResult, QueryFunctionContext, QueryHandle, QueryOptions, RetryPolicy, infiniteQueryOptions, queryOptions } from "./options.mjs";
4
+ import { CancelledError, FetchRequest, Query, QueryObserver, QueryState } from "./query.mjs";
5
+ import { CacheEvent, LoadArgs, QueryClient, QueryClientConfig, QueryClientEffect, QueryFilter, Updater } from "./client.mjs";
6
+ import { DehydrateOptions, DehydratedQuery, DehydratedState, HydrateOptions, canDehydrate, dehydrate, dehydrateQuery, hydrate } from "./hydration.mjs";
7
+ export { type AnyQueryOptions, type CacheEvent, CancelledError, type DataOf, type DehydrateOptions, type DehydratedQuery, type DehydratedState, type FetchRequest, type HydrateOptions, type InfiniteData, type InfiniteQueryOptions, type InputArgs, type InputOf, type IsPaged, type LoadArgs, type LoadOptions, type PageFunctionContext, type PageOf, type Query, type QueryBehavior, QueryClient, type QueryClientConfig, type QueryClientEffect, type QueryFactory, type QueryFilter, type QueryFnResult, type QueryFunctionContext, type QueryHandle, type QueryKey, type QueryObserver, type QueryOptions, type QueryState, type RetryPolicy, type Settled, type Updater, allKeyed, canDehydrate, dehydrate, dehydrateQuery, hashKey, hydrate, infiniteQueryOptions, matchesKey, queryOptions };
package/dist/index.mjs CHANGED
@@ -1,5 +1,7 @@
1
- //#region src/index.ts
2
- /** Placeholder export until the core API lands. */
3
- const name = "@raf-query/core";
4
- //#endregion
5
- export { name };
1
+ import { allKeyed } from "./all-keyed.mjs";
2
+ import { hashKey, matchesKey } from "./key.mjs";
3
+ import { infiniteQueryOptions, queryOptions } from "./options.mjs";
4
+ import { CancelledError } from "./query.mjs";
5
+ import { QueryClient } from "./client.mjs";
6
+ import { canDehydrate, dehydrate, dehydrateQuery, hydrate } from "./hydration.mjs";
7
+ export { CancelledError, QueryClient, allKeyed, canDehydrate, dehydrate, dehydrateQuery, hashKey, hydrate, infiniteQueryOptions, matchesKey, queryOptions };
package/dist/key.d.mts ADDED
@@ -0,0 +1,18 @@
1
+ //#region src/key.d.ts
2
+ /**
3
+ * A query's cache key. Must be JSON-serializable (plain objects, arrays and
4
+ * primitives): it travels inside handles.
5
+ */
6
+ export type QueryKey = ReadonlyArray<unknown>;
7
+ /**
8
+ * Stable string form of a key. Object keys are sorted, so `{ a, b }` and
9
+ * `{ b, a }` hash the same.
10
+ */
11
+ export declare function hashKey(key: QueryKey): string;
12
+ /**
13
+ * Whether `key` starts with `prefix`. Object segments match when every field
14
+ * in the prefix's segment matches, so `["posts", { sort: "new" }]` matches
15
+ * `["posts", { sort: "new", page: 2 }]`.
16
+ */
17
+ export declare function matchesKey(key: QueryKey, prefix: QueryKey): boolean;
18
+ //#endregion
package/dist/key.mjs ADDED
@@ -0,0 +1,36 @@
1
+ import { Predicate } from "effect";
2
+ //#region src/key.ts
3
+ function isPlainRecord(value) {
4
+ if (!Predicate.isObject(value) || Array.isArray(value)) return false;
5
+ const prototype = Object.getPrototypeOf(value);
6
+ return prototype === Object.prototype || prototype === null;
7
+ }
8
+ function sortKeys(_name, value) {
9
+ if (!isPlainRecord(value)) return value;
10
+ const sorted = {};
11
+ for (const name of Object.keys(value).toSorted()) sorted[name] = value[name];
12
+ return sorted;
13
+ }
14
+ /**
15
+ * Stable string form of a key. Object keys are sorted, so `{ a, b }` and
16
+ * `{ b, a }` hash the same.
17
+ */
18
+ function hashKey(key) {
19
+ return JSON.stringify(key, sortKeys);
20
+ }
21
+ function partialMatch(actual, expected) {
22
+ if (actual === expected) return true;
23
+ if (Array.isArray(actual) && Array.isArray(expected)) return actual.length === expected.length && expected.every((part, index) => partialMatch(actual[index], part));
24
+ if (isPlainRecord(actual) && isPlainRecord(expected)) return Object.keys(expected).every((name) => partialMatch(actual[name], expected[name]));
25
+ return false;
26
+ }
27
+ /**
28
+ * Whether `key` starts with `prefix`. Object segments match when every field
29
+ * in the prefix's segment matches, so `["posts", { sort: "new" }]` matches
30
+ * `["posts", { sort: "new", page: 2 }]`.
31
+ */
32
+ function matchesKey(key, prefix) {
33
+ return prefix.length <= key.length && prefix.every((part, index) => partialMatch(key[index], part));
34
+ }
35
+ //#endregion
36
+ export { hashKey, matchesKey };
@@ -0,0 +1,21 @@
1
+ //#region src/notify.ts
2
+ let queued;
3
+ /**
4
+ * Calls `listener` in a microtask, once per batch. State changes made while
5
+ * React renders (a fetch started by a read) must not notify subscribers
6
+ * synchronously, and several changes in one turn should re-render once.
7
+ */
8
+ function scheduleNotify(listener) {
9
+ if (queued) {
10
+ queued.add(listener);
11
+ return;
12
+ }
13
+ const batch = /* @__PURE__ */ new Set([listener]);
14
+ queued = batch;
15
+ queueMicrotask(() => {
16
+ queued = void 0;
17
+ for (const queuedListener of batch) queuedListener();
18
+ });
19
+ }
20
+ //#endregion
21
+ export { scheduleNotify };
@@ -0,0 +1,111 @@
1
+ import { QueryKey } from "./key.mjs";
2
+ import { Effect, Schedule } from "effect";
3
+ //#region src/options.d.ts
4
+ /** What a `queryFn` may return: a value, a promise, or an `Effect`. */
5
+ export type QueryFnResult<TData> = TData | PromiseLike<TData> | Effect.Effect<TData, unknown>;
6
+ export interface QueryFunctionContext {
7
+ /** The query's key, as returned by the factory. */
8
+ readonly queryKey: QueryKey;
9
+ /** Aborted when the request is cancelled or superseded. */
10
+ readonly signal: AbortSignal;
11
+ }
12
+ export interface PageFunctionContext<TPageParam> extends QueryFunctionContext {
13
+ /** Which end of the list this page is being added to. */
14
+ readonly direction: "backward" | "forward";
15
+ readonly pageParam: TPageParam;
16
+ }
17
+ /**
18
+ * How many times to retry a failed request: `false`, `true` (forever), a
19
+ * count, a predicate, or an Effect `Schedule`. The predicate gets the number
20
+ * of failures so far (1 after the first).
21
+ */
22
+ export type RetryPolicy = boolean | number | Schedule.Schedule<unknown, unknown> | ((failureCount: number, error: unknown) => boolean);
23
+ /** Options every query accepts. `QueryClient` defaults fill in the rest. */
24
+ export interface QueryBehavior {
25
+ /** How long an unread entry stays cached, in ms. `Infinity` keeps it. */
26
+ readonly gcTime?: number;
27
+ /** Refetch read, stale queries when the browser comes back online. */
28
+ readonly refetchOnReconnect?: boolean;
29
+ /** Refetch read, stale queries when the window regains focus. */
30
+ readonly refetchOnWindowFocus?: boolean;
31
+ /** Retries after a failed request. */
32
+ readonly retry?: RetryPolicy;
33
+ /** Delay before retry number `failureCount` (1-based), in ms. */
34
+ readonly retryDelay?: number | ((failureCount: number, error: unknown) => number);
35
+ /** How long data counts as fresh, in ms. Fresh data is never refetched. */
36
+ readonly staleTime?: number;
37
+ }
38
+ export interface QueryOptions<TData> extends QueryBehavior {
39
+ readonly kind: "query";
40
+ queryFn(context: QueryFunctionContext): QueryFnResult<TData>;
41
+ readonly queryKey: QueryKey;
42
+ }
43
+ export interface InfiniteQueryOptions<TPage, TPageParam> extends QueryBehavior {
44
+ /** Param for the page after `lastPage`, or `null` / `undefined` at the end. */
45
+ getNextPageParam(lastPage: TPage, pages: ReadonlyArray<TPage>, lastPageParam: TPageParam, pageParams: ReadonlyArray<TPageParam>): TPageParam | null | undefined;
46
+ /** Param for the page before `firstPage`, or `null` / `undefined` at the start. */
47
+ getPreviousPageParam?(firstPage: TPage, pages: ReadonlyArray<TPage>, firstPageParam: TPageParam, pageParams: ReadonlyArray<TPageParam>): TPageParam | null | undefined;
48
+ readonly initialPageParam: TPageParam;
49
+ readonly kind: "infinite";
50
+ /** Keep at most this many pages; the far end is dropped when paging. */
51
+ readonly maxPages?: number;
52
+ queryFn(context: PageFunctionContext<TPageParam>): QueryFnResult<TPage>;
53
+ readonly queryKey: QueryKey;
54
+ }
55
+ /** Data of a paged query. */
56
+ export interface InfiniteData<TPage, TPageParam = unknown> {
57
+ readonly pageParams: ReadonlyArray<TPageParam>;
58
+ readonly pages: ReadonlyArray<TPage>;
59
+ }
60
+ export type AnyQueryOptions = QueryOptions<unknown> | InfiniteQueryOptions<unknown, unknown>;
61
+ /**
62
+ * Typed options for a query factory:
63
+ *
64
+ * ```ts
65
+ * export function postQuery(input: { slug: string }) {
66
+ * return queryOptions({
67
+ * queryKey: ["post", input.slug],
68
+ * queryFn: () => getPost(input),
69
+ * });
70
+ * }
71
+ * ```
72
+ */
73
+ export declare function queryOptions<TData>(options: Omit<QueryOptions<TData>, "kind">): QueryOptions<TData>;
74
+ /** Typed options for a paged query factory. */
75
+ export declare function infiniteQueryOptions<TPage, TPageParam>(options: Omit<InfiniteQueryOptions<TPage, TPageParam>, "kind">): InfiniteQueryOptions<TPage, TPageParam>;
76
+ /**
77
+ * A function of zero or one argument returning `queryOptions(...)` or
78
+ * `infiniteQueryOptions(...)`. A second parameter is a type error: the single
79
+ * argument is the query's input, and a handle carries it.
80
+ */
81
+ export type QueryFactory = (input: never) => AnyQueryOptions;
82
+ /** The factory's input type; `undefined` for factories without one. */
83
+ export type InputOf<TFactory extends QueryFactory> = Parameters<TFactory> extends [] ? undefined : Parameters<TFactory>[0];
84
+ /** Arguments after the factory: the input, optional when the factory's is. */
85
+ export type InputArgs<TFactory extends QueryFactory> = [] extends Parameters<TFactory> ? [input?: InputOf<TFactory>] : [input: InputOf<TFactory>];
86
+ /** Whether the factory returns `infiniteQueryOptions(...)`. */
87
+ export type IsPaged<TFactory extends QueryFactory> = ReturnType<TFactory> extends {
88
+ readonly kind: "infinite";
89
+ } ? true : false;
90
+ /** The data a factory's query resolves to. */
91
+ export type DataOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends InfiniteQueryOptions<infer TPage, infer TPageParam> ? InfiniteData<TPage, TPageParam> : ReturnType<TFactory> extends QueryOptions<infer TData> ? TData : never;
92
+ /** Page type of a paged factory. */
93
+ export type PageOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends InfiniteQueryOptions<infer TPage, unknown> ? TPage : never;
94
+ /** `ensure` / `fetch` options. Only paged factories accept `pages`. */
95
+ export type LoadOptions<TFactory extends QueryFactory> = IsPaged<TFactory> extends true ? {
96
+ /** Load this many pages up front (default 1). */
97
+ readonly pages?: number;
98
+ } : never;
99
+ declare const factoryType: unique symbol;
100
+ /**
101
+ * The serializable reference a loader or server component hands to the
102
+ * component that reads the query. It holds no data and no functions.
103
+ */
104
+ export interface QueryHandle<TFactory extends QueryFactory = QueryFactory> {
105
+ /** Type-only: ties the handle to its factory. Never present at runtime. */
106
+ readonly [factoryType]?: TFactory;
107
+ /** The factory's input; `unknown` on a handle of no particular factory. */
108
+ readonly input: QueryFactory extends TFactory ? unknown : InputOf<TFactory>;
109
+ readonly key: QueryKey;
110
+ }
111
+ //#endregion
@@ -0,0 +1,35 @@
1
+ //#region src/options.ts
2
+ /**
3
+ * Typed options for a query factory:
4
+ *
5
+ * ```ts
6
+ * export function postQuery(input: { slug: string }) {
7
+ * return queryOptions({
8
+ * queryKey: ["post", input.slug],
9
+ * queryFn: () => getPost(input),
10
+ * });
11
+ * }
12
+ * ```
13
+ */
14
+ function queryOptions(options) {
15
+ return {
16
+ ...options,
17
+ kind: "query"
18
+ };
19
+ }
20
+ /** Typed options for a paged query factory. */
21
+ function infiniteQueryOptions(options) {
22
+ return {
23
+ ...options,
24
+ kind: "infinite"
25
+ };
26
+ }
27
+ /**
28
+ * Calls a factory with an input the caller has already type-checked against
29
+ * `InputArgs<TFactory>`.
30
+ */
31
+ function optionsFor(factory, input) {
32
+ return factory(input);
33
+ }
34
+ //#endregion
35
+ export { infiniteQueryOptions, optionsFor, queryOptions };
@@ -0,0 +1,77 @@
1
+ import { QueryKey } from "./key.mjs";
2
+ import { AnyQueryOptions, QueryFactory, QueryHandle } from "./options.mjs";
3
+ //#region src/query.d.ts
4
+ /** A snapshot of one cache entry. Replaced, never mutated, on every change. */
5
+ export interface QueryState<TData> {
6
+ /** Last successful data, kept through background errors. */
7
+ readonly data: TData | undefined;
8
+ /** Error of the last failed request, or `null`. */
9
+ readonly error: unknown;
10
+ /** Failed attempts of the current request, retries included. */
11
+ readonly failureCount: number;
12
+ /** Set while a paged query loads a page at one end of the list. */
13
+ readonly fetchDirection: "backward" | "forward" | undefined;
14
+ readonly hasData: boolean;
15
+ /** A request is in flight. */
16
+ readonly isFetching: boolean;
17
+ /** Marked stale by `invalidate`; cleared by the next successful request. */
18
+ readonly isInvalidated: boolean;
19
+ /** Older than `staleTime`, invalidated, or without data. */
20
+ readonly isStale: boolean;
21
+ /**
22
+ * Settles with `data`. A new promise is created per data version, already
23
+ * fulfilled once data exists, so React's `use()` reads it synchronously.
24
+ */
25
+ readonly promise: Promise<TData>;
26
+ readonly status: "error" | "pending" | "success";
27
+ /** When `data` was fetched (ms since epoch); `0` without data. */
28
+ readonly updatedAt: number;
29
+ }
30
+ /** Per-reader preferences, registered with {@link Query.subscribe}. */
31
+ export interface QueryObserver {
32
+ readonly refetchOnReconnect?: boolean;
33
+ readonly refetchOnWindowFocus?: boolean;
34
+ }
35
+ export interface FetchRequest {
36
+ /** Load one more page at this end of a paged query. */
37
+ readonly direction?: "backward" | "forward";
38
+ /** Pages to load when a paged query has none yet. */
39
+ readonly pages?: number;
40
+ /** Abort an in-flight request and start over (used by `invalidate`). */
41
+ readonly restart?: boolean;
42
+ }
43
+ /**
44
+ * A cache entry, as seen by bindings. Obtain one with
45
+ * `QueryClient.getQuery(factory, input)`.
46
+ */
47
+ export interface Query<TData = unknown, TFactory extends QueryFactory = QueryFactory> {
48
+ /** Start a request, or join the one in flight. */
49
+ fetch(request?: FetchRequest): Promise<TData>;
50
+ fetchNextPage(): Promise<void>;
51
+ fetchPreviousPage(): Promise<void>;
52
+ getState(): QueryState<TData>;
53
+ /** The serializable handle for this entry's factory call. */
54
+ readonly handle: QueryHandle<TFactory>;
55
+ /** Stable string form of `key`. */
56
+ readonly hash: string;
57
+ hasNextPage(): boolean;
58
+ hasPreviousPage(): boolean;
59
+ invalidate(): Promise<void>;
60
+ readonly key: QueryKey;
61
+ /** Options from the latest factory call; `undefined` for an entry only hydrated so far. */
62
+ readonly options: AnyQueryOptions | undefined;
63
+ /**
64
+ * The promise a render should unwrap. Starts the first request if nothing
65
+ * has, and retries a failed first load once its error has been shown.
66
+ */
67
+ read(): Promise<TData>;
68
+ /** Refetch now; resolves when the request settles. */
69
+ refetch(): Promise<void>;
70
+ /** Listen for state changes. Subscribed entries count as read. */
71
+ subscribe(listener: () => void, observer?: QueryObserver): () => void;
72
+ }
73
+ /** Thrown into a request's awaiters when the cache is cleared mid-request. */
74
+ export declare class CancelledError extends Error {
75
+ constructor();
76
+ }
77
+ //#endregion