@rozenite/storage-plugin 2.2.0 → 2.3.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 +16 -0
- package/dist/devtools/assets/panel-CcThj-0Z.js +32 -0
- package/dist/devtools/assets/panel-DTYbHqnH.css +1 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/cjs/package.json +3 -0
- package/dist/react-native/cjs/react-native.js +38 -0
- package/dist/react-native/cjs/src/react-native/adapters/async-storage.js +60 -0
- package/dist/react-native/cjs/src/react-native/adapters/index.js +10 -0
- package/dist/react-native/cjs/src/react-native/adapters/mmkv.js +143 -0
- package/dist/react-native/cjs/src/react-native/adapters/secure-storage.js +51 -0
- package/dist/react-native/cjs/src/react-native/entry-preview-pagination.js +198 -0
- package/dist/react-native/cjs/src/react-native/export-snapshot.js +70 -0
- package/dist/react-native/cjs/src/react-native/full-entry-request.js +72 -0
- package/dist/react-native/cjs/src/react-native/import.js +185 -0
- package/dist/react-native/cjs/src/react-native/storage-discovery.js +69 -0
- package/dist/react-native/cjs/src/react-native/storage-view.js +128 -0
- package/dist/react-native/cjs/src/react-native/useRozeniteStoragePlugin.js +145 -0
- package/dist/react-native/cjs/src/react-native/useStorageAgentTools.js +228 -0
- package/dist/react-native/cjs/src/shared/agent-tools.js +119 -0
- package/dist/react-native/cjs/src/shared/entry-preview.js +48 -0
- package/dist/react-native/cjs/src/shared/messaging.js +2 -0
- package/dist/react-native/cjs/src/shared/snapshot.js +185 -0
- package/dist/react-native/cjs/src/shared/types.js +13 -0
- package/dist/react-native/package.json +3 -0
- package/dist/react-native/react-native.d.ts +7 -0
- package/dist/react-native/react-native.js +40 -0
- package/dist/react-native/src/react-native/adapters/async-storage.d.ts +34 -0
- package/dist/react-native/src/react-native/adapters/async-storage.js +56 -0
- package/dist/react-native/src/react-native/adapters/index.d.ts +6 -0
- package/dist/react-native/src/react-native/adapters/index.js +3 -0
- package/dist/react-native/src/react-native/adapters/mmkv.d.ts +13 -0
- package/dist/react-native/src/react-native/adapters/mmkv.js +139 -0
- package/dist/react-native/src/react-native/adapters/secure-storage.d.ts +18 -0
- package/dist/react-native/src/react-native/adapters/secure-storage.js +47 -0
- package/dist/react-native/src/react-native/entry-preview-pagination.d.ts +7 -0
- package/dist/react-native/src/react-native/entry-preview-pagination.js +194 -0
- package/dist/react-native/src/react-native/export-snapshot.d.ts +5 -0
- package/dist/react-native/src/react-native/export-snapshot.js +66 -0
- package/dist/react-native/src/react-native/full-entry-request.d.ts +5 -0
- package/dist/react-native/src/react-native/full-entry-request.js +68 -0
- package/dist/react-native/src/react-native/import.d.ts +7 -0
- package/dist/react-native/src/react-native/import.js +180 -0
- package/dist/react-native/src/react-native/storage-discovery.d.ts +5 -0
- package/dist/react-native/src/react-native/storage-discovery.js +65 -0
- package/dist/react-native/src/react-native/storage-view.d.ts +19 -0
- package/dist/react-native/src/react-native/storage-view.js +123 -0
- package/dist/react-native/src/react-native/useRozeniteStoragePlugin.d.ts +6 -0
- package/dist/react-native/src/react-native/useRozeniteStoragePlugin.js +141 -0
- package/dist/react-native/src/react-native/useStorageAgentTools.d.ts +37 -0
- package/dist/react-native/src/react-native/useStorageAgentTools.js +223 -0
- package/dist/react-native/src/shared/agent-tools.d.ts +76 -0
- package/dist/react-native/src/shared/agent-tools.js +116 -0
- package/dist/react-native/src/shared/entry-preview.d.ts +4 -0
- package/dist/react-native/src/shared/entry-preview.js +44 -0
- package/dist/react-native/src/shared/messaging.d.ts +134 -0
- package/dist/react-native/src/shared/messaging.js +1 -0
- package/dist/react-native/src/shared/snapshot.d.ts +53 -0
- package/dist/react-native/src/shared/snapshot.js +179 -0
- package/dist/react-native/src/shared/types.d.ts +78 -0
- package/dist/react-native/src/shared/types.js +8 -0
- package/dist/rozenite.json +1 -1
- package/dist/sdk/package.json +3 -0
- package/dist/sdk/sdk.d.ts +44 -0
- package/dist/sdk/sdk.js +9 -0
- package/dist/sdk/src/shared/agent-tools.d.ts +76 -0
- package/dist/sdk/src/shared/agent-tools.js +119 -0
- package/dist/sdk/src/shared/types.d.ts +78 -0
- package/dist/sdk/src/shared/types.js +13 -0
- package/package.json +16 -16
- package/react-native.ts +8 -1
- package/rozenite.config.ts +1 -0
- package/src/__tests__/release-bundle.test.ts +32 -0
- package/dist/devtools/assets/panel-D5z1BY8U.js +0 -32
- package/dist/devtools/assets/panel-DVlWRg6d.css +0 -1
- package/dist/react-native/chunks/async-storage.require.cjs +0 -1
- package/dist/react-native/chunks/async-storage.require.js +0 -53
- package/dist/react-native/chunks/index.require.cjs +0 -1
- package/dist/react-native/chunks/index.require.js +0 -103
- package/dist/react-native/chunks/secure-storage.require.cjs +0 -1
- package/dist/react-native/chunks/secure-storage.require.js +0 -46
- package/dist/react-native/chunks/types.cjs +0 -1
- package/dist/react-native/chunks/types.js +0 -11
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +0 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +0 -1037
- package/dist/react-native/index.cjs +0 -1
- package/dist/react-native/index.d.ts +0 -365
- package/dist/react-native/index.js +0 -28
- package/dist/sdk/index.cjs +0 -1
- package/dist/sdk/index.d.ts +0 -206
- package/dist/sdk/index.js +0 -122
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { StorageImportPreviewResponseEvent, StorageImportProgressEvent, StorageImportResultEvent, StorageInvalidatedEvent, StorageRequestErrorEvent } from '../shared/messaging';
|
|
2
|
+
import type { StorageView } from './storage-view';
|
|
3
|
+
export type ImportEmittedEvent = StorageImportProgressEvent | StorageImportResultEvent | StorageInvalidatedEvent;
|
|
4
|
+
type ImportPreviewResult = StorageImportPreviewResponseEvent | StorageRequestErrorEvent;
|
|
5
|
+
export declare const handleImportPreviewRequest: (views: readonly StorageView[], payload: unknown) => Promise<ImportPreviewResult>;
|
|
6
|
+
export declare const handleImportEntries: (views: StorageView[], payload: unknown, emit: (out: ImportEmittedEvent) => void) => Promise<void>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { computePreview, parseSnapshot } from '../shared/snapshot';
|
|
2
|
+
import { supportsType } from '../shared/types';
|
|
3
|
+
const isSameTarget = (left, right) => left.adapterId === right.adapterId && left.storageId === right.storageId;
|
|
4
|
+
const isTarget = (value) => typeof value === 'object' &&
|
|
5
|
+
value != null &&
|
|
6
|
+
!Array.isArray(value) &&
|
|
7
|
+
typeof value.adapterId === 'string' &&
|
|
8
|
+
value.adapterId.trim().length > 0 &&
|
|
9
|
+
typeof value.storageId === 'string' &&
|
|
10
|
+
value.storageId.trim().length > 0;
|
|
11
|
+
const getRequestId = (payload) => typeof payload === 'object' &&
|
|
12
|
+
payload != null &&
|
|
13
|
+
!Array.isArray(payload) &&
|
|
14
|
+
typeof payload.requestId === 'string'
|
|
15
|
+
? payload.requestId
|
|
16
|
+
: '';
|
|
17
|
+
const isEntry = (value) => {
|
|
18
|
+
if (typeof value !== 'object' || value == null || Array.isArray(value)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const entry = value;
|
|
22
|
+
if (typeof entry.key !== 'string')
|
|
23
|
+
return false;
|
|
24
|
+
if (entry.type === 'string')
|
|
25
|
+
return typeof entry.value === 'string';
|
|
26
|
+
if (entry.type === 'number')
|
|
27
|
+
return typeof entry.value === 'number';
|
|
28
|
+
if (entry.type === 'boolean')
|
|
29
|
+
return typeof entry.value === 'boolean';
|
|
30
|
+
return (entry.type === 'buffer' &&
|
|
31
|
+
Array.isArray(entry.value) &&
|
|
32
|
+
entry.value.every((byte) => typeof byte === 'number' && Number.isInteger(byte) && byte >= 0 && byte <= 255));
|
|
33
|
+
};
|
|
34
|
+
const isBlacklisted = (view, key) => {
|
|
35
|
+
if (!view.blacklist)
|
|
36
|
+
return false;
|
|
37
|
+
view.blacklist.lastIndex = 0;
|
|
38
|
+
return view.blacklist.test(key);
|
|
39
|
+
};
|
|
40
|
+
const findView = (views, target) => views.find((candidate) => isSameTarget(candidate.target, target));
|
|
41
|
+
export const handleImportPreviewRequest = async (views, payload) => {
|
|
42
|
+
const requestId = getRequestId(payload);
|
|
43
|
+
const request = payload;
|
|
44
|
+
const parsedSnapshot = parseSnapshot(request.snapshot);
|
|
45
|
+
if (typeof request !== 'object' ||
|
|
46
|
+
request == null ||
|
|
47
|
+
request.type !== 'preview-import' ||
|
|
48
|
+
typeof request.requestId !== 'string' ||
|
|
49
|
+
request.requestId.trim().length === 0 ||
|
|
50
|
+
!isTarget(request.target) ||
|
|
51
|
+
!parsedSnapshot.ok) {
|
|
52
|
+
return {
|
|
53
|
+
type: 'storage-request-error',
|
|
54
|
+
requestId,
|
|
55
|
+
code: 'INVALID_REQUEST',
|
|
56
|
+
message: 'Import preview requests require a valid target, request ID, and snapshot.',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const view = findView(views, request.target);
|
|
60
|
+
if (!view) {
|
|
61
|
+
return {
|
|
62
|
+
type: 'storage-request-error',
|
|
63
|
+
requestId,
|
|
64
|
+
code: 'TARGET_NOT_FOUND',
|
|
65
|
+
message: 'The requested storage target was not found.',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const entryKeys = new Set(await view.getAllKeys());
|
|
70
|
+
return {
|
|
71
|
+
type: 'import-preview',
|
|
72
|
+
requestId,
|
|
73
|
+
target: view.target,
|
|
74
|
+
preview: computePreview(parsedSnapshot.snapshot, {
|
|
75
|
+
target: view.target,
|
|
76
|
+
capabilities: view.capabilities,
|
|
77
|
+
entryKeys,
|
|
78
|
+
isBlacklisted: (key) => isBlacklisted(view, key),
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return {
|
|
84
|
+
type: 'storage-request-error',
|
|
85
|
+
requestId,
|
|
86
|
+
code: 'READ_FAILED',
|
|
87
|
+
message: 'Failed to inspect the requested storage for import.',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
export const handleImportEntries = async (views, payload, emit) => {
|
|
92
|
+
const requestId = getRequestId(payload);
|
|
93
|
+
const event = payload;
|
|
94
|
+
if (typeof event !== 'object' ||
|
|
95
|
+
event == null ||
|
|
96
|
+
event.type !== 'import-entries' ||
|
|
97
|
+
typeof event.requestId !== 'string' ||
|
|
98
|
+
event.requestId.trim().length === 0 ||
|
|
99
|
+
!isTarget(event.target) ||
|
|
100
|
+
!Array.isArray(event.entries) ||
|
|
101
|
+
!event.entries.every(isEntry)) {
|
|
102
|
+
emit({
|
|
103
|
+
type: 'import-result',
|
|
104
|
+
requestId,
|
|
105
|
+
target: isTarget(event.target) ? event.target : { adapterId: '', storageId: '' },
|
|
106
|
+
ok: false,
|
|
107
|
+
written: 0,
|
|
108
|
+
total: 0,
|
|
109
|
+
error: 'Import requests require a valid target, request ID, and entries.',
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const view = findView(views, event.target);
|
|
114
|
+
if (!view) {
|
|
115
|
+
emit({
|
|
116
|
+
type: 'import-result',
|
|
117
|
+
requestId: event.requestId,
|
|
118
|
+
target: event.target,
|
|
119
|
+
ok: false,
|
|
120
|
+
written: 0,
|
|
121
|
+
total: event.entries.length,
|
|
122
|
+
error: 'Target storage not found',
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Recheck policy at apply time so a preview cannot bypass a changed target.
|
|
127
|
+
const entries = event.entries.filter((entry) => supportsType(view.capabilities, entry.type) && !isBlacklisted(view, entry.key));
|
|
128
|
+
for (let i = 0; i < entries.length; i++) {
|
|
129
|
+
const entry = entries[i];
|
|
130
|
+
try {
|
|
131
|
+
await view.set(entry);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (i > 0) {
|
|
135
|
+
const entryCount = (await view.getAllKeys()).length;
|
|
136
|
+
emit({
|
|
137
|
+
type: 'storage-invalidated',
|
|
138
|
+
target: event.target,
|
|
139
|
+
operation: 'import',
|
|
140
|
+
entryCount,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
emit({
|
|
144
|
+
type: 'import-result',
|
|
145
|
+
requestId: event.requestId,
|
|
146
|
+
target: event.target,
|
|
147
|
+
ok: false,
|
|
148
|
+
written: i,
|
|
149
|
+
total: entries.length,
|
|
150
|
+
failedKey: entry.key,
|
|
151
|
+
error: error instanceof Error ? error.message : String(error),
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
emit({
|
|
156
|
+
type: 'import-progress',
|
|
157
|
+
requestId: event.requestId,
|
|
158
|
+
target: event.target,
|
|
159
|
+
written: i + 1,
|
|
160
|
+
total: entries.length,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (entries.length > 0) {
|
|
164
|
+
const entryCount = (await view.getAllKeys()).length;
|
|
165
|
+
emit({
|
|
166
|
+
type: 'storage-invalidated',
|
|
167
|
+
target: event.target,
|
|
168
|
+
operation: 'import',
|
|
169
|
+
entryCount,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
emit({
|
|
173
|
+
type: 'import-result',
|
|
174
|
+
requestId: event.requestId,
|
|
175
|
+
target: event.target,
|
|
176
|
+
ok: true,
|
|
177
|
+
written: entries.length,
|
|
178
|
+
total: entries.length,
|
|
179
|
+
});
|
|
180
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { StorageDiscoverStoragesResponseEvent, StorageRequestErrorEvent } from '../shared/messaging';
|
|
2
|
+
import type { StorageView } from './storage-view';
|
|
3
|
+
type DiscoveryResult = StorageDiscoverStoragesResponseEvent | StorageRequestErrorEvent;
|
|
4
|
+
export declare const handleStorageDiscoveryRequest: (views: readonly StorageView[], payload: unknown) => Promise<DiscoveryResult>;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const getRequestId = (payload) => {
|
|
2
|
+
if (typeof payload === 'object' &&
|
|
3
|
+
payload != null &&
|
|
4
|
+
!Array.isArray(payload) &&
|
|
5
|
+
typeof payload.requestId === 'string') {
|
|
6
|
+
return payload.requestId;
|
|
7
|
+
}
|
|
8
|
+
return '';
|
|
9
|
+
};
|
|
10
|
+
const validateRequest = (payload) => typeof payload === 'object' &&
|
|
11
|
+
payload != null &&
|
|
12
|
+
!Array.isArray(payload) &&
|
|
13
|
+
payload.type === 'discover-storages' &&
|
|
14
|
+
typeof payload.requestId === 'string' &&
|
|
15
|
+
payload.requestId.trim().length > 0;
|
|
16
|
+
const toDescriptor = async (view) => ({
|
|
17
|
+
target: view.target,
|
|
18
|
+
adapterName: view.adapterName,
|
|
19
|
+
storageName: view.storageName,
|
|
20
|
+
entryCount: (await view.getAllKeys()).length,
|
|
21
|
+
capabilities: view.capabilities,
|
|
22
|
+
supportsSubscriptions: view.supportsSubscriptions,
|
|
23
|
+
});
|
|
24
|
+
const hasDuplicateTargets = (views) => {
|
|
25
|
+
const targets = new Set();
|
|
26
|
+
return views.some(({ target }) => {
|
|
27
|
+
const id = `${target.adapterId}:${target.storageId}`;
|
|
28
|
+
if (targets.has(id)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
targets.add(id);
|
|
32
|
+
return false;
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
export const handleStorageDiscoveryRequest = async (views, payload) => {
|
|
36
|
+
const requestId = getRequestId(payload);
|
|
37
|
+
if (!validateRequest(payload)) {
|
|
38
|
+
return {
|
|
39
|
+
type: 'storage-request-error',
|
|
40
|
+
requestId,
|
|
41
|
+
code: 'INVALID_REQUEST',
|
|
42
|
+
message: 'Storage discovery requires a non-empty request ID.',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
if (hasDuplicateTargets(views)) {
|
|
47
|
+
return {
|
|
48
|
+
type: 'storage-request-error',
|
|
49
|
+
requestId,
|
|
50
|
+
code: 'INVALID_REQUEST',
|
|
51
|
+
message: 'Configured storages must have unique targets.',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const storages = await Promise.all(views.map(toDescriptor));
|
|
55
|
+
return { type: 'storage-descriptors', requestId, storages };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return {
|
|
59
|
+
type: 'storage-request-error',
|
|
60
|
+
requestId,
|
|
61
|
+
code: 'READ_FAILED',
|
|
62
|
+
message: 'Failed to discover configured storages.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type StorageAdapter, type StorageCapabilities, type StorageEntry, type StorageNode, type StorageSubscription, type StorageTarget } from '../shared/types';
|
|
2
|
+
export type StorageView = {
|
|
3
|
+
id: string;
|
|
4
|
+
target: StorageTarget;
|
|
5
|
+
adapterName: string;
|
|
6
|
+
storageName: string;
|
|
7
|
+
capabilities: StorageCapabilities;
|
|
8
|
+
supportsSubscriptions: boolean;
|
|
9
|
+
blacklist?: RegExp;
|
|
10
|
+
get: (key: string) => Promise<StorageEntry | undefined>;
|
|
11
|
+
set: (entry: StorageEntry) => Promise<void>;
|
|
12
|
+
delete: (key: string) => Promise<void>;
|
|
13
|
+
purge: () => Promise<void>;
|
|
14
|
+
getAllKeys: () => Promise<readonly string[]>;
|
|
15
|
+
getAllEntries: () => Promise<StorageEntry[]>;
|
|
16
|
+
watch?: (onChange: (key: string) => void) => Promise<StorageSubscription>;
|
|
17
|
+
};
|
|
18
|
+
export declare const createStorageView: (adapter: StorageAdapter, storageNode: StorageNode) => StorageView;
|
|
19
|
+
export declare const createStorageViews: (storages: StorageAdapter[]) => StorageView[];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { getStorageViewId, supportsType, } from '../shared/types';
|
|
2
|
+
const isAsyncStorage = (storage) => storage.kind === 'async';
|
|
3
|
+
const shouldFilterKey = (storage, key) => {
|
|
4
|
+
if (storage.shouldFilterKey?.(key)) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
if (!storage.blacklist) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
storage.blacklist.lastIndex = 0;
|
|
11
|
+
return storage.blacklist.test(key);
|
|
12
|
+
};
|
|
13
|
+
const checkTypeSupport = (capabilities, type, target) => {
|
|
14
|
+
if (supportsType(capabilities, type)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
throw new Error(`Type "${type}" is not supported by storage "${target.storageId}" in adapter "${target.adapterId}".`);
|
|
18
|
+
};
|
|
19
|
+
const getAllKeys = async (storage) => {
|
|
20
|
+
if (isAsyncStorage(storage)) {
|
|
21
|
+
return storage.getAllKeys();
|
|
22
|
+
}
|
|
23
|
+
return storage.getAllKeys();
|
|
24
|
+
};
|
|
25
|
+
const getEntry = async (storage, key) => {
|
|
26
|
+
if (isAsyncStorage(storage)) {
|
|
27
|
+
return storage.get(key);
|
|
28
|
+
}
|
|
29
|
+
return storage.get(key);
|
|
30
|
+
};
|
|
31
|
+
const setEntry = async (storage, entry) => {
|
|
32
|
+
if (isAsyncStorage(storage)) {
|
|
33
|
+
await storage.set(entry);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
storage.set(entry);
|
|
37
|
+
};
|
|
38
|
+
const deleteEntry = async (storage, key) => {
|
|
39
|
+
if (isAsyncStorage(storage)) {
|
|
40
|
+
await storage.delete(key);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
storage.delete(key);
|
|
44
|
+
};
|
|
45
|
+
const clearStorage = async (storage) => {
|
|
46
|
+
await storage.clear();
|
|
47
|
+
};
|
|
48
|
+
export const createStorageView = (adapter, storageNode) => {
|
|
49
|
+
const storage = storageNode.storage;
|
|
50
|
+
const subscribe = storage.subscribe;
|
|
51
|
+
const locallyMutatingKeys = new Set();
|
|
52
|
+
let isLocallyPurging = false;
|
|
53
|
+
const target = {
|
|
54
|
+
adapterId: adapter.id,
|
|
55
|
+
storageId: storageNode.id,
|
|
56
|
+
};
|
|
57
|
+
const get = async (key) => {
|
|
58
|
+
if (shouldFilterKey(storageNode, key)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return getEntry(storage, key);
|
|
62
|
+
};
|
|
63
|
+
const getAllEntries = async () => {
|
|
64
|
+
const keys = await getAllKeys(storage);
|
|
65
|
+
const visibleEntries = await Promise.all(keys.filter((key) => !shouldFilterKey(storageNode, key)).map((key) => getEntry(storage, key)));
|
|
66
|
+
return visibleEntries.filter((entry) => !!entry);
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
id: getStorageViewId(target),
|
|
70
|
+
target,
|
|
71
|
+
adapterName: adapter.name,
|
|
72
|
+
storageName: storageNode.name,
|
|
73
|
+
capabilities: storageNode.capabilities,
|
|
74
|
+
supportsSubscriptions: subscribe != null,
|
|
75
|
+
blacklist: storageNode.blacklist,
|
|
76
|
+
get,
|
|
77
|
+
set: async (entry) => {
|
|
78
|
+
checkTypeSupport(storageNode.capabilities, entry.type, target);
|
|
79
|
+
locallyMutatingKeys.add(entry.key);
|
|
80
|
+
try {
|
|
81
|
+
await setEntry(storage, entry);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
locallyMutatingKeys.delete(entry.key);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
delete: async (key) => {
|
|
88
|
+
locallyMutatingKeys.add(key);
|
|
89
|
+
try {
|
|
90
|
+
await deleteEntry(storage, key);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
locallyMutatingKeys.delete(key);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
purge: async () => {
|
|
97
|
+
isLocallyPurging = true;
|
|
98
|
+
try {
|
|
99
|
+
await clearStorage(storage);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
isLocallyPurging = false;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
getAllKeys: async () => {
|
|
106
|
+
const keys = await getAllKeys(storage);
|
|
107
|
+
return keys.filter((key) => !shouldFilterKey(storageNode, key));
|
|
108
|
+
},
|
|
109
|
+
getAllEntries,
|
|
110
|
+
...(subscribe
|
|
111
|
+
? {
|
|
112
|
+
watch: async (onChange) => subscribe((key) => {
|
|
113
|
+
if (!isLocallyPurging &&
|
|
114
|
+
!locallyMutatingKeys.has(key) &&
|
|
115
|
+
!shouldFilterKey(storageNode, key)) {
|
|
116
|
+
onChange(key);
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
}
|
|
120
|
+
: {}),
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
export const createStorageViews = (storages) => storages.flatMap((adapter) => adapter.storages.map((storageNode) => createStorageView(adapter, storageNode)));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StorageEventMap } from '../shared/messaging';
|
|
2
|
+
import type { StorageAdapter } from '../shared/types';
|
|
3
|
+
export type RozeniteStoragePluginOptions = {
|
|
4
|
+
storages: StorageAdapter[];
|
|
5
|
+
};
|
|
6
|
+
export declare const useRozeniteStoragePlugin: ({ storages }: RozeniteStoragePluginOptions) => import("@rozenite/plugin-bridge").RozeniteDevToolsRequestClient<StorageEventMap> | null;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
|
|
2
|
+
import { useEffect, useMemo } from 'react';
|
|
3
|
+
import { handleImportEntries, handleImportPreviewRequest } from './import';
|
|
4
|
+
import { handleStorageDiscoveryRequest } from './storage-discovery';
|
|
5
|
+
import { handleListEntryPreviewsRequest } from './entry-preview-pagination';
|
|
6
|
+
import { handleFullEntryRequest } from './full-entry-request';
|
|
7
|
+
import { handleExportSnapshotRequest } from './export-snapshot';
|
|
8
|
+
import { createStorageViews } from './storage-view';
|
|
9
|
+
import { useStorageAgentTools } from './useStorageAgentTools';
|
|
10
|
+
export const useRozeniteStoragePlugin = ({ storages }) => {
|
|
11
|
+
const views = useMemo(() => createStorageViews(storages), [storages]);
|
|
12
|
+
useStorageAgentTools(views);
|
|
13
|
+
const client = useRozeniteDevToolsClient({
|
|
14
|
+
pluginId: '@rozenite/storage-plugin',
|
|
15
|
+
});
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!client) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const viewSubscriptions = [];
|
|
21
|
+
let disposed = false;
|
|
22
|
+
const sendInvalidation = async (target, key, operation) => {
|
|
23
|
+
const view = views.find((candidate) => candidate.target.adapterId === target.adapterId &&
|
|
24
|
+
candidate.target.storageId === target.storageId);
|
|
25
|
+
const entryCount = view ? (await view.getAllKeys()).length : 0;
|
|
26
|
+
client.send('storage-invalidated', {
|
|
27
|
+
type: 'storage-invalidated',
|
|
28
|
+
target,
|
|
29
|
+
key,
|
|
30
|
+
operation,
|
|
31
|
+
entryCount,
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
// Prevent one storage watcher failure from breaking the whole plugin.
|
|
35
|
+
void Promise.all(views
|
|
36
|
+
.filter((view) => view.supportsSubscriptions && view.watch)
|
|
37
|
+
.map(async (view) => {
|
|
38
|
+
try {
|
|
39
|
+
const watch = view.watch;
|
|
40
|
+
if (!watch) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const subscription = await watch((key) => {
|
|
44
|
+
void sendInvalidation(view.target, key);
|
|
45
|
+
});
|
|
46
|
+
if (disposed) {
|
|
47
|
+
subscription.remove();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
viewSubscriptions.push(subscription);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.warn(`[Rozenite] Storage Plugin: Failed to attach watcher for ${view.target.adapterId}/${view.target.storageId}.`, error);
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
const messageSubscriptions = [
|
|
57
|
+
client.onMessage('set-entry', async ({ target, entry }) => {
|
|
58
|
+
const view = views.find((candidate) => candidate.target.adapterId === target.adapterId &&
|
|
59
|
+
candidate.target.storageId === target.storageId);
|
|
60
|
+
if (!view) {
|
|
61
|
+
console.warn(`[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await view.set(entry);
|
|
66
|
+
await sendInvalidation(view.target, entry.key, 'set');
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.warn(`[Rozenite] Storage Plugin: Failed to set entry in ${target.adapterId}/${target.storageId}.`, error);
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
client.onMessage('delete-entry', async ({ target, key }) => {
|
|
73
|
+
const view = views.find((candidate) => candidate.target.adapterId === target.adapterId &&
|
|
74
|
+
candidate.target.storageId === target.storageId);
|
|
75
|
+
if (!view) {
|
|
76
|
+
console.warn(`[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await view.delete(key);
|
|
81
|
+
await sendInvalidation(view.target, key, 'delete');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.warn(`[Rozenite] Storage Plugin: Failed to delete entry in ${target.adapterId}/${target.storageId}.`, error);
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
client.onMessage('purge-storage', async ({ target }) => {
|
|
88
|
+
const view = views.find((candidate) => candidate.target.adapterId === target.adapterId &&
|
|
89
|
+
candidate.target.storageId === target.storageId);
|
|
90
|
+
if (!view) {
|
|
91
|
+
console.warn(`[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await view.purge();
|
|
96
|
+
await sendInvalidation(view.target, undefined, 'purge');
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.warn(`[Rozenite] Storage Plugin: Failed to purge storage in ${target.adapterId}/${target.storageId}.`, error);
|
|
100
|
+
}
|
|
101
|
+
}),
|
|
102
|
+
client.onMessage('discover-storages', async (event) => {
|
|
103
|
+
const response = await handleStorageDiscoveryRequest(views, event);
|
|
104
|
+
client.send(response.type, response);
|
|
105
|
+
}),
|
|
106
|
+
client.onMessage('list-entry-previews', async (event) => {
|
|
107
|
+
const response = await handleListEntryPreviewsRequest(views, event);
|
|
108
|
+
client.send(response.type, response);
|
|
109
|
+
}),
|
|
110
|
+
client.onMessage('get-entry', async (event) => {
|
|
111
|
+
const response = await handleFullEntryRequest(views, event);
|
|
112
|
+
client.send(response.type, response);
|
|
113
|
+
}),
|
|
114
|
+
client.onMessage('export-snapshot', async (event) => {
|
|
115
|
+
const response = await handleExportSnapshotRequest(views, event);
|
|
116
|
+
client.send(response.type, response);
|
|
117
|
+
}),
|
|
118
|
+
client.onMessage('preview-import', async (event) => {
|
|
119
|
+
const response = await handleImportPreviewRequest(views, event);
|
|
120
|
+
client.send(response.type, response);
|
|
121
|
+
}),
|
|
122
|
+
client.onMessage('import-entries', async (event) => {
|
|
123
|
+
await handleImportEntries(views, event, (out) => {
|
|
124
|
+
client.send(out.type, out);
|
|
125
|
+
});
|
|
126
|
+
}),
|
|
127
|
+
];
|
|
128
|
+
// Sent last, so a panel that reacts to it is guaranteed to find the
|
|
129
|
+
// handlers above already listening. The panel is recreated on every app
|
|
130
|
+
// reload and asks for storages as soon as it boots, which usually beats the
|
|
131
|
+
// app's React tree to the punch; without this the panel's only request is
|
|
132
|
+
// dropped and it waits for storages forever.
|
|
133
|
+
client.send('device-ready', { type: 'device-ready' });
|
|
134
|
+
return () => {
|
|
135
|
+
disposed = true;
|
|
136
|
+
viewSubscriptions.forEach((subscription) => subscription.remove());
|
|
137
|
+
messageSubscriptions.forEach((subscription) => subscription.remove());
|
|
138
|
+
};
|
|
139
|
+
}, [client, views]);
|
|
140
|
+
return client;
|
|
141
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type StorageListEntriesArgs } from '../shared/agent-tools';
|
|
2
|
+
import type { StorageEntry } from '../shared/types';
|
|
3
|
+
import type { StorageView } from './storage-view';
|
|
4
|
+
export declare const createStorageAgentHandlers: (views: readonly StorageView[]) => {
|
|
5
|
+
listStorages: () => Promise<{
|
|
6
|
+
storages: {
|
|
7
|
+
adapterId: string;
|
|
8
|
+
storageId: string;
|
|
9
|
+
adapterName: string;
|
|
10
|
+
storageName: string;
|
|
11
|
+
supportedTypes: ("string" | "number" | "boolean" | "buffer")[];
|
|
12
|
+
}[];
|
|
13
|
+
}>;
|
|
14
|
+
listEntries: (input: StorageListEntriesArgs) => Promise<{
|
|
15
|
+
adapterId: string;
|
|
16
|
+
storageId: string;
|
|
17
|
+
total: number;
|
|
18
|
+
items: {
|
|
19
|
+
key: string;
|
|
20
|
+
}[];
|
|
21
|
+
page: {
|
|
22
|
+
nextCursor?: string | undefined;
|
|
23
|
+
limit: number;
|
|
24
|
+
hasMore: boolean;
|
|
25
|
+
};
|
|
26
|
+
}>;
|
|
27
|
+
readEntry: ({ adapterId, storageId, key, }: {
|
|
28
|
+
adapterId?: string;
|
|
29
|
+
storageId?: string;
|
|
30
|
+
key: string;
|
|
31
|
+
}) => Promise<{
|
|
32
|
+
adapterId: string;
|
|
33
|
+
storageId: string;
|
|
34
|
+
entry: StorageEntry;
|
|
35
|
+
}>;
|
|
36
|
+
};
|
|
37
|
+
export declare const useStorageAgentTools: (views: StorageView[]) => void;
|