@rozenite/tanstack-query-plugin 1.7.0-rc.2 → 1.8.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.
@@ -0,0 +1,403 @@
1
+ import {
2
+ defineAgentToolContract,
3
+ type AgentToolContract,
4
+ type JSONSchema7,
5
+ } from '@rozenite/agent-shared';
6
+ import type { FetchStatus, MutationStatus, QueryStatus } from '@tanstack/react-query';
7
+ import type { applyTanStackQueryDevtoolsAction } from '../react-native/devtools-actions';
8
+
9
+ export const TANSTACK_QUERY_AGENT_PLUGIN_ID = '@rozenite/tanstack-query-plugin';
10
+
11
+ export type TanStackQueryAgentPaginationInput = {
12
+ limit?: number;
13
+ cursor?: string;
14
+ };
15
+
16
+ export type TanStackQueryAgentQueryHashInput = {
17
+ queryHash: string;
18
+ };
19
+
20
+ export type TanStackQueryAgentQueryToggleInput =
21
+ TanStackQueryAgentQueryHashInput & {
22
+ enabled: boolean;
23
+ };
24
+
25
+ export type TanStackQueryAgentMutationIdInput = {
26
+ mutationId: number;
27
+ };
28
+
29
+ export type TanStackQueryAgentOnlineStatusInput = {
30
+ online: boolean;
31
+ };
32
+
33
+ export type TanStackQueryAgentSafeValue =
34
+ | null
35
+ | boolean
36
+ | number
37
+ | string
38
+ | TanStackQueryAgentSafeValue[]
39
+ | { [key: string]: TanStackQueryAgentSafeValue };
40
+
41
+ export type TanStackQueryCacheSummary = {
42
+ online: boolean;
43
+ queries: {
44
+ total: number;
45
+ active: number;
46
+ fetching: number;
47
+ pending: number;
48
+ success: number;
49
+ error: number;
50
+ invalidated: number;
51
+ };
52
+ mutations: {
53
+ total: number;
54
+ pending: number;
55
+ success: number;
56
+ error: number;
57
+ paused: number;
58
+ };
59
+ };
60
+
61
+ export type TanStackQueryPage = {
62
+ limit: number;
63
+ hasMore: boolean;
64
+ nextCursor?: string;
65
+ };
66
+
67
+ export type TanStackQueryQuerySummary = {
68
+ queryHash: string;
69
+ queryKey: TanStackQueryAgentSafeValue;
70
+ status: QueryStatus;
71
+ fetchStatus: FetchStatus;
72
+ observersCount: number;
73
+ isInvalidated: boolean;
74
+ dataUpdatedAt: number;
75
+ errorUpdatedAt: number;
76
+ hasData: boolean;
77
+ hasError: boolean;
78
+ };
79
+
80
+ export type TanStackQueryObserverOptionsSummary = {
81
+ enabled: TanStackQueryAgentSafeValue;
82
+ networkMode: TanStackQueryAgentSafeValue;
83
+ staleTime: TanStackQueryAgentSafeValue;
84
+ gcTime: TanStackQueryAgentSafeValue;
85
+ retry: TanStackQueryAgentSafeValue;
86
+ retryDelay: TanStackQueryAgentSafeValue;
87
+ refetchInterval: TanStackQueryAgentSafeValue;
88
+ refetchOnMount: TanStackQueryAgentSafeValue;
89
+ refetchOnReconnect: TanStackQueryAgentSafeValue;
90
+ refetchOnWindowFocus: TanStackQueryAgentSafeValue;
91
+ subscribed: TanStackQueryAgentSafeValue;
92
+ meta: TanStackQueryAgentSafeValue;
93
+ hasQueryFn: boolean;
94
+ hasSelect: boolean;
95
+ hasPlaceholderData: boolean;
96
+ hasInitialData: boolean;
97
+ };
98
+
99
+ export type TanStackQueryListQueriesResult = TanStackQueryCacheSummary & {
100
+ total: number;
101
+ items: TanStackQueryQuerySummary[];
102
+ page: TanStackQueryPage;
103
+ };
104
+
105
+ export type TanStackQueryGetQueryDetailsResult = {
106
+ summary: TanStackQueryCacheSummary;
107
+ query: TanStackQueryQuerySummary & {
108
+ data: TanStackQueryAgentSafeValue;
109
+ error: TanStackQueryAgentSafeValue;
110
+ observers: Array<{
111
+ queryHash: string;
112
+ options: TanStackQueryObserverOptionsSummary | null;
113
+ }>;
114
+ };
115
+ };
116
+
117
+ export type TanStackQueryActionResult = Awaited<
118
+ ReturnType<typeof applyTanStackQueryDevtoolsAction>
119
+ >;
120
+
121
+ export type TanStackQueryMutationSummary = {
122
+ mutationId: number;
123
+ mutationKey: TanStackQueryAgentSafeValue;
124
+ status: MutationStatus;
125
+ isPaused: boolean;
126
+ submittedAt: number;
127
+ failureCount: number;
128
+ hasData: boolean;
129
+ hasError: boolean;
130
+ };
131
+
132
+ export type TanStackQueryMutationOptionsSummary = {
133
+ mutationKey: TanStackQueryAgentSafeValue;
134
+ networkMode: TanStackQueryAgentSafeValue;
135
+ gcTime: TanStackQueryAgentSafeValue;
136
+ retry: TanStackQueryAgentSafeValue;
137
+ retryDelay: TanStackQueryAgentSafeValue;
138
+ meta: TanStackQueryAgentSafeValue;
139
+ scope: TanStackQueryAgentSafeValue;
140
+ hasMutationFn: boolean;
141
+ hasOnMutate: boolean;
142
+ hasOnSuccess: boolean;
143
+ hasOnError: boolean;
144
+ hasOnSettled: boolean;
145
+ };
146
+
147
+ export type TanStackQueryListMutationsResult = TanStackQueryCacheSummary & {
148
+ total: number;
149
+ items: TanStackQueryMutationSummary[];
150
+ page: TanStackQueryPage;
151
+ };
152
+
153
+ export type TanStackQueryGetMutationDetailsResult = {
154
+ summary: TanStackQueryCacheSummary;
155
+ mutation: TanStackQueryMutationSummary & {
156
+ variables: TanStackQueryAgentSafeValue;
157
+ data: TanStackQueryAgentSafeValue;
158
+ error: TanStackQueryAgentSafeValue;
159
+ context: TanStackQueryAgentSafeValue;
160
+ failureReason: TanStackQueryAgentSafeValue;
161
+ options: TanStackQueryMutationOptionsSummary;
162
+ };
163
+ };
164
+
165
+ export type TanStackQueryGetCacheSummaryArgs = undefined;
166
+ export type TanStackQueryGetCacheSummaryResult = TanStackQueryCacheSummary;
167
+ export type TanStackQueryGetOnlineStatusArgs = undefined;
168
+ export type TanStackQueryGetOnlineStatusResult = { online: boolean };
169
+ export type TanStackQuerySetOnlineStatusArgs = TanStackQueryAgentOnlineStatusInput;
170
+ export type TanStackQuerySetOnlineStatusResult = { online: boolean };
171
+ export type TanStackQueryListQueriesArgs = TanStackQueryAgentPaginationInput;
172
+ export type TanStackQueryGetQueryDetailsArgs = TanStackQueryAgentQueryHashInput;
173
+ export type TanStackQueryRefetchQueryArgs = TanStackQueryAgentQueryHashInput;
174
+ export type TanStackQuerySetQueryLoadingArgs = TanStackQueryAgentQueryToggleInput;
175
+ export type TanStackQuerySetQueryErrorArgs = TanStackQueryAgentQueryToggleInput;
176
+ export type TanStackQueryInvalidateQueryArgs = TanStackQueryAgentQueryHashInput;
177
+ export type TanStackQueryResetQueryArgs = TanStackQueryAgentQueryHashInput;
178
+ export type TanStackQueryRemoveQueryArgs = TanStackQueryAgentQueryHashInput;
179
+ export type TanStackQueryClearQueryCacheArgs = undefined;
180
+ export type TanStackQueryClearQueryCacheResult = TanStackQueryActionResult;
181
+ export type TanStackQueryListMutationsArgs = TanStackQueryAgentPaginationInput;
182
+ export type TanStackQueryGetMutationDetailsArgs = TanStackQueryAgentMutationIdInput;
183
+ export type TanStackQueryClearMutationCacheArgs = undefined;
184
+ export type TanStackQueryClearMutationCacheResult = TanStackQueryActionResult;
185
+
186
+ const queryActionProperties = {
187
+ queryHash: {
188
+ type: 'string',
189
+ description: 'TanStack Query queryHash identifying the query.',
190
+ },
191
+ } satisfies Record<string, JSONSchema7>;
192
+
193
+ const mutationActionProperties = {
194
+ mutationId: {
195
+ type: 'number',
196
+ description: 'TanStack Query mutationId identifying the mutation.',
197
+ },
198
+ } satisfies Record<string, JSONSchema7>;
199
+
200
+ const paginationProperties = {
201
+ limit: {
202
+ type: 'number',
203
+ description: 'Maximum number of items to return. Defaults to 20. Maximum 100.',
204
+ },
205
+ cursor: {
206
+ type: 'string',
207
+ description: 'Opaque pagination cursor from a previous list call.',
208
+ },
209
+ } satisfies Record<string, JSONSchema7>;
210
+
211
+ const emptyInputSchema: JSONSchema7 = {
212
+ type: 'object',
213
+ properties: {},
214
+ };
215
+
216
+ export const tanstackQueryToolDefinitions = {
217
+ getCacheSummary: defineAgentToolContract<
218
+ TanStackQueryGetCacheSummaryArgs,
219
+ TanStackQueryGetCacheSummaryResult
220
+ >({
221
+ name: 'get-cache-summary',
222
+ description:
223
+ 'Return aggregate TanStack Query cache health and count information.',
224
+ inputSchema: emptyInputSchema,
225
+ }),
226
+ getOnlineStatus: defineAgentToolContract<
227
+ TanStackQueryGetOnlineStatusArgs,
228
+ TanStackQueryGetOnlineStatusResult
229
+ >({
230
+ name: 'get-online-status',
231
+ description: 'Return the current TanStack Query onlineManager status.',
232
+ inputSchema: emptyInputSchema,
233
+ }),
234
+ setOnlineStatus: defineAgentToolContract<
235
+ TanStackQuerySetOnlineStatusArgs,
236
+ TanStackQuerySetOnlineStatusResult
237
+ >({
238
+ name: 'set-online-status',
239
+ description:
240
+ 'Set the TanStack Query onlineManager status for testing offline/online behavior.',
241
+ inputSchema: {
242
+ type: 'object',
243
+ properties: {
244
+ online: {
245
+ type: 'boolean',
246
+ description: 'Whether the TanStack Query onlineManager should be online.',
247
+ },
248
+ },
249
+ required: ['online'],
250
+ },
251
+ }),
252
+ listQueries: defineAgentToolContract<
253
+ TanStackQueryListQueriesArgs,
254
+ TanStackQueryListQueriesResult
255
+ >({
256
+ name: 'list-queries',
257
+ description: 'List TanStack Query query summaries using cursor pagination.',
258
+ inputSchema: {
259
+ type: 'object',
260
+ properties: paginationProperties,
261
+ },
262
+ }),
263
+ getQueryDetails: defineAgentToolContract<
264
+ TanStackQueryGetQueryDetailsArgs,
265
+ TanStackQueryGetQueryDetailsResult
266
+ >({
267
+ name: 'get-query-details',
268
+ description:
269
+ 'Return a JSON-safe TanStack Query query snapshot and observer summary.',
270
+ inputSchema: {
271
+ type: 'object',
272
+ properties: queryActionProperties,
273
+ required: ['queryHash'],
274
+ },
275
+ }),
276
+ refetchQuery: defineAgentToolContract<
277
+ TanStackQueryRefetchQueryArgs,
278
+ TanStackQueryActionResult
279
+ >({
280
+ name: 'refetch-query',
281
+ description: 'Refetch a TanStack Query query by queryHash.',
282
+ inputSchema: {
283
+ type: 'object',
284
+ properties: queryActionProperties,
285
+ required: ['queryHash'],
286
+ },
287
+ }),
288
+ setQueryLoading: defineAgentToolContract<
289
+ TanStackQuerySetQueryLoadingArgs,
290
+ TanStackQueryActionResult
291
+ >({
292
+ name: 'set-query-loading',
293
+ description:
294
+ 'Enable or disable TanStack Query loading-state simulation for a query by queryHash.',
295
+ inputSchema: {
296
+ type: 'object',
297
+ properties: {
298
+ ...queryActionProperties,
299
+ enabled: {
300
+ type: 'boolean',
301
+ description:
302
+ 'Whether the loading-state simulation should be enabled.',
303
+ },
304
+ },
305
+ required: ['queryHash', 'enabled'],
306
+ },
307
+ }),
308
+ setQueryError: defineAgentToolContract<
309
+ TanStackQuerySetQueryErrorArgs,
310
+ TanStackQueryActionResult
311
+ >({
312
+ name: 'set-query-error',
313
+ description:
314
+ 'Enable or disable TanStack Query error-state simulation for a query by queryHash.',
315
+ inputSchema: {
316
+ type: 'object',
317
+ properties: {
318
+ ...queryActionProperties,
319
+ enabled: {
320
+ type: 'boolean',
321
+ description: 'Whether the error-state simulation should be enabled.',
322
+ },
323
+ },
324
+ required: ['queryHash', 'enabled'],
325
+ },
326
+ }),
327
+ invalidateQuery: defineAgentToolContract<
328
+ TanStackQueryInvalidateQueryArgs,
329
+ TanStackQueryActionResult
330
+ >({
331
+ name: 'invalidate-query',
332
+ description: 'Invalidate a TanStack Query query by queryHash.',
333
+ inputSchema: {
334
+ type: 'object',
335
+ properties: queryActionProperties,
336
+ required: ['queryHash'],
337
+ },
338
+ }),
339
+ resetQuery: defineAgentToolContract<
340
+ TanStackQueryResetQueryArgs,
341
+ TanStackQueryActionResult
342
+ >({
343
+ name: 'reset-query',
344
+ description: 'Reset a TanStack Query query by queryHash.',
345
+ inputSchema: {
346
+ type: 'object',
347
+ properties: queryActionProperties,
348
+ required: ['queryHash'],
349
+ },
350
+ }),
351
+ removeQuery: defineAgentToolContract<
352
+ TanStackQueryRemoveQueryArgs,
353
+ TanStackQueryActionResult
354
+ >({
355
+ name: 'remove-query',
356
+ description: 'Remove a TanStack Query query from the cache by queryHash.',
357
+ inputSchema: {
358
+ type: 'object',
359
+ properties: queryActionProperties,
360
+ required: ['queryHash'],
361
+ },
362
+ }),
363
+ clearQueryCache: defineAgentToolContract<
364
+ TanStackQueryClearQueryCacheArgs,
365
+ TanStackQueryClearQueryCacheResult
366
+ >({
367
+ name: 'clear-query-cache',
368
+ description: 'Clear the full TanStack Query query cache.',
369
+ inputSchema: emptyInputSchema,
370
+ }),
371
+ listMutations: defineAgentToolContract<
372
+ TanStackQueryListMutationsArgs,
373
+ TanStackQueryListMutationsResult
374
+ >({
375
+ name: 'list-mutations',
376
+ description: 'List TanStack Query mutation summaries using cursor pagination.',
377
+ inputSchema: {
378
+ type: 'object',
379
+ properties: paginationProperties,
380
+ },
381
+ }),
382
+ getMutationDetails: defineAgentToolContract<
383
+ TanStackQueryGetMutationDetailsArgs,
384
+ TanStackQueryGetMutationDetailsResult
385
+ >({
386
+ name: 'get-mutation-details',
387
+ description:
388
+ 'Return a JSON-safe TanStack Query mutation snapshot for a mutationId.',
389
+ inputSchema: {
390
+ type: 'object',
391
+ properties: mutationActionProperties,
392
+ required: ['mutationId'],
393
+ },
394
+ }),
395
+ clearMutationCache: defineAgentToolContract<
396
+ TanStackQueryClearMutationCacheArgs,
397
+ TanStackQueryClearMutationCacheResult
398
+ >({
399
+ name: 'clear-mutation-cache',
400
+ description: 'Clear the full TanStack Query mutation cache.',
401
+ inputSchema: emptyInputSchema,
402
+ }),
403
+ } as const satisfies Record<string, AgentToolContract<unknown, unknown>>;
@@ -14,6 +14,7 @@ import type {
14
14
  SerializableObserver,
15
15
  PartialQueryState,
16
16
  } from './types';
17
+ import { applyRemoteQueryState, instrumentQuery } from './query-data-sync';
17
18
 
18
19
  const mockQueryFn = () => {
19
20
  return Promise.resolve(null);
@@ -22,7 +23,7 @@ const mockQueryFn = () => {
22
23
  const hydrateObservers = (
23
24
  client: QueryClient,
24
25
  queryHash: string,
25
- dehydratedObservers: SerializableObserver[]
26
+ dehydratedObservers: SerializableObserver[],
26
27
  ) => {
27
28
  const query = client.getQueryCache().get(queryHash);
28
29
 
@@ -50,7 +51,7 @@ const hydrateObservers = (
50
51
 
51
52
  const observer = new QueryObserver(
52
53
  client,
53
- hydratedOptions as QueryObserverOptions
54
+ hydratedOptions as QueryObserverOptions,
54
55
  );
55
56
  query.addObserver(observer);
56
57
  });
@@ -58,7 +59,7 @@ const hydrateObservers = (
58
59
 
59
60
  const hydrateMutation = (
60
61
  client: QueryClient,
61
- dehydratedMutation: SerializableMutation
62
+ dehydratedMutation: SerializableMutation,
62
63
  ) => {
63
64
  const mutationCache = client.getMutationCache();
64
65
  const { options, state } = dehydratedMutation;
@@ -79,7 +80,7 @@ const hydrateMutation = (
79
80
 
80
81
  const hydrateQuery = (
81
82
  client: QueryClient,
82
- dehydratedQuery: SerializableQuery
83
+ dehydratedQuery: SerializableQuery,
83
84
  ) => {
84
85
  const queryCache = client.getQueryCache();
85
86
  const { queryKey, state, queryHash, observers } = dehydratedQuery;
@@ -94,7 +95,7 @@ const hydrateQuery = (
94
95
  query.state.dataUpdatedAt < state.dataUpdatedAt ||
95
96
  query.state.fetchStatus !== state.fetchStatus
96
97
  ) {
97
- query.setState({
98
+ applyRemoteQueryState(query, {
98
99
  ...hydratedState,
99
100
  data,
100
101
  });
@@ -117,20 +118,22 @@ const hydrateQuery = (
117
118
  {
118
119
  ...hydratedState,
119
120
  data,
120
- }
121
+ },
121
122
  );
122
123
  }
123
124
 
125
+ instrumentQuery(query);
126
+
124
127
  hydrateObservers(client, queryHash, observers);
125
128
  };
126
129
 
127
130
  export const hydrateQueryClient = (
128
131
  client: QueryClient,
129
- dehydratedState: SerializableQueryClient
132
+ dehydratedState: SerializableQueryClient,
130
133
  ): void => {
131
134
  // Hydrate mutations
132
135
  dehydratedState.mutations.forEach((mutation) =>
133
- hydrateMutation(client, mutation)
136
+ hydrateMutation(client, mutation),
134
137
  );
135
138
 
136
139
  // Hydrate queries
@@ -148,7 +151,7 @@ export const applyQueryEvent = (
148
151
  | 'observerResultsUpdated'
149
152
  | 'observerOptionsUpdated',
150
153
  data: SerializableQuery | PartialQueryState,
151
- action?: string
154
+ action?: string,
152
155
  ): void => {
153
156
  const queryCache = queryClient.getQueryCache();
154
157
 
@@ -186,7 +189,7 @@ export const applyQueryEvent = (
186
189
  // New function to handle action-based partial state updates
187
190
  export const applyPartialQueryState = (
188
191
  queryClient: QueryClient,
189
- data: PartialQueryState
192
+ data: PartialQueryState,
190
193
  ): void => {
191
194
  const queryCache = queryClient.getQueryCache();
192
195
  const query = queryCache.get(data.queryHash);
@@ -199,7 +202,7 @@ export const applyPartialQueryState = (
199
202
 
200
203
  // Apply the partial state changes
201
204
  if (data.state) {
202
- query.setState(data.state);
205
+ applyRemoteQueryState(query, data.state);
203
206
  }
204
207
  };
205
208
 
@@ -213,7 +216,7 @@ export const applyMutationEvent = (
213
216
  | 'observerRemoved'
214
217
  | 'observerResultsUpdated'
215
218
  | 'observerOptionsUpdated',
216
- data: SerializableMutation
219
+ data: SerializableMutation,
217
220
  ): void => {
218
221
  const mutationCache = queryClient.getMutationCache();
219
222
 
@@ -244,7 +247,7 @@ export const applyQueryObserverEvent = (
244
247
  data: {
245
248
  queryHash: string;
246
249
  observers: SerializableObserver[];
247
- }
250
+ },
248
251
  ): void => {
249
252
  hydrateObservers(queryClient, data.queryHash, data.observers);
250
253
  };
@@ -15,6 +15,9 @@ export type TanStackQueryPluginEventMap = {
15
15
  'devtools-action': {
16
16
  type: DevToolsActionType;
17
17
  queryHash: string;
18
+ metadata?: {
19
+ data?: unknown;
20
+ };
18
21
  };
19
22
  'request-initial-data': unknown;
20
23
  'sync-data': {
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { QueryClient } from '@tanstack/react-query';
3
+ import {
4
+ applyRemoteQueryState,
5
+ instrumentQuery,
6
+ instrumentQueryClient,
7
+ } from './query-data-sync';
8
+
9
+ const createQueryClient = () =>
10
+ new QueryClient({
11
+ defaultOptions: {
12
+ queries: {
13
+ retry: false,
14
+ gcTime: Infinity,
15
+ },
16
+ },
17
+ });
18
+
19
+ describe('query data sync instrumentation', () => {
20
+ it('emits query data updates for data-only setState edits', () => {
21
+ const queryClient = createQueryClient();
22
+ const handler = vi.fn();
23
+
24
+ queryClient.setQueryData(['edited'], { value: 1 });
25
+ const query = queryClient.getQueryCache().find({ queryKey: ['edited'] })!;
26
+
27
+ instrumentQuery(query, handler);
28
+
29
+ query.setState({
30
+ ...query.state,
31
+ data: { value: 2 },
32
+ });
33
+
34
+ expect(handler).toHaveBeenCalledTimes(1);
35
+ expect(handler).toHaveBeenCalledWith(
36
+ expect.objectContaining({
37
+ data: { value: 2 },
38
+ }),
39
+ );
40
+ });
41
+
42
+ it('emits query data updates for setQueryData user edits', () => {
43
+ const queryClient = createQueryClient();
44
+ const handler = vi.fn();
45
+
46
+ instrumentQueryClient(queryClient, handler);
47
+
48
+ queryClient.setQueryData(['edited'], { value: 1 });
49
+
50
+ expect(handler).toHaveBeenCalledTimes(1);
51
+ expect(handler).toHaveBeenCalledWith(
52
+ expect.objectContaining({
53
+ data: { value: 1 },
54
+ }),
55
+ );
56
+ });
57
+
58
+ it('does not emit query data updates for non-data state changes', () => {
59
+ const queryClient = createQueryClient();
60
+ const handler = vi.fn();
61
+
62
+ queryClient.setQueryData(['edited'], { value: 1 });
63
+ const query = queryClient.getQueryCache().find({ queryKey: ['edited'] })!;
64
+
65
+ instrumentQuery(query, handler);
66
+
67
+ query.setState({
68
+ data: undefined,
69
+ status: 'pending',
70
+ fetchStatus: 'fetching',
71
+ });
72
+
73
+ expect(handler).not.toHaveBeenCalled();
74
+ });
75
+
76
+ it('does not emit query data updates for remote state application', () => {
77
+ const queryClient = createQueryClient();
78
+ const handler = vi.fn();
79
+
80
+ queryClient.setQueryData(['edited'], { value: 1 });
81
+ const query = queryClient.getQueryCache().find({ queryKey: ['edited'] })!;
82
+
83
+ instrumentQuery(query, handler);
84
+
85
+ applyRemoteQueryState(query, {
86
+ ...query.state,
87
+ data: { value: 2 },
88
+ });
89
+
90
+ expect(handler).not.toHaveBeenCalled();
91
+ expect(query.state.data).toEqual({ value: 2 });
92
+ });
93
+ });