From 39992f55c7ab3e0d8a6812432e7d301e37ebb4e8 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 12 Aug 2026 12:07:52 +0200 Subject: [PATCH] fix(storage-plugin): restore storages after an app reload The panel iframe is recreated on every reload and asks for storages before the app's React tree has mounted the plugin, so its single discover-storages request is dropped and the panel waits forever. The device now announces itself once its handlers are registered, and the panel re-runs discovery and drops its stale query cache in response. --- .changeset/storage-plugin-reload-discovery.md | 7 +++++++ .../react-native/useRozeniteStoragePlugin.ts | 7 +++++++ .../storage-plugin/src/shared/messaging.ts | 11 ++++++++++ .../src/ui/__tests__/panel.test.tsx | 19 ++++++++++++++++- packages/storage-plugin/src/ui/panel.tsx | 21 +++++++++++++++---- 5 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 .changeset/storage-plugin-reload-discovery.md 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]);