@classytic/arc-next 0.3.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/dist/query.d.ts CHANGED
@@ -103,6 +103,22 @@ declare const DEFAULT_QUERY_CONFIG: {
103
103
  readonly refetchOnWindowFocus: false;
104
104
  readonly retry: 0;
105
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
+ };
106
122
  declare function getItemId(item: unknown): string | null;
107
123
  declare function extractItem<T>(data: unknown): T | null;
108
124
  declare function updateListCache<T>(listData: unknown, updater: (items: T[]) => T[]): unknown;
@@ -117,6 +133,8 @@ interface CreateListQueryConfig {
117
133
  options?: Record<string, unknown>;
118
134
  prefillDetailCache?: boolean;
119
135
  detailKeyBuilder?: (id: string) => QueryKey;
136
+ /** Custom ID extractor for cache prefill. Falls back to getItemId (_id → id). */
137
+ itemIdResolver?: (item: unknown) => string | null;
120
138
  select?: (data: unknown) => unknown;
121
139
  }
122
140
  declare function useListQuery<T>({
@@ -126,6 +144,7 @@ declare function useListQuery<T>({
126
144
  options,
127
145
  prefillDetailCache,
128
146
  detailKeyBuilder,
147
+ itemIdResolver,
129
148
  select
130
149
  }: CreateListQueryConfig): ListQueryResult<T>;
131
150
  interface CreateDetailQueryConfig {
@@ -155,6 +174,12 @@ interface InfiniteListQueryOptions {
155
174
  refetchIntervalInBackground?: boolean;
156
175
  _scope?: string;
157
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;
158
183
  }
159
184
  interface InfiniteListQueryResult<T> {
160
185
  items: T[];
@@ -183,6 +208,8 @@ interface CreateInfiniteListQueryConfig {
183
208
  initialPageParam?: unknown;
184
209
  getNextPageParam: (lastPage: unknown) => unknown;
185
210
  getPreviousPageParam?: (firstPage: unknown) => unknown;
211
+ /** Max pages to keep in memory. Old pages are evicted when exceeded. */
212
+ maxPages?: number;
186
213
  }
187
214
  declare function useInfiniteListQuery<T>({
188
215
  queryKey,
@@ -191,7 +218,8 @@ declare function useInfiniteListQuery<T>({
191
218
  options,
192
219
  initialPageParam,
193
220
  getNextPageParam,
194
- getPreviousPageParam
221
+ getPreviousPageParam,
222
+ maxPages
195
223
  }: CreateInfiniteListQueryConfig): InfiniteListQueryResult<T>;
196
224
  /** @deprecated Use `useListQuery` */
197
225
  declare const createListQuery: typeof useListQuery;
@@ -200,4 +228,4 @@ declare const createDetailQuery: typeof useDetailQuery;
200
228
  /** @deprecated Use `useInfiniteListQuery` */
201
229
  declare const createInfiniteListQuery: typeof useInfiniteListQuery;
202
230
  //#endregion
203
- export { CacheUtils, CreateDetailQueryConfig, CreateInfiniteListQueryConfig, CreateListQueryConfig, DEFAULT_QUERY_CONFIG, DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, PaginationData, QueryKeys, RequestPassthrough, createCacheUtils, createDetailQuery, createInfiniteListQuery, createListQuery, createQueryKeys, extractItem, getItemId, updateListCache, useDetailQuery, useInfiniteListQuery, useListQuery };
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
@@ -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;
@@ -137,7 +147,7 @@ function createCacheUtils(KEYS) {
137
147
  removeDetail: (client, id) => client.removeQueries({ queryKey: KEYS.detail(id) })
138
148
  };
139
149
  }
140
- function useListQuery({ queryKey, queryFn, enabled = true, options = {}, prefillDetailCache = true, detailKeyBuilder, select }) {
150
+ function useListQuery({ queryKey, queryFn, enabled = true, options = {}, prefillDetailCache = true, detailKeyBuilder, itemIdResolver, select }) {
141
151
  const queryClient = useQueryClient();
142
152
  const query = useQuery({
143
153
  queryKey,
@@ -152,8 +162,9 @@ function useListQuery({ queryKey, queryFn, enabled = true, options = {}, prefill
152
162
  const pagination = useMemo(() => normalizePagination(query.data), [query.data]);
153
163
  useEffect(() => {
154
164
  if (!prefillDetailCache || !detailKeyBuilder || items.length === 0) return;
165
+ const resolveId = itemIdResolver ?? getItemId;
155
166
  items.forEach((item) => {
156
- const id = getItemId(item);
167
+ const id = resolveId(item);
157
168
  if (id) queryClient.setQueryData(detailKeyBuilder(id), { data: item });
158
169
  });
159
170
  }, [
@@ -196,7 +207,7 @@ function useDetailQuery({ queryKey, queryFn, enabled = true, options = {}, selec
196
207
  data: query.data
197
208
  };
198
209
  }
199
- function useInfiniteListQuery({ queryKey, queryFn, enabled = true, options = {}, initialPageParam = 1, getNextPageParam, getPreviousPageParam }) {
210
+ function useInfiniteListQuery({ queryKey, queryFn, enabled = true, options = {}, initialPageParam = 1, getNextPageParam, getPreviousPageParam, maxPages }) {
200
211
  const query = useInfiniteQuery({
201
212
  queryKey,
202
213
  queryFn: ({ pageParam, signal }) => queryFn({
@@ -207,6 +218,7 @@ function useInfiniteListQuery({ queryKey, queryFn, enabled = true, options = {},
207
218
  initialPageParam,
208
219
  getNextPageParam,
209
220
  getPreviousPageParam,
221
+ ...maxPages != null ? { maxPages } : {},
210
222
  ...DEFAULT_QUERY_CONFIG,
211
223
  ...options
212
224
  });
@@ -235,4 +247,4 @@ const createDetailQuery = useDetailQuery;
235
247
  const createInfiniteListQuery = useInfiniteListQuery;
236
248
 
237
249
  //#endregion
238
- export { DEFAULT_QUERY_CONFIG, createCacheUtils, createDetailQuery, createInfiniteListQuery, createListQuery, createQueryKeys, extractItem, getItemId, updateListCache, useDetailQuery, useInfiniteListQuery, useListQuery };
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 };
package/dist/sse.js ADDED
@@ -0,0 +1,144 @@
1
+ "use client";
2
+
3
+ import { getAuthContext, getAuthMode } from "./client.js";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+
7
+ //#region src/sse.ts
8
+ /**
9
+ * Subscribe to Arc server-sent events for real-time cache invalidation.
10
+ *
11
+ * Uses the browser's native `EventSource` API for automatic reconnection
12
+ * and efficient server-push. Events trigger query invalidation so TanStack Query
13
+ * refetches affected data automatically.
14
+ *
15
+ * @example
16
+ * const { isConnected } = useEventStream({
17
+ * resource: 'agents',
18
+ * invalidateQueries: [agentKeys.lists()],
19
+ * });
20
+ */
21
+ function useEventStream(options) {
22
+ const { url, resource, basePath = "/api/v1", enabled = true, reconnectDelay = 3e3, maxReconnectAttempts = Infinity, withCredentials } = options;
23
+ const queryClient = useQueryClient();
24
+ const [isConnected, setIsConnected] = useState(false);
25
+ const [lastEvent, setLastEvent] = useState(null);
26
+ const [eventCount, setEventCount] = useState(0);
27
+ const esRef = useRef(null);
28
+ const reconnectAttemptsRef = useRef(0);
29
+ const reconnectTimerRef = useRef(null);
30
+ const manualCloseRef = useRef(false);
31
+ const onEventRef = useRef(options.onEvent);
32
+ onEventRef.current = options.onEvent;
33
+ const onConnectionChangeRef = useRef(options.onConnectionChange);
34
+ onConnectionChangeRef.current = options.onConnectionChange;
35
+ const patternsRef = useRef(options.patterns ?? []);
36
+ patternsRef.current = options.patterns ?? [];
37
+ const invalidateKeysRef = useRef(options.invalidateQueries ?? []);
38
+ invalidateKeysRef.current = options.invalidateQueries ?? [];
39
+ const buildUrl = useCallback(() => {
40
+ if (url) return url;
41
+ if (!resource) throw new Error("[arc-next] useEventStream requires either `url` or `resource`");
42
+ const auth = getAuthContext();
43
+ const params = new URLSearchParams();
44
+ const patterns = patternsRef.current;
45
+ if (patterns.length > 0) params.set("patterns", patterns.join(","));
46
+ if (auth.organizationId) params.set("organizationId", auth.organizationId);
47
+ if (auth.token) params.set("token", auth.token);
48
+ const qs = params.toString();
49
+ const base = `${basePath}/${resource}/events/stream`;
50
+ return qs ? `${base}?${qs}` : base;
51
+ }, [
52
+ url,
53
+ resource,
54
+ basePath
55
+ ]);
56
+ const connect = useCallback(() => {
57
+ if (esRef.current) esRef.current.close();
58
+ manualCloseRef.current = false;
59
+ const eventUrl = buildUrl();
60
+ const authMode = getAuthMode();
61
+ const es = new EventSource(eventUrl, { withCredentials: withCredentials ?? authMode === "cookie" });
62
+ esRef.current = es;
63
+ es.onopen = () => {
64
+ reconnectAttemptsRef.current = 0;
65
+ setIsConnected(true);
66
+ onConnectionChangeRef.current?.(true);
67
+ };
68
+ es.onmessage = (event) => {
69
+ try {
70
+ const parsed = JSON.parse(event.data);
71
+ const patterns = patternsRef.current;
72
+ if (patterns.length > 0 && !patterns.includes(parsed.type)) return;
73
+ setLastEvent(parsed);
74
+ setEventCount((c) => c + 1);
75
+ onEventRef.current?.(parsed);
76
+ const keys = invalidateKeysRef.current;
77
+ for (const key of keys) queryClient.invalidateQueries({ queryKey: key });
78
+ } catch {}
79
+ };
80
+ es.onerror = () => {
81
+ es.close();
82
+ setIsConnected(false);
83
+ onConnectionChangeRef.current?.(false);
84
+ if (manualCloseRef.current) return;
85
+ if (reconnectAttemptsRef.current < maxReconnectAttempts) {
86
+ reconnectAttemptsRef.current += 1;
87
+ const delay = Math.min(reconnectDelay * Math.pow(1.5, reconnectAttemptsRef.current - 1), 3e4);
88
+ reconnectTimerRef.current = setTimeout(connect, delay);
89
+ }
90
+ };
91
+ }, [
92
+ buildUrl,
93
+ queryClient,
94
+ withCredentials,
95
+ reconnectDelay,
96
+ maxReconnectAttempts
97
+ ]);
98
+ const close = useCallback(() => {
99
+ manualCloseRef.current = true;
100
+ if (reconnectTimerRef.current) {
101
+ clearTimeout(reconnectTimerRef.current);
102
+ reconnectTimerRef.current = null;
103
+ }
104
+ if (esRef.current) {
105
+ esRef.current.close();
106
+ esRef.current = null;
107
+ }
108
+ setIsConnected(false);
109
+ onConnectionChangeRef.current?.(false);
110
+ }, []);
111
+ const reconnect = useCallback(() => {
112
+ reconnectAttemptsRef.current = 0;
113
+ connect();
114
+ }, [connect]);
115
+ useEffect(() => {
116
+ if (!enabled) {
117
+ close();
118
+ return;
119
+ }
120
+ connect();
121
+ return () => {
122
+ manualCloseRef.current = true;
123
+ if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
124
+ if (esRef.current) {
125
+ esRef.current.close();
126
+ esRef.current = null;
127
+ }
128
+ };
129
+ }, [
130
+ enabled,
131
+ connect,
132
+ close
133
+ ]);
134
+ return {
135
+ isConnected,
136
+ lastEvent,
137
+ eventCount,
138
+ close,
139
+ reconnect
140
+ };
141
+ }
142
+
143
+ //#endregion
144
+ export { useEventStream };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc-next",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "React + TanStack Query SDK for Arc resources",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -21,7 +21,11 @@
21
21
  "pagination",
22
22
  "multi-tenant",
23
23
  "arc",
24
- "mongokit"
24
+ "mongokit",
25
+ "sse",
26
+ "real-time",
27
+ "soft-delete",
28
+ "bulk-operations"
25
29
  ],
26
30
  "main": "./dist/hooks.js",
27
31
  "types": "./dist/hooks.d.ts",
@@ -57,7 +61,12 @@
57
61
  "./prefetch": {
58
62
  "types": "./dist/prefetch.d.ts",
59
63
  "default": "./dist/prefetch.js"
60
- }
64
+ },
65
+ "./sse": {
66
+ "types": "./dist/sse.d.ts",
67
+ "default": "./dist/sse.js"
68
+ },
69
+ "./package.json": "./package.json"
61
70
  },
62
71
  "files": [
63
72
  "dist"
@@ -78,20 +87,20 @@
78
87
  "prepublishOnly": "npm run typecheck && npm test && npm run build"
79
88
  },
80
89
  "peerDependencies": {
81
- "@tanstack/react-query": "^5.0.0",
82
- "react": "^19.0.0"
90
+ "@tanstack/react-query": ">=5.0.0",
91
+ "react": ">=19.0.0"
83
92
  },
84
93
  "devDependencies": {
85
- "@tanstack/react-query": "^5.95.2",
94
+ "@tanstack/react-query": "^5.97.0",
86
95
  "@testing-library/jest-dom": "^6.9.1",
87
96
  "@testing-library/react": "^16.3.2",
88
- "@types/react": "^19.0.0",
97
+ "@types/react": "^19.2.14",
89
98
  "@types/react-dom": "^19.2.3",
90
- "jsdom": "^28.1.0",
91
- "react": "^19.0.0",
92
- "react-dom": "^19.2.4",
93
- "tsdown": "^0.21.4",
94
- "typescript": "^5.7.2",
95
- "vitest": "^4.0.18"
99
+ "jsdom": "^29.0.2",
100
+ "react": "^19.2.5",
101
+ "react-dom": "^19.2.5",
102
+ "tsdown": "^0.21.7",
103
+ "typescript": "^6.0.2",
104
+ "vitest": "^4.1.4"
96
105
  }
97
106
  }