@rozenite/storage-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 (84) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +6 -1
  3. package/dist/devtools/assets/panel-BlDRXgVW.js +32 -0
  4. package/dist/devtools/assets/panel-DBweZnW5.css +1 -0
  5. package/dist/devtools/panel.html +2 -2
  6. package/dist/react-native/chunks/async-storage.require.cjs +1 -1
  7. package/dist/react-native/chunks/async-storage.require.js +8 -7
  8. package/dist/react-native/chunks/index.require.cjs +1 -1
  9. package/dist/react-native/chunks/index.require.js +70 -66
  10. package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
  11. package/dist/react-native/chunks/secure-storage.require.js +17 -13
  12. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  13. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -344
  14. package/dist/react-native/index.d.ts +166 -18
  15. package/dist/rozenite.json +1 -1
  16. package/dist/sdk/index.cjs +1 -1
  17. package/dist/sdk/index.d.ts +13 -5
  18. package/dist/sdk/index.js +21 -12
  19. package/package.json +35 -37
  20. package/react-native.ts +12 -22
  21. package/rozenite.config.ts +74 -0
  22. package/sdk.ts +1 -5
  23. package/src/react-native/__tests__/entry-preview-pagination.test.ts +234 -0
  24. package/src/react-native/__tests__/export-snapshot.test.ts +138 -0
  25. package/src/react-native/__tests__/full-entry-request.test.ts +143 -0
  26. package/src/react-native/__tests__/import.test.ts +174 -11
  27. package/src/react-native/__tests__/storage-discovery.test.ts +144 -0
  28. package/src/react-native/__tests__/storage-view.test.ts +172 -0
  29. package/src/react-native/__tests__/stress-verification.test.ts +140 -0
  30. package/src/react-native/__tests__/use-storage-agent-tools.test.ts +221 -0
  31. package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
  32. package/src/react-native/adapters/async-storage.ts +8 -9
  33. package/src/react-native/adapters/index.ts +2 -8
  34. package/src/react-native/adapters/mmkv.ts +12 -24
  35. package/src/react-native/adapters/secure-storage.ts +4 -0
  36. package/src/react-native/entry-preview-pagination.ts +261 -0
  37. package/src/react-native/export-snapshot.ts +88 -0
  38. package/src/react-native/full-entry-request.ts +91 -0
  39. package/src/react-native/import.ts +179 -16
  40. package/src/react-native/storage-discovery.ts +92 -0
  41. package/src/react-native/storage-view.ts +47 -109
  42. package/src/react-native/useRozeniteStoragePlugin.ts +153 -129
  43. package/src/react-native/useStorageAgentTools.ts +196 -91
  44. package/src/shared/__tests__/entry-preview.test.ts +99 -0
  45. package/src/shared/__tests__/snapshot.test.ts +31 -32
  46. package/src/shared/agent-tools.ts +33 -38
  47. package/src/shared/entry-preview.ts +54 -0
  48. package/src/shared/messaging.ts +140 -21
  49. package/src/shared/snapshot.ts +12 -18
  50. package/src/shared/types.ts +21 -4
  51. package/src/ui/__tests__/binary-value-editor-state.test.ts +16 -13
  52. package/src/ui/__tests__/binary.test.ts +31 -33
  53. package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
  54. package/src/ui/__tests__/large-value-viewer.test.tsx +54 -0
  55. package/src/ui/__tests__/panel.test.tsx +382 -0
  56. package/src/ui/__tests__/storage-groups.test.ts +88 -0
  57. package/src/ui/__tests__/type-conversion.test.ts +8 -24
  58. package/src/ui/__tests__/use-storage-entries.test.tsx +337 -0
  59. package/src/ui/add-entry-dialog.tsx +66 -117
  60. package/src/ui/binary-value-editor-state.ts +16 -25
  61. package/src/ui/binary-value-editor.tsx +59 -47
  62. package/src/ui/binary.ts +54 -15
  63. package/src/ui/edit-entry-dialog.tsx +68 -104
  64. package/src/ui/editor-switcher.tsx +9 -16
  65. package/src/ui/entry-detail-dialog.tsx +62 -181
  66. package/src/ui/format-value.tsx +25 -0
  67. package/src/ui/globals.css +1 -123
  68. package/src/ui/import-dialog.tsx +72 -109
  69. package/src/ui/large-value-viewer-state.ts +25 -0
  70. package/src/ui/large-value-viewer.tsx +65 -0
  71. package/src/ui/panel.tsx +649 -509
  72. package/src/ui/query-client.tsx +23 -0
  73. package/src/ui/storage-groups.ts +66 -0
  74. package/src/ui/type-conversion.ts +2 -5
  75. package/src/ui/typed-value-editor.tsx +15 -20
  76. package/src/ui/use-storage-entries.ts +192 -0
  77. package/src/ui/utils.ts +1 -4
  78. package/tsconfig.json +5 -1
  79. package/dist/devtools/assets/panel-Bm-SWF7d.js +0 -33
  80. package/dist/devtools/assets/panel-DIqI4WSp.css +0 -1
  81. package/postcss.config.js +0 -6
  82. package/src/ui/confirm-dialog.tsx +0 -100
  83. package/src/ui/editable-table.tsx +0 -300
  84. package/tailwind.config.ts +0 -94
@@ -0,0 +1,92 @@
1
+ import type {
2
+ StorageDiscoverStoragesRequestEvent,
3
+ StorageDiscoverStoragesResponseEvent,
4
+ StorageRequestErrorEvent,
5
+ } from '../shared/messaging';
6
+ import type { StorageDescriptor } from '../shared/types';
7
+ import type { StorageView } from './storage-view';
8
+
9
+ type DiscoveryResult = StorageDiscoverStoragesResponseEvent | StorageRequestErrorEvent;
10
+
11
+ const getRequestId = (payload: unknown) => {
12
+ if (
13
+ typeof payload === 'object' &&
14
+ payload != null &&
15
+ !Array.isArray(payload) &&
16
+ typeof (payload as { requestId?: unknown }).requestId === 'string'
17
+ ) {
18
+ return (payload as { requestId: string }).requestId;
19
+ }
20
+
21
+ return '';
22
+ };
23
+
24
+ const validateRequest = (payload: unknown): payload is StorageDiscoverStoragesRequestEvent =>
25
+ typeof payload === 'object' &&
26
+ payload != null &&
27
+ !Array.isArray(payload) &&
28
+ (payload as { type?: unknown }).type === 'discover-storages' &&
29
+ typeof (payload as { requestId?: unknown }).requestId === 'string' &&
30
+ (payload as { requestId: string }).requestId.trim().length > 0;
31
+
32
+ const toDescriptor = async (view: StorageView): Promise<StorageDescriptor> => ({
33
+ target: view.target,
34
+ adapterName: view.adapterName,
35
+ storageName: view.storageName,
36
+ entryCount: (await view.getAllKeys()).length,
37
+ capabilities: view.capabilities,
38
+ supportsSubscriptions: view.supportsSubscriptions,
39
+ });
40
+
41
+ const hasDuplicateTargets = (views: readonly Pick<StorageView, 'target'>[]) => {
42
+ const targets = new Set<string>();
43
+
44
+ return views.some(({ target }) => {
45
+ const id = `${target.adapterId}:${target.storageId}`;
46
+
47
+ if (targets.has(id)) {
48
+ return true;
49
+ }
50
+
51
+ targets.add(id);
52
+ return false;
53
+ });
54
+ };
55
+
56
+ export const handleStorageDiscoveryRequest = async (
57
+ views: readonly StorageView[],
58
+ payload: unknown,
59
+ ): Promise<DiscoveryResult> => {
60
+ const requestId = getRequestId(payload);
61
+
62
+ if (!validateRequest(payload)) {
63
+ return {
64
+ type: 'storage-request-error',
65
+ requestId,
66
+ code: 'INVALID_REQUEST',
67
+ message: 'Storage discovery requires a non-empty request ID.',
68
+ };
69
+ }
70
+
71
+ try {
72
+ if (hasDuplicateTargets(views)) {
73
+ return {
74
+ type: 'storage-request-error',
75
+ requestId,
76
+ code: 'INVALID_REQUEST',
77
+ message: 'Configured storages must have unique targets.',
78
+ };
79
+ }
80
+
81
+ const storages = await Promise.all(views.map(toDescriptor));
82
+
83
+ return { type: 'storage-descriptors', requestId, storages };
84
+ } catch {
85
+ return {
86
+ type: 'storage-request-error',
87
+ requestId,
88
+ code: 'READ_FAILED',
89
+ message: 'Failed to discover configured storages.',
90
+ };
91
+ }
92
+ };
@@ -12,27 +12,11 @@ import {
12
12
  type SyncStorage,
13
13
  } from '../shared/types';
14
14
 
15
- const POLLING_INTERVAL_MS = 1500;
16
-
17
- type StorageSnapshotMap = Map<string, StorageEntry>;
18
-
19
15
  type AsyncStorageLike = SyncStorage | AsyncStorage;
20
16
 
21
17
  const isAsyncStorage = (storage: AsyncStorageLike): storage is AsyncStorage =>
22
18
  storage.kind === 'async';
23
19
 
24
- const fingerprintEntry = (entry: StorageEntry) => {
25
- if (entry.type === 'buffer') {
26
- return `${entry.type}:${entry.value.join(',')}`;
27
- }
28
-
29
- return `${entry.type}:${String(entry.value)}`;
30
- };
31
-
32
- const toSnapshotMap = (entries: StorageEntry[]) => {
33
- return new Map(entries.map((entry) => [entry.key, entry]));
34
- };
35
-
36
20
  const shouldFilterKey = (storage: StorageNode, key: string) => {
37
21
  if (storage.shouldFilterKey?.(key)) {
38
22
  return true;
@@ -94,83 +78,25 @@ const deleteEntry = async (storage: AsyncStorageLike, key: string) => {
94
78
  storage.delete(key);
95
79
  };
96
80
 
81
+ const clearStorage = async (storage: AsyncStorageLike) => {
82
+ await storage.clear();
83
+ };
84
+
97
85
  export type StorageView = {
98
86
  id: string;
99
87
  target: StorageTarget;
100
88
  adapterName: string;
101
89
  storageName: string;
102
90
  capabilities: StorageCapabilities;
91
+ supportsSubscriptions: boolean;
103
92
  blacklist?: RegExp;
104
93
  get: (key: string) => Promise<StorageEntry | undefined>;
105
94
  set: (entry: StorageEntry) => Promise<void>;
106
95
  delete: (key: string) => Promise<void>;
96
+ purge: () => Promise<void>;
107
97
  getAllKeys: () => Promise<readonly string[]>;
108
98
  getAllEntries: () => Promise<StorageEntry[]>;
109
- watch: (callbacks: {
110
- onSet: (entry: StorageEntry) => void;
111
- onDelete: (key: string) => void;
112
- }) => Promise<StorageSubscription>;
113
- };
114
-
115
- const buildSnapshotMap = async (
116
- getAllEntries: () => Promise<StorageEntry[]>,
117
- ): Promise<StorageSnapshotMap> => {
118
- const entries = await getAllEntries();
119
- return toSnapshotMap(entries);
120
- };
121
-
122
- const diffSnapshots = (
123
- previous: StorageSnapshotMap,
124
- next: StorageSnapshotMap,
125
- handlers: {
126
- onSet: (entry: StorageEntry) => void;
127
- onDelete: (key: string) => void;
128
- },
129
- ) => {
130
- next.forEach((nextEntry, key) => {
131
- const previousEntry = previous.get(key);
132
-
133
- if (!previousEntry) {
134
- handlers.onSet(nextEntry);
135
- return;
136
- }
137
-
138
- if (fingerprintEntry(previousEntry) !== fingerprintEntry(nextEntry)) {
139
- handlers.onSet(nextEntry);
140
- }
141
- });
142
-
143
- previous.forEach((_value, key) => {
144
- if (!next.has(key)) {
145
- handlers.onDelete(key);
146
- }
147
- });
148
- };
149
-
150
- const createPollingSubscription = async (
151
- getAllEntries: () => Promise<StorageEntry[]>,
152
- handlers: {
153
- onSet: (entry: StorageEntry) => void;
154
- onDelete: (key: string) => void;
155
- },
156
- ): Promise<StorageSubscription> => {
157
- let previousSnapshot = await buildSnapshotMap(getAllEntries);
158
-
159
- const interval = setInterval(async () => {
160
- try {
161
- const nextSnapshot = await buildSnapshotMap(getAllEntries);
162
- diffSnapshots(previousSnapshot, nextSnapshot, handlers);
163
- previousSnapshot = nextSnapshot;
164
- } catch {
165
- // Silently ignore polling errors and try again on next tick.
166
- }
167
- }, POLLING_INTERVAL_MS);
168
-
169
- return {
170
- remove: () => {
171
- clearInterval(interval);
172
- },
173
- };
99
+ watch?: (onChange: (key: string) => void) => Promise<StorageSubscription>;
174
100
  };
175
101
 
176
102
  export const createStorageView = (
@@ -178,6 +104,9 @@ export const createStorageView = (
178
104
  storageNode: StorageNode,
179
105
  ): StorageView => {
180
106
  const storage = storageNode.storage;
107
+ const subscribe = storage.subscribe;
108
+ const locallyMutatingKeys = new Set<string>();
109
+ let isLocallyPurging = false;
181
110
  const target: StorageTarget = {
182
111
  adapterId: adapter.id,
183
112
  storageId: storageNode.id,
@@ -194,9 +123,7 @@ export const createStorageView = (
194
123
  const getAllEntries = async () => {
195
124
  const keys = await getAllKeys(storage);
196
125
  const visibleEntries = await Promise.all(
197
- keys
198
- .filter((key) => !shouldFilterKey(storageNode, key))
199
- .map((key) => getEntry(storage, key)),
126
+ keys.filter((key) => !shouldFilterKey(storageNode, key)).map((key) => getEntry(storage, key)),
200
127
  );
201
128
 
202
129
  return visibleEntries.filter((entry): entry is StorageEntry => !!entry);
@@ -208,46 +135,57 @@ export const createStorageView = (
208
135
  adapterName: adapter.name,
209
136
  storageName: storageNode.name,
210
137
  capabilities: storageNode.capabilities,
138
+ supportsSubscriptions: subscribe != null,
211
139
  blacklist: storageNode.blacklist,
212
140
  get,
213
141
  set: async (entry) => {
214
142
  checkTypeSupport(storageNode.capabilities, entry.type, target);
215
- await setEntry(storage, entry);
143
+ locallyMutatingKeys.add(entry.key);
144
+ try {
145
+ await setEntry(storage, entry);
146
+ } finally {
147
+ locallyMutatingKeys.delete(entry.key);
148
+ }
216
149
  },
217
150
  delete: async (key) => {
218
- await deleteEntry(storage, key);
151
+ locallyMutatingKeys.add(key);
152
+ try {
153
+ await deleteEntry(storage, key);
154
+ } finally {
155
+ locallyMutatingKeys.delete(key);
156
+ }
157
+ },
158
+ purge: async () => {
159
+ isLocallyPurging = true;
160
+ try {
161
+ await clearStorage(storage);
162
+ } finally {
163
+ isLocallyPurging = false;
164
+ }
219
165
  },
220
166
  getAllKeys: async () => {
221
167
  const keys = await getAllKeys(storage);
222
168
  return keys.filter((key) => !shouldFilterKey(storageNode, key));
223
169
  },
224
170
  getAllEntries,
225
- watch: async ({ onSet, onDelete }) => {
226
- if (storage.subscribe) {
227
- return storage.subscribe(async (key) => {
228
- try {
229
- const entry = await get(key);
230
-
231
- if (!entry) {
232
- onDelete(key);
233
- return;
234
- }
235
-
236
- onSet(entry);
237
- } catch {
238
- // Ignore runtime callback errors; polling fallback is not needed when subscribe exists.
239
- }
240
- });
241
- }
242
-
243
- return createPollingSubscription(getAllEntries, { onSet, onDelete });
244
- },
171
+ ...(subscribe
172
+ ? {
173
+ watch: async (onChange) =>
174
+ subscribe((key) => {
175
+ if (
176
+ !isLocallyPurging &&
177
+ !locallyMutatingKeys.has(key) &&
178
+ !shouldFilterKey(storageNode, key)
179
+ ) {
180
+ onChange(key);
181
+ }
182
+ }),
183
+ }
184
+ : {}),
245
185
  };
246
186
  };
247
187
 
248
188
  export const createStorageViews = (storages: StorageAdapter[]) =>
249
189
  storages.flatMap((adapter) =>
250
- adapter.storages.map((storageNode) =>
251
- createStorageView(adapter, storageNode),
252
- ),
190
+ adapter.storages.map((storageNode) => createStorageView(adapter, storageNode)),
253
191
  );
@@ -2,13 +2,23 @@ import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
2
2
  import { useEffect, useMemo } from 'react';
3
3
  import type {
4
4
  StorageDeleteEntryEvent,
5
+ StorageDiscoverStoragesRequestEvent,
5
6
  StorageEventMap,
6
- StorageGetSnapshotEvent,
7
+ StorageExportSnapshotRequestEvent,
8
+ StorageGetEntryRequestEvent,
7
9
  StorageImportEntriesEvent,
10
+ StorageImportPreviewRequestEvent,
11
+ StorageInvalidationOperation,
12
+ StorageListEntryPreviewsRequestEvent,
8
13
  StorageSetEntryEvent,
14
+ StoragePurgeEvent,
9
15
  } from '../shared/messaging';
10
16
  import type { StorageAdapter } from '../shared/types';
11
- import { handleImportEntries } from './import';
17
+ import { handleImportEntries, handleImportPreviewRequest } from './import';
18
+ import { handleStorageDiscoveryRequest } from './storage-discovery';
19
+ import { handleListEntryPreviewsRequest } from './entry-preview-pagination';
20
+ import { handleFullEntryRequest } from './full-entry-request';
21
+ import { handleExportSnapshotRequest } from './export-snapshot';
12
22
  import { createStorageViews } from './storage-view';
13
23
  import { useStorageAgentTools } from './useStorageAgentTools';
14
24
 
@@ -16,9 +26,7 @@ export type RozeniteStoragePluginOptions = {
16
26
  storages: StorageAdapter[];
17
27
  };
18
28
 
19
- export const useRozeniteStoragePlugin = ({
20
- storages,
21
- }: RozeniteStoragePluginOptions) => {
29
+ export const useRozeniteStoragePlugin = ({ storages }: RozeniteStoragePluginOptions) => {
22
30
  const views = useMemo(() => createStorageViews(storages), [storages]);
23
31
 
24
32
  useStorageAgentTools(views);
@@ -32,153 +40,169 @@ export const useRozeniteStoragePlugin = ({
32
40
  return;
33
41
  }
34
42
 
35
- const pushSnapshot = async (viewId?: string) => {
36
- const selectedViews = viewId
37
- ? views.filter((view) => view.id === viewId)
38
- : views;
43
+ const viewSubscriptions: { remove: () => void }[] = [];
44
+ let disposed = false;
45
+
46
+ const sendInvalidation = async (
47
+ target: StorageSetEntryEvent['target'],
48
+ key?: string,
49
+ operation?: StorageInvalidationOperation,
50
+ ) => {
51
+ const view = views.find(
52
+ (candidate) =>
53
+ candidate.target.adapterId === target.adapterId &&
54
+ candidate.target.storageId === target.storageId,
55
+ );
56
+ const entryCount = view ? (await view.getAllKeys()).length : 0;
57
+ client.send('storage-invalidated', {
58
+ type: 'storage-invalidated',
59
+ target,
60
+ key,
61
+ operation,
62
+ entryCount,
63
+ });
64
+ };
65
+
66
+ // Prevent one storage watcher failure from breaking the whole plugin.
67
+ void Promise.all(
68
+ views
69
+ .filter((view) => view.supportsSubscriptions && view.watch)
70
+ .map(async (view) => {
71
+ try {
72
+ const watch = view.watch;
73
+ if (!watch) {
74
+ return;
75
+ }
76
+
77
+ const subscription = await watch((key) => {
78
+ void sendInvalidation(view.target, key);
79
+ });
80
+
81
+ if (disposed) {
82
+ subscription.remove();
83
+ return;
84
+ }
85
+
86
+ viewSubscriptions.push(subscription);
87
+ } catch (error) {
88
+ console.warn(
89
+ `[Rozenite] Storage Plugin: Failed to attach watcher for ${view.target.adapterId}/${view.target.storageId}.`,
90
+ error,
91
+ );
92
+ }
93
+ }),
94
+ );
95
+
96
+ const messageSubscriptions = [
97
+ client.onMessage('set-entry', async ({ target, entry }: StorageSetEntryEvent) => {
98
+ const view = views.find(
99
+ (candidate) =>
100
+ candidate.target.adapterId === target.adapterId &&
101
+ candidate.target.storageId === target.storageId,
102
+ );
103
+
104
+ if (!view) {
105
+ console.warn(
106
+ `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
107
+ );
108
+ return;
109
+ }
39
110
 
40
- for (const view of selectedViews) {
41
111
  try {
42
- const entries = await view.getAllEntries();
43
- client.send('snapshot', {
44
- type: 'snapshot',
45
- target: view.target,
46
- adapterName: view.adapterName,
47
- storageName: view.storageName,
48
- capabilities: view.capabilities,
49
- blacklist: view.blacklist
50
- ? { source: view.blacklist.source, flags: view.blacklist.flags }
51
- : undefined,
52
- entries,
53
- });
112
+ await view.set(entry);
113
+ await sendInvalidation(view.target, entry.key, 'set');
54
114
  } catch (error) {
55
115
  console.warn(
56
- `[Rozenite] Storage Plugin: Failed to snapshot ${view.target.adapterId}/${view.target.storageId}.`,
116
+ `[Rozenite] Storage Plugin: Failed to set entry in ${target.adapterId}/${target.storageId}.`,
57
117
  error,
58
118
  );
59
119
  }
60
- }
61
- };
62
-
63
- void pushSnapshot();
64
-
65
- const viewSubscriptions: { remove: () => void }[] = [];
66
- let disposed = false;
120
+ }),
121
+ client.onMessage('delete-entry', async ({ target, key }: StorageDeleteEntryEvent) => {
122
+ const view = views.find(
123
+ (candidate) =>
124
+ candidate.target.adapterId === target.adapterId &&
125
+ candidate.target.storageId === target.storageId,
126
+ );
127
+
128
+ if (!view) {
129
+ console.warn(
130
+ `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
131
+ );
132
+ return;
133
+ }
67
134
 
68
- // Prevent one storage watcher failure from breaking the whole plugin.
69
- void Promise.all(
70
- views.map(async (view) => {
71
135
  try {
72
- const subscription = await view.watch({
73
- onSet: (entry) => {
74
- client.send('set-entry', {
75
- type: 'set-entry',
76
- target: view.target,
77
- entry,
78
- });
79
- },
80
- onDelete: (key) => {
81
- client.send('delete-entry', {
82
- type: 'delete-entry',
83
- target: view.target,
84
- key,
85
- });
86
- },
87
- });
88
-
89
- if (disposed) {
90
- subscription.remove();
91
- return;
92
- }
93
-
94
- viewSubscriptions.push(subscription);
136
+ await view.delete(key);
137
+ await sendInvalidation(view.target, key, 'delete');
95
138
  } catch (error) {
96
139
  console.warn(
97
- `[Rozenite] Storage Plugin: Failed to attach watcher for ${view.target.adapterId}/${view.target.storageId}.`,
140
+ `[Rozenite] Storage Plugin: Failed to delete entry in ${target.adapterId}/${target.storageId}.`,
98
141
  error,
99
142
  );
100
143
  }
101
144
  }),
102
- );
103
-
104
- const messageSubscriptions = [
105
- client.onMessage(
106
- 'set-entry',
107
- async ({ target, entry }: StorageSetEntryEvent) => {
108
- const view = views.find(
109
- (candidate) =>
110
- candidate.target.adapterId === target.adapterId &&
111
- candidate.target.storageId === target.storageId,
145
+ client.onMessage('purge-storage', async ({ target }: StoragePurgeEvent) => {
146
+ const view = views.find(
147
+ (candidate) =>
148
+ candidate.target.adapterId === target.adapterId &&
149
+ candidate.target.storageId === target.storageId,
150
+ );
151
+
152
+ if (!view) {
153
+ console.warn(
154
+ `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
112
155
  );
156
+ return;
157
+ }
113
158
 
114
- if (!view) {
115
- console.warn(
116
- `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
117
- );
118
- return;
119
- }
120
-
121
- try {
122
- await view.set(entry);
123
- } catch (error) {
124
- console.warn(
125
- `[Rozenite] Storage Plugin: Failed to set entry in ${target.adapterId}/${target.storageId}.`,
126
- error,
127
- );
128
- }
129
- },
130
- ),
131
- client.onMessage(
132
- 'delete-entry',
133
- async ({ target, key }: StorageDeleteEntryEvent) => {
134
- const view = views.find(
135
- (candidate) =>
136
- candidate.target.adapterId === target.adapterId &&
137
- candidate.target.storageId === target.storageId,
159
+ try {
160
+ await view.purge();
161
+ await sendInvalidation(view.target, undefined, 'purge');
162
+ } catch (error) {
163
+ console.warn(
164
+ `[Rozenite] Storage Plugin: Failed to purge storage in ${target.adapterId}/${target.storageId}.`,
165
+ error,
138
166
  );
139
-
140
- if (!view) {
141
- console.warn(
142
- `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
143
- );
144
- return;
145
- }
146
-
147
- try {
148
- await view.delete(key);
149
- } catch (error) {
150
- console.warn(
151
- `[Rozenite] Storage Plugin: Failed to delete entry in ${target.adapterId}/${target.storageId}.`,
152
- error,
153
- );
154
- }
155
- },
156
- ),
157
- client.onMessage(
158
- 'get-snapshot',
159
- async ({ target }: StorageGetSnapshotEvent) => {
160
- if (target === 'all') {
161
- await pushSnapshot();
162
- return;
163
- }
164
-
165
- await pushSnapshot(`${target.adapterId}:${target.storageId}`);
166
- },
167
- ),
167
+ }
168
+ }),
169
+ client.onMessage('discover-storages', async (event: StorageDiscoverStoragesRequestEvent) => {
170
+ const response = await handleStorageDiscoveryRequest(views, event);
171
+ client.send(response.type, response);
172
+ }),
168
173
  client.onMessage(
169
- 'import-entries',
170
- async (event: StorageImportEntriesEvent) => {
171
- await handleImportEntries(views, event, (out) => {
172
- if (out.type === 'set-entry') {
173
- client.send('set-entry', out);
174
- } else {
175
- client.send('import-result', out);
176
- }
177
- });
174
+ 'list-entry-previews',
175
+ async (event: StorageListEntryPreviewsRequestEvent) => {
176
+ const response = await handleListEntryPreviewsRequest(views, event);
177
+ client.send(response.type, response);
178
178
  },
179
179
  ),
180
+ client.onMessage('get-entry', async (event: StorageGetEntryRequestEvent) => {
181
+ const response = await handleFullEntryRequest(views, event);
182
+ client.send(response.type, response);
183
+ }),
184
+ client.onMessage('export-snapshot', async (event: StorageExportSnapshotRequestEvent) => {
185
+ const response = await handleExportSnapshotRequest(views, event);
186
+ client.send(response.type, response);
187
+ }),
188
+ client.onMessage('preview-import', async (event: StorageImportPreviewRequestEvent) => {
189
+ const response = await handleImportPreviewRequest(views, event);
190
+ client.send(response.type, response);
191
+ }),
192
+ client.onMessage('import-entries', async (event: StorageImportEntriesEvent) => {
193
+ await handleImportEntries(views, event, (out) => {
194
+ client.send(out.type, out);
195
+ });
196
+ }),
180
197
  ];
181
198
 
199
+ // Sent last, so a panel that reacts to it is guaranteed to find the
200
+ // handlers above already listening. The panel is recreated on every app
201
+ // reload and asks for storages as soon as it boots, which usually beats the
202
+ // app's React tree to the punch; without this the panel's only request is
203
+ // dropped and it waits for storages forever.
204
+ client.send('device-ready', { type: 'device-ready' });
205
+
182
206
  return () => {
183
207
  disposed = true;
184
208
  viewSubscriptions.forEach((subscription) => subscription.remove());