@rozenite/tanstack-query-plugin 1.7.0-rc.1 → 1.7.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/CHANGELOG.md +5 -11
- package/dist/devtools/assets/{CXEL7IU7-DJlSmt0p.js → CXEL7IU7-OLImOSaA.js} +1 -1
- package/dist/devtools/assets/{tanstack-query-_leiFatc.js → tanstack-query-Dk7i2Fss.js} +12 -12
- package/dist/devtools/tanstack-query.html +1 -1
- package/dist/react-native/chunks/useTanStackQueryDevTools.require.cjs +1 -1
- package/dist/react-native/chunks/useTanStackQueryDevTools.require.js +103 -91
- package/dist/rozenite.json +1 -1
- package/package.json +5 -5
- package/src/react-native/agent/__tests__/tanstack-query-agent.test.ts +65 -27
- package/src/react-native/devtools-actions.ts +17 -3
- package/src/react-native/useHandleDevToolsMessages.ts +14 -11
- package/src/shared/hydrate.ts +16 -13
- package/src/shared/messaging.ts +3 -0
- package/src/shared/query-data-sync.test.ts +93 -0
- package/src/shared/query-data-sync.ts +156 -0
- package/src/shared/types.ts +1 -0
- package/src/ui/tanstack-query.tsx +12 -0
|
@@ -5,9 +5,7 @@ import {
|
|
|
5
5
|
QueryObserver,
|
|
6
6
|
onlineManager,
|
|
7
7
|
} from '@tanstack/react-query';
|
|
8
|
-
import {
|
|
9
|
-
applyTanStackQueryDevtoolsAction,
|
|
10
|
-
} from '../../devtools-actions';
|
|
8
|
+
import { applyTanStackQueryDevtoolsAction } from '../../devtools-actions';
|
|
11
9
|
import {
|
|
12
10
|
createTanStackQueryAgentController,
|
|
13
11
|
serializeForAgent,
|
|
@@ -30,7 +28,7 @@ const createQueryClient = () =>
|
|
|
30
28
|
|
|
31
29
|
const subscribeController = (
|
|
32
30
|
queryClient: QueryClient,
|
|
33
|
-
controller: ReturnType<typeof createTanStackQueryAgentController
|
|
31
|
+
controller: ReturnType<typeof createTanStackQueryAgentController>,
|
|
34
32
|
) => {
|
|
35
33
|
const unsubscribeQuery = queryClient
|
|
36
34
|
.getQueryCache()
|
|
@@ -58,7 +56,7 @@ const createMutation = (
|
|
|
58
56
|
context?: unknown;
|
|
59
57
|
failureCount?: number;
|
|
60
58
|
failureReason?: Error | null;
|
|
61
|
-
}
|
|
59
|
+
},
|
|
62
60
|
) => {
|
|
63
61
|
return queryClient.getMutationCache().build(
|
|
64
62
|
queryClient,
|
|
@@ -78,7 +76,7 @@ const createMutation = (
|
|
|
78
76
|
status: options?.status ?? 'success',
|
|
79
77
|
submittedAt: options?.submittedAt ?? Date.now(),
|
|
80
78
|
variables: options?.variables,
|
|
81
|
-
} as any
|
|
79
|
+
} as any,
|
|
82
80
|
) as Mutation<unknown, Error, unknown, unknown>;
|
|
83
81
|
};
|
|
84
82
|
|
|
@@ -126,8 +124,10 @@ describe('tanstack query agent controller', () => {
|
|
|
126
124
|
controller.listQueries({
|
|
127
125
|
limit: 1,
|
|
128
126
|
cursor: firstPage.page.nextCursor,
|
|
129
|
-
})
|
|
130
|
-
).toThrow(
|
|
127
|
+
}),
|
|
128
|
+
).toThrow(
|
|
129
|
+
'Cursor does not match the requested listing. Run the command again.',
|
|
130
|
+
);
|
|
131
131
|
|
|
132
132
|
unsubscribe();
|
|
133
133
|
});
|
|
@@ -147,8 +147,10 @@ describe('tanstack query agent controller', () => {
|
|
|
147
147
|
controller.listQueries({
|
|
148
148
|
limit: 1,
|
|
149
149
|
cursor: firstPage.page.nextCursor,
|
|
150
|
-
})
|
|
151
|
-
).toThrow(
|
|
150
|
+
}),
|
|
151
|
+
).toThrow(
|
|
152
|
+
'Cursor does not match the requested listing. Run the command again.',
|
|
153
|
+
);
|
|
152
154
|
|
|
153
155
|
queryClient.setQueryData(['third'], { value: 3 });
|
|
154
156
|
queryClient.setQueryData(['fourth'], { value: 4 });
|
|
@@ -159,8 +161,10 @@ describe('tanstack query agent controller', () => {
|
|
|
159
161
|
controller.listQueries({
|
|
160
162
|
limit: 1,
|
|
161
163
|
cursor: secondPageSeed.page.nextCursor,
|
|
162
|
-
})
|
|
163
|
-
).toThrow(
|
|
164
|
+
}),
|
|
165
|
+
).toThrow(
|
|
166
|
+
'Cursor does not match the requested listing. Run the command again.',
|
|
167
|
+
);
|
|
164
168
|
|
|
165
169
|
unsubscribe();
|
|
166
170
|
});
|
|
@@ -194,8 +198,10 @@ describe('tanstack query agent controller', () => {
|
|
|
194
198
|
controller.listMutations({
|
|
195
199
|
limit: 1,
|
|
196
200
|
cursor: firstPage.page.nextCursor,
|
|
197
|
-
})
|
|
198
|
-
).toThrow(
|
|
201
|
+
}),
|
|
202
|
+
).toThrow(
|
|
203
|
+
'Cursor does not match the requested listing. Run the command again.',
|
|
204
|
+
);
|
|
199
205
|
|
|
200
206
|
const mutation = queryClient.getMutationCache().getAll()[0];
|
|
201
207
|
queryClient.getMutationCache().remove(mutation);
|
|
@@ -207,8 +213,10 @@ describe('tanstack query agent controller', () => {
|
|
|
207
213
|
controller.listMutations({
|
|
208
214
|
limit: 1,
|
|
209
215
|
cursor: secondPageSeed.page.nextCursor,
|
|
210
|
-
})
|
|
211
|
-
).toThrow(
|
|
216
|
+
}),
|
|
217
|
+
).toThrow(
|
|
218
|
+
'Cursor does not match the requested listing. Run the command again.',
|
|
219
|
+
);
|
|
212
220
|
|
|
213
221
|
unsubscribe();
|
|
214
222
|
});
|
|
@@ -234,8 +242,9 @@ describe('tanstack query agent controller', () => {
|
|
|
234
242
|
queryFn,
|
|
235
243
|
});
|
|
236
244
|
|
|
237
|
-
const queryHash = queryClient
|
|
238
|
-
.
|
|
245
|
+
const queryHash = queryClient
|
|
246
|
+
.getQueryCache()
|
|
247
|
+
.find({ queryKey: ['todos', 1] })!.queryHash;
|
|
239
248
|
const details = controller.getQueryDetails({ queryHash });
|
|
240
249
|
|
|
241
250
|
expect(details.query).toMatchObject({
|
|
@@ -299,9 +308,9 @@ describe('tanstack query agent controller', () => {
|
|
|
299
308
|
},
|
|
300
309
|
});
|
|
301
310
|
|
|
302
|
-
expect(() =>
|
|
303
|
-
|
|
304
|
-
)
|
|
311
|
+
expect(() => controller.getMutationDetails({ mutationId: 9999 })).toThrow(
|
|
312
|
+
'Unknown mutationId "9999"',
|
|
313
|
+
);
|
|
305
314
|
|
|
306
315
|
unsubscribe();
|
|
307
316
|
});
|
|
@@ -310,9 +319,9 @@ describe('tanstack query agent controller', () => {
|
|
|
310
319
|
const queryClient = createQueryClient();
|
|
311
320
|
const controller = createTanStackQueryAgentController(queryClient);
|
|
312
321
|
|
|
313
|
-
expect(() =>
|
|
314
|
-
|
|
315
|
-
)
|
|
322
|
+
expect(() => controller.getQueryDetails({ queryHash: 'missing' })).toThrow(
|
|
323
|
+
'Unknown queryHash "missing"',
|
|
324
|
+
);
|
|
316
325
|
});
|
|
317
326
|
|
|
318
327
|
it('gets and sets the online manager status', () => {
|
|
@@ -461,6 +470,37 @@ describe('tanstack query agent actions', () => {
|
|
|
461
470
|
});
|
|
462
471
|
});
|
|
463
472
|
|
|
473
|
+
it('sets query data through the shared UI dispatcher', async () => {
|
|
474
|
+
const queryClient = createQueryClient();
|
|
475
|
+
|
|
476
|
+
await queryClient.fetchQuery({
|
|
477
|
+
queryKey: ['editable-query'],
|
|
478
|
+
queryFn: async () => ({ value: 'initial' }),
|
|
479
|
+
initialData: { value: 'initial' },
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const query = queryClient.getQueryCache().find({
|
|
483
|
+
queryKey: ['editable-query'],
|
|
484
|
+
})!;
|
|
485
|
+
|
|
486
|
+
const result = await applyTanStackQueryDevtoolsAction(queryClient, {
|
|
487
|
+
type: 'SET_QUERY_DATA',
|
|
488
|
+
queryHash: query.queryHash,
|
|
489
|
+
metadata: {
|
|
490
|
+
data: { value: 'changed' },
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
expect(result).toMatchObject({
|
|
495
|
+
applied: true,
|
|
496
|
+
action: 'SET_QUERY_DATA',
|
|
497
|
+
queryHash: query.queryHash,
|
|
498
|
+
});
|
|
499
|
+
expect(queryClient.getQueryData(['editable-query'])).toEqual({
|
|
500
|
+
value: 'changed',
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
464
504
|
it('clears mutation cache through the shared UI/agent dispatcher', async () => {
|
|
465
505
|
const queryClient = createQueryClient();
|
|
466
506
|
createMutation(queryClient, {
|
|
@@ -501,8 +541,6 @@ describe('serializeForAgent', () => {
|
|
|
501
541
|
ok: true,
|
|
502
542
|
self: '[circular]',
|
|
503
543
|
});
|
|
504
|
-
expect(serializeForAgent(new Example())).toBe(
|
|
505
|
-
'[non-serializable:Example]'
|
|
506
|
-
);
|
|
544
|
+
expect(serializeForAgent(new Example())).toBe('[non-serializable:Example]');
|
|
507
545
|
});
|
|
508
546
|
});
|
|
@@ -7,6 +7,9 @@ type QueryScopedActionInput = {
|
|
|
7
7
|
'CLEAR_MUTATION_CACHE' | 'CLEAR_QUERY_CACHE'
|
|
8
8
|
>;
|
|
9
9
|
queryHash: string;
|
|
10
|
+
metadata?: {
|
|
11
|
+
data?: unknown;
|
|
12
|
+
};
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
type CacheScopedActionInput = {
|
|
@@ -33,7 +36,7 @@ const getActiveQuery = (queryClient: QueryClient, queryHash?: string) => {
|
|
|
33
36
|
|
|
34
37
|
export const applyTanStackQueryDevtoolsAction = async (
|
|
35
38
|
queryClient: QueryClient,
|
|
36
|
-
input: TanStackQueryDevtoolsActionInput
|
|
39
|
+
input: TanStackQueryDevtoolsActionInput,
|
|
37
40
|
) => {
|
|
38
41
|
switch (input.type) {
|
|
39
42
|
case 'CLEAR_QUERY_CACHE': {
|
|
@@ -49,8 +52,9 @@ export const applyTanStackQueryDevtoolsAction = async (
|
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
case 'CLEAR_MUTATION_CACHE': {
|
|
52
|
-
const mutationCountBefore =
|
|
53
|
-
|
|
55
|
+
const mutationCountBefore = queryClient
|
|
56
|
+
.getMutationCache()
|
|
57
|
+
.getAll().length;
|
|
54
58
|
queryClient.getMutationCache().clear();
|
|
55
59
|
return {
|
|
56
60
|
applied: true,
|
|
@@ -61,6 +65,16 @@ export const applyTanStackQueryDevtoolsAction = async (
|
|
|
61
65
|
};
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
case 'SET_QUERY_DATA': {
|
|
69
|
+
const activeQuery = getActiveQuery(queryClient, input.queryHash);
|
|
70
|
+
queryClient.setQueryData(activeQuery.queryKey, input.metadata?.data);
|
|
71
|
+
return {
|
|
72
|
+
applied: true,
|
|
73
|
+
action: input.type,
|
|
74
|
+
queryHash: activeQuery.queryHash,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
64
78
|
case 'TRIGGER_ERROR': {
|
|
65
79
|
const activeQuery = getActiveQuery(queryClient, input.queryHash);
|
|
66
80
|
const previousQueryOptions = activeQuery.options;
|
|
@@ -5,7 +5,7 @@ import { applyTanStackQueryDevtoolsAction } from './devtools-actions';
|
|
|
5
5
|
|
|
6
6
|
export const useHandleDevToolsMessages = (
|
|
7
7
|
queryClient: QueryClient,
|
|
8
|
-
client: TanStackQueryPluginClient | null
|
|
8
|
+
client: TanStackQueryPluginClient | null,
|
|
9
9
|
) => {
|
|
10
10
|
useEffect(() => {
|
|
11
11
|
if (!client) {
|
|
@@ -14,16 +14,19 @@ export const useHandleDevToolsMessages = (
|
|
|
14
14
|
|
|
15
15
|
const subscription = client.onMessage(
|
|
16
16
|
'devtools-action',
|
|
17
|
-
({ type, queryHash }) => {
|
|
18
|
-
void applyTanStackQueryDevtoolsAction(queryClient, {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
({ type, queryHash, metadata }) => {
|
|
18
|
+
void applyTanStackQueryDevtoolsAction(queryClient, {
|
|
19
|
+
type,
|
|
20
|
+
queryHash,
|
|
21
|
+
metadata,
|
|
22
|
+
}).catch((error) => {
|
|
23
|
+
const message =
|
|
24
|
+
error instanceof Error ? error.message : String(error);
|
|
25
|
+
console.warn(
|
|
26
|
+
`[Rozenite, tanstack-query-plugin] Failed to apply devtools action "${type}": ${message}`,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
},
|
|
27
30
|
);
|
|
28
31
|
|
|
29
32
|
return () => {
|
package/src/shared/hydrate.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
};
|
package/src/shared/messaging.ts
CHANGED
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Query,
|
|
3
|
+
QueryClient,
|
|
4
|
+
QueryKey,
|
|
5
|
+
QueryState,
|
|
6
|
+
} from '@tanstack/react-query';
|
|
7
|
+
|
|
8
|
+
type QuerySetState = Query['setState'];
|
|
9
|
+
type QueryClientSetQueryData = QueryClient['setQueryData'];
|
|
10
|
+
|
|
11
|
+
type SyncQueryDataPayload = {
|
|
12
|
+
queryHash: string;
|
|
13
|
+
data: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type QueryDataSyncHandler = (payload: SyncQueryDataPayload) => void;
|
|
17
|
+
|
|
18
|
+
const originalQuerySetStateMap = new WeakMap<Query, QuerySetState>();
|
|
19
|
+
const originalSetQueryDataMap = new WeakMap<
|
|
20
|
+
QueryClient,
|
|
21
|
+
QueryClientSetQueryData
|
|
22
|
+
>();
|
|
23
|
+
const instrumentedQueryClients = new WeakSet<QueryClient>();
|
|
24
|
+
const instrumentedQueries = new WeakSet<Query>();
|
|
25
|
+
const queryClientHandlers = new WeakMap<
|
|
26
|
+
QueryClient,
|
|
27
|
+
QueryDataSyncHandler | undefined
|
|
28
|
+
>();
|
|
29
|
+
const queryHandlers = new WeakMap<Query, QueryDataSyncHandler | undefined>();
|
|
30
|
+
|
|
31
|
+
const getOriginalQuerySetState = (query: Query): QuerySetState => {
|
|
32
|
+
return originalQuerySetStateMap.get(query) ?? query.setState.bind(query);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const emitIfDataChanged = (
|
|
36
|
+
query: Query,
|
|
37
|
+
previousData: unknown,
|
|
38
|
+
handler?: QueryDataSyncHandler,
|
|
39
|
+
) => {
|
|
40
|
+
if (!handler || Object.is(previousData, query.state.data)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
handler({
|
|
45
|
+
queryHash: query.queryHash,
|
|
46
|
+
data: query.state.data,
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const isDataOnlyStateUpdate = (
|
|
51
|
+
previousState: QueryState,
|
|
52
|
+
nextState: Partial<QueryState>,
|
|
53
|
+
) => {
|
|
54
|
+
if (!('data' in nextState)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const changedKeys = Object.keys(nextState).filter((key) => {
|
|
59
|
+
const typedKey = key as keyof QueryState;
|
|
60
|
+
return !Object.is(previousState[typedKey], nextState[typedKey]);
|
|
61
|
+
}) as Array<keyof QueryState>;
|
|
62
|
+
|
|
63
|
+
return changedKeys.every((key) => key === 'data');
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const applyRemoteQueryState = (
|
|
67
|
+
query: Query,
|
|
68
|
+
state: Partial<QueryState>,
|
|
69
|
+
) => {
|
|
70
|
+
const originalSetState = getOriginalQuerySetState(query);
|
|
71
|
+
originalSetState(state);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const instrumentQuery = (
|
|
75
|
+
query: Query,
|
|
76
|
+
handler?: QueryDataSyncHandler,
|
|
77
|
+
) => {
|
|
78
|
+
queryHandlers.set(query, handler);
|
|
79
|
+
|
|
80
|
+
if (instrumentedQueries.has(query)) {
|
|
81
|
+
return query;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const originalSetState = query.setState.bind(query);
|
|
85
|
+
originalQuerySetStateMap.set(query, originalSetState);
|
|
86
|
+
|
|
87
|
+
query.setState = ((state) => {
|
|
88
|
+
const shouldEmit = isDataOnlyStateUpdate(query.state, state);
|
|
89
|
+
const previousData = query.state.data;
|
|
90
|
+
const result = originalSetState(state);
|
|
91
|
+
|
|
92
|
+
if (shouldEmit) {
|
|
93
|
+
emitIfDataChanged(query, previousData, queryHandlers.get(query));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}) as QuerySetState;
|
|
98
|
+
|
|
99
|
+
instrumentedQueries.add(query);
|
|
100
|
+
return query;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const resolveQueryHash = (queryClient: QueryClient, queryKey: QueryKey) => {
|
|
104
|
+
return queryClient.getQueryCache().find({ queryKey })?.queryHash;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const instrumentQueryClient = (
|
|
108
|
+
queryClient: QueryClient,
|
|
109
|
+
handler?: QueryDataSyncHandler,
|
|
110
|
+
) => {
|
|
111
|
+
queryClientHandlers.set(queryClient, handler);
|
|
112
|
+
|
|
113
|
+
if (instrumentedQueryClients.has(queryClient)) {
|
|
114
|
+
queryClient
|
|
115
|
+
.getQueryCache()
|
|
116
|
+
.getAll()
|
|
117
|
+
.forEach((query) => {
|
|
118
|
+
instrumentQuery(query, handler);
|
|
119
|
+
});
|
|
120
|
+
return queryClient;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const originalSetQueryData = queryClient.setQueryData.bind(queryClient);
|
|
124
|
+
originalSetQueryDataMap.set(queryClient, originalSetQueryData);
|
|
125
|
+
|
|
126
|
+
queryClient.setQueryData = ((queryKey, updater, options) => {
|
|
127
|
+
const result = originalSetQueryData(queryKey, updater, options);
|
|
128
|
+
const query = queryClient.getQueryCache().find({ queryKey });
|
|
129
|
+
const activeQueryClientHandler = queryClientHandlers.get(queryClient);
|
|
130
|
+
|
|
131
|
+
if (query) {
|
|
132
|
+
instrumentQuery(query, activeQueryClientHandler);
|
|
133
|
+
const queryHash = resolveQueryHash(queryClient, queryKey);
|
|
134
|
+
const activeHandler = queryHandlers.get(query);
|
|
135
|
+
|
|
136
|
+
if (activeHandler && queryHash) {
|
|
137
|
+
activeHandler({
|
|
138
|
+
queryHash,
|
|
139
|
+
data: query.state.data,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result;
|
|
145
|
+
}) as QueryClientSetQueryData;
|
|
146
|
+
|
|
147
|
+
queryClient
|
|
148
|
+
.getQueryCache()
|
|
149
|
+
.getAll()
|
|
150
|
+
.forEach((query) => {
|
|
151
|
+
instrumentQuery(query, handler);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
instrumentedQueryClients.add(queryClient);
|
|
155
|
+
return queryClient;
|
|
156
|
+
};
|
package/src/shared/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
1
2
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
3
|
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools/production';
|
|
3
4
|
import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
|
|
@@ -6,12 +7,23 @@ import { useSyncInitialData } from './useSyncInitialData';
|
|
|
6
7
|
import { useSyncDevToolsEvents } from './useSyncDevToolsEvents';
|
|
7
8
|
import { useSyncOnlineStatus } from '../shared/useSyncOnlineStatus';
|
|
8
9
|
import { useHandleSyncMessages } from './useHandleSyncMessages';
|
|
10
|
+
import { instrumentQueryClient } from '../shared/query-data-sync';
|
|
9
11
|
|
|
10
12
|
const App = () => {
|
|
11
13
|
const client = useRozeniteDevToolsClient<TanStackQueryPluginEventMap>({
|
|
12
14
|
pluginId: '@rozenite/tanstack-query-plugin',
|
|
13
15
|
});
|
|
14
16
|
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
instrumentQueryClient(queryClient, ({ queryHash, data }) => {
|
|
19
|
+
client?.send('devtools-action', {
|
|
20
|
+
type: 'SET_QUERY_DATA',
|
|
21
|
+
queryHash,
|
|
22
|
+
metadata: { data },
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}, [client]);
|
|
26
|
+
|
|
15
27
|
useSyncInitialData(client);
|
|
16
28
|
|
|
17
29
|
useSyncDevToolsEvents(client);
|