@zeno-lib/query 0.0.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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @zeno-lib/query
2
+
3
+ See [Queries](https://www.zeno-lib.com/docs/core-framework/data-management/queries).
@@ -0,0 +1,17 @@
1
+ import { DehydratedState } from "@tanstack/react-query";
2
+ import { ReactNode } from "react";
3
+ //#region src/prefetched-queries.d.ts
4
+ /** Whether the nearest `PrefetchedQueries` above was handed at least one dehydrated query. */
5
+ declare function useHasPrefetchedQueries(): boolean;
6
+ type PrefetchedQueriesProps = {
7
+ children: ReactNode;
8
+ state: DehydratedState;
9
+ };
10
+ /**
11
+ * TanStack's `HydrationBoundary` plus the flag above. It hydrates `state` into the cache **during
12
+ * render** (on the server pass as well as in the browser), so a `useSuspenseQuery` below it
13
+ * resolves synchronously in both and the two passes produce the same markup.
14
+ */
15
+ declare function PrefetchedQueries({ children, state }: PrefetchedQueriesProps): import("react/jsx-runtime").JSX.Element;
16
+ //#endregion
17
+ export { PrefetchedQueries, PrefetchedQueriesProps, useHasPrefetchedQueries };
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { HydrationBoundary } from "@tanstack/react-query";
3
+ import { createContext, useContext } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/prefetched-queries.tsx
6
+ /**
7
+ * True inside a subtree that was handed a filled dehydrated cache.
8
+ *
9
+ * `QuerySuspense` reads it to skip the gate that otherwise keeps a suspense query off the server
10
+ * render. It is **derived from the payload**, not passed as a flag, so nothing can opt in without
11
+ * having prefetched: the only way to make it true is to arrive here with cache entries.
12
+ *
13
+ * What it does *not* promise is that every query below was prefetched. One that was not still runs
14
+ * its `queryFn` during the server render, so a route that opts in has to prefetch every suspense
15
+ * query its **server pass** reaches.
16
+ */
17
+ const PrefetchedQueriesContext = createContext(false);
18
+ /** Whether the nearest `PrefetchedQueries` above was handed at least one dehydrated query. */
19
+ function useHasPrefetchedQueries() {
20
+ return useContext(PrefetchedQueriesContext);
21
+ }
22
+ /**
23
+ * TanStack's `HydrationBoundary` plus the flag above. It hydrates `state` into the cache **during
24
+ * render** (on the server pass as well as in the browser), so a `useSuspenseQuery` below it
25
+ * resolves synchronously in both and the two passes produce the same markup.
26
+ */
27
+ function PrefetchedQueries({ children, state }) {
28
+ return /* @__PURE__ */ jsx(HydrationBoundary, {
29
+ state,
30
+ children: /* @__PURE__ */ jsx(PrefetchedQueriesContext.Provider, {
31
+ value: state.queries.length > 0,
32
+ children
33
+ })
34
+ });
35
+ }
36
+ //#endregion
37
+ export { PrefetchedQueries, useHasPrefetchedQueries };
@@ -0,0 +1,41 @@
1
+ //#region src/query-defaults.d.ts
2
+ /**
3
+ * Timing presets to spread as the first property of a `queryOptions(…)` call, so freshness stays
4
+ * consistent across a codebase instead of every query inventing its own `staleTime`.
5
+ *
6
+ * ```ts
7
+ * queryOptions({ ...cachedQueryDefaults, queryKey: ["countries"], queryFn: fetchCountries })
8
+ * ```
9
+ *
10
+ * Every duration is in milliseconds.
11
+ */
12
+ /** Fast-moving data that keeps updating even while the tab is hidden: 15 s, refetched in the background. */
13
+ declare const liveBackgroundQueryDefaults: {
14
+ readonly refetchInterval: number;
15
+ readonly refetchIntervalInBackground: true;
16
+ readonly staleTime: number;
17
+ };
18
+ /** Fast-moving data refreshed while it is on screen: 30 s. */
19
+ declare const liveQueryDefaults: {
20
+ readonly refetchInterval: number;
21
+ readonly staleTime: number;
22
+ };
23
+ /** Data that can be slightly stale: 1 min. */
24
+ declare const shortLivedQueryDefaults: {
25
+ readonly staleTime: number;
26
+ };
27
+ /** Slower-changing detail data: 2 min. */
28
+ declare const longLivedQueryDefaults: {
29
+ readonly staleTime: number;
30
+ };
31
+ /** Reference data worth keeping longer: 5 min stale, 30 min in the cache (the one preset that raises `gcTime`). */
32
+ declare const cachedQueryDefaults: {
33
+ readonly gcTime: number;
34
+ readonly staleTime: number;
35
+ };
36
+ /** Effectively immutable data: never stale. */
37
+ declare const staticQueryDefaults: {
38
+ readonly staleTime: number;
39
+ };
40
+ //#endregion
41
+ export { cachedQueryDefaults, liveBackgroundQueryDefaults, liveQueryDefaults, longLivedQueryDefaults, shortLivedQueryDefaults, staticQueryDefaults };
@@ -0,0 +1,37 @@
1
+ //#region src/query-defaults.ts
2
+ /**
3
+ * Timing presets to spread as the first property of a `queryOptions(…)` call, so freshness stays
4
+ * consistent across a codebase instead of every query inventing its own `staleTime`.
5
+ *
6
+ * ```ts
7
+ * queryOptions({ ...cachedQueryDefaults, queryKey: ["countries"], queryFn: fetchCountries })
8
+ * ```
9
+ *
10
+ * Every duration is in milliseconds.
11
+ */
12
+ const SECOND = 1e3;
13
+ const MINUTE = 60 * SECOND;
14
+ /** Fast-moving data that keeps updating even while the tab is hidden: 15 s, refetched in the background. */
15
+ const liveBackgroundQueryDefaults = {
16
+ refetchInterval: 15 * SECOND,
17
+ refetchIntervalInBackground: true,
18
+ staleTime: 15 * SECOND
19
+ };
20
+ /** Fast-moving data refreshed while it is on screen: 30 s. */
21
+ const liveQueryDefaults = {
22
+ refetchInterval: 30 * SECOND,
23
+ staleTime: 30 * SECOND
24
+ };
25
+ /** Data that can be slightly stale: 1 min. */
26
+ const shortLivedQueryDefaults = { staleTime: MINUTE };
27
+ /** Slower-changing detail data: 2 min. */
28
+ const longLivedQueryDefaults = { staleTime: 2 * MINUTE };
29
+ /** Reference data worth keeping longer: 5 min stale, 30 min in the cache (the one preset that raises `gcTime`). */
30
+ const cachedQueryDefaults = {
31
+ gcTime: 30 * MINUTE,
32
+ staleTime: 5 * MINUTE
33
+ };
34
+ /** Effectively immutable data: never stale. */
35
+ const staticQueryDefaults = { staleTime: Number.POSITIVE_INFINITY };
36
+ //#endregion
37
+ export { cachedQueryDefaults, liveBackgroundQueryDefaults, liveQueryDefaults, longLivedQueryDefaults, shortLivedQueryDefaults, staticQueryDefaults };
@@ -0,0 +1,37 @@
1
+ import { DehydrateOptions, QueryClient } from "@tanstack/react-query";
2
+ import { ReactNode } from "react";
3
+ //#region src/query-hydration-boundary.d.ts
4
+ type QueryHydrationBoundaryProps = {
5
+ children: ReactNode;
6
+ /**
7
+ * Fills the server-side cache. Call each query through the same `queryOptions` factory the
8
+ * browser uses, unchanged, so the entry has the key *and* the value the client would build.
9
+ * Use `fetchQuery` for a read the route cannot render without (`prefetchQuery` swallows
10
+ * failures); everything else can be `prefetchQuery`.
11
+ */
12
+ prefetch: (queryClient: QueryClient) => Promise<unknown>;
13
+ /**
14
+ * Builds the per-request `QueryClient` handed to `prefetch`. Defaults to `new QueryClient()`.
15
+ * Pass one when your queries depend on client-level defaults (e.g. `defaultOptions.queries`).
16
+ */
17
+ createQueryClient?: () => QueryClient;
18
+ /** Forwarded to TanStack's `dehydrate` (e.g. to also dehydrate pending queries). */
19
+ dehydrateOptions?: DehydrateOptions;
20
+ };
21
+ /**
22
+ * Async **server component**: runs `prefetch` against a fresh `QueryClient` and dehydrates the
23
+ * result into `PrefetchedQueries`, so the suspense queries below resolve from the cache in the
24
+ * server render *and* in the browser. That is what lets a `QuerySuspense` subtree render real
25
+ * HTML instead of holding its fallback until hydration.
26
+ *
27
+ * ```tsx
28
+ * <QueryHydrationBoundary
29
+ * prefetch={(queryClient) => queryClient.fetchQuery(dealsQueryOptions(filters))}
30
+ * >
31
+ * <Deals />
32
+ * </QueryHydrationBoundary>
33
+ * ```
34
+ */
35
+ declare function QueryHydrationBoundary({ children, createQueryClient, dehydrateOptions, prefetch }: QueryHydrationBoundaryProps): Promise<import("react/jsx-runtime").JSX.Element>;
36
+ //#endregion
37
+ export { QueryHydrationBoundary, QueryHydrationBoundaryProps };
@@ -0,0 +1,28 @@
1
+ import { PrefetchedQueries } from "./prefetched-queries.mjs";
2
+ import { QueryClient, dehydrate } from "@tanstack/react-query";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/query-hydration-boundary.tsx
5
+ /**
6
+ * Async **server component**: runs `prefetch` against a fresh `QueryClient` and dehydrates the
7
+ * result into `PrefetchedQueries`, so the suspense queries below resolve from the cache in the
8
+ * server render *and* in the browser. That is what lets a `QuerySuspense` subtree render real
9
+ * HTML instead of holding its fallback until hydration.
10
+ *
11
+ * ```tsx
12
+ * <QueryHydrationBoundary
13
+ * prefetch={(queryClient) => queryClient.fetchQuery(dealsQueryOptions(filters))}
14
+ * >
15
+ * <Deals />
16
+ * </QueryHydrationBoundary>
17
+ * ```
18
+ */
19
+ async function QueryHydrationBoundary({ children, createQueryClient = () => new QueryClient(), dehydrateOptions, prefetch }) {
20
+ const queryClient = createQueryClient();
21
+ await prefetch(queryClient);
22
+ return /* @__PURE__ */ jsx(PrefetchedQueries, {
23
+ state: dehydrate(queryClient, dehydrateOptions),
24
+ children
25
+ });
26
+ }
27
+ //#endregion
28
+ export { QueryHydrationBoundary };
@@ -0,0 +1,11 @@
1
+ import { ComponentPropsWithoutRef } from "react";
2
+ //#region src/query-suspense-loading.d.ts
3
+ type QuerySuspenseLoadingProps = ComponentPropsWithoutRef<"div">;
4
+ /**
5
+ * Single layout root for Suspense loading UI: set `className` and children here so `aria-busy` /
6
+ * `aria-live` apply to the same element as your skeleton stack (no inner wrapper div). Accepts
7
+ * all `div` props; the `aria-busy` and `aria-live` defaults can be overridden.
8
+ */
9
+ declare function QuerySuspenseLoading({ "aria-busy": ariaBusy, "aria-live": ariaLive, children, ...props }: QuerySuspenseLoadingProps): import("react/jsx-runtime").JSX.Element;
10
+ //#endregion
11
+ export { QuerySuspenseLoading, QuerySuspenseLoadingProps };
@@ -0,0 +1,17 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ //#region src/query-suspense-loading.tsx
3
+ /**
4
+ * Single layout root for Suspense loading UI: set `className` and children here so `aria-busy` /
5
+ * `aria-live` apply to the same element as your skeleton stack (no inner wrapper div). Accepts
6
+ * all `div` props; the `aria-busy` and `aria-live` defaults can be overridden.
7
+ */
8
+ function QuerySuspenseLoading({ "aria-busy": ariaBusy = true, "aria-live": ariaLive = "polite", children, ...props }) {
9
+ return /* @__PURE__ */ jsx("div", {
10
+ "aria-busy": ariaBusy,
11
+ "aria-live": ariaLive,
12
+ ...props,
13
+ children
14
+ });
15
+ }
16
+ //#endregion
17
+ export { QuerySuspenseLoading };
@@ -0,0 +1,30 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/query-suspense.d.ts
3
+ type QuerySuspenseErrorFallbackProps = {
4
+ error: Error;
5
+ onRetry: () => void;
6
+ };
7
+ /** Dev/test only: force a boundary's loading or error UI (e.g. for screenshots). Ignored in production. */
8
+ type QuerySuspenseDebugState = "loading" | "error";
9
+ /** Exported so callers can type wrapper components that forward `fallback` / `errorFallback`. */
10
+ type QuerySuspenseProps = {
11
+ children: ReactNode;
12
+ /** Suspense fallback: usually a skeleton of the same layout. */
13
+ fallback: ReactNode;
14
+ /** Rendered when a query below throws; `onRetry` resets the failed queries and re-renders. */
15
+ errorFallback: (props: QuerySuspenseErrorFallbackProps) => ReactNode;
16
+ /**
17
+ * Development / test only: render `fallback` or `errorFallback` without running queries.
18
+ * No effect when `NODE_ENV` is `"production"`.
19
+ */
20
+ debugState?: QuerySuspenseDebugState;
21
+ };
22
+ /**
23
+ * TanStack's recommended suspense stack (`QueryErrorResetBoundary` → error boundary →
24
+ * `Suspense`, see https://tanstack.com/query/v5/docs/framework/react/guides/suspense), plus a
25
+ * hydration gate: `fallback` is all it renders on the server and through hydration, unless a
26
+ * `PrefetchedQueries` (e.g. from `QueryHydrationBoundary`) above it was handed dehydrated queries.
27
+ */
28
+ declare function QuerySuspense({ children, errorFallback, fallback, debugState }: QuerySuspenseProps): import("react/jsx-runtime").JSX.Element;
29
+ //#endregion
30
+ export { QuerySuspense, QuerySuspenseDebugState, QuerySuspenseErrorFallbackProps, QuerySuspenseProps };
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ import { useHasPrefetchedQueries } from "./prefetched-queries.mjs";
3
+ import { QueryErrorResetBoundary } from "@tanstack/react-query";
4
+ import { Component, Suspense, useSyncExternalStore } from "react";
5
+ import { Fragment, jsx } from "react/jsx-runtime";
6
+ //#region src/query-suspense.tsx
7
+ const debugStateEnabled = typeof process !== "undefined" && process.env.NODE_ENV !== "production";
8
+ const subscribeToNothing = () => () => {};
9
+ const useIsHydrated = () => useSyncExternalStore(subscribeToNothing, () => true, () => false);
10
+ const debugError = Object.freeze(/* @__PURE__ */ new Error("QuerySuspense: debugState=\"error\" (development / test only; for screenshots)"));
11
+ const noop = () => {};
12
+ /** Next.js control-flow errors (`notFound()`, `redirect()`, …) carry a `NEXT_`-prefixed digest. */
13
+ const isNextControlFlowError = (error) => typeof error === "object" && error !== null && "digest" in error && typeof error.digest === "string" && error.digest.startsWith("NEXT_");
14
+ var QueryErrorBoundary = class extends Component {
15
+ constructor(props) {
16
+ super(props);
17
+ this.handleRetry = () => {
18
+ this.props.onQueryReset();
19
+ this.setState({ error: null });
20
+ };
21
+ this.state = { error: null };
22
+ }
23
+ static getDerivedStateFromError(error) {
24
+ if (isNextControlFlowError(error)) throw error;
25
+ return { error };
26
+ }
27
+ componentDidCatch(error) {
28
+ console.error("QueryErrorBoundary:", error);
29
+ }
30
+ render() {
31
+ if (this.state.error) return this.props.errorFallback({
32
+ error: this.state.error,
33
+ onRetry: this.handleRetry
34
+ });
35
+ return this.props.children;
36
+ }
37
+ };
38
+ /**
39
+ * TanStack's recommended suspense stack (`QueryErrorResetBoundary` → error boundary →
40
+ * `Suspense`, see https://tanstack.com/query/v5/docs/framework/react/guides/suspense), plus a
41
+ * hydration gate: `fallback` is all it renders on the server and through hydration, unless a
42
+ * `PrefetchedQueries` (e.g. from `QueryHydrationBoundary`) above it was handed dehydrated queries.
43
+ */
44
+ function QuerySuspense({ children, errorFallback, fallback, debugState }) {
45
+ const isHydrated = useIsHydrated();
46
+ const hasPrefetchedQueries = useHasPrefetchedQueries();
47
+ if (debugStateEnabled && debugState === "loading") return /* @__PURE__ */ jsx(Fragment, { children: fallback });
48
+ if (debugStateEnabled && debugState === "error") return /* @__PURE__ */ jsx(Fragment, { children: errorFallback({
49
+ error: debugError,
50
+ onRetry: noop
51
+ }) });
52
+ if (!(isHydrated || hasPrefetchedQueries)) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
53
+ return /* @__PURE__ */ jsx(QueryErrorResetBoundary, { children: ({ reset }) => /* @__PURE__ */ jsx(QueryErrorBoundary, {
54
+ errorFallback,
55
+ onQueryReset: reset,
56
+ children: /* @__PURE__ */ jsx(Suspense, {
57
+ fallback,
58
+ children
59
+ })
60
+ }) });
61
+ }
62
+ //#endregion
63
+ export { QuerySuspense };
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@zeno-lib/query",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "exports": {
6
+ "./prefetched-queries": {
7
+ "types": "./dist/prefetched-queries.d.mts",
8
+ "import": "./dist/prefetched-queries.mjs",
9
+ "default": "./dist/prefetched-queries.mjs"
10
+ },
11
+ "./query-defaults": {
12
+ "types": "./dist/query-defaults.d.mts",
13
+ "import": "./dist/query-defaults.mjs",
14
+ "default": "./dist/query-defaults.mjs"
15
+ },
16
+ "./query-hydration-boundary": {
17
+ "types": "./dist/query-hydration-boundary.d.mts",
18
+ "import": "./dist/query-hydration-boundary.mjs",
19
+ "default": "./dist/query-hydration-boundary.mjs"
20
+ },
21
+ "./query-suspense": {
22
+ "types": "./dist/query-suspense.d.mts",
23
+ "import": "./dist/query-suspense.mjs",
24
+ "default": "./dist/query-suspense.mjs"
25
+ },
26
+ "./query-suspense-loading": {
27
+ "types": "./dist/query-suspense-loading.d.mts",
28
+ "import": "./dist/query-suspense-loading.mjs",
29
+ "default": "./dist/query-suspense-loading.mjs"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "repository": {
39
+ "url": "https://github.com/zeno-lib/zeno.git",
40
+ "directory": "packages/query"
41
+ },
42
+ "peerDependencies": {
43
+ "@tanstack/react-query": ">=5",
44
+ "react": ">=19"
45
+ },
46
+ "devDependencies": {
47
+ "@tanstack/react-query": "^5.103.2",
48
+ "@types/node": "25.6.0",
49
+ "@types/react": "19.2.14",
50
+ "@types/react-dom": "19.2.3",
51
+ "react": "^19.2.5",
52
+ "react-dom": "^19.2.5",
53
+ "tsdown": "^0.22.14",
54
+ "typescript": "7.0.2",
55
+ "vite": "8.0.16",
56
+ "vitest": "4.1.11",
57
+ "@zeno-lib/test": "^0.0.3",
58
+ "@zeno-lib/typescript": "^1.1.0",
59
+ "@zeno-lib/ui": "^0.0.1"
60
+ },
61
+ "scripts": {
62
+ "build": "tsdown",
63
+ "test": "vitest --run",
64
+ "test:watch": "vitest watch",
65
+ "types:check": "tsc --noEmit"
66
+ }
67
+ }