diff --git a/.changeset/storage-plugin-reload-discovery.md b/.changeset/storage-plugin-reload-discovery.md new file mode 100644 index 00000000..0ca0b927 --- /dev/null +++ b/.changeset/storage-plugin-reload-discovery.md @@ -0,0 +1,7 @@ +--- +'@rozenite/storage-plugin': patch +--- + +Fix the Storage panel losing its storages after an app reload. The device now +announces itself once it is listening, so the panel runs discovery again instead +of waiting forever on a request that was sent before the app finished mounting. diff --git a/packages/storage-plugin/src/react-native/useRozeniteStoragePlugin.ts b/packages/storage-plugin/src/react-native/useRozeniteStoragePlugin.ts index 7e926713..31590331 100644 --- a/packages/storage-plugin/src/react-native/useRozeniteStoragePlugin.ts +++ b/packages/storage-plugin/src/react-native/useRozeniteStoragePlugin.ts @@ -196,6 +196,13 @@ export const useRozeniteStoragePlugin = ({ storages }: RozeniteStoragePluginOpti }), ]; + // Sent last, so a panel that reacts to it is guaranteed to find the + // handlers above already listening. The panel is recreated on every app + // reload and asks for storages as soon as it boots, which usually beats the + // app's React tree to the punch; without this the panel's only request is + // dropped and it waits for storages forever. + client.send('device-ready', { type: 'device-ready' }); + return () => { disposed = true; viewSubscriptions.forEach((subscription) => subscription.remove()); diff --git a/packages/storage-plugin/src/shared/messaging.ts b/packages/storage-plugin/src/shared/messaging.ts index fab3e190..0a13ede6 100644 --- a/packages/storage-plugin/src/shared/messaging.ts +++ b/packages/storage-plugin/src/shared/messaging.ts @@ -30,6 +30,16 @@ export type StorageInvalidatedEvent = { entryCount: number; }; +/** + * Announced by the device once it is listening for panel requests. The panel is + * recreated on every app reload and asks for storages immediately, which can + * land before the app's React tree has mounted the plugin; this lets the panel + * ask again instead of waiting forever on a dropped request. + */ +export type StorageDeviceReadyEvent = { + type: 'device-ready'; +}; + export type StorageDiscoverStoragesRequestEvent = { type: 'discover-storages'; requestId: string; @@ -149,6 +159,7 @@ export type StorageEvent = | StorageDeleteEntryEvent | StoragePurgeEvent | StorageInvalidatedEvent + | StorageDeviceReadyEvent | StorageDiscoverStoragesRequestEvent | StorageDiscoverStoragesResponseEvent | StorageListEntryPreviewsRequestEvent diff --git a/packages/storage-plugin/src/ui/__tests__/panel.test.tsx b/packages/storage-plugin/src/ui/__tests__/panel.test.tsx index 69eac1f1..a19449eb 100644 --- a/packages/storage-plugin/src/ui/__tests__/panel.test.tsx +++ b/packages/storage-plugin/src/ui/__tests__/panel.test.tsx @@ -186,8 +186,10 @@ const renderPanel = async () => { await act(async () => root.render()); return { root, container }; }; +const discoveryRequests = () => + mocks.client.send.mock.calls.filter(([type]) => type === 'discover-storages'); const discover = async () => { - const request = mocks.client.send.mock.calls.find(([type]) => type === 'discover-storages')?.[1]; + const request = discoveryRequests().at(-1)?.[1]; await act(async () => mocks.emit('storage-descriptors', { type: 'storage-descriptors', @@ -362,4 +364,19 @@ describe('StoragePanel preview query cutover', () => { await vi.waitFor(() => expect(mocks.client.request).toHaveBeenCalledTimes(2)); await act(async () => root.unmount()); }); + + // The panel is recreated on every app reload and asks for storages before the + // app's React tree has mounted the plugin, so its first request is dropped. + it('runs discovery again when the device announces itself', async () => { + const { root, container } = await renderPanel(); + expect(discoveryRequests()).toHaveLength(1); + expect(container.textContent).toContain('Waiting for storages'); + + await act(async () => mocks.emit('device-ready', { type: 'device-ready' })); + expect(discoveryRequests()).toHaveLength(2); + await discover(); + + expect(container.textContent).toContain('First'); + await act(async () => root.unmount()); + }); }); diff --git a/packages/storage-plugin/src/ui/panel.tsx b/packages/storage-plugin/src/ui/panel.tsx index a3716b80..67cebb53 100644 --- a/packages/storage-plugin/src/ui/panel.tsx +++ b/packages/storage-plugin/src/ui/panel.tsx @@ -141,6 +141,14 @@ function StoragePanelContent() { useEffect(() => { if (!client) return; + const requestDiscovery = () => { + discoveryRequestIdRef.current += 1; + client.send('discover-storages', { + type: 'discover-storages', + requestId: `discovery-${discoveryRequestIdRef.current}`, + }); + }; + const descriptorsSubscription = client.onMessage( 'storage-descriptors', (event: StorageDiscoverStoragesResponseEvent) => { @@ -228,16 +236,21 @@ function StoragePanelContent() { }, ); - discoveryRequestIdRef.current += 1; - client.send('discover-storages', { - type: 'discover-storages', - requestId: `discovery-${discoveryRequestIdRef.current}`, + // The device announces itself once it is listening. It reconnects on every + // app reload, so discovery has to run again — the descriptors and cached + // entries the panel holds belong to the previous JS context. + const deviceReadySubscription = client.onMessage('device-ready', () => { + void queryClient.resetQueries(); + requestDiscovery(); }); + + requestDiscovery(); return () => { descriptorsSubscription.remove(); importProgressSubscription.remove(); importResultSubscription.remove(); invalidationSubscription.remove(); + deviceReadySubscription.remove(); }; }, [client, queryClient]);