@rozenite/tanstack-query-plugin 1.13.0 → 2.1.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/devtools/assets/{CXEL7IU7-DmbowaBr.js → CXEL7IU7-BWccRGN3.js} +1 -1
  3. package/dist/devtools/assets/{tanstack-query-DRBLkSCy.js → tanstack-query-CbF_imft.js} +13 -13
  4. package/dist/devtools/tanstack-query.html +1 -1
  5. package/dist/react-native/chunks/useTanStackQueryDevTools.require.cjs +1 -1
  6. package/dist/react-native/chunks/useTanStackQueryDevTools.require.js +165 -173
  7. package/dist/rozenite.json +1 -1
  8. package/dist/sdk/index.cjs +1 -1
  9. package/dist/sdk/index.js +65 -33
  10. package/package.json +30 -34
  11. package/sdk.ts +1 -4
  12. package/src/react-native/agent/__tests__/tanstack-query-agent.test.ts +32 -31
  13. package/src/react-native/agent/tanstack-query-agent.ts +33 -97
  14. package/src/react-native/agent/useTanStackQueryAgentTools.ts +2 -7
  15. package/src/react-native/devtools-actions.ts +3 -10
  16. package/src/react-native/useHandleDevToolsMessages.ts +12 -16
  17. package/src/react-native/useHandleInitialData.ts +8 -1
  18. package/src/react-native/useSyncTanStackCache.ts +7 -22
  19. package/src/shared/agent-tools.ts +59 -44
  20. package/src/shared/dehydrate.ts +1 -3
  21. package/src/shared/hydrate.ts +11 -21
  22. package/src/shared/messaging.ts +8 -2
  23. package/src/shared/query-data-sync.test.ts +1 -5
  24. package/src/shared/query-data-sync.ts +8 -35
  25. package/src/shared/useSyncOnlineStatus.ts +4 -9
  26. package/src/ui/useHandleSyncMessages.ts +23 -31
  27. package/src/ui/useSyncDevToolsEvents.ts +4 -13
  28. package/src/ui/useSyncInitialData.test.tsx +76 -0
  29. package/src/ui/useSyncInitialData.ts +17 -4
@@ -8,9 +8,7 @@ import {
8
8
  applyQueryObserverEvent,
9
9
  } from '../shared/hydrate';
10
10
 
11
- export const useHandleSyncMessages = (
12
- client: TanStackQueryPluginClient | null
13
- ) => {
11
+ export const useHandleSyncMessages = (client: TanStackQueryPluginClient | null) => {
14
12
  const queryClient = useQueryClient();
15
13
 
16
14
  useEffect(() => {
@@ -18,39 +16,33 @@ export const useHandleSyncMessages = (
18
16
  return;
19
17
  }
20
18
 
21
- const querySubscription = client.onMessage(
22
- 'sync-query-event',
23
- (message) => {
24
- const { type, data } = message;
19
+ const querySubscription = client.onMessage('sync-query-event', (message) => {
20
+ const { type, data } = message;
25
21
 
26
- if (type === 'added' || type === 'removed') {
27
- applyQueryEvent(queryClient, type, data);
28
- return;
29
- }
30
-
31
- if (type === 'updated') {
32
- const action = 'action' in message ? message.action : undefined;
33
- applyQueryEvent(queryClient, type, data, action);
34
- return;
35
- }
22
+ if (type === 'added' || type === 'removed') {
23
+ applyQueryEvent(queryClient, type, data);
24
+ return;
25
+ }
36
26
 
37
- if (
38
- type === 'observerAdded' ||
39
- type === 'observerRemoved' ||
40
- type === 'observerOptionsUpdated'
41
- ) {
42
- applyQueryObserverEvent(queryClient, data);
43
- return;
44
- }
27
+ if (type === 'updated') {
28
+ const action = 'action' in message ? message.action : undefined;
29
+ applyQueryEvent(queryClient, type, data, action);
30
+ return;
45
31
  }
46
- );
47
32
 
48
- const mutationSubscription = client.onMessage(
49
- 'sync-mutation-event',
50
- ({ type, data }) => {
51
- applyMutationEvent(queryClient, type, data);
33
+ if (
34
+ type === 'observerAdded' ||
35
+ type === 'observerRemoved' ||
36
+ type === 'observerOptionsUpdated'
37
+ ) {
38
+ applyQueryObserverEvent(queryClient, data);
39
+ return;
52
40
  }
53
- );
41
+ });
42
+
43
+ const mutationSubscription = client.onMessage('sync-mutation-event', ({ type, data }) => {
44
+ applyMutationEvent(queryClient, type, data);
45
+ });
54
46
 
55
47
  // Keep the full sync for initial data requests
56
48
  const fullSyncSubscription = client.onMessage('sync-data', ({ data }) => {
@@ -2,9 +2,7 @@ import { useQueryClient } from '@tanstack/react-query';
2
2
  import { useEffect } from 'react';
3
3
  import { TanStackQueryPluginClient } from '../shared/messaging';
4
4
 
5
- export const useSyncDevToolsEvents = (
6
- client: TanStackQueryPluginClient | null
7
- ) => {
5
+ export const useSyncDevToolsEvents = (client: TanStackQueryPluginClient | null) => {
8
6
  const queryClient = useQueryClient();
9
7
 
10
8
  useEffect(() => {
@@ -17,14 +15,8 @@ export const useSyncDevToolsEvents = (
17
15
  const { type, queryHash } = detail;
18
16
 
19
17
  // Get the query from cache if we have a hash
20
- const query = queryHash
21
- ? queryClient.getQueryCache().get(queryHash)
22
- : undefined;
23
- if (
24
- !query &&
25
- type !== 'CLEAR_QUERY_CACHE' &&
26
- type !== 'CLEAR_MUTATION_CACHE'
27
- ) {
18
+ const query = queryHash ? queryClient.getQueryCache().get(queryHash) : undefined;
19
+ if (!query && type !== 'CLEAR_QUERY_CACHE' && type !== 'CLEAR_MUTATION_CACHE') {
28
20
  return;
29
21
  }
30
22
 
@@ -49,7 +41,6 @@ export const useSyncDevToolsEvents = (
49
41
  };
50
42
 
51
43
  window.addEventListener('@tanstack/query-devtools-event', handleEvent);
52
- return () =>
53
- window.removeEventListener('@tanstack/query-devtools-event', handleEvent);
44
+ return () => window.removeEventListener('@tanstack/query-devtools-event', handleEvent);
54
45
  }, [client]);
55
46
  };
@@ -0,0 +1,76 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, type ReactNode } from 'react';
4
+ import { createRoot } from 'react-dom/client';
5
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import { TanStackQueryPluginClient } from '../shared/messaging';
8
+ import { useSyncInitialData } from './useSyncInitialData';
9
+
10
+ declare global {
11
+ var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
12
+ }
13
+
14
+ type Listener = (payload: unknown) => void;
15
+
16
+ const createClient = () => {
17
+ const listeners = new Map<string, Set<Listener>>();
18
+
19
+ return {
20
+ send: vi.fn(),
21
+ onMessage: vi.fn((type: string, listener: Listener) => {
22
+ const typeListeners = listeners.get(type) ?? new Set<Listener>();
23
+ typeListeners.add(listener);
24
+ listeners.set(type, typeListeners);
25
+ return { remove: () => typeListeners.delete(listener) };
26
+ }),
27
+ close: vi.fn(),
28
+ emit: (type: string, payload: unknown) =>
29
+ listeners.get(type)?.forEach((listener) => listener(payload)),
30
+ };
31
+ };
32
+
33
+ const requests = (client: ReturnType<typeof createClient>) =>
34
+ client.send.mock.calls.filter(([type]) => type === 'request-initial-data');
35
+
36
+ describe('useSyncInitialData', () => {
37
+ beforeEach(() => {
38
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
39
+ });
40
+ afterEach(() => {
41
+ document.body.innerHTML = '';
42
+ delete globalThis.IS_REACT_ACT_ENVIRONMENT;
43
+ });
44
+
45
+ // The panel is recreated on every app reload and asks for the cache before the
46
+ // app's React tree has mounted the plugin, so its first request is dropped.
47
+ it('asks for the cache again when the device announces itself', async () => {
48
+ const client = createClient();
49
+ const queryClient = new QueryClient();
50
+ queryClient.setQueryData(['stale-from-previous-context'], 'value');
51
+
52
+ const Harness = ({ children }: { children?: ReactNode }) => {
53
+ useSyncInitialData(client as unknown as TanStackQueryPluginClient);
54
+ return <>{children}</>;
55
+ };
56
+ const container = document.createElement('div');
57
+ document.body.append(container);
58
+ const root = createRoot(container);
59
+ await act(async () =>
60
+ root.render(
61
+ <QueryClientProvider client={queryClient}>
62
+ <Harness />
63
+ </QueryClientProvider>,
64
+ ),
65
+ );
66
+
67
+ expect(requests(client)).toHaveLength(1);
68
+
69
+ await act(async () => client.emit('device-ready', {}));
70
+
71
+ expect(requests(client)).toHaveLength(2);
72
+ expect(queryClient.getQueryData(['stale-from-previous-context'])).toBeUndefined();
73
+
74
+ await act(async () => root.unmount());
75
+ });
76
+ });
@@ -1,14 +1,27 @@
1
1
  import { useEffect } from 'react';
2
+ import { useQueryClient } from '@tanstack/react-query';
2
3
  import { TanStackQueryPluginClient } from '../shared/messaging';
3
4
 
4
- export const useSyncInitialData = (
5
- client: TanStackQueryPluginClient | null
6
- ) => {
5
+ export const useSyncInitialData = (client: TanStackQueryPluginClient | null) => {
6
+ const queryClient = useQueryClient();
7
+
7
8
  useEffect(() => {
8
9
  if (!client) {
9
10
  return;
10
11
  }
11
12
 
13
+ // The device announces itself once it is listening. It reconnects on every
14
+ // app reload, so the cache has to be pulled again — what the panel holds
15
+ // belongs to the previous JS context.
16
+ const subscription = client.onMessage('device-ready', () => {
17
+ queryClient.clear();
18
+ client.send('request-initial-data', {});
19
+ });
20
+
12
21
  client.send('request-initial-data', {});
13
- }, [client]);
22
+
23
+ return () => {
24
+ subscription.remove();
25
+ };
26
+ }, [client, queryClient]);
14
27
  };