@rozenite/storage-plugin 1.12.0 → 2.0.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 +37 -0
- package/README.md +6 -1
- package/dist/devtools/assets/panel-B6Wp0eie.css +1 -0
- package/dist/devtools/assets/panel-D7MFc4HG.js +32 -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 -64
- package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
- package/dist/react-native/chunks/secure-storage.require.js +22 -14
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -315
- package/dist/react-native/index.d.ts +154 -18
- package/dist/react-native/index.js +10 -2
- 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 +13 -15
- package/react-native.ts +29 -14
- package/rozenite.config.ts +78 -0
- package/src/react-native/__tests__/entry-preview-pagination.test.ts +262 -0
- package/src/react-native/__tests__/export-snapshot.test.ts +142 -0
- package/src/react-native/__tests__/full-entry-request.test.ts +155 -0
- package/src/react-native/__tests__/import.test.ts +172 -10
- package/src/react-native/__tests__/storage-discovery.test.ts +155 -0
- package/src/react-native/__tests__/storage-view.test.ts +172 -0
- package/src/react-native/__tests__/stress-verification.test.ts +153 -0
- package/src/react-native/__tests__/use-storage-agent-tools.test.ts +231 -0
- package/src/react-native/adapters/async-storage.ts +9 -7
- package/src/react-native/adapters/mmkv.ts +5 -0
- package/src/react-native/adapters/secure-storage.ts +9 -1
- package/src/react-native/entry-preview-pagination.ts +277 -0
- package/src/react-native/export-snapshot.ts +94 -0
- package/src/react-native/full-entry-request.ts +96 -0
- package/src/react-native/import.ts +190 -16
- package/src/react-native/storage-discovery.ts +96 -0
- package/src/react-native/storage-view.ts +45 -103
- package/src/react-native/useRozeniteStoragePlugin.ts +108 -72
- package/src/react-native/useStorageAgentTools.ts +207 -86
- package/src/shared/__tests__/entry-preview.test.ts +117 -0
- package/src/shared/__tests__/snapshot.test.ts +21 -0
- package/src/shared/agent-tools.ts +27 -11
- package/src/shared/entry-preview.ts +59 -0
- package/src/shared/messaging.ts +136 -21
- package/src/shared/snapshot.ts +8 -1
- package/src/shared/types.ts +20 -1
- package/src/ui/__tests__/binary-value-editor-state.test.ts +13 -0
- package/src/ui/__tests__/binary.test.ts +18 -0
- package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
- package/src/ui/__tests__/large-value-viewer.test.tsx +63 -0
- package/src/ui/__tests__/panel.test.tsx +409 -0
- package/src/ui/__tests__/storage-groups.test.ts +91 -0
- package/src/ui/__tests__/use-storage-entries.test.tsx +353 -0
- package/src/ui/add-entry-dialog.tsx +65 -105
- package/src/ui/binary-value-editor-state.ts +10 -1
- package/src/ui/binary-value-editor.tsx +57 -33
- package/src/ui/binary.ts +56 -4
- package/src/ui/edit-entry-dialog.tsx +71 -99
- package/src/ui/editor-switcher.tsx +7 -5
- package/src/ui/entry-detail-dialog.tsx +69 -179
- package/src/ui/format-value.tsx +32 -0
- package/src/ui/globals.css +1 -123
- package/src/ui/import-dialog.tsx +66 -91
- package/src/ui/large-value-viewer-state.ts +32 -0
- package/src/ui/large-value-viewer.tsx +71 -0
- package/src/ui/panel.tsx +724 -496
- package/src/ui/query-client.tsx +34 -0
- package/src/ui/storage-groups.ts +66 -0
- package/src/ui/typed-value-editor.tsx +14 -15
- package/src/ui/use-storage-entries.ts +207 -0
- 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
|
@@ -1,28 +1,176 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
StorageImportEntriesEvent,
|
|
3
|
+
StorageImportPreviewRequestEvent,
|
|
4
|
+
StorageImportPreviewResponseEvent,
|
|
5
|
+
StorageImportProgressEvent,
|
|
3
6
|
StorageImportResultEvent,
|
|
4
|
-
|
|
7
|
+
StorageInvalidatedEvent,
|
|
8
|
+
StorageRequestErrorEvent,
|
|
5
9
|
} from '../shared/messaging';
|
|
10
|
+
import { computePreview, parseSnapshot } from '../shared/snapshot';
|
|
11
|
+
import {
|
|
12
|
+
supportsType,
|
|
13
|
+
type StorageEntry,
|
|
14
|
+
type StorageTarget,
|
|
15
|
+
} from '../shared/types';
|
|
6
16
|
import type { StorageView } from './storage-view';
|
|
7
17
|
|
|
8
18
|
export type ImportEmittedEvent =
|
|
9
|
-
|
|
|
10
|
-
| StorageImportResultEvent
|
|
19
|
+
| StorageImportProgressEvent
|
|
20
|
+
| StorageImportResultEvent
|
|
21
|
+
| StorageInvalidatedEvent;
|
|
22
|
+
|
|
23
|
+
type ImportPreviewResult =
|
|
24
|
+
| StorageImportPreviewResponseEvent
|
|
25
|
+
| StorageRequestErrorEvent;
|
|
26
|
+
|
|
27
|
+
const isSameTarget = (left: StorageTarget, right: StorageTarget) =>
|
|
28
|
+
left.adapterId === right.adapterId && left.storageId === right.storageId;
|
|
29
|
+
|
|
30
|
+
const isTarget = (value: unknown): value is StorageTarget =>
|
|
31
|
+
typeof value === 'object' &&
|
|
32
|
+
value != null &&
|
|
33
|
+
!Array.isArray(value) &&
|
|
34
|
+
typeof (value as { adapterId?: unknown }).adapterId === 'string' &&
|
|
35
|
+
(value as { adapterId: string }).adapterId.trim().length > 0 &&
|
|
36
|
+
typeof (value as { storageId?: unknown }).storageId === 'string' &&
|
|
37
|
+
(value as { storageId: string }).storageId.trim().length > 0;
|
|
38
|
+
|
|
39
|
+
const getRequestId = (payload: unknown) =>
|
|
40
|
+
typeof payload === 'object' &&
|
|
41
|
+
payload != null &&
|
|
42
|
+
!Array.isArray(payload) &&
|
|
43
|
+
typeof (payload as { requestId?: unknown }).requestId === 'string'
|
|
44
|
+
? (payload as { requestId: string }).requestId
|
|
45
|
+
: '';
|
|
46
|
+
|
|
47
|
+
const isEntry = (value: unknown): value is StorageEntry => {
|
|
48
|
+
if (typeof value !== 'object' || value == null || Array.isArray(value)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entry = value as { key?: unknown; type?: unknown; value?: unknown };
|
|
53
|
+
if (typeof entry.key !== 'string') return false;
|
|
54
|
+
if (entry.type === 'string') return typeof entry.value === 'string';
|
|
55
|
+
if (entry.type === 'number') return typeof entry.value === 'number';
|
|
56
|
+
if (entry.type === 'boolean') return typeof entry.value === 'boolean';
|
|
57
|
+
return (
|
|
58
|
+
entry.type === 'buffer' &&
|
|
59
|
+
Array.isArray(entry.value) &&
|
|
60
|
+
entry.value.every(
|
|
61
|
+
(byte) =>
|
|
62
|
+
typeof byte === 'number' &&
|
|
63
|
+
Number.isInteger(byte) &&
|
|
64
|
+
byte >= 0 &&
|
|
65
|
+
byte <= 255,
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const isBlacklisted = (view: StorageView, key: string) => {
|
|
71
|
+
if (!view.blacklist) return false;
|
|
72
|
+
view.blacklist.lastIndex = 0;
|
|
73
|
+
return view.blacklist.test(key);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const findView = (views: readonly StorageView[], target: StorageTarget) =>
|
|
77
|
+
views.find((candidate) => isSameTarget(candidate.target, target));
|
|
78
|
+
|
|
79
|
+
export const handleImportPreviewRequest = async (
|
|
80
|
+
views: readonly StorageView[],
|
|
81
|
+
payload: unknown,
|
|
82
|
+
): Promise<ImportPreviewResult> => {
|
|
83
|
+
const requestId = getRequestId(payload);
|
|
84
|
+
const request = payload as Partial<StorageImportPreviewRequestEvent>;
|
|
85
|
+
const parsedSnapshot = parseSnapshot(request.snapshot);
|
|
86
|
+
if (
|
|
87
|
+
typeof request !== 'object' ||
|
|
88
|
+
request == null ||
|
|
89
|
+
request.type !== 'preview-import' ||
|
|
90
|
+
typeof request.requestId !== 'string' ||
|
|
91
|
+
request.requestId.trim().length === 0 ||
|
|
92
|
+
!isTarget(request.target) ||
|
|
93
|
+
!parsedSnapshot.ok
|
|
94
|
+
) {
|
|
95
|
+
return {
|
|
96
|
+
type: 'storage-request-error',
|
|
97
|
+
requestId,
|
|
98
|
+
code: 'INVALID_REQUEST',
|
|
99
|
+
message:
|
|
100
|
+
'Import preview requests require a valid target, request ID, and snapshot.',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const view = findView(views, request.target);
|
|
105
|
+
if (!view) {
|
|
106
|
+
return {
|
|
107
|
+
type: 'storage-request-error',
|
|
108
|
+
requestId,
|
|
109
|
+
code: 'TARGET_NOT_FOUND',
|
|
110
|
+
message: 'The requested storage target was not found.',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const entryKeys = new Set(await view.getAllKeys());
|
|
116
|
+
return {
|
|
117
|
+
type: 'import-preview',
|
|
118
|
+
requestId,
|
|
119
|
+
target: view.target,
|
|
120
|
+
preview: computePreview(parsedSnapshot.snapshot, {
|
|
121
|
+
target: view.target,
|
|
122
|
+
capabilities: view.capabilities,
|
|
123
|
+
entryKeys,
|
|
124
|
+
isBlacklisted: (key) => isBlacklisted(view, key),
|
|
125
|
+
}),
|
|
126
|
+
};
|
|
127
|
+
} catch {
|
|
128
|
+
return {
|
|
129
|
+
type: 'storage-request-error',
|
|
130
|
+
requestId,
|
|
131
|
+
code: 'READ_FAILED',
|
|
132
|
+
message: 'Failed to inspect the requested storage for import.',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
};
|
|
11
136
|
|
|
12
137
|
export const handleImportEntries = async (
|
|
13
138
|
views: StorageView[],
|
|
14
|
-
|
|
139
|
+
payload: unknown,
|
|
15
140
|
emit: (out: ImportEmittedEvent) => void,
|
|
16
141
|
): Promise<void> => {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
142
|
+
const requestId = getRequestId(payload);
|
|
143
|
+
const event = payload as Partial<StorageImportEntriesEvent>;
|
|
144
|
+
if (
|
|
145
|
+
typeof event !== 'object' ||
|
|
146
|
+
event == null ||
|
|
147
|
+
event.type !== 'import-entries' ||
|
|
148
|
+
typeof event.requestId !== 'string' ||
|
|
149
|
+
event.requestId.trim().length === 0 ||
|
|
150
|
+
!isTarget(event.target) ||
|
|
151
|
+
!Array.isArray(event.entries) ||
|
|
152
|
+
!event.entries.every(isEntry)
|
|
153
|
+
) {
|
|
154
|
+
emit({
|
|
155
|
+
type: 'import-result',
|
|
156
|
+
requestId,
|
|
157
|
+
target: isTarget(event.target)
|
|
158
|
+
? event.target
|
|
159
|
+
: { adapterId: '', storageId: '' },
|
|
160
|
+
ok: false,
|
|
161
|
+
written: 0,
|
|
162
|
+
total: 0,
|
|
163
|
+
error: 'Import requests require a valid target, request ID, and entries.',
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const view = findView(views, event.target);
|
|
22
169
|
|
|
23
170
|
if (!view) {
|
|
24
171
|
emit({
|
|
25
172
|
type: 'import-result',
|
|
173
|
+
requestId: event.requestId,
|
|
26
174
|
target: event.target,
|
|
27
175
|
ok: false,
|
|
28
176
|
written: 0,
|
|
@@ -32,18 +180,33 @@ export const handleImportEntries = async (
|
|
|
32
180
|
return;
|
|
33
181
|
}
|
|
34
182
|
|
|
35
|
-
|
|
36
|
-
|
|
183
|
+
// Recheck policy at apply time so a preview cannot bypass a changed target.
|
|
184
|
+
const entries = event.entries.filter(
|
|
185
|
+
(entry) =>
|
|
186
|
+
supportsType(view.capabilities, entry.type) &&
|
|
187
|
+
!isBlacklisted(view, entry.key),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < entries.length; i++) {
|
|
191
|
+
const entry = entries[i];
|
|
37
192
|
|
|
38
193
|
try {
|
|
39
194
|
await view.set(entry);
|
|
40
195
|
} catch (error) {
|
|
196
|
+
if (i > 0) {
|
|
197
|
+
emit({
|
|
198
|
+
type: 'storage-invalidated',
|
|
199
|
+
target: event.target,
|
|
200
|
+
operation: 'import',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
41
203
|
emit({
|
|
42
204
|
type: 'import-result',
|
|
205
|
+
requestId: event.requestId,
|
|
43
206
|
target: event.target,
|
|
44
207
|
ok: false,
|
|
45
208
|
written: i,
|
|
46
|
-
total:
|
|
209
|
+
total: entries.length,
|
|
47
210
|
failedKey: entry.key,
|
|
48
211
|
error: error instanceof Error ? error.message : String(error),
|
|
49
212
|
});
|
|
@@ -51,17 +214,28 @@ export const handleImportEntries = async (
|
|
|
51
214
|
}
|
|
52
215
|
|
|
53
216
|
emit({
|
|
54
|
-
type: '
|
|
217
|
+
type: 'import-progress',
|
|
218
|
+
requestId: event.requestId,
|
|
219
|
+
target: event.target,
|
|
220
|
+
written: i + 1,
|
|
221
|
+
total: entries.length,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (entries.length > 0) {
|
|
226
|
+
emit({
|
|
227
|
+
type: 'storage-invalidated',
|
|
55
228
|
target: event.target,
|
|
56
|
-
|
|
229
|
+
operation: 'import',
|
|
57
230
|
});
|
|
58
231
|
}
|
|
59
232
|
|
|
60
233
|
emit({
|
|
61
234
|
type: 'import-result',
|
|
235
|
+
requestId: event.requestId,
|
|
62
236
|
target: event.target,
|
|
63
237
|
ok: true,
|
|
64
|
-
written:
|
|
65
|
-
total:
|
|
238
|
+
written: entries.length,
|
|
239
|
+
total: entries.length,
|
|
66
240
|
});
|
|
67
241
|
};
|
|
@@ -0,0 +1,96 @@
|
|
|
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 =
|
|
10
|
+
| StorageDiscoverStoragesResponseEvent
|
|
11
|
+
| StorageRequestErrorEvent;
|
|
12
|
+
|
|
13
|
+
const getRequestId = (payload: unknown) => {
|
|
14
|
+
if (
|
|
15
|
+
typeof payload === 'object' &&
|
|
16
|
+
payload != null &&
|
|
17
|
+
!Array.isArray(payload) &&
|
|
18
|
+
typeof (payload as { requestId?: unknown }).requestId === 'string'
|
|
19
|
+
) {
|
|
20
|
+
return (payload as { requestId: string }).requestId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return '';
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const validateRequest = (
|
|
27
|
+
payload: unknown,
|
|
28
|
+
): payload is StorageDiscoverStoragesRequestEvent =>
|
|
29
|
+
typeof payload === 'object' &&
|
|
30
|
+
payload != null &&
|
|
31
|
+
!Array.isArray(payload) &&
|
|
32
|
+
(payload as { type?: unknown }).type === 'discover-storages' &&
|
|
33
|
+
typeof (payload as { requestId?: unknown }).requestId === 'string' &&
|
|
34
|
+
(payload as { requestId: string }).requestId.trim().length > 0;
|
|
35
|
+
|
|
36
|
+
const toDescriptor = async (view: StorageView): Promise<StorageDescriptor> => ({
|
|
37
|
+
target: view.target,
|
|
38
|
+
adapterName: view.adapterName,
|
|
39
|
+
storageName: view.storageName,
|
|
40
|
+
entryCount: (await view.getAllKeys()).length,
|
|
41
|
+
capabilities: view.capabilities,
|
|
42
|
+
supportsSubscriptions: view.supportsSubscriptions,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const hasDuplicateTargets = (views: readonly Pick<StorageView, 'target'>[]) => {
|
|
46
|
+
const targets = new Set<string>();
|
|
47
|
+
|
|
48
|
+
return views.some(({ target }) => {
|
|
49
|
+
const id = `${target.adapterId}:${target.storageId}`;
|
|
50
|
+
|
|
51
|
+
if (targets.has(id)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
targets.add(id);
|
|
56
|
+
return false;
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const handleStorageDiscoveryRequest = async (
|
|
61
|
+
views: readonly StorageView[],
|
|
62
|
+
payload: unknown,
|
|
63
|
+
): Promise<DiscoveryResult> => {
|
|
64
|
+
const requestId = getRequestId(payload);
|
|
65
|
+
|
|
66
|
+
if (!validateRequest(payload)) {
|
|
67
|
+
return {
|
|
68
|
+
type: 'storage-request-error',
|
|
69
|
+
requestId,
|
|
70
|
+
code: 'INVALID_REQUEST',
|
|
71
|
+
message: 'Storage discovery requires a non-empty request ID.',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
if (hasDuplicateTargets(views)) {
|
|
77
|
+
return {
|
|
78
|
+
type: 'storage-request-error',
|
|
79
|
+
requestId,
|
|
80
|
+
code: 'INVALID_REQUEST',
|
|
81
|
+
message: 'Configured storages must have unique targets.',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const storages = await Promise.all(views.map(toDescriptor));
|
|
86
|
+
|
|
87
|
+
return { type: 'storage-descriptors', requestId, storages };
|
|
88
|
+
} catch {
|
|
89
|
+
return {
|
|
90
|
+
type: 'storage-request-error',
|
|
91
|
+
requestId,
|
|
92
|
+
code: 'READ_FAILED',
|
|
93
|
+
message: 'Failed to discover configured storages.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
};
|
|
@@ -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,
|
|
@@ -208,40 +137,53 @@ export const createStorageView = (
|
|
|
208
137
|
adapterName: adapter.name,
|
|
209
138
|
storageName: storageNode.name,
|
|
210
139
|
capabilities: storageNode.capabilities,
|
|
140
|
+
supportsSubscriptions: subscribe != null,
|
|
211
141
|
blacklist: storageNode.blacklist,
|
|
212
142
|
get,
|
|
213
143
|
set: async (entry) => {
|
|
214
144
|
checkTypeSupport(storageNode.capabilities, entry.type, target);
|
|
215
|
-
|
|
145
|
+
locallyMutatingKeys.add(entry.key);
|
|
146
|
+
try {
|
|
147
|
+
await setEntry(storage, entry);
|
|
148
|
+
} finally {
|
|
149
|
+
locallyMutatingKeys.delete(entry.key);
|
|
150
|
+
}
|
|
216
151
|
},
|
|
217
152
|
delete: async (key) => {
|
|
218
|
-
|
|
153
|
+
locallyMutatingKeys.add(key);
|
|
154
|
+
try {
|
|
155
|
+
await deleteEntry(storage, key);
|
|
156
|
+
} finally {
|
|
157
|
+
locallyMutatingKeys.delete(key);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
purge: async () => {
|
|
161
|
+
isLocallyPurging = true;
|
|
162
|
+
try {
|
|
163
|
+
await clearStorage(storage);
|
|
164
|
+
} finally {
|
|
165
|
+
isLocallyPurging = false;
|
|
166
|
+
}
|
|
219
167
|
},
|
|
220
168
|
getAllKeys: async () => {
|
|
221
169
|
const keys = await getAllKeys(storage);
|
|
222
170
|
return keys.filter((key) => !shouldFilterKey(storageNode, key));
|
|
223
171
|
},
|
|
224
172
|
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
|
-
},
|
|
173
|
+
...(subscribe
|
|
174
|
+
? {
|
|
175
|
+
watch: async (onChange) =>
|
|
176
|
+
subscribe((key) => {
|
|
177
|
+
if (
|
|
178
|
+
!isLocallyPurging &&
|
|
179
|
+
!locallyMutatingKeys.has(key) &&
|
|
180
|
+
!shouldFilterKey(storageNode, key)
|
|
181
|
+
) {
|
|
182
|
+
onChange(key);
|
|
183
|
+
}
|
|
184
|
+
}),
|
|
185
|
+
}
|
|
186
|
+
: {}),
|
|
245
187
|
};
|
|
246
188
|
};
|
|
247
189
|
|