@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.
- package/CHANGELOG.md +46 -0
- package/README.md +6 -1
- package/dist/devtools/assets/panel-BlDRXgVW.js +32 -0
- package/dist/devtools/assets/panel-DBweZnW5.css +1 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/async-storage.require.cjs +1 -1
- package/dist/react-native/chunks/async-storage.require.js +8 -7
- package/dist/react-native/chunks/index.require.cjs +1 -1
- package/dist/react-native/chunks/index.require.js +70 -66
- package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
- package/dist/react-native/chunks/secure-storage.require.js +17 -13
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -344
- package/dist/react-native/index.d.ts +166 -18
- package/dist/rozenite.json +1 -1
- package/dist/sdk/index.cjs +1 -1
- package/dist/sdk/index.d.ts +13 -5
- package/dist/sdk/index.js +21 -12
- package/package.json +35 -37
- package/react-native.ts +12 -22
- package/rozenite.config.ts +74 -0
- package/sdk.ts +1 -5
- package/src/react-native/__tests__/entry-preview-pagination.test.ts +234 -0
- package/src/react-native/__tests__/export-snapshot.test.ts +138 -0
- package/src/react-native/__tests__/full-entry-request.test.ts +143 -0
- package/src/react-native/__tests__/import.test.ts +174 -11
- package/src/react-native/__tests__/storage-discovery.test.ts +144 -0
- package/src/react-native/__tests__/storage-view.test.ts +172 -0
- package/src/react-native/__tests__/stress-verification.test.ts +140 -0
- package/src/react-native/__tests__/use-storage-agent-tools.test.ts +221 -0
- package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
- package/src/react-native/adapters/async-storage.ts +8 -9
- package/src/react-native/adapters/index.ts +2 -8
- package/src/react-native/adapters/mmkv.ts +12 -24
- package/src/react-native/adapters/secure-storage.ts +4 -0
- package/src/react-native/entry-preview-pagination.ts +261 -0
- package/src/react-native/export-snapshot.ts +88 -0
- package/src/react-native/full-entry-request.ts +91 -0
- package/src/react-native/import.ts +179 -16
- package/src/react-native/storage-discovery.ts +92 -0
- package/src/react-native/storage-view.ts +47 -109
- package/src/react-native/useRozeniteStoragePlugin.ts +153 -129
- package/src/react-native/useStorageAgentTools.ts +196 -91
- package/src/shared/__tests__/entry-preview.test.ts +99 -0
- package/src/shared/__tests__/snapshot.test.ts +31 -32
- package/src/shared/agent-tools.ts +33 -38
- package/src/shared/entry-preview.ts +54 -0
- package/src/shared/messaging.ts +140 -21
- package/src/shared/snapshot.ts +12 -18
- package/src/shared/types.ts +21 -4
- package/src/ui/__tests__/binary-value-editor-state.test.ts +16 -13
- package/src/ui/__tests__/binary.test.ts +31 -33
- package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
- package/src/ui/__tests__/large-value-viewer.test.tsx +54 -0
- package/src/ui/__tests__/panel.test.tsx +382 -0
- package/src/ui/__tests__/storage-groups.test.ts +88 -0
- package/src/ui/__tests__/type-conversion.test.ts +8 -24
- package/src/ui/__tests__/use-storage-entries.test.tsx +337 -0
- package/src/ui/add-entry-dialog.tsx +66 -117
- package/src/ui/binary-value-editor-state.ts +16 -25
- package/src/ui/binary-value-editor.tsx +59 -47
- package/src/ui/binary.ts +54 -15
- package/src/ui/edit-entry-dialog.tsx +68 -104
- package/src/ui/editor-switcher.tsx +9 -16
- package/src/ui/entry-detail-dialog.tsx +62 -181
- package/src/ui/format-value.tsx +25 -0
- package/src/ui/globals.css +1 -123
- package/src/ui/import-dialog.tsx +72 -109
- package/src/ui/large-value-viewer-state.ts +25 -0
- package/src/ui/large-value-viewer.tsx +65 -0
- package/src/ui/panel.tsx +649 -509
- package/src/ui/query-client.tsx +23 -0
- package/src/ui/storage-groups.ts +66 -0
- package/src/ui/type-conversion.ts +2 -5
- package/src/ui/typed-value-editor.tsx +15 -20
- package/src/ui/use-storage-entries.ts +192 -0
- package/src/ui/utils.ts +1 -4
- package/tsconfig.json +5 -1
- package/dist/devtools/assets/panel-Bm-SWF7d.js +0 -33
- package/dist/devtools/assets/panel-DIqI4WSp.css +0 -1
- package/postcss.config.js +0 -6
- package/src/ui/confirm-dialog.tsx +0 -100
- package/src/ui/editable-table.tsx +0 -300
- 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: (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
116
|
+
`[Rozenite] Storage Plugin: Failed to set entry in ${target.adapterId}/${target.storageId}.`,
|
|
57
117
|
error,
|
|
58
118
|
);
|
|
59
119
|
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
'
|
|
170
|
-
async (event:
|
|
171
|
-
await
|
|
172
|
-
|
|
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());
|