@classytic/arc-next 0.2.1 → 0.4.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 +93 -27
- package/dist/api.d.ts +120 -33
- package/dist/api.js +116 -13
- package/dist/client.d.ts +38 -12
- package/dist/client.js +24 -7
- package/dist/hooks.d.ts +66 -70
- package/dist/hooks.js +410 -110
- package/dist/mutation.d.ts +10 -21
- package/dist/mutation.js +20 -15
- package/dist/prefetch.d.ts +40 -3
- package/dist/prefetch.js +57 -22
- package/dist/query-client.d.ts +1 -2
- package/dist/query-client.js +2 -2
- package/dist/query.d.ts +47 -6
- package/dist/query.js +67 -20
- package/dist/sse.d.ts +62 -0
- package/dist/sse.js +144 -0
- package/package.json +28 -13
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/hooks.d.ts.map +0 -1
- package/dist/hooks.js.map +0 -1
- package/dist/mutation.d.ts.map +0 -1
- package/dist/mutation.js.map +0 -1
- package/dist/prefetch.d.ts.map +0 -1
- package/dist/prefetch.js.map +0 -1
- package/dist/query-client.d.ts.map +0 -1
- package/dist/query-client.js.map +0 -1
- package/dist/query.d.ts.map +0 -1
- package/dist/query.js.map +0 -1
package/dist/mutation.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ToastHandler } from "./client.js";
|
|
2
|
-
import
|
|
2
|
+
import { QUERY_CONFIGS } from "./query.js";
|
|
3
|
+
import * as _$_tanstack_react_query0 from "@tanstack/react-query";
|
|
3
4
|
import { QueryClient, QueryKey, UseMutateAsyncFunction, UseMutateFunction } from "@tanstack/react-query";
|
|
4
5
|
|
|
5
6
|
//#region src/mutation.d.ts
|
|
@@ -26,6 +27,8 @@ interface TransitionMutationReturn<TData, TVariables> {
|
|
|
26
27
|
/**
|
|
27
28
|
* Configure toast handler. Call once at app init.
|
|
28
29
|
*
|
|
30
|
+
* **SSR safety:** This sets module-level state. Call only in client-side code.
|
|
31
|
+
*
|
|
29
32
|
* @example
|
|
30
33
|
* import { toast } from "sonner";
|
|
31
34
|
* configureToast({ success: toast.success, error: toast.error });
|
|
@@ -45,8 +48,8 @@ interface TransitionMutationConfig<TData, TVariables> {
|
|
|
45
48
|
toastHandler?: ToastHandler;
|
|
46
49
|
}
|
|
47
50
|
declare function useMutationWithTransition<TData, TVariables>(config: TransitionMutationConfig<TData, TVariables>): {
|
|
48
|
-
mutate: UseMutateFunction<TData, Error, TVariables,
|
|
49
|
-
mutateAsync: UseMutateAsyncFunction<TData, Error, TVariables,
|
|
51
|
+
mutate: UseMutateFunction<TData, Error, TVariables, void>;
|
|
52
|
+
mutateAsync: UseMutateAsyncFunction<TData, Error, TVariables, void>;
|
|
50
53
|
isPending: boolean;
|
|
51
54
|
isSuccess: boolean;
|
|
52
55
|
isError: boolean;
|
|
@@ -98,27 +101,13 @@ interface CreateOptimisticMutationConfig<TData, TVariables> {
|
|
|
98
101
|
shouldToast?: () => boolean;
|
|
99
102
|
toastHandler?: ToastHandler;
|
|
100
103
|
}
|
|
101
|
-
declare function
|
|
104
|
+
declare function useOptimisticMutation<TData, TVariables>(config: CreateOptimisticMutationConfig<TData, TVariables>): _$_tanstack_react_query0.UseMutationResult<TData, Error, TVariables, {
|
|
102
105
|
previous: {
|
|
103
106
|
key: readonly unknown[];
|
|
104
107
|
data: [readonly unknown[], unknown][];
|
|
105
108
|
}[];
|
|
106
109
|
}>;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
readonly staleTime: 20000;
|
|
110
|
-
readonly refetchInterval: 30000;
|
|
111
|
-
};
|
|
112
|
-
readonly frequent: {
|
|
113
|
-
readonly staleTime: 60000;
|
|
114
|
-
};
|
|
115
|
-
readonly stable: {
|
|
116
|
-
readonly staleTime: 300000;
|
|
117
|
-
};
|
|
118
|
-
readonly static: {
|
|
119
|
-
readonly staleTime: 600000;
|
|
120
|
-
};
|
|
121
|
-
};
|
|
110
|
+
/** @deprecated Use `useOptimisticMutation` */
|
|
111
|
+
declare const createOptimisticMutation: typeof useOptimisticMutation;
|
|
122
112
|
//#endregion
|
|
123
|
-
export { CreateOptimisticMutationConfig, MutationCallbacks, MutationMessages, OptimisticMutationConfig, QUERY_CONFIGS,
|
|
124
|
-
//# sourceMappingURL=mutation.d.ts.map
|
|
113
|
+
export { CreateOptimisticMutationConfig, MutationCallbacks, MutationMessages, OptimisticMutationConfig, QUERY_CONFIGS, TransitionMutationConfig, TransitionMutationReturn, configureToast, createOptimisticMutation, useMutationWithOptimistic, useMutationWithTransition, useOptimisticMutation };
|
package/dist/mutation.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { isArcApiError } from "./client.js";
|
|
3
|
+
import { isArcApiError, isAutoIdempotency } from "./client.js";
|
|
4
|
+
import { QUERY_CONFIGS } from "./query.js";
|
|
4
5
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
5
|
-
import { useTransition } from "react";
|
|
6
|
+
import { useCallback, useRef, useTransition } from "react";
|
|
6
7
|
|
|
7
8
|
//#region src/mutation.ts
|
|
8
9
|
let toastHandler = {
|
|
@@ -12,6 +13,8 @@ let toastHandler = {
|
|
|
12
13
|
/**
|
|
13
14
|
* Configure toast handler. Call once at app init.
|
|
14
15
|
*
|
|
16
|
+
* **SSR safety:** This sets module-level state. Call only in client-side code.
|
|
17
|
+
*
|
|
15
18
|
* @example
|
|
16
19
|
* import { toast } from "sonner";
|
|
17
20
|
* configureToast({ success: toast.success, error: toast.error });
|
|
@@ -41,8 +44,18 @@ function useMutationWithTransition(config) {
|
|
|
41
44
|
const { mutationFn, invalidateQueries = [], onSuccess, onError, onSettled, messages, useTransition: withTransition = true, showToast: toast = true, toastHandler: instanceToast } = config;
|
|
42
45
|
const queryClient = useQueryClient();
|
|
43
46
|
const [isTransitioning, startTransition] = useTransition();
|
|
47
|
+
const idempotencyKeyRef = useRef(null);
|
|
44
48
|
const mutation = useMutation({
|
|
45
|
-
mutationFn
|
|
49
|
+
mutationFn: (variables) => {
|
|
50
|
+
if (idempotencyKeyRef.current && typeof variables === "object" && variables !== null) {
|
|
51
|
+
const vars = variables;
|
|
52
|
+
if (vars.options && typeof vars.options === "object") vars.options.idempotencyKey ??= idempotencyKeyRef.current;
|
|
53
|
+
}
|
|
54
|
+
return mutationFn(variables);
|
|
55
|
+
},
|
|
56
|
+
onMutate: () => {
|
|
57
|
+
idempotencyKeyRef.current = isAutoIdempotency() ? globalThis.crypto.randomUUID() : null;
|
|
58
|
+
},
|
|
46
59
|
onSuccess: (data, variables) => {
|
|
47
60
|
const invalidate = () => {
|
|
48
61
|
invalidateQueries.forEach((key) => queryClient.invalidateQueries({ queryKey: key }));
|
|
@@ -112,7 +125,7 @@ function useMutationWithOptimistic(config) {
|
|
|
112
125
|
reset: mutation.reset
|
|
113
126
|
};
|
|
114
127
|
}
|
|
115
|
-
function
|
|
128
|
+
function useOptimisticMutation(config) {
|
|
116
129
|
const { mutationFn, queryClient, queryKeys, optimisticUpdate, onSuccess, onError, onSettled, messages, toastHandler: instanceToast } = config;
|
|
117
130
|
return useMutation({
|
|
118
131
|
mutationFn,
|
|
@@ -149,16 +162,8 @@ function createOptimisticMutation(config) {
|
|
|
149
162
|
}
|
|
150
163
|
});
|
|
151
164
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
staleTime: 2e4,
|
|
155
|
-
refetchInterval: 3e4
|
|
156
|
-
},
|
|
157
|
-
frequent: { staleTime: 6e4 },
|
|
158
|
-
stable: { staleTime: 3e5 },
|
|
159
|
-
static: { staleTime: 6e5 }
|
|
160
|
-
};
|
|
165
|
+
/** @deprecated Use `useOptimisticMutation` */
|
|
166
|
+
const createOptimisticMutation = useOptimisticMutation;
|
|
161
167
|
|
|
162
168
|
//#endregion
|
|
163
|
-
export { QUERY_CONFIGS, configureToast, createOptimisticMutation, useMutationWithOptimistic, useMutationWithTransition };
|
|
164
|
-
//# sourceMappingURL=mutation.js.map
|
|
169
|
+
export { QUERY_CONFIGS, configureToast, createOptimisticMutation, useMutationWithOptimistic, useMutationWithTransition, useOptimisticMutation };
|
package/dist/prefetch.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ import { QueryClient, dehydrate } from "@tanstack/react-query";
|
|
|
4
4
|
interface PrefetchOptions {
|
|
5
5
|
staleTime?: number;
|
|
6
6
|
}
|
|
7
|
+
interface PrefetchDetailOptions extends PrefetchOptions {
|
|
8
|
+
/** Query params (select, populate) — key must match useDetail's params to share cache */
|
|
9
|
+
params?: {
|
|
10
|
+
select?: string;
|
|
11
|
+
populate?: string | string[];
|
|
12
|
+
};
|
|
13
|
+
}
|
|
7
14
|
interface CrudPrefetcher {
|
|
8
15
|
/**
|
|
9
16
|
* Prefetch a list query on the server. Uses the same query keys as useList.
|
|
@@ -20,7 +27,22 @@ interface CrudPrefetcher {
|
|
|
20
27
|
* const queryClient = getQueryClient();
|
|
21
28
|
* await productsPrefetcher.prefetchDetail(queryClient, productId);
|
|
22
29
|
*/
|
|
23
|
-
prefetchDetail: (queryClient: QueryClient, id: string, options?:
|
|
30
|
+
prefetchDetail: (queryClient: QueryClient, id: string, options?: PrefetchDetailOptions) => Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Prefetch a detail-by-slug query. Uses the same query keys as useDetailBySlug.
|
|
33
|
+
* Only available when the API has a `getBySlug` method (slugLookup preset).
|
|
34
|
+
*/
|
|
35
|
+
prefetchBySlug: (queryClient: QueryClient, slug: string, options?: PrefetchDetailOptions) => Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Prefetch soft-deleted items. Uses the same query keys as useDeleted.
|
|
38
|
+
* Only available when the API has a `getDeleted` method (softDelete preset).
|
|
39
|
+
*/
|
|
40
|
+
prefetchDeleted: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Prefetch a tree query. Uses the same query keys as useTree.
|
|
43
|
+
* Only available when the API has a `getTree` method (tree preset).
|
|
44
|
+
*/
|
|
45
|
+
prefetchTree: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
|
|
24
46
|
}
|
|
25
47
|
/**
|
|
26
48
|
* Create server-safe prefetch helpers for CRUD queries.
|
|
@@ -58,7 +80,22 @@ declare function createCrudPrefetcher(api: {
|
|
|
58
80
|
token?: string | null;
|
|
59
81
|
organizationId?: string | null;
|
|
60
82
|
}) => Promise<unknown>;
|
|
83
|
+
getBySlug?: (opts: {
|
|
84
|
+
slug: string;
|
|
85
|
+
token?: string | null;
|
|
86
|
+
organizationId?: string | null;
|
|
87
|
+
params?: Record<string, unknown>;
|
|
88
|
+
}) => Promise<unknown>;
|
|
89
|
+
getDeleted?: (opts: {
|
|
90
|
+
params?: Record<string, unknown>;
|
|
91
|
+
token?: string | null;
|
|
92
|
+
organizationId?: string | null;
|
|
93
|
+
}) => Promise<unknown>;
|
|
94
|
+
getTree?: (opts: {
|
|
95
|
+
params?: Record<string, unknown>;
|
|
96
|
+
token?: string | null;
|
|
97
|
+
organizationId?: string | null;
|
|
98
|
+
}) => Promise<unknown>;
|
|
61
99
|
}, entityKey: string): CrudPrefetcher;
|
|
62
100
|
//#endregion
|
|
63
|
-
export { CrudPrefetcher, PrefetchOptions, createCrudPrefetcher, dehydrate };
|
|
64
|
-
//# sourceMappingURL=prefetch.d.ts.map
|
|
101
|
+
export { CrudPrefetcher, PrefetchDetailOptions, PrefetchOptions, createCrudPrefetcher, dehydrate };
|
package/dist/prefetch.js
CHANGED
|
@@ -1,23 +1,7 @@
|
|
|
1
|
+
import { createQueryKeys } from "./query.js";
|
|
1
2
|
import { dehydrate } from "@tanstack/react-query";
|
|
2
3
|
|
|
3
4
|
//#region src/prefetch.ts
|
|
4
|
-
function scopedListKey(entityKey, scope, params) {
|
|
5
|
-
return [
|
|
6
|
-
entityKey,
|
|
7
|
-
"list",
|
|
8
|
-
{
|
|
9
|
-
_scope: scope,
|
|
10
|
-
...params || {}
|
|
11
|
-
}
|
|
12
|
-
];
|
|
13
|
-
}
|
|
14
|
-
function detailKey(entityKey, id) {
|
|
15
|
-
return [
|
|
16
|
-
entityKey,
|
|
17
|
-
"detail",
|
|
18
|
-
id
|
|
19
|
-
];
|
|
20
|
-
}
|
|
21
5
|
/**
|
|
22
6
|
* Create server-safe prefetch helpers for CRUD queries.
|
|
23
7
|
* Use in Next.js server components to pre-populate the query cache before rendering.
|
|
@@ -44,10 +28,12 @@ function detailKey(entityKey, id) {
|
|
|
44
28
|
* }
|
|
45
29
|
*/
|
|
46
30
|
function createCrudPrefetcher(api, entityKey) {
|
|
31
|
+
const KEYS = createQueryKeys(entityKey);
|
|
47
32
|
return {
|
|
48
33
|
async prefetchList(queryClient, params = {}, options = {}) {
|
|
49
34
|
const { organizationId, ...restParams } = params;
|
|
50
|
-
const
|
|
35
|
+
const scope = organizationId ? "tenant" : "super-admin";
|
|
36
|
+
const queryKey = KEYS.scopedList(scope, {
|
|
51
37
|
organizationId,
|
|
52
38
|
...restParams
|
|
53
39
|
});
|
|
@@ -61,10 +47,60 @@ function createCrudPrefetcher(api, entityKey) {
|
|
|
61
47
|
});
|
|
62
48
|
},
|
|
63
49
|
async prefetchDetail(queryClient, id, options = {}) {
|
|
64
|
-
const
|
|
50
|
+
const { params, staleTime } = options;
|
|
51
|
+
const baseKey = KEYS.detail(id);
|
|
52
|
+
const queryKey = params ? [...baseKey, params] : baseKey;
|
|
53
|
+
await queryClient.prefetchQuery({
|
|
54
|
+
queryKey,
|
|
55
|
+
queryFn: () => api.getById({
|
|
56
|
+
id,
|
|
57
|
+
...params ? { params } : {}
|
|
58
|
+
}),
|
|
59
|
+
staleTime
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
async prefetchBySlug(queryClient, slug, options = {}) {
|
|
63
|
+
if (!api.getBySlug) throw new Error(`[arc-next] prefetchBySlug requires an api with getBySlug (slugLookup preset)`);
|
|
64
|
+
const { params, staleTime } = options;
|
|
65
|
+
const queryKey = params ? KEYS.custom("slug", slug, params) : KEYS.custom("slug", slug);
|
|
66
|
+
await queryClient.prefetchQuery({
|
|
67
|
+
queryKey,
|
|
68
|
+
queryFn: () => api.getBySlug({
|
|
69
|
+
slug,
|
|
70
|
+
...params ? { params } : {}
|
|
71
|
+
}),
|
|
72
|
+
staleTime
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
async prefetchDeleted(queryClient, params = {}, options = {}) {
|
|
76
|
+
if (!api.getDeleted) throw new Error(`[arc-next] prefetchDeleted requires an api with getDeleted (softDelete preset)`);
|
|
77
|
+
const { organizationId, ...restParams } = params;
|
|
78
|
+
const queryKey = KEYS.custom("deleted", {
|
|
79
|
+
organizationId,
|
|
80
|
+
...restParams
|
|
81
|
+
});
|
|
82
|
+
await queryClient.prefetchQuery({
|
|
83
|
+
queryKey,
|
|
84
|
+
queryFn: () => api.getDeleted({
|
|
85
|
+
params: restParams,
|
|
86
|
+
organizationId: organizationId ?? null
|
|
87
|
+
}),
|
|
88
|
+
staleTime: options.staleTime
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
async prefetchTree(queryClient, params = {}, options = {}) {
|
|
92
|
+
if (!api.getTree) throw new Error(`[arc-next] prefetchTree requires an api with getTree (tree preset)`);
|
|
93
|
+
const { organizationId, ...restParams } = params;
|
|
94
|
+
const queryKey = KEYS.custom("tree", {
|
|
95
|
+
organizationId,
|
|
96
|
+
...restParams
|
|
97
|
+
});
|
|
65
98
|
await queryClient.prefetchQuery({
|
|
66
99
|
queryKey,
|
|
67
|
-
queryFn: () => api.
|
|
100
|
+
queryFn: () => api.getTree({
|
|
101
|
+
params: restParams,
|
|
102
|
+
organizationId: organizationId ?? null
|
|
103
|
+
}),
|
|
68
104
|
staleTime: options.staleTime
|
|
69
105
|
});
|
|
70
106
|
}
|
|
@@ -72,5 +108,4 @@ function createCrudPrefetcher(api, entityKey) {
|
|
|
72
108
|
}
|
|
73
109
|
|
|
74
110
|
//#endregion
|
|
75
|
-
export { createCrudPrefetcher, dehydrate };
|
|
76
|
-
//# sourceMappingURL=prefetch.js.map
|
|
111
|
+
export { createCrudPrefetcher, dehydrate };
|
package/dist/query-client.d.ts
CHANGED
|
@@ -21,5 +21,4 @@ interface QueryClientOverrides {
|
|
|
21
21
|
*/
|
|
22
22
|
declare function getQueryClient(overrides?: QueryClientOverrides): QueryClient;
|
|
23
23
|
//#endregion
|
|
24
|
-
export { QueryClientOverrides, getQueryClient };
|
|
25
|
-
//# sourceMappingURL=query-client.d.ts.map
|
|
24
|
+
export { QueryClientOverrides, getQueryClient };
|
package/dist/query-client.js
CHANGED
|
@@ -38,9 +38,9 @@ let browserQueryClient;
|
|
|
38
38
|
function getQueryClient(overrides) {
|
|
39
39
|
if (isServer) return makeQueryClient(overrides);
|
|
40
40
|
if (!browserQueryClient) browserQueryClient = makeQueryClient(overrides);
|
|
41
|
+
else if (overrides) console.warn("[arc-next] getQueryClient(): Browser singleton already exists — overrides are ignored. Pass overrides only on the first call.");
|
|
41
42
|
return browserQueryClient;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
//#endregion
|
|
45
|
-
export { getQueryClient };
|
|
46
|
-
//# sourceMappingURL=query-client.js.map
|
|
46
|
+
export { getQueryClient };
|
package/dist/query.d.ts
CHANGED
|
@@ -2,12 +2,16 @@ import { InfiniteData, QueryClient, QueryKey } from "@tanstack/react-query";
|
|
|
2
2
|
|
|
3
3
|
//#region src/query.d.ts
|
|
4
4
|
interface PaginationData {
|
|
5
|
+
/** Pagination method detected from response (offset | keyset | aggregate) */
|
|
6
|
+
method: 'offset' | 'keyset' | 'aggregate' | null;
|
|
5
7
|
total: number;
|
|
6
8
|
pages: number;
|
|
7
9
|
page: number;
|
|
8
10
|
limit: number;
|
|
9
11
|
hasNext: boolean;
|
|
10
12
|
hasPrev: boolean;
|
|
13
|
+
/** Keyset cursor for next page (keyset pagination only) */
|
|
14
|
+
next?: string | null;
|
|
11
15
|
}
|
|
12
16
|
/** Request-level options passed through to the fetch call */
|
|
13
17
|
interface RequestPassthrough {
|
|
@@ -39,6 +43,7 @@ interface DetailQueryOptions<TData = unknown> {
|
|
|
39
43
|
enabled?: boolean;
|
|
40
44
|
staleTime?: number;
|
|
41
45
|
gcTime?: number;
|
|
46
|
+
refetchOnWindowFocus?: boolean;
|
|
42
47
|
structuralSharing?: boolean;
|
|
43
48
|
refetchInterval?: number | false;
|
|
44
49
|
refetchIntervalInBackground?: boolean;
|
|
@@ -98,7 +103,24 @@ declare const DEFAULT_QUERY_CONFIG: {
|
|
|
98
103
|
readonly refetchOnWindowFocus: false;
|
|
99
104
|
readonly retry: 0;
|
|
100
105
|
};
|
|
106
|
+
/** Pre-built query config presets for common data freshness patterns. */
|
|
107
|
+
declare const QUERY_CONFIGS: {
|
|
108
|
+
/** Live data: 20s stale, 30s polling */readonly realtime: {
|
|
109
|
+
readonly staleTime: 20000;
|
|
110
|
+
readonly refetchInterval: 30000;
|
|
111
|
+
}; /** Frequently updated: 60s stale */
|
|
112
|
+
readonly frequent: {
|
|
113
|
+
readonly staleTime: 60000;
|
|
114
|
+
}; /** Stable data: 5min stale (same as default) */
|
|
115
|
+
readonly stable: {
|
|
116
|
+
readonly staleTime: 300000;
|
|
117
|
+
}; /** Rarely changes: 10min stale */
|
|
118
|
+
readonly static: {
|
|
119
|
+
readonly staleTime: 600000;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
101
122
|
declare function getItemId(item: unknown): string | null;
|
|
123
|
+
declare function extractItem<T>(data: unknown): T | null;
|
|
102
124
|
declare function updateListCache<T>(listData: unknown, updater: (items: T[]) => T[]): unknown;
|
|
103
125
|
declare function createQueryKeys(entityKey: string): QueryKeys;
|
|
104
126
|
declare function createCacheUtils<T>(KEYS: QueryKeys): CacheUtils<T>;
|
|
@@ -111,15 +133,18 @@ interface CreateListQueryConfig {
|
|
|
111
133
|
options?: Record<string, unknown>;
|
|
112
134
|
prefillDetailCache?: boolean;
|
|
113
135
|
detailKeyBuilder?: (id: string) => QueryKey;
|
|
136
|
+
/** Custom ID extractor for cache prefill. Falls back to getItemId (_id → id). */
|
|
137
|
+
itemIdResolver?: (item: unknown) => string | null;
|
|
114
138
|
select?: (data: unknown) => unknown;
|
|
115
139
|
}
|
|
116
|
-
declare function
|
|
140
|
+
declare function useListQuery<T>({
|
|
117
141
|
queryKey,
|
|
118
142
|
queryFn,
|
|
119
143
|
enabled,
|
|
120
144
|
options,
|
|
121
145
|
prefillDetailCache,
|
|
122
146
|
detailKeyBuilder,
|
|
147
|
+
itemIdResolver,
|
|
123
148
|
select
|
|
124
149
|
}: CreateListQueryConfig): ListQueryResult<T>;
|
|
125
150
|
interface CreateDetailQueryConfig {
|
|
@@ -131,7 +156,7 @@ interface CreateDetailQueryConfig {
|
|
|
131
156
|
options?: Record<string, unknown>;
|
|
132
157
|
select?: (data: unknown) => unknown;
|
|
133
158
|
}
|
|
134
|
-
declare function
|
|
159
|
+
declare function useDetailQuery<T>({
|
|
135
160
|
queryKey,
|
|
136
161
|
queryFn,
|
|
137
162
|
enabled,
|
|
@@ -145,8 +170,16 @@ interface InfiniteListQueryOptions {
|
|
|
145
170
|
gcTime?: number;
|
|
146
171
|
refetchOnWindowFocus?: boolean;
|
|
147
172
|
structuralSharing?: boolean;
|
|
173
|
+
refetchInterval?: number | false;
|
|
174
|
+
refetchIntervalInBackground?: boolean;
|
|
148
175
|
_scope?: string;
|
|
149
176
|
request?: RequestPassthrough;
|
|
177
|
+
/**
|
|
178
|
+
* Max pages to keep in memory. Old pages are evicted and re-fetched on scroll-back.
|
|
179
|
+
* Requires `getPreviousPageParam` for backward re-fetching.
|
|
180
|
+
* When unset, all fetched pages are retained (default TanStack Query behavior).
|
|
181
|
+
*/
|
|
182
|
+
maxPages?: number;
|
|
150
183
|
}
|
|
151
184
|
interface InfiniteListQueryResult<T> {
|
|
152
185
|
items: T[];
|
|
@@ -175,16 +208,24 @@ interface CreateInfiniteListQueryConfig {
|
|
|
175
208
|
initialPageParam?: unknown;
|
|
176
209
|
getNextPageParam: (lastPage: unknown) => unknown;
|
|
177
210
|
getPreviousPageParam?: (firstPage: unknown) => unknown;
|
|
211
|
+
/** Max pages to keep in memory. Old pages are evicted when exceeded. */
|
|
212
|
+
maxPages?: number;
|
|
178
213
|
}
|
|
179
|
-
declare function
|
|
214
|
+
declare function useInfiniteListQuery<T>({
|
|
180
215
|
queryKey,
|
|
181
216
|
queryFn,
|
|
182
217
|
enabled,
|
|
183
218
|
options,
|
|
184
219
|
initialPageParam,
|
|
185
220
|
getNextPageParam,
|
|
186
|
-
getPreviousPageParam
|
|
221
|
+
getPreviousPageParam,
|
|
222
|
+
maxPages
|
|
187
223
|
}: CreateInfiniteListQueryConfig): InfiniteListQueryResult<T>;
|
|
224
|
+
/** @deprecated Use `useListQuery` */
|
|
225
|
+
declare const createListQuery: typeof useListQuery;
|
|
226
|
+
/** @deprecated Use `useDetailQuery` */
|
|
227
|
+
declare const createDetailQuery: typeof useDetailQuery;
|
|
228
|
+
/** @deprecated Use `useInfiniteListQuery` */
|
|
229
|
+
declare const createInfiniteListQuery: typeof useInfiniteListQuery;
|
|
188
230
|
//#endregion
|
|
189
|
-
export { CacheUtils, CreateDetailQueryConfig, CreateInfiniteListQueryConfig, CreateListQueryConfig, DEFAULT_QUERY_CONFIG, DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, PaginationData, QueryKeys, RequestPassthrough, createCacheUtils, createDetailQuery, createInfiniteListQuery, createListQuery, createQueryKeys, getItemId, updateListCache };
|
|
190
|
-
//# sourceMappingURL=query.d.ts.map
|
|
231
|
+
export { CacheUtils, CreateDetailQueryConfig, CreateInfiniteListQueryConfig, CreateListQueryConfig, DEFAULT_QUERY_CONFIG, DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, PaginationData, QUERY_CONFIGS, QueryKeys, RequestPassthrough, createCacheUtils, createDetailQuery, createInfiniteListQuery, createListQuery, createQueryKeys, extractItem, getItemId, updateListCache, useDetailQuery, useInfiniteListQuery, useListQuery };
|
package/dist/query.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { keepPreviousData, useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
-
import { useEffect } from "react";
|
|
4
|
+
import { useEffect, useMemo } from "react";
|
|
5
5
|
|
|
6
6
|
//#region src/query.ts
|
|
7
7
|
const DEFAULT_QUERY_CONFIG = {
|
|
@@ -10,6 +10,16 @@ const DEFAULT_QUERY_CONFIG = {
|
|
|
10
10
|
refetchOnWindowFocus: false,
|
|
11
11
|
retry: 0
|
|
12
12
|
};
|
|
13
|
+
/** Pre-built query config presets for common data freshness patterns. */
|
|
14
|
+
const QUERY_CONFIGS = {
|
|
15
|
+
realtime: {
|
|
16
|
+
staleTime: 2e4,
|
|
17
|
+
refetchInterval: 3e4
|
|
18
|
+
},
|
|
19
|
+
frequent: { staleTime: 6e4 },
|
|
20
|
+
stable: { staleTime: 3e5 },
|
|
21
|
+
static: { staleTime: 6e5 }
|
|
22
|
+
};
|
|
13
23
|
function getItemId(item) {
|
|
14
24
|
if (!item || typeof item !== "object") return null;
|
|
15
25
|
const obj = item;
|
|
@@ -19,38 +29,68 @@ function getItemId(item) {
|
|
|
19
29
|
function normalizePagination(data) {
|
|
20
30
|
if (!data || typeof data !== "object") return null;
|
|
21
31
|
const d = data;
|
|
32
|
+
const method = d.method ?? null;
|
|
33
|
+
const isKeyset = method === "keyset" || d.hasMore != null && d.total == null && d.pages == null;
|
|
22
34
|
const hasTotal = d.total != null || d.totalDocs != null;
|
|
23
35
|
const hasPages = d.pages != null || d.totalPages != null;
|
|
24
|
-
if (!hasTotal && !hasPages) return null;
|
|
36
|
+
if (!hasTotal && !hasPages && !isKeyset) return null;
|
|
25
37
|
return {
|
|
38
|
+
method,
|
|
26
39
|
total: Number(d.total ?? d.totalDocs ?? 0),
|
|
27
|
-
pages: Number(d.pages ?? d.totalPages ?? 1),
|
|
28
|
-
page: Number(d.page ?? d.currentPage ?? 1),
|
|
40
|
+
pages: Number(d.pages ?? d.totalPages ?? (isKeyset ? 0 : 1)),
|
|
41
|
+
page: Number(d.page ?? d.currentPage ?? (isKeyset ? 0 : 1)),
|
|
29
42
|
limit: Number(d.limit ?? 10),
|
|
30
43
|
hasNext: Boolean(d.hasNext ?? d.hasNextPage ?? d.hasMore ?? false),
|
|
31
|
-
hasPrev: Boolean(d.hasPrev ?? d.hasPrevPage ?? false)
|
|
44
|
+
hasPrev: Boolean(d.hasPrev ?? d.hasPrevPage ?? false),
|
|
45
|
+
...isKeyset ? { next: d.next ?? null } : {}
|
|
32
46
|
};
|
|
33
47
|
}
|
|
48
|
+
/** Well-known keys checked in order for list responses. */
|
|
49
|
+
const LIST_KEYS = [
|
|
50
|
+
"docs",
|
|
51
|
+
"data",
|
|
52
|
+
"items",
|
|
53
|
+
"results"
|
|
54
|
+
];
|
|
55
|
+
/** Well-known keys checked in order for detail responses. */
|
|
56
|
+
const DETAIL_KEYS = [
|
|
57
|
+
"data",
|
|
58
|
+
"doc",
|
|
59
|
+
"item",
|
|
60
|
+
"result"
|
|
61
|
+
];
|
|
34
62
|
function extractItems(data) {
|
|
35
63
|
if (!data) return [];
|
|
36
64
|
if (Array.isArray(data)) return data;
|
|
37
65
|
if (typeof data !== "object") return [];
|
|
38
66
|
const d = data;
|
|
39
|
-
const
|
|
40
|
-
|
|
67
|
+
for (const key of LIST_KEYS) if (Array.isArray(d[key])) return d[key];
|
|
68
|
+
for (const value of Object.values(d)) if (Array.isArray(value)) return value;
|
|
69
|
+
return [];
|
|
41
70
|
}
|
|
42
71
|
function extractItem(data) {
|
|
43
|
-
if (
|
|
44
|
-
if (typeof data !== "object") return
|
|
72
|
+
if (data == null) return null;
|
|
73
|
+
if (typeof data !== "object") return data;
|
|
45
74
|
const d = data;
|
|
46
|
-
|
|
75
|
+
for (const key of DETAIL_KEYS) if (d[key] != null) return d[key];
|
|
76
|
+
return d;
|
|
47
77
|
}
|
|
48
78
|
function updateListCache(listData, updater) {
|
|
49
79
|
if (!listData) return listData;
|
|
50
80
|
if (Array.isArray(listData)) return updater(listData);
|
|
51
81
|
if (typeof listData !== "object") return listData;
|
|
52
82
|
const d = listData;
|
|
53
|
-
|
|
83
|
+
let arrayField = null;
|
|
84
|
+
for (const key of LIST_KEYS) if (Array.isArray(d[key])) {
|
|
85
|
+
arrayField = key;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (!arrayField) {
|
|
89
|
+
for (const [key, value] of Object.entries(d)) if (Array.isArray(value)) {
|
|
90
|
+
arrayField = key;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
54
94
|
if (!arrayField) return listData;
|
|
55
95
|
const updated = updater(d[arrayField]);
|
|
56
96
|
const original = d[arrayField];
|
|
@@ -107,7 +147,7 @@ function createCacheUtils(KEYS) {
|
|
|
107
147
|
removeDetail: (client, id) => client.removeQueries({ queryKey: KEYS.detail(id) })
|
|
108
148
|
};
|
|
109
149
|
}
|
|
110
|
-
function
|
|
150
|
+
function useListQuery({ queryKey, queryFn, enabled = true, options = {}, prefillDetailCache = true, detailKeyBuilder, itemIdResolver, select }) {
|
|
111
151
|
const queryClient = useQueryClient();
|
|
112
152
|
const query = useQuery({
|
|
113
153
|
queryKey,
|
|
@@ -118,12 +158,13 @@ function createListQuery({ queryKey, queryFn, enabled = true, options = {}, pref
|
|
|
118
158
|
...select ? { select } : {},
|
|
119
159
|
placeholderData: keepPreviousData
|
|
120
160
|
});
|
|
121
|
-
const items = extractItems(query.data);
|
|
122
|
-
const pagination = normalizePagination(query.data);
|
|
161
|
+
const items = useMemo(() => extractItems(query.data), [query.data]);
|
|
162
|
+
const pagination = useMemo(() => normalizePagination(query.data), [query.data]);
|
|
123
163
|
useEffect(() => {
|
|
124
164
|
if (!prefillDetailCache || !detailKeyBuilder || items.length === 0) return;
|
|
165
|
+
const resolveId = itemIdResolver ?? getItemId;
|
|
125
166
|
items.forEach((item) => {
|
|
126
|
-
const id =
|
|
167
|
+
const id = resolveId(item);
|
|
127
168
|
if (id) queryClient.setQueryData(detailKeyBuilder(id), { data: item });
|
|
128
169
|
});
|
|
129
170
|
}, [
|
|
@@ -145,7 +186,7 @@ function createListQuery({ queryKey, queryFn, enabled = true, options = {}, pref
|
|
|
145
186
|
data: query.data
|
|
146
187
|
};
|
|
147
188
|
}
|
|
148
|
-
function
|
|
189
|
+
function useDetailQuery({ queryKey, queryFn, enabled = true, options = {}, select }) {
|
|
149
190
|
const query = useQuery({
|
|
150
191
|
queryKey,
|
|
151
192
|
queryFn: ({ signal }) => queryFn({ signal }),
|
|
@@ -166,7 +207,7 @@ function createDetailQuery({ queryKey, queryFn, enabled = true, options = {}, se
|
|
|
166
207
|
data: query.data
|
|
167
208
|
};
|
|
168
209
|
}
|
|
169
|
-
function
|
|
210
|
+
function useInfiniteListQuery({ queryKey, queryFn, enabled = true, options = {}, initialPageParam = 1, getNextPageParam, getPreviousPageParam, maxPages }) {
|
|
170
211
|
const query = useInfiniteQuery({
|
|
171
212
|
queryKey,
|
|
172
213
|
queryFn: ({ pageParam, signal }) => queryFn({
|
|
@@ -177,11 +218,12 @@ function createInfiniteListQuery({ queryKey, queryFn, enabled = true, options =
|
|
|
177
218
|
initialPageParam,
|
|
178
219
|
getNextPageParam,
|
|
179
220
|
getPreviousPageParam,
|
|
221
|
+
...maxPages != null ? { maxPages } : {},
|
|
180
222
|
...DEFAULT_QUERY_CONFIG,
|
|
181
223
|
...options
|
|
182
224
|
});
|
|
183
225
|
return {
|
|
184
|
-
items: query.data?.pages.flatMap((page) => extractItems(page)) ?? [],
|
|
226
|
+
items: useMemo(() => query.data?.pages.flatMap((page) => extractItems(page)) ?? [], [query.data]),
|
|
185
227
|
hasNextPage: query.hasNextPage,
|
|
186
228
|
hasPreviousPage: query.hasPreviousPage,
|
|
187
229
|
isFetchingNextPage: query.isFetchingNextPage,
|
|
@@ -197,7 +239,12 @@ function createInfiniteListQuery({ queryKey, queryFn, enabled = true, options =
|
|
|
197
239
|
data: query.data
|
|
198
240
|
};
|
|
199
241
|
}
|
|
242
|
+
/** @deprecated Use `useListQuery` */
|
|
243
|
+
const createListQuery = useListQuery;
|
|
244
|
+
/** @deprecated Use `useDetailQuery` */
|
|
245
|
+
const createDetailQuery = useDetailQuery;
|
|
246
|
+
/** @deprecated Use `useInfiniteListQuery` */
|
|
247
|
+
const createInfiniteListQuery = useInfiniteListQuery;
|
|
200
248
|
|
|
201
249
|
//#endregion
|
|
202
|
-
export { DEFAULT_QUERY_CONFIG, createCacheUtils, createDetailQuery, createInfiniteListQuery, createListQuery, createQueryKeys, getItemId, updateListCache };
|
|
203
|
-
//# sourceMappingURL=query.js.map
|
|
250
|
+
export { DEFAULT_QUERY_CONFIG, QUERY_CONFIGS, createCacheUtils, createDetailQuery, createInfiniteListQuery, createListQuery, createQueryKeys, extractItem, getItemId, updateListCache, useDetailQuery, useInfiniteListQuery, useListQuery };
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { QueryKey } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
//#region src/sse.d.ts
|
|
4
|
+
interface ArcServerEvent {
|
|
5
|
+
type: string;
|
|
6
|
+
resource: string;
|
|
7
|
+
data: unknown;
|
|
8
|
+
timestamp: string;
|
|
9
|
+
id?: string;
|
|
10
|
+
}
|
|
11
|
+
interface EventStreamOptions {
|
|
12
|
+
/** SSE endpoint URL (absolute or relative to baseUrl). Default: `/{basePath}/{resource}/events/stream` */
|
|
13
|
+
url?: string;
|
|
14
|
+
/** Resource name used for the default endpoint path and event filtering. */
|
|
15
|
+
resource?: string;
|
|
16
|
+
/** Base path for the events endpoint. Default: '/api/v1' */
|
|
17
|
+
basePath?: string;
|
|
18
|
+
/** Event patterns to listen for (e.g., ['agents.created', 'agents.updated']). When empty, all events are received. */
|
|
19
|
+
patterns?: string[];
|
|
20
|
+
/** Query keys to invalidate when any event is received. */
|
|
21
|
+
invalidateQueries?: QueryKey[];
|
|
22
|
+
/** Callback for each event. */
|
|
23
|
+
onEvent?: (event: ArcServerEvent) => void;
|
|
24
|
+
/** Callback for connection state changes. */
|
|
25
|
+
onConnectionChange?: (connected: boolean) => void;
|
|
26
|
+
/** Whether the stream is enabled. Default: true */
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
/** Reconnect delay in ms. Default: 3000 */
|
|
29
|
+
reconnectDelay?: number;
|
|
30
|
+
/** Maximum reconnect attempts before giving up. Default: Infinity */
|
|
31
|
+
maxReconnectAttempts?: number;
|
|
32
|
+
/** Whether to include credentials (cookies). Derived from authMode when not set. */
|
|
33
|
+
withCredentials?: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface EventStreamResult {
|
|
36
|
+
/** Whether the EventSource is currently connected. */
|
|
37
|
+
isConnected: boolean;
|
|
38
|
+
/** The most recently received event. */
|
|
39
|
+
lastEvent: ArcServerEvent | null;
|
|
40
|
+
/** Number of events received since connection. */
|
|
41
|
+
eventCount: number;
|
|
42
|
+
/** Close the connection manually. */
|
|
43
|
+
close: () => void;
|
|
44
|
+
/** Reconnect after a manual close. */
|
|
45
|
+
reconnect: () => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Subscribe to Arc server-sent events for real-time cache invalidation.
|
|
49
|
+
*
|
|
50
|
+
* Uses the browser's native `EventSource` API for automatic reconnection
|
|
51
|
+
* and efficient server-push. Events trigger query invalidation so TanStack Query
|
|
52
|
+
* refetches affected data automatically.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* const { isConnected } = useEventStream({
|
|
56
|
+
* resource: 'agents',
|
|
57
|
+
* invalidateQueries: [agentKeys.lists()],
|
|
58
|
+
* });
|
|
59
|
+
*/
|
|
60
|
+
declare function useEventStream(options: EventStreamOptions): EventStreamResult;
|
|
61
|
+
//#endregion
|
|
62
|
+
export { ArcServerEvent, EventStreamOptions, EventStreamResult, useEventStream };
|