@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +6 -1
  3. package/dist/devtools/assets/panel-B6Wp0eie.css +1 -0
  4. package/dist/devtools/assets/panel-D7MFc4HG.js +32 -0
  5. package/dist/devtools/panel.html +2 -2
  6. package/dist/react-native/chunks/async-storage.require.cjs +1 -1
  7. package/dist/react-native/chunks/async-storage.require.js +8 -7
  8. package/dist/react-native/chunks/index.require.cjs +1 -1
  9. package/dist/react-native/chunks/index.require.js +70 -64
  10. package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
  11. package/dist/react-native/chunks/secure-storage.require.js +22 -14
  12. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  13. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -315
  14. package/dist/react-native/index.d.ts +154 -18
  15. package/dist/react-native/index.js +10 -2
  16. package/dist/rozenite.json +1 -1
  17. package/dist/sdk/index.cjs +1 -1
  18. package/dist/sdk/index.d.ts +13 -5
  19. package/dist/sdk/index.js +21 -12
  20. package/package.json +13 -15
  21. package/react-native.ts +29 -14
  22. package/rozenite.config.ts +78 -0
  23. package/src/react-native/__tests__/entry-preview-pagination.test.ts +262 -0
  24. package/src/react-native/__tests__/export-snapshot.test.ts +142 -0
  25. package/src/react-native/__tests__/full-entry-request.test.ts +155 -0
  26. package/src/react-native/__tests__/import.test.ts +172 -10
  27. package/src/react-native/__tests__/storage-discovery.test.ts +155 -0
  28. package/src/react-native/__tests__/storage-view.test.ts +172 -0
  29. package/src/react-native/__tests__/stress-verification.test.ts +153 -0
  30. package/src/react-native/__tests__/use-storage-agent-tools.test.ts +231 -0
  31. package/src/react-native/adapters/async-storage.ts +9 -7
  32. package/src/react-native/adapters/mmkv.ts +5 -0
  33. package/src/react-native/adapters/secure-storage.ts +9 -1
  34. package/src/react-native/entry-preview-pagination.ts +277 -0
  35. package/src/react-native/export-snapshot.ts +94 -0
  36. package/src/react-native/full-entry-request.ts +96 -0
  37. package/src/react-native/import.ts +190 -16
  38. package/src/react-native/storage-discovery.ts +96 -0
  39. package/src/react-native/storage-view.ts +45 -103
  40. package/src/react-native/useRozeniteStoragePlugin.ts +108 -72
  41. package/src/react-native/useStorageAgentTools.ts +207 -86
  42. package/src/shared/__tests__/entry-preview.test.ts +117 -0
  43. package/src/shared/__tests__/snapshot.test.ts +21 -0
  44. package/src/shared/agent-tools.ts +27 -11
  45. package/src/shared/entry-preview.ts +59 -0
  46. package/src/shared/messaging.ts +136 -21
  47. package/src/shared/snapshot.ts +8 -1
  48. package/src/shared/types.ts +20 -1
  49. package/src/ui/__tests__/binary-value-editor-state.test.ts +13 -0
  50. package/src/ui/__tests__/binary.test.ts +18 -0
  51. package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
  52. package/src/ui/__tests__/large-value-viewer.test.tsx +63 -0
  53. package/src/ui/__tests__/panel.test.tsx +409 -0
  54. package/src/ui/__tests__/storage-groups.test.ts +91 -0
  55. package/src/ui/__tests__/use-storage-entries.test.tsx +353 -0
  56. package/src/ui/add-entry-dialog.tsx +65 -105
  57. package/src/ui/binary-value-editor-state.ts +10 -1
  58. package/src/ui/binary-value-editor.tsx +57 -33
  59. package/src/ui/binary.ts +56 -4
  60. package/src/ui/edit-entry-dialog.tsx +71 -99
  61. package/src/ui/editor-switcher.tsx +7 -5
  62. package/src/ui/entry-detail-dialog.tsx +69 -179
  63. package/src/ui/format-value.tsx +32 -0
  64. package/src/ui/globals.css +1 -123
  65. package/src/ui/import-dialog.tsx +66 -91
  66. package/src/ui/large-value-viewer-state.ts +32 -0
  67. package/src/ui/large-value-viewer.tsx +71 -0
  68. package/src/ui/panel.tsx +724 -496
  69. package/src/ui/query-client.tsx +34 -0
  70. package/src/ui/storage-groups.ts +66 -0
  71. package/src/ui/typed-value-editor.tsx +14 -15
  72. package/src/ui/use-storage-entries.ts +207 -0
  73. package/tsconfig.json +5 -1
  74. package/dist/devtools/assets/panel-Bm-SWF7d.js +0 -33
  75. package/dist/devtools/assets/panel-DIqI4WSp.css +0 -1
  76. package/postcss.config.js +0 -6
  77. package/src/ui/confirm-dialog.tsx +0 -100
  78. package/src/ui/editable-table.tsx +0 -300
  79. package/tailwind.config.ts +0 -94
@@ -0,0 +1,277 @@
1
+ import { toStorageEntryPreview } from '../shared/entry-preview';
2
+ import type {
3
+ StorageListEntryPreviewsRequestEvent,
4
+ StorageListEntryPreviewsResponseEvent,
5
+ StorageRequestErrorEvent,
6
+ } from '../shared/messaging';
7
+ import type { StorageEntryPreview, StorageTarget } from '../shared/types';
8
+ import type { StorageView } from './storage-view';
9
+
10
+ export const MAX_ENTRY_PREVIEW_PAGE_SIZE = 100;
11
+ export const ENTRY_PREVIEW_READ_CONCURRENCY = 4;
12
+
13
+ type PaginationResult =
14
+ | StorageListEntryPreviewsResponseEvent
15
+ | StorageRequestErrorEvent;
16
+
17
+ type Cursor = {
18
+ version: 1;
19
+ target: StorageTarget;
20
+ search: string;
21
+ keySortDirection: 'ascending' | 'descending';
22
+ boundaryKey: string;
23
+ direction: 'next' | 'previous';
24
+ };
25
+
26
+ type ValidRequest = Omit<
27
+ StorageListEntryPreviewsRequestEvent,
28
+ 'search' | 'keySortDirection' | 'limit'
29
+ > & {
30
+ search: string;
31
+ keySortDirection: 'ascending' | 'descending';
32
+ limit: number;
33
+ };
34
+
35
+ const getRequestId = (payload: unknown) =>
36
+ typeof payload === 'object' &&
37
+ payload != null &&
38
+ !Array.isArray(payload) &&
39
+ typeof (payload as { requestId?: unknown }).requestId === 'string'
40
+ ? (payload as { requestId: string }).requestId
41
+ : '';
42
+
43
+ const isTarget = (value: unknown): value is StorageTarget =>
44
+ typeof value === 'object' &&
45
+ value != null &&
46
+ !Array.isArray(value) &&
47
+ typeof (value as { adapterId?: unknown }).adapterId === 'string' &&
48
+ (value as { adapterId: string }).adapterId.trim().length > 0 &&
49
+ typeof (value as { storageId?: unknown }).storageId === 'string' &&
50
+ (value as { storageId: string }).storageId.trim().length > 0;
51
+
52
+ const normalizeSearch = (search: string | undefined) =>
53
+ (search ?? '').trim().toLowerCase();
54
+
55
+ const validateRequest = (payload: unknown): ValidRequest | undefined => {
56
+ if (
57
+ typeof payload !== 'object' ||
58
+ payload == null ||
59
+ Array.isArray(payload) ||
60
+ (payload as { type?: unknown }).type !== 'list-entry-previews' ||
61
+ typeof (payload as { requestId?: unknown }).requestId !== 'string' ||
62
+ (payload as { requestId: string }).requestId.trim().length === 0 ||
63
+ !isTarget((payload as { target?: unknown }).target) ||
64
+ ((payload as { search?: unknown }).search !== undefined &&
65
+ typeof (payload as { search?: unknown }).search !== 'string') ||
66
+ ((payload as { cursor?: unknown }).cursor !== undefined &&
67
+ typeof (payload as { cursor?: unknown }).cursor !== 'string') ||
68
+ !Number.isFinite((payload as { limit?: unknown }).limit) ||
69
+ !Number.isInteger((payload as { limit: number }).limit) ||
70
+ (payload as { limit: number }).limit < 1
71
+ ) {
72
+ return undefined;
73
+ }
74
+
75
+ const keySortDirection = (payload as { keySortDirection?: unknown })
76
+ .keySortDirection;
77
+ if (
78
+ keySortDirection !== undefined &&
79
+ keySortDirection !== 'ascending' &&
80
+ keySortDirection !== 'descending'
81
+ ) {
82
+ return undefined;
83
+ }
84
+
85
+ const request = payload as StorageListEntryPreviewsRequestEvent;
86
+ return {
87
+ ...request,
88
+ search: normalizeSearch(request.search),
89
+ keySortDirection: keySortDirection ?? 'ascending',
90
+ limit: Math.min(request.limit, MAX_ENTRY_PREVIEW_PAGE_SIZE),
91
+ };
92
+ };
93
+
94
+ // Cursor contents are intentionally private to the device protocol. Callers only
95
+ // receive and return this opaque token; its representation may change freely.
96
+ const encodeCursor = (cursor: Cursor) =>
97
+ btoa(encodeURIComponent(JSON.stringify(cursor)));
98
+
99
+ const decodeCursor = (value: string): Cursor | undefined => {
100
+ try {
101
+ const parsed: unknown = JSON.parse(decodeURIComponent(atob(value)));
102
+ if (
103
+ typeof parsed !== 'object' ||
104
+ parsed == null ||
105
+ Array.isArray(parsed) ||
106
+ (parsed as { version?: unknown }).version !== 1 ||
107
+ !isTarget((parsed as { target?: unknown }).target) ||
108
+ typeof (parsed as { search?: unknown }).search !== 'string' ||
109
+ ((parsed as { keySortDirection?: unknown }).keySortDirection !==
110
+ 'ascending' &&
111
+ (parsed as { keySortDirection?: unknown }).keySortDirection !==
112
+ 'descending') ||
113
+ typeof (parsed as { boundaryKey?: unknown }).boundaryKey !== 'string' ||
114
+ ((parsed as { direction?: unknown }).direction !== 'next' &&
115
+ (parsed as { direction?: unknown }).direction !== 'previous')
116
+ ) {
117
+ return undefined;
118
+ }
119
+
120
+ return parsed as Cursor;
121
+ } catch {
122
+ return undefined;
123
+ }
124
+ };
125
+
126
+ const isSameTarget = (left: StorageTarget, right: StorageTarget) =>
127
+ left.adapterId === right.adapterId && left.storageId === right.storageId;
128
+
129
+ const compareKeys = (left: string, right: string) =>
130
+ left < right ? -1 : left > right ? 1 : 0;
131
+
132
+ const readPreviews = async (
133
+ view: StorageView,
134
+ keys: readonly string[],
135
+ ): Promise<StorageEntryPreview[]> => {
136
+ const previews: StorageEntryPreview[] = new Array(keys.length);
137
+ let nextIndex = 0;
138
+
139
+ const worker = async () => {
140
+ while (nextIndex < keys.length) {
141
+ const index = nextIndex;
142
+ nextIndex += 1;
143
+ const entry = await view.get(keys[index]);
144
+ if (entry) {
145
+ previews[index] = toStorageEntryPreview(entry);
146
+ }
147
+ }
148
+ };
149
+
150
+ await Promise.all(
151
+ Array.from(
152
+ { length: Math.min(ENTRY_PREVIEW_READ_CONCURRENCY, keys.length) },
153
+ worker,
154
+ ),
155
+ );
156
+ return previews.filter(
157
+ (preview): preview is StorageEntryPreview => preview != null,
158
+ );
159
+ };
160
+
161
+ export const handleListEntryPreviewsRequest = async (
162
+ views: readonly StorageView[],
163
+ payload: unknown,
164
+ ): Promise<PaginationResult> => {
165
+ const requestId = getRequestId(payload);
166
+ const request = validateRequest(payload);
167
+ if (!request) {
168
+ return {
169
+ type: 'storage-request-error',
170
+ requestId,
171
+ code: 'INVALID_REQUEST',
172
+ message:
173
+ 'Entry preview requests require a valid target, request ID, and positive integer limit.',
174
+ };
175
+ }
176
+
177
+ const view = views.find((candidate) =>
178
+ isSameTarget(candidate.target, request.target),
179
+ );
180
+ if (!view) {
181
+ return {
182
+ type: 'storage-request-error',
183
+ requestId,
184
+ code: 'TARGET_NOT_FOUND',
185
+ message: 'The requested storage target was not found.',
186
+ };
187
+ }
188
+
189
+ let cursor: Cursor | undefined;
190
+ if (request.cursor !== undefined) {
191
+ cursor = decodeCursor(request.cursor);
192
+ if (
193
+ !cursor ||
194
+ !isSameTarget(cursor.target, request.target) ||
195
+ cursor.search !== request.search ||
196
+ cursor.keySortDirection !== request.keySortDirection
197
+ ) {
198
+ return {
199
+ type: 'storage-request-error',
200
+ requestId,
201
+ code: 'INVALID_CURSOR',
202
+ message:
203
+ 'The entry preview cursor is invalid or belongs to a different query.',
204
+ resetPagination: true,
205
+ };
206
+ }
207
+ }
208
+
209
+ try {
210
+ const keys = (await view.getAllKeys())
211
+ .filter((key) => key.toLowerCase().includes(request.search))
212
+ .sort(compareKeys);
213
+ if (request.keySortDirection === 'descending') {
214
+ keys.reverse();
215
+ }
216
+
217
+ let start = 0;
218
+ if (cursor) {
219
+ const boundaryIndex = keys.indexOf(cursor.boundaryKey);
220
+ if (boundaryIndex === -1) {
221
+ return {
222
+ type: 'storage-request-error',
223
+ requestId,
224
+ code: 'INVALID_CURSOR',
225
+ message: 'The entry preview cursor is stale after storage changes.',
226
+ resetPagination: true,
227
+ };
228
+ }
229
+ start =
230
+ cursor.direction === 'next'
231
+ ? boundaryIndex + 1
232
+ : Math.max(0, boundaryIndex - request.limit);
233
+ }
234
+
235
+ const pageKeys = keys.slice(start, start + request.limit);
236
+ const items = await readPreviews(view, pageKeys);
237
+ const end = start + pageKeys.length;
238
+ const cursorBase = {
239
+ version: 1 as const,
240
+ target: request.target,
241
+ search: request.search,
242
+ keySortDirection: request.keySortDirection,
243
+ };
244
+
245
+ return {
246
+ type: 'entry-previews',
247
+ requestId,
248
+ target: request.target,
249
+ items,
250
+ ...(end < keys.length && pageKeys.length > 0
251
+ ? {
252
+ nextCursor: encodeCursor({
253
+ ...cursorBase,
254
+ boundaryKey: pageKeys.at(-1)!,
255
+ direction: 'next',
256
+ }),
257
+ }
258
+ : {}),
259
+ ...(start > 0 && pageKeys.length > 0
260
+ ? {
261
+ previousCursor: encodeCursor({
262
+ ...cursorBase,
263
+ boundaryKey: pageKeys[0],
264
+ direction: 'previous',
265
+ }),
266
+ }
267
+ : {}),
268
+ };
269
+ } catch {
270
+ return {
271
+ type: 'storage-request-error',
272
+ requestId,
273
+ code: 'READ_FAILED',
274
+ message: 'Failed to read entry previews from the requested storage.',
275
+ };
276
+ }
277
+ };
@@ -0,0 +1,94 @@
1
+ import { buildSnapshot } from '../shared/snapshot';
2
+ import type {
3
+ StorageExportSnapshotRequestEvent,
4
+ StorageExportSnapshotResponseEvent,
5
+ StorageRequestErrorEvent,
6
+ } from '../shared/messaging';
7
+ import type { StorageTarget } from '../shared/types';
8
+ import type { StorageView } from './storage-view';
9
+
10
+ type ExportSnapshotResult =
11
+ | StorageExportSnapshotResponseEvent
12
+ | StorageRequestErrorEvent;
13
+
14
+ const getRequestId = (payload: unknown) =>
15
+ typeof payload === 'object' &&
16
+ payload != null &&
17
+ !Array.isArray(payload) &&
18
+ typeof (payload as { requestId?: unknown }).requestId === 'string'
19
+ ? (payload as { requestId: string }).requestId
20
+ : '';
21
+
22
+ const isTarget = (value: unknown): value is StorageTarget =>
23
+ typeof value === 'object' &&
24
+ value != null &&
25
+ !Array.isArray(value) &&
26
+ typeof (value as { adapterId?: unknown }).adapterId === 'string' &&
27
+ (value as { adapterId: string }).adapterId.trim().length > 0 &&
28
+ typeof (value as { storageId?: unknown }).storageId === 'string' &&
29
+ (value as { storageId: string }).storageId.trim().length > 0;
30
+
31
+ const isSameTarget = (left: StorageTarget, right: StorageTarget) =>
32
+ left.adapterId === right.adapterId && left.storageId === right.storageId;
33
+
34
+ const validateRequest = (
35
+ payload: unknown,
36
+ ): payload is StorageExportSnapshotRequestEvent =>
37
+ typeof payload === 'object' &&
38
+ payload != null &&
39
+ !Array.isArray(payload) &&
40
+ (payload as { type?: unknown }).type === 'export-snapshot' &&
41
+ typeof (payload as { requestId?: unknown }).requestId === 'string' &&
42
+ (payload as { requestId: string }).requestId.trim().length > 0 &&
43
+ isTarget((payload as { target?: unknown }).target);
44
+
45
+ export const handleExportSnapshotRequest = async (
46
+ views: readonly StorageView[],
47
+ payload: unknown,
48
+ ): Promise<ExportSnapshotResult> => {
49
+ const requestId = getRequestId(payload);
50
+ if (!validateRequest(payload)) {
51
+ return {
52
+ type: 'storage-request-error',
53
+ requestId,
54
+ code: 'INVALID_REQUEST',
55
+ message: 'Export requests require a valid target and request ID.',
56
+ };
57
+ }
58
+
59
+ const view = views.find((candidate) =>
60
+ isSameTarget(candidate.target, payload.target),
61
+ );
62
+ if (!view) {
63
+ return {
64
+ type: 'storage-request-error',
65
+ requestId,
66
+ code: 'TARGET_NOT_FOUND',
67
+ message: 'The requested storage target was not found.',
68
+ };
69
+ }
70
+
71
+ try {
72
+ // getAllEntries applies the storage view's blacklist before reading values.
73
+ const entries = await view.getAllEntries();
74
+ return {
75
+ type: 'export-snapshot-result',
76
+ requestId,
77
+ target: view.target,
78
+ snapshot: buildSnapshot({
79
+ target: view.target,
80
+ adapterName: view.adapterName,
81
+ storageName: view.storageName,
82
+ capabilities: view.capabilities,
83
+ entries,
84
+ }),
85
+ };
86
+ } catch {
87
+ return {
88
+ type: 'storage-request-error',
89
+ requestId,
90
+ code: 'READ_FAILED',
91
+ message: 'Failed to export the requested storage.',
92
+ };
93
+ }
94
+ };
@@ -0,0 +1,96 @@
1
+ import type {
2
+ StorageGetEntryRequestEvent,
3
+ StorageGetEntryResponseEvent,
4
+ StorageRequestErrorEvent,
5
+ } from '../shared/messaging';
6
+ import type { StorageTarget } from '../shared/types';
7
+ import type { StorageView } from './storage-view';
8
+
9
+ type FullEntryResult = StorageGetEntryResponseEvent | StorageRequestErrorEvent;
10
+
11
+ const getRequestId = (payload: unknown) =>
12
+ typeof payload === 'object' &&
13
+ payload != null &&
14
+ !Array.isArray(payload) &&
15
+ typeof (payload as { requestId?: unknown }).requestId === 'string'
16
+ ? (payload as { requestId: string }).requestId
17
+ : '';
18
+
19
+ const isTarget = (value: unknown): value is StorageTarget =>
20
+ typeof value === 'object' &&
21
+ value != null &&
22
+ !Array.isArray(value) &&
23
+ typeof (value as { adapterId?: unknown }).adapterId === 'string' &&
24
+ (value as { adapterId: string }).adapterId.trim().length > 0 &&
25
+ typeof (value as { storageId?: unknown }).storageId === 'string' &&
26
+ (value as { storageId: string }).storageId.trim().length > 0;
27
+
28
+ const isSameTarget = (left: StorageTarget, right: StorageTarget) =>
29
+ left.adapterId === right.adapterId && left.storageId === right.storageId;
30
+
31
+ const validateRequest = (
32
+ payload: unknown,
33
+ ): payload is StorageGetEntryRequestEvent =>
34
+ typeof payload === 'object' &&
35
+ payload != null &&
36
+ !Array.isArray(payload) &&
37
+ (payload as { type?: unknown }).type === 'get-entry' &&
38
+ typeof (payload as { requestId?: unknown }).requestId === 'string' &&
39
+ (payload as { requestId: string }).requestId.trim().length > 0 &&
40
+ isTarget((payload as { target?: unknown }).target) &&
41
+ typeof (payload as { key?: unknown }).key === 'string';
42
+
43
+ export const handleFullEntryRequest = async (
44
+ views: readonly StorageView[],
45
+ payload: unknown,
46
+ ): Promise<FullEntryResult> => {
47
+ const requestId = getRequestId(payload);
48
+ if (!validateRequest(payload)) {
49
+ return {
50
+ type: 'storage-request-error',
51
+ requestId,
52
+ code: 'INVALID_REQUEST',
53
+ message:
54
+ 'Full entry requests require a valid target, request ID, and string key.',
55
+ };
56
+ }
57
+
58
+ const view = views.find((candidate) =>
59
+ isSameTarget(candidate.target, payload.target),
60
+ );
61
+ if (!view) {
62
+ return {
63
+ type: 'storage-request-error',
64
+ requestId,
65
+ code: 'TARGET_NOT_FOUND',
66
+ message: 'The requested storage target was not found.',
67
+ };
68
+ }
69
+
70
+ try {
71
+ // StorageView applies the same blacklist policy used by preview requests.
72
+ const entry = await view.get(payload.key);
73
+ if (!entry) {
74
+ return {
75
+ type: 'storage-request-error',
76
+ requestId,
77
+ code: 'ENTRY_NOT_FOUND',
78
+ message: 'The requested storage entry was not found.',
79
+ };
80
+ }
81
+
82
+ return {
83
+ type: 'entry',
84
+ requestId,
85
+ target: view.target,
86
+ entry,
87
+ };
88
+ } catch {
89
+ return {
90
+ type: 'storage-request-error',
91
+ requestId,
92
+ code: 'READ_FAILED',
93
+ message: 'Failed to read the requested storage entry.',
94
+ };
95
+ }
96
+ };