@rozenite/storage-plugin 1.4.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 +28 -0
- package/LICENSE +20 -0
- package/README.md +77 -0
- package/dist/assets/panel-C7usUotG.js +22 -0
- package/dist/assets/panel-DMhXYHH4.css +1 -0
- package/dist/panel.html +31 -0
- package/dist/react-native.cjs +1 -0
- package/dist/react-native.d.ts +4 -0
- package/dist/react-native.js +200 -0
- package/dist/rozenite.config.d.ts +7 -0
- package/dist/rozenite.json +1 -0
- package/dist/src/react-native/adapters/async-storage.d.ts +33 -0
- package/dist/src/react-native/adapters/index.d.ts +6 -0
- package/dist/src/react-native/adapters/mmkv.d.ts +13 -0
- package/dist/src/react-native/adapters/secure-storage.d.ts +18 -0
- package/dist/src/react-native/is-garbled.d.ts +1 -0
- package/dist/src/react-native/storage-view.d.ts +19 -0
- package/dist/src/react-native/useRozeniteStoragePlugin.d.ts +6 -0
- package/dist/src/shared/messaging.d.ts +29 -0
- package/dist/src/shared/types.d.ts +60 -0
- package/dist/src/ui/add-entry-dialog.d.ts +9 -0
- package/dist/src/ui/confirm-dialog.d.ts +11 -0
- package/dist/src/ui/edit-entry-dialog.d.ts +9 -0
- package/dist/src/ui/editable-table.d.ts +10 -0
- package/dist/src/ui/entry-detail-dialog.d.ts +8 -0
- package/dist/src/ui/panel.d.ts +1 -0
- package/dist/types.cjs +1 -0
- package/dist/types.js +11 -0
- package/dist/useRozeniteStoragePlugin.cjs +1 -0
- package/dist/useRozeniteStoragePlugin.js +212 -0
- package/package.json +59 -0
- package/postcss.config.js +6 -0
- package/react-native.ts +40 -0
- package/rozenite.config.ts +8 -0
- package/src/css-modules.d.ts +4 -0
- package/src/react-native/adapters/async-storage.ts +115 -0
- package/src/react-native/adapters/index.ts +19 -0
- package/src/react-native/adapters/mmkv.ts +187 -0
- package/src/react-native/adapters/secure-storage.ts +76 -0
- package/src/react-native/is-garbled.ts +17 -0
- package/src/react-native/storage-view.ts +245 -0
- package/src/react-native/useRozeniteStoragePlugin.ts +162 -0
- package/src/shared/messaging.ts +37 -0
- package/src/shared/types.ts +66 -0
- package/src/ui/add-entry-dialog.tsx +310 -0
- package/src/ui/confirm-dialog.tsx +100 -0
- package/src/ui/edit-entry-dialog.tsx +280 -0
- package/src/ui/editable-table.tsx +298 -0
- package/src/ui/entry-detail-dialog.tsx +220 -0
- package/src/ui/globals.css +123 -0
- package/src/ui/panel.tsx +442 -0
- package/tailwind.config.ts +94 -0
- package/tsconfig.json +36 -0
- package/vite.config.ts +20 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { StorageAdapter, StorageNode } from '../../shared/types';
|
|
2
|
+
|
|
3
|
+
export type SecureStorageLike = {
|
|
4
|
+
getItemAsync: (key: string) => Promise<string | null>;
|
|
5
|
+
setItemAsync: (key: string, value: string) => Promise<void>;
|
|
6
|
+
deleteItemAsync: (key: string) => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type KeySource = string[] | (() => Promise<string[]>);
|
|
10
|
+
|
|
11
|
+
export type CreateExpoSecureStorageAdapterOptions = {
|
|
12
|
+
storage: SecureStorageLike;
|
|
13
|
+
keys: KeySource;
|
|
14
|
+
adapterId?: string;
|
|
15
|
+
adapterName?: string;
|
|
16
|
+
storageId?: string;
|
|
17
|
+
storageName?: string;
|
|
18
|
+
blacklist?: RegExp;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const resolveKeys = async (source: KeySource) => {
|
|
22
|
+
if (Array.isArray(source)) {
|
|
23
|
+
return source;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return source();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const createExpoSecureStorageAdapter = ({
|
|
30
|
+
storage,
|
|
31
|
+
keys,
|
|
32
|
+
adapterId = 'expo-secure-store',
|
|
33
|
+
adapterName = 'Expo SecureStore',
|
|
34
|
+
storageId = 'default',
|
|
35
|
+
storageName = 'Default Secure Storage',
|
|
36
|
+
blacklist,
|
|
37
|
+
}: CreateExpoSecureStorageAdapterOptions): StorageAdapter => {
|
|
38
|
+
const storageNode: StorageNode = {
|
|
39
|
+
id: storageId,
|
|
40
|
+
name: storageName,
|
|
41
|
+
blacklist,
|
|
42
|
+
capabilities: {
|
|
43
|
+
supportedTypes: ['string'],
|
|
44
|
+
},
|
|
45
|
+
storage: {
|
|
46
|
+
kind: 'async',
|
|
47
|
+
getAllKeys: () => resolveKeys(keys),
|
|
48
|
+
get: async (key) => {
|
|
49
|
+
const value = await storage.getItemAsync(key);
|
|
50
|
+
if (value === null) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
key,
|
|
56
|
+
type: 'string',
|
|
57
|
+
value,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
set: async (entry) => {
|
|
61
|
+
if (entry.type !== 'string') {
|
|
62
|
+
throw new Error('Expo SecureStore adapter supports only string values.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await storage.setItemAsync(entry.key, entry.value);
|
|
66
|
+
},
|
|
67
|
+
delete: (key) => storage.deleteItemAsync(key),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
id: adapterId,
|
|
73
|
+
name: adapterName,
|
|
74
|
+
storages: [storageNode],
|
|
75
|
+
};
|
|
76
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// This is a heuristic to determine if a string is garbled.
|
|
2
|
+
export const looksLikeGarbled = (str: string): boolean => {
|
|
3
|
+
// 1. Check for replacement character (�)
|
|
4
|
+
if (str.includes('\uFFFD')) return true;
|
|
5
|
+
|
|
6
|
+
// 2. Check for unusual control characters
|
|
7
|
+
// eslint-disable-next-line no-control-regex
|
|
8
|
+
const controlChars = /[\u0000-\u001F\u007F-\u009F]/;
|
|
9
|
+
if (controlChars.test(str)) return true;
|
|
10
|
+
|
|
11
|
+
// 3. Optionally, check if most chars are non-printable
|
|
12
|
+
const printableRatio =
|
|
13
|
+
[...str].filter((c) => c >= ' ' && c <= '~').length / str.length;
|
|
14
|
+
if (printableRatio < 0.7) return true; // mostly non-printable → probably binary
|
|
15
|
+
|
|
16
|
+
return false; // seems like valid string
|
|
17
|
+
};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getStorageViewId,
|
|
3
|
+
supportsType,
|
|
4
|
+
type AsyncStorage,
|
|
5
|
+
type StorageAdapter,
|
|
6
|
+
type StorageCapabilities,
|
|
7
|
+
type StorageEntry,
|
|
8
|
+
type StorageEntryType,
|
|
9
|
+
type StorageNode,
|
|
10
|
+
type StorageSubscription,
|
|
11
|
+
type StorageTarget,
|
|
12
|
+
type SyncStorage,
|
|
13
|
+
} from '../shared/types';
|
|
14
|
+
|
|
15
|
+
const POLLING_INTERVAL_MS = 1500;
|
|
16
|
+
|
|
17
|
+
type StorageSnapshotMap = Map<string, StorageEntry>;
|
|
18
|
+
|
|
19
|
+
type AsyncStorageLike = SyncStorage | AsyncStorage;
|
|
20
|
+
|
|
21
|
+
const isAsyncStorage = (storage: AsyncStorageLike): storage is AsyncStorage =>
|
|
22
|
+
storage.kind === 'async';
|
|
23
|
+
|
|
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
|
+
const shouldFilterKey = (storage: StorageNode, key: string) => {
|
|
37
|
+
if (!storage.blacklist) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
storage.blacklist.lastIndex = 0;
|
|
42
|
+
return storage.blacklist.test(key);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const checkTypeSupport = (
|
|
46
|
+
capabilities: StorageCapabilities,
|
|
47
|
+
type: StorageEntryType,
|
|
48
|
+
target: StorageTarget
|
|
49
|
+
) => {
|
|
50
|
+
if (supportsType(capabilities, type)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Type "${type}" is not supported by storage "${target.storageId}" in adapter "${target.adapterId}".`
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getAllKeys = async (storage: AsyncStorageLike) => {
|
|
60
|
+
if (isAsyncStorage(storage)) {
|
|
61
|
+
return storage.getAllKeys();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return storage.getAllKeys();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const getEntry = async (storage: AsyncStorageLike, key: string) => {
|
|
68
|
+
if (isAsyncStorage(storage)) {
|
|
69
|
+
return storage.get(key);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return storage.get(key);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const setEntry = async (storage: AsyncStorageLike, entry: StorageEntry) => {
|
|
76
|
+
if (isAsyncStorage(storage)) {
|
|
77
|
+
await storage.set(entry);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
storage.set(entry);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const deleteEntry = async (storage: AsyncStorageLike, key: string) => {
|
|
85
|
+
if (isAsyncStorage(storage)) {
|
|
86
|
+
await storage.delete(key);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
storage.delete(key);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type StorageView = {
|
|
94
|
+
id: string;
|
|
95
|
+
target: StorageTarget;
|
|
96
|
+
adapterName: string;
|
|
97
|
+
storageName: string;
|
|
98
|
+
capabilities: StorageCapabilities;
|
|
99
|
+
get: (key: string) => Promise<StorageEntry | undefined>;
|
|
100
|
+
set: (entry: StorageEntry) => Promise<void>;
|
|
101
|
+
delete: (key: string) => Promise<void>;
|
|
102
|
+
getAllKeys: () => Promise<string[]>;
|
|
103
|
+
getAllEntries: () => Promise<StorageEntry[]>;
|
|
104
|
+
watch: (callbacks: {
|
|
105
|
+
onSet: (entry: StorageEntry) => void;
|
|
106
|
+
onDelete: (key: string) => void;
|
|
107
|
+
}) => Promise<StorageSubscription>;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const buildSnapshotMap = async (
|
|
111
|
+
getAllEntries: () => Promise<StorageEntry[]>
|
|
112
|
+
): Promise<StorageSnapshotMap> => {
|
|
113
|
+
const entries = await getAllEntries();
|
|
114
|
+
return toSnapshotMap(entries);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const diffSnapshots = (
|
|
118
|
+
previous: StorageSnapshotMap,
|
|
119
|
+
next: StorageSnapshotMap,
|
|
120
|
+
handlers: {
|
|
121
|
+
onSet: (entry: StorageEntry) => void;
|
|
122
|
+
onDelete: (key: string) => void;
|
|
123
|
+
}
|
|
124
|
+
) => {
|
|
125
|
+
next.forEach((nextEntry, key) => {
|
|
126
|
+
const previousEntry = previous.get(key);
|
|
127
|
+
|
|
128
|
+
if (!previousEntry) {
|
|
129
|
+
handlers.onSet(nextEntry);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (fingerprintEntry(previousEntry) !== fingerprintEntry(nextEntry)) {
|
|
134
|
+
handlers.onSet(nextEntry);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
previous.forEach((_value, key) => {
|
|
139
|
+
if (!next.has(key)) {
|
|
140
|
+
handlers.onDelete(key);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const createPollingSubscription = async (
|
|
146
|
+
getAllEntries: () => Promise<StorageEntry[]>,
|
|
147
|
+
handlers: {
|
|
148
|
+
onSet: (entry: StorageEntry) => void;
|
|
149
|
+
onDelete: (key: string) => void;
|
|
150
|
+
}
|
|
151
|
+
): Promise<StorageSubscription> => {
|
|
152
|
+
let previousSnapshot = await buildSnapshotMap(getAllEntries);
|
|
153
|
+
|
|
154
|
+
const interval = setInterval(async () => {
|
|
155
|
+
try {
|
|
156
|
+
const nextSnapshot = await buildSnapshotMap(getAllEntries);
|
|
157
|
+
diffSnapshots(previousSnapshot, nextSnapshot, handlers);
|
|
158
|
+
previousSnapshot = nextSnapshot;
|
|
159
|
+
} catch {
|
|
160
|
+
// Silently ignore polling errors and try again on next tick.
|
|
161
|
+
}
|
|
162
|
+
}, POLLING_INTERVAL_MS);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
remove: () => {
|
|
166
|
+
clearInterval(interval);
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const createStorageView = (
|
|
172
|
+
adapter: StorageAdapter,
|
|
173
|
+
storageNode: StorageNode
|
|
174
|
+
): StorageView => {
|
|
175
|
+
const storage = storageNode.storage;
|
|
176
|
+
const target: StorageTarget = {
|
|
177
|
+
adapterId: adapter.id,
|
|
178
|
+
storageId: storageNode.id,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const get = async (key: string) => {
|
|
182
|
+
if (shouldFilterKey(storageNode, key)) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return getEntry(storage, key);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const getAllEntries = async () => {
|
|
190
|
+
const keys = await getAllKeys(storage);
|
|
191
|
+
const visibleEntries = await Promise.all(
|
|
192
|
+
keys
|
|
193
|
+
.filter((key) => !shouldFilterKey(storageNode, key))
|
|
194
|
+
.map((key) => getEntry(storage, key))
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return visibleEntries.filter((entry): entry is StorageEntry => !!entry);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
id: getStorageViewId(target),
|
|
202
|
+
target,
|
|
203
|
+
adapterName: adapter.name,
|
|
204
|
+
storageName: storageNode.name,
|
|
205
|
+
capabilities: storageNode.capabilities,
|
|
206
|
+
get,
|
|
207
|
+
set: async (entry) => {
|
|
208
|
+
checkTypeSupport(storageNode.capabilities, entry.type, target);
|
|
209
|
+
await setEntry(storage, entry);
|
|
210
|
+
},
|
|
211
|
+
delete: async (key) => {
|
|
212
|
+
await deleteEntry(storage, key);
|
|
213
|
+
},
|
|
214
|
+
getAllKeys: async () => {
|
|
215
|
+
const keys = await getAllKeys(storage);
|
|
216
|
+
return keys.filter((key) => !shouldFilterKey(storageNode, key));
|
|
217
|
+
},
|
|
218
|
+
getAllEntries,
|
|
219
|
+
watch: async ({ onSet, onDelete }) => {
|
|
220
|
+
if (storage.subscribe) {
|
|
221
|
+
return storage.subscribe(async (key) => {
|
|
222
|
+
try {
|
|
223
|
+
const entry = await get(key);
|
|
224
|
+
|
|
225
|
+
if (!entry) {
|
|
226
|
+
onDelete(key);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
onSet(entry);
|
|
231
|
+
} catch {
|
|
232
|
+
// Ignore runtime callback errors; polling fallback is not needed when subscribe exists.
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return createPollingSubscription(getAllEntries, { onSet, onDelete });
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export const createStorageViews = (storages: StorageAdapter[]) =>
|
|
243
|
+
storages.flatMap((adapter) =>
|
|
244
|
+
adapter.storages.map((storageNode) => createStorageView(adapter, storageNode))
|
|
245
|
+
);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
|
|
2
|
+
import { useEffect, useMemo } from 'react';
|
|
3
|
+
import type {
|
|
4
|
+
StorageDeleteEntryEvent,
|
|
5
|
+
StorageEventMap,
|
|
6
|
+
StorageGetSnapshotEvent,
|
|
7
|
+
StorageSetEntryEvent,
|
|
8
|
+
} from '../shared/messaging';
|
|
9
|
+
import type { StorageAdapter } from '../shared/types';
|
|
10
|
+
import { createStorageViews } from './storage-view';
|
|
11
|
+
|
|
12
|
+
export type RozeniteStoragePluginOptions = {
|
|
13
|
+
storages: StorageAdapter[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const useRozeniteStoragePlugin = ({
|
|
17
|
+
storages,
|
|
18
|
+
}: RozeniteStoragePluginOptions) => {
|
|
19
|
+
const views = useMemo(() => createStorageViews(storages), [storages]);
|
|
20
|
+
|
|
21
|
+
const client = useRozeniteDevToolsClient<StorageEventMap>({
|
|
22
|
+
pluginId: '@rozenite/storage-plugin',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!client) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pushSnapshot = async (viewId?: string) => {
|
|
31
|
+
const selectedViews = viewId ? views.filter((view) => view.id === viewId) : views;
|
|
32
|
+
|
|
33
|
+
for (const view of selectedViews) {
|
|
34
|
+
try {
|
|
35
|
+
const entries = await view.getAllEntries();
|
|
36
|
+
client.send('snapshot', {
|
|
37
|
+
type: 'snapshot',
|
|
38
|
+
target: view.target,
|
|
39
|
+
adapterName: view.adapterName,
|
|
40
|
+
storageName: view.storageName,
|
|
41
|
+
capabilities: view.capabilities,
|
|
42
|
+
entries,
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.warn(
|
|
46
|
+
`[Rozenite] Storage Plugin: Failed to snapshot ${view.target.adapterId}/${view.target.storageId}.`,
|
|
47
|
+
error
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
void pushSnapshot();
|
|
54
|
+
|
|
55
|
+
const viewSubscriptions: { remove: () => void }[] = [];
|
|
56
|
+
let disposed = false;
|
|
57
|
+
|
|
58
|
+
// Prevent one storage watcher failure from breaking the whole plugin.
|
|
59
|
+
void Promise.all(
|
|
60
|
+
views.map(async (view) => {
|
|
61
|
+
try {
|
|
62
|
+
const subscription = await view.watch({
|
|
63
|
+
onSet: (entry) => {
|
|
64
|
+
client.send('set-entry', {
|
|
65
|
+
type: 'set-entry',
|
|
66
|
+
target: view.target,
|
|
67
|
+
entry,
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
onDelete: (key) => {
|
|
71
|
+
client.send('delete-entry', {
|
|
72
|
+
type: 'delete-entry',
|
|
73
|
+
target: view.target,
|
|
74
|
+
key,
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (disposed) {
|
|
80
|
+
subscription.remove();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
viewSubscriptions.push(subscription);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.warn(
|
|
87
|
+
`[Rozenite] Storage Plugin: Failed to attach watcher for ${view.target.adapterId}/${view.target.storageId}.`,
|
|
88
|
+
error
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const messageSubscriptions = [
|
|
95
|
+
client.onMessage('set-entry', async ({ target, entry }: StorageSetEntryEvent) => {
|
|
96
|
+
const view = views.find(
|
|
97
|
+
(candidate) =>
|
|
98
|
+
candidate.target.adapterId === target.adapterId &&
|
|
99
|
+
candidate.target.storageId === target.storageId
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (!view) {
|
|
103
|
+
console.warn(
|
|
104
|
+
`[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await view.set(entry);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn(
|
|
113
|
+
`[Rozenite] Storage Plugin: Failed to set entry in ${target.adapterId}/${target.storageId}.`,
|
|
114
|
+
error
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}),
|
|
118
|
+
client.onMessage(
|
|
119
|
+
'delete-entry',
|
|
120
|
+
async ({ target, key }: StorageDeleteEntryEvent) => {
|
|
121
|
+
const view = views.find(
|
|
122
|
+
(candidate) =>
|
|
123
|
+
candidate.target.adapterId === target.adapterId &&
|
|
124
|
+
candidate.target.storageId === target.storageId
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (!view) {
|
|
128
|
+
console.warn(
|
|
129
|
+
`[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await view.delete(key);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.warn(
|
|
138
|
+
`[Rozenite] Storage Plugin: Failed to delete entry in ${target.adapterId}/${target.storageId}.`,
|
|
139
|
+
error
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
),
|
|
144
|
+
client.onMessage('get-snapshot', async ({ target }: StorageGetSnapshotEvent) => {
|
|
145
|
+
if (target === 'all') {
|
|
146
|
+
await pushSnapshot();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await pushSnapshot(`${target.adapterId}:${target.storageId}`);
|
|
151
|
+
}),
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
return () => {
|
|
155
|
+
disposed = true;
|
|
156
|
+
viewSubscriptions.forEach((subscription) => subscription.remove());
|
|
157
|
+
messageSubscriptions.forEach((subscription) => subscription.remove());
|
|
158
|
+
};
|
|
159
|
+
}, [client, views]);
|
|
160
|
+
|
|
161
|
+
return client;
|
|
162
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { StorageCapabilities, StorageEntry, StorageTarget } from './types';
|
|
2
|
+
|
|
3
|
+
export type StorageSnapshotEvent = {
|
|
4
|
+
type: 'snapshot';
|
|
5
|
+
target: StorageTarget;
|
|
6
|
+
adapterName: string;
|
|
7
|
+
storageName: string;
|
|
8
|
+
capabilities: StorageCapabilities;
|
|
9
|
+
entries: StorageEntry[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type StorageSetEntryEvent = {
|
|
13
|
+
type: 'set-entry';
|
|
14
|
+
target: StorageTarget;
|
|
15
|
+
entry: StorageEntry;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type StorageDeleteEntryEvent = {
|
|
19
|
+
type: 'delete-entry';
|
|
20
|
+
target: StorageTarget;
|
|
21
|
+
key: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type StorageGetSnapshotEvent = {
|
|
25
|
+
type: 'get-snapshot';
|
|
26
|
+
target: StorageTarget | 'all';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type StorageEvent =
|
|
30
|
+
| StorageSnapshotEvent
|
|
31
|
+
| StorageSetEntryEvent
|
|
32
|
+
| StorageDeleteEntryEvent
|
|
33
|
+
| StorageGetSnapshotEvent;
|
|
34
|
+
|
|
35
|
+
export type StorageEventMap = {
|
|
36
|
+
[K in StorageEvent['type']]: Extract<StorageEvent, { type: K }>;
|
|
37
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type StorageEntry =
|
|
2
|
+
| { key: string; type: 'string'; value: string }
|
|
3
|
+
| { key: string; type: 'number'; value: number }
|
|
4
|
+
| { key: string; type: 'boolean'; value: boolean }
|
|
5
|
+
| { key: string; type: 'buffer'; value: number[] };
|
|
6
|
+
|
|
7
|
+
export type StorageEntryType = StorageEntry['type'];
|
|
8
|
+
export type StorageEntryValue = StorageEntry['value'];
|
|
9
|
+
|
|
10
|
+
export type StorageCapabilities = {
|
|
11
|
+
supportedTypes: StorageEntryType[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type StorageSubscription = { remove: () => void };
|
|
15
|
+
|
|
16
|
+
export type SyncStorage = {
|
|
17
|
+
kind: 'sync';
|
|
18
|
+
getAllKeys: () => string[];
|
|
19
|
+
get: (key: string) => StorageEntry | undefined;
|
|
20
|
+
set: (entry: StorageEntry) => void;
|
|
21
|
+
delete: (key: string) => void;
|
|
22
|
+
subscribe?: (callback: (key: string) => void) => StorageSubscription;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type AsyncStorage = {
|
|
26
|
+
kind: 'async';
|
|
27
|
+
getAllKeys: () => Promise<string[]>;
|
|
28
|
+
get: (key: string) => Promise<StorageEntry | undefined>;
|
|
29
|
+
set: (entry: StorageEntry) => Promise<void>;
|
|
30
|
+
delete: (key: string) => Promise<void>;
|
|
31
|
+
subscribe?: (callback: (key: string) => void) => StorageSubscription;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type StorageNode = {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
storage: SyncStorage | AsyncStorage;
|
|
38
|
+
capabilities: StorageCapabilities;
|
|
39
|
+
blacklist?: RegExp;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type StorageAdapter = {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
storages: StorageNode[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type StorageTarget = {
|
|
49
|
+
adapterId: string;
|
|
50
|
+
storageId: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const DEFAULT_SUPPORTED_TYPES: StorageEntryType[] = [
|
|
54
|
+
'string',
|
|
55
|
+
'number',
|
|
56
|
+
'boolean',
|
|
57
|
+
'buffer',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export const getStorageViewId = ({ adapterId, storageId }: StorageTarget) =>
|
|
61
|
+
`${adapterId}:${storageId}`;
|
|
62
|
+
|
|
63
|
+
export const supportsType = (
|
|
64
|
+
capabilities: StorageCapabilities,
|
|
65
|
+
type: StorageEntryType
|
|
66
|
+
) => capabilities.supportedTypes.includes(type);
|