@rozenite/storage-plugin 1.13.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +6 -1
- package/dist/devtools/assets/panel-BlDRXgVW.js +32 -0
- package/dist/devtools/assets/panel-DBweZnW5.css +1 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/async-storage.require.cjs +1 -1
- package/dist/react-native/chunks/async-storage.require.js +8 -7
- package/dist/react-native/chunks/index.require.cjs +1 -1
- package/dist/react-native/chunks/index.require.js +70 -66
- package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
- package/dist/react-native/chunks/secure-storage.require.js +17 -13
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -344
- package/dist/react-native/index.d.ts +166 -18
- package/dist/rozenite.json +1 -1
- package/dist/sdk/index.cjs +1 -1
- package/dist/sdk/index.d.ts +13 -5
- package/dist/sdk/index.js +21 -12
- package/package.json +35 -37
- package/react-native.ts +12 -22
- package/rozenite.config.ts +74 -0
- package/sdk.ts +1 -5
- package/src/react-native/__tests__/entry-preview-pagination.test.ts +234 -0
- package/src/react-native/__tests__/export-snapshot.test.ts +138 -0
- package/src/react-native/__tests__/full-entry-request.test.ts +143 -0
- package/src/react-native/__tests__/import.test.ts +174 -11
- package/src/react-native/__tests__/storage-discovery.test.ts +144 -0
- package/src/react-native/__tests__/storage-view.test.ts +172 -0
- package/src/react-native/__tests__/stress-verification.test.ts +140 -0
- package/src/react-native/__tests__/use-storage-agent-tools.test.ts +221 -0
- package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
- package/src/react-native/adapters/async-storage.ts +8 -9
- package/src/react-native/adapters/index.ts +2 -8
- package/src/react-native/adapters/mmkv.ts +12 -24
- package/src/react-native/adapters/secure-storage.ts +4 -0
- package/src/react-native/entry-preview-pagination.ts +261 -0
- package/src/react-native/export-snapshot.ts +88 -0
- package/src/react-native/full-entry-request.ts +91 -0
- package/src/react-native/import.ts +179 -16
- package/src/react-native/storage-discovery.ts +92 -0
- package/src/react-native/storage-view.ts +47 -109
- package/src/react-native/useRozeniteStoragePlugin.ts +153 -129
- package/src/react-native/useStorageAgentTools.ts +196 -91
- package/src/shared/__tests__/entry-preview.test.ts +99 -0
- package/src/shared/__tests__/snapshot.test.ts +31 -32
- package/src/shared/agent-tools.ts +33 -38
- package/src/shared/entry-preview.ts +54 -0
- package/src/shared/messaging.ts +140 -21
- package/src/shared/snapshot.ts +12 -18
- package/src/shared/types.ts +21 -4
- package/src/ui/__tests__/binary-value-editor-state.test.ts +16 -13
- package/src/ui/__tests__/binary.test.ts +31 -33
- package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
- package/src/ui/__tests__/large-value-viewer.test.tsx +54 -0
- package/src/ui/__tests__/panel.test.tsx +382 -0
- package/src/ui/__tests__/storage-groups.test.ts +88 -0
- package/src/ui/__tests__/type-conversion.test.ts +8 -24
- package/src/ui/__tests__/use-storage-entries.test.tsx +337 -0
- package/src/ui/add-entry-dialog.tsx +66 -117
- package/src/ui/binary-value-editor-state.ts +16 -25
- package/src/ui/binary-value-editor.tsx +59 -47
- package/src/ui/binary.ts +54 -15
- package/src/ui/edit-entry-dialog.tsx +68 -104
- package/src/ui/editor-switcher.tsx +9 -16
- package/src/ui/entry-detail-dialog.tsx +62 -181
- package/src/ui/format-value.tsx +25 -0
- package/src/ui/globals.css +1 -123
- package/src/ui/import-dialog.tsx +72 -109
- package/src/ui/large-value-viewer-state.ts +25 -0
- package/src/ui/large-value-viewer.tsx +65 -0
- package/src/ui/panel.tsx +649 -509
- package/src/ui/query-client.tsx +23 -0
- package/src/ui/storage-groups.ts +66 -0
- package/src/ui/type-conversion.ts +2 -5
- package/src/ui/typed-value-editor.tsx +15 -20
- package/src/ui/use-storage-entries.ts +192 -0
- package/src/ui/utils.ts +1 -4
- package/tsconfig.json +5 -1
- package/dist/devtools/assets/panel-Bm-SWF7d.js +0 -33
- package/dist/devtools/assets/panel-DIqI4WSp.css +0 -1
- package/postcss.config.js +0 -6
- package/src/ui/confirm-dialog.tsx +0 -100
- package/src/ui/editable-table.tsx +0 -300
- package/tailwind.config.ts +0 -94
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineAgentToolContract,
|
|
3
|
+
definePaginatedAgentToolContract,
|
|
3
4
|
type AgentToolContract,
|
|
4
5
|
} from '@rozenite/agent-shared';
|
|
5
6
|
|
|
@@ -10,8 +11,7 @@ type StorageSelection = Partial<StorageTarget>;
|
|
|
10
11
|
const sharedStorageProperties = {
|
|
11
12
|
adapterId: {
|
|
12
13
|
type: 'string',
|
|
13
|
-
description:
|
|
14
|
-
'Storage adapter ID. Required when multiple adapters are configured.',
|
|
14
|
+
description: 'Storage adapter ID. Required when multiple adapters are configured.',
|
|
15
15
|
},
|
|
16
16
|
storageId: {
|
|
17
17
|
type: 'string',
|
|
@@ -36,7 +36,6 @@ export type StorageListStoragesItem = {
|
|
|
36
36
|
storageId: string;
|
|
37
37
|
adapterName: string;
|
|
38
38
|
storageName: string;
|
|
39
|
-
entryCount: number;
|
|
40
39
|
supportedTypes: StorageEntryType[];
|
|
41
40
|
};
|
|
42
41
|
|
|
@@ -45,17 +44,22 @@ export type StorageListStoragesResult = {
|
|
|
45
44
|
};
|
|
46
45
|
|
|
47
46
|
export type StorageListEntriesArgs = StorageSelection & {
|
|
48
|
-
offset?: number;
|
|
49
47
|
limit?: number;
|
|
48
|
+
cursor?: string;
|
|
49
|
+
/** @deprecated Use the opaque cursor returned by a previous page. */
|
|
50
|
+
offset?: number;
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
export type StorageListEntriesResult = {
|
|
53
54
|
adapterId: string;
|
|
54
55
|
storageId: string;
|
|
55
56
|
total: number;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
items: Array<{ key: string }>;
|
|
58
|
+
page: {
|
|
59
|
+
limit: number;
|
|
60
|
+
hasMore: boolean;
|
|
61
|
+
nextCursor?: string;
|
|
62
|
+
};
|
|
59
63
|
};
|
|
60
64
|
|
|
61
65
|
export type StorageReadEntryArgs = StorageSelection & {
|
|
@@ -100,19 +104,13 @@ export type StorageRemoveEntryResult = {
|
|
|
100
104
|
};
|
|
101
105
|
|
|
102
106
|
export const storageToolDefinitions = {
|
|
103
|
-
listStorages: defineAgentToolContract<
|
|
104
|
-
StorageListStoragesArgs,
|
|
105
|
-
StorageListStoragesResult
|
|
106
|
-
>({
|
|
107
|
+
listStorages: defineAgentToolContract<StorageListStoragesArgs, StorageListStoragesResult>({
|
|
107
108
|
name: 'list-storages',
|
|
108
109
|
description:
|
|
109
|
-
'List all storage adapters and their storage nodes currently available on the device, including supported entry types
|
|
110
|
+
'List all storage adapters and their storage nodes currently available on the device, including supported entry types. Entry counts are omitted to avoid enumerating every storage.',
|
|
110
111
|
inputSchema: { type: 'object', properties: {} },
|
|
111
112
|
}),
|
|
112
|
-
listEntries:
|
|
113
|
-
StorageListEntriesArgs,
|
|
114
|
-
StorageListEntriesResult
|
|
115
|
-
>({
|
|
113
|
+
listEntries: definePaginatedAgentToolContract<StorageListEntriesArgs, StorageListEntriesResult>({
|
|
116
114
|
name: 'list-entries',
|
|
117
115
|
description:
|
|
118
116
|
'List keys in a storage. This call intentionally does not return entry values to keep responses token-efficient.',
|
|
@@ -120,21 +118,28 @@ export const storageToolDefinitions = {
|
|
|
120
118
|
type: 'object',
|
|
121
119
|
properties: {
|
|
122
120
|
...sharedStorageProperties,
|
|
123
|
-
|
|
121
|
+
limit: {
|
|
124
122
|
type: 'number',
|
|
125
|
-
description: 'Pagination
|
|
123
|
+
description: 'Pagination size. Defaults to 20 and is capped at 100.',
|
|
126
124
|
},
|
|
127
|
-
|
|
125
|
+
cursor: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: 'Opaque pagination cursor from a previous list-entries call.',
|
|
128
|
+
},
|
|
129
|
+
offset: {
|
|
128
130
|
type: 'number',
|
|
129
|
-
description:
|
|
131
|
+
description:
|
|
132
|
+
'Deprecated initial pagination offset. Must be a non-negative integer and cannot be combined with cursor.',
|
|
130
133
|
},
|
|
131
134
|
},
|
|
132
135
|
},
|
|
136
|
+
pagination: {
|
|
137
|
+
kind: 'cursor',
|
|
138
|
+
fields: ['key'],
|
|
139
|
+
defaultFields: ['key'],
|
|
140
|
+
},
|
|
133
141
|
}),
|
|
134
|
-
readEntry: defineAgentToolContract<
|
|
135
|
-
StorageReadEntryArgs,
|
|
136
|
-
StorageReadEntryResult
|
|
137
|
-
>({
|
|
142
|
+
readEntry: defineAgentToolContract<StorageReadEntryArgs, StorageReadEntryResult>({
|
|
138
143
|
name: 'read-entry',
|
|
139
144
|
description: 'Read a single storage entry value by key.',
|
|
140
145
|
inputSchema: {
|
|
@@ -146,10 +151,7 @@ export const storageToolDefinitions = {
|
|
|
146
151
|
required: ['key'],
|
|
147
152
|
},
|
|
148
153
|
}),
|
|
149
|
-
createEntry: defineAgentToolContract<
|
|
150
|
-
StorageCreateEntryArgs,
|
|
151
|
-
StorageCreateEntryResult
|
|
152
|
-
>({
|
|
154
|
+
createEntry: defineAgentToolContract<StorageCreateEntryArgs, StorageCreateEntryResult>({
|
|
153
155
|
name: 'create-entry',
|
|
154
156
|
description: 'Create a new storage entry. Fails if the key already exists.',
|
|
155
157
|
inputSchema: {
|
|
@@ -170,13 +172,9 @@ export const storageToolDefinitions = {
|
|
|
170
172
|
required: ['key', 'type', 'value'],
|
|
171
173
|
},
|
|
172
174
|
}),
|
|
173
|
-
editEntry: defineAgentToolContract<
|
|
174
|
-
StorageEditEntryArgs,
|
|
175
|
-
StorageEditEntryResult
|
|
176
|
-
>({
|
|
175
|
+
editEntry: defineAgentToolContract<StorageEditEntryArgs, StorageEditEntryResult>({
|
|
177
176
|
name: 'edit-entry',
|
|
178
|
-
description:
|
|
179
|
-
'Edit an existing storage entry. Fails if the key does not exist.',
|
|
177
|
+
description: 'Edit an existing storage entry. Fails if the key does not exist.',
|
|
180
178
|
inputSchema: {
|
|
181
179
|
type: 'object',
|
|
182
180
|
properties: {
|
|
@@ -195,10 +193,7 @@ export const storageToolDefinitions = {
|
|
|
195
193
|
required: ['key', 'type', 'value'],
|
|
196
194
|
},
|
|
197
195
|
}),
|
|
198
|
-
removeEntry: defineAgentToolContract<
|
|
199
|
-
StorageRemoveEntryArgs,
|
|
200
|
-
StorageRemoveEntryResult
|
|
201
|
-
>({
|
|
196
|
+
removeEntry: defineAgentToolContract<StorageRemoveEntryArgs, StorageRemoveEntryResult>({
|
|
202
197
|
name: 'remove-entry',
|
|
203
198
|
description: 'Remove a storage entry by key.',
|
|
204
199
|
inputSchema: {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { StorageEntry, StorageEntryPreview } from './types';
|
|
2
|
+
|
|
3
|
+
export const MAX_STRING_PREVIEW_CODE_POINTS = 256;
|
|
4
|
+
export const MAX_BUFFER_PREVIEW_BYTES = 16;
|
|
5
|
+
|
|
6
|
+
const toHexPair = (byte: number): string => byte.toString(16).toUpperCase().padStart(2, '0');
|
|
7
|
+
|
|
8
|
+
const truncateString = (value: string): Pick<StorageEntryPreview, 'preview' | 'isTruncated'> => {
|
|
9
|
+
let preview = '';
|
|
10
|
+
let codePointCount = 0;
|
|
11
|
+
|
|
12
|
+
for (const codePoint of value) {
|
|
13
|
+
if (codePointCount === MAX_STRING_PREVIEW_CODE_POINTS) {
|
|
14
|
+
return { preview, isTruncated: true };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
preview += codePoint;
|
|
18
|
+
codePointCount += 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { preview, isTruncated: false };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const toStorageEntryPreview = (entry: StorageEntry): StorageEntryPreview => {
|
|
25
|
+
if (entry.type === 'string') {
|
|
26
|
+
return {
|
|
27
|
+
key: entry.key,
|
|
28
|
+
type: entry.type,
|
|
29
|
+
...truncateString(entry.value),
|
|
30
|
+
// JavaScript strings expose their length in UTF-16 code units.
|
|
31
|
+
valueSize: entry.value.length,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (entry.type === 'buffer') {
|
|
36
|
+
const shownBytes = entry.value.slice(0, MAX_BUFFER_PREVIEW_BYTES);
|
|
37
|
+
return {
|
|
38
|
+
key: entry.key,
|
|
39
|
+
type: entry.type,
|
|
40
|
+
preview: shownBytes.map(toHexPair).join(' '),
|
|
41
|
+
valueSize: entry.value.length,
|
|
42
|
+
isTruncated: entry.value.length > MAX_BUFFER_PREVIEW_BYTES,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const preview = String(entry.value);
|
|
47
|
+
return {
|
|
48
|
+
key: entry.key,
|
|
49
|
+
type: entry.type,
|
|
50
|
+
preview,
|
|
51
|
+
valueSize: preview.length,
|
|
52
|
+
isTruncated: false,
|
|
53
|
+
};
|
|
54
|
+
};
|
package/src/shared/messaging.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export type SerializedBlacklist = {
|
|
4
|
-
source: string;
|
|
5
|
-
flags: string;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export type StorageSnapshotEvent = {
|
|
9
|
-
type: 'snapshot';
|
|
10
|
-
target: StorageTarget;
|
|
11
|
-
adapterName: string;
|
|
12
|
-
storageName: string;
|
|
13
|
-
capabilities: StorageCapabilities;
|
|
14
|
-
blacklist?: SerializedBlacklist;
|
|
15
|
-
entries: StorageEntry[];
|
|
16
|
-
};
|
|
1
|
+
import type { StorageDescriptor, StorageEntry, StorageEntryPreview, StorageTarget } from './types';
|
|
2
|
+
import type { ImportPreview, StorageSnapshotV1 } from './snapshot';
|
|
17
3
|
|
|
18
4
|
export type StorageSetEntryEvent = {
|
|
19
5
|
type: 'set-entry';
|
|
@@ -27,19 +13,139 @@ export type StorageDeleteEntryEvent = {
|
|
|
27
13
|
key: string;
|
|
28
14
|
};
|
|
29
15
|
|
|
30
|
-
export type
|
|
31
|
-
type: '
|
|
32
|
-
target: StorageTarget
|
|
16
|
+
export type StoragePurgeEvent = {
|
|
17
|
+
type: 'purge-storage';
|
|
18
|
+
target: StorageTarget;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type StorageInvalidationOperation = 'set' | 'delete' | 'import' | 'purge';
|
|
22
|
+
|
|
23
|
+
export type StorageInvalidatedEvent = {
|
|
24
|
+
type: 'storage-invalidated';
|
|
25
|
+
target: StorageTarget;
|
|
26
|
+
/** Native subscriptions only expose a key, not the operation. */
|
|
27
|
+
key?: string;
|
|
28
|
+
operation?: StorageInvalidationOperation;
|
|
29
|
+
/** Updated entry count for the storage after the invalidation. */
|
|
30
|
+
entryCount: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Announced by the device once it is listening for panel requests. The panel is
|
|
35
|
+
* recreated on every app reload and asks for storages immediately, which can
|
|
36
|
+
* land before the app's React tree has mounted the plugin; this lets the panel
|
|
37
|
+
* ask again instead of waiting forever on a dropped request.
|
|
38
|
+
*/
|
|
39
|
+
export type StorageDeviceReadyEvent = {
|
|
40
|
+
type: 'device-ready';
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type StorageDiscoverStoragesRequestEvent = {
|
|
44
|
+
type: 'discover-storages';
|
|
45
|
+
requestId: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type StorageDiscoverStoragesResponseEvent = {
|
|
49
|
+
type: 'storage-descriptors';
|
|
50
|
+
requestId: string;
|
|
51
|
+
storages: StorageDescriptor[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type StorageListEntryPreviewsRequestEvent = {
|
|
55
|
+
type: 'list-entry-previews';
|
|
56
|
+
requestId: string;
|
|
57
|
+
target: StorageTarget;
|
|
58
|
+
search?: string;
|
|
59
|
+
keySortDirection?: 'ascending' | 'descending';
|
|
60
|
+
cursor?: string;
|
|
61
|
+
limit: number;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type StorageListEntryPreviewsResponseEvent = {
|
|
65
|
+
type: 'entry-previews';
|
|
66
|
+
requestId: string;
|
|
67
|
+
target: StorageTarget;
|
|
68
|
+
items: StorageEntryPreview[];
|
|
69
|
+
nextCursor?: string;
|
|
70
|
+
previousCursor?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type StorageGetEntryRequestEvent = {
|
|
74
|
+
type: 'get-entry';
|
|
75
|
+
requestId: string;
|
|
76
|
+
target: StorageTarget;
|
|
77
|
+
key: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type StorageGetEntryResponseEvent = {
|
|
81
|
+
type: 'entry';
|
|
82
|
+
requestId: string;
|
|
83
|
+
target: StorageTarget;
|
|
84
|
+
entry: StorageEntry;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type StorageExportSnapshotRequestEvent = {
|
|
88
|
+
type: 'export-snapshot';
|
|
89
|
+
requestId: string;
|
|
90
|
+
target: StorageTarget;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type StorageExportSnapshotResponseEvent = {
|
|
94
|
+
type: 'export-snapshot-result';
|
|
95
|
+
requestId: string;
|
|
96
|
+
target: StorageTarget;
|
|
97
|
+
snapshot: StorageSnapshotV1;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type StorageImportPreviewRequestEvent = {
|
|
101
|
+
type: 'preview-import';
|
|
102
|
+
requestId: string;
|
|
103
|
+
target: StorageTarget;
|
|
104
|
+
snapshot: StorageSnapshotV1;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type StorageImportPreviewResponseEvent = {
|
|
108
|
+
type: 'import-preview';
|
|
109
|
+
requestId: string;
|
|
110
|
+
target: StorageTarget;
|
|
111
|
+
preview: ImportPreview;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type StorageRequestError = {
|
|
115
|
+
requestId: string;
|
|
116
|
+
code:
|
|
117
|
+
| 'TARGET_NOT_FOUND'
|
|
118
|
+
| 'ENTRY_NOT_FOUND'
|
|
119
|
+
| 'INVALID_CURSOR'
|
|
120
|
+
| 'INVALID_REQUEST'
|
|
121
|
+
| 'READ_FAILED'
|
|
122
|
+
| 'WRITE_FAILED';
|
|
123
|
+
message: string;
|
|
124
|
+
resetPagination?: boolean;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type StorageRequestErrorEvent = StorageRequestError & {
|
|
128
|
+
type: 'storage-request-error';
|
|
33
129
|
};
|
|
34
130
|
|
|
35
131
|
export type StorageImportEntriesEvent = {
|
|
36
132
|
type: 'import-entries';
|
|
133
|
+
requestId: string;
|
|
37
134
|
target: StorageTarget;
|
|
38
135
|
entries: StorageEntry[];
|
|
39
136
|
};
|
|
40
137
|
|
|
138
|
+
export type StorageImportProgressEvent = {
|
|
139
|
+
type: 'import-progress';
|
|
140
|
+
requestId: string;
|
|
141
|
+
target: StorageTarget;
|
|
142
|
+
written: number;
|
|
143
|
+
total: number;
|
|
144
|
+
};
|
|
145
|
+
|
|
41
146
|
export type StorageImportResultEvent = {
|
|
42
147
|
type: 'import-result';
|
|
148
|
+
requestId: string;
|
|
43
149
|
target: StorageTarget;
|
|
44
150
|
ok: boolean;
|
|
45
151
|
written: number;
|
|
@@ -49,11 +155,24 @@ export type StorageImportResultEvent = {
|
|
|
49
155
|
};
|
|
50
156
|
|
|
51
157
|
export type StorageEvent =
|
|
52
|
-
| StorageSnapshotEvent
|
|
53
158
|
| StorageSetEntryEvent
|
|
54
159
|
| StorageDeleteEntryEvent
|
|
55
|
-
|
|
|
160
|
+
| StoragePurgeEvent
|
|
161
|
+
| StorageInvalidatedEvent
|
|
162
|
+
| StorageDeviceReadyEvent
|
|
163
|
+
| StorageDiscoverStoragesRequestEvent
|
|
164
|
+
| StorageDiscoverStoragesResponseEvent
|
|
165
|
+
| StorageListEntryPreviewsRequestEvent
|
|
166
|
+
| StorageListEntryPreviewsResponseEvent
|
|
167
|
+
| StorageGetEntryRequestEvent
|
|
168
|
+
| StorageGetEntryResponseEvent
|
|
169
|
+
| StorageExportSnapshotRequestEvent
|
|
170
|
+
| StorageExportSnapshotResponseEvent
|
|
171
|
+
| StorageImportPreviewRequestEvent
|
|
172
|
+
| StorageImportPreviewResponseEvent
|
|
173
|
+
| StorageRequestErrorEvent
|
|
56
174
|
| StorageImportEntriesEvent
|
|
175
|
+
| StorageImportProgressEvent
|
|
57
176
|
| StorageImportResultEvent;
|
|
58
177
|
|
|
59
178
|
export type StorageEventMap = {
|
package/src/shared/snapshot.ts
CHANGED
|
@@ -48,10 +48,7 @@ const describe = (value: unknown): string => {
|
|
|
48
48
|
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
49
49
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
50
50
|
|
|
51
|
-
const expectObject = (
|
|
52
|
-
value: unknown,
|
|
53
|
-
path: string,
|
|
54
|
-
): Record<string, unknown> => {
|
|
51
|
+
const expectObject = (value: unknown, path: string): Record<string, unknown> => {
|
|
55
52
|
if (!isPlainObject(value)) {
|
|
56
53
|
throw new ParseException(path, `Expected object, got ${describe(value)}`);
|
|
57
54
|
}
|
|
@@ -73,10 +70,7 @@ const expectArray = (value: unknown, path: string): unknown[] => {
|
|
|
73
70
|
};
|
|
74
71
|
|
|
75
72
|
const expectEntryType = (value: unknown, path: string): StorageEntryType => {
|
|
76
|
-
if (
|
|
77
|
-
typeof value !== 'string' ||
|
|
78
|
-
!DEFAULT_SUPPORTED_TYPES.includes(value as StorageEntryType)
|
|
79
|
-
) {
|
|
73
|
+
if (typeof value !== 'string' || !DEFAULT_SUPPORTED_TYPES.includes(value as StorageEntryType)) {
|
|
80
74
|
throw new ParseException(
|
|
81
75
|
path,
|
|
82
76
|
`Expected one of ${DEFAULT_SUPPORTED_TYPES.join(', ')}, got ${describe(value)}`,
|
|
@@ -151,12 +145,7 @@ const parseEntry = (raw: unknown, path: string): StorageEntry => {
|
|
|
151
145
|
);
|
|
152
146
|
}
|
|
153
147
|
const bytes = value.map((byte, index) => {
|
|
154
|
-
if (
|
|
155
|
-
typeof byte !== 'number' ||
|
|
156
|
-
!Number.isInteger(byte) ||
|
|
157
|
-
byte < 0 ||
|
|
158
|
-
byte > 255
|
|
159
|
-
) {
|
|
148
|
+
if (typeof byte !== 'number' || !Number.isInteger(byte) || byte < 0 || byte > 255) {
|
|
160
149
|
throw new ParseException(
|
|
161
150
|
`${valuePath}[${index}]`,
|
|
162
151
|
`Expected uint8 (integer 0-255), got ${describe(byte)}`,
|
|
@@ -183,9 +172,7 @@ export const parseSnapshot = (raw: unknown): ParseResult => {
|
|
|
183
172
|
const createdAt = expectString(root.createdAt, 'createdAt');
|
|
184
173
|
const storage = parseStorageMeta(root.storage);
|
|
185
174
|
const rawEntries = expectArray(root.entries, 'entries');
|
|
186
|
-
const entries = rawEntries.map((entry, index) =>
|
|
187
|
-
parseEntry(entry, `entries[${index}]`),
|
|
188
|
-
);
|
|
175
|
+
const entries = rawEntries.map((entry, index) => parseEntry(entry, `entries[${index}]`));
|
|
189
176
|
return {
|
|
190
177
|
ok: true,
|
|
191
178
|
snapshot: {
|
|
@@ -229,6 +216,7 @@ export type ImportPreview = {
|
|
|
229
216
|
overwriteKeys: string[];
|
|
230
217
|
skippedKeys: { key: string; reason: 'blacklist' }[];
|
|
231
218
|
unsupportedTypes: { key: string; type: StorageEntryType }[];
|
|
219
|
+
acceptedEntryIndexes: number[];
|
|
232
220
|
metadataMismatch: boolean;
|
|
233
221
|
};
|
|
234
222
|
|
|
@@ -245,8 +233,9 @@ export const computePreview = (
|
|
|
245
233
|
const overwriteKeys: string[] = [];
|
|
246
234
|
const skippedKeys: { key: string; reason: 'blacklist' }[] = [];
|
|
247
235
|
const unsupportedTypes: { key: string; type: StorageEntryType }[] = [];
|
|
236
|
+
const acceptedEntryIndexes: number[] = [];
|
|
248
237
|
|
|
249
|
-
for (const entry of snapshot.entries) {
|
|
238
|
+
for (const [index, entry] of snapshot.entries.entries()) {
|
|
250
239
|
if (!supportsType(current.capabilities, entry.type)) {
|
|
251
240
|
unsupportedTypes.push({ key: entry.key, type: entry.type });
|
|
252
241
|
continue;
|
|
@@ -260,6 +249,10 @@ export const computePreview = (
|
|
|
260
249
|
} else {
|
|
261
250
|
newKeys.push(entry.key);
|
|
262
251
|
}
|
|
252
|
+
acceptedEntryIndexes.push(index);
|
|
253
|
+
// Duplicate keys are applied in source order, so every accepted duplicate
|
|
254
|
+
// after the first overwrites the value that preceded it in this import.
|
|
255
|
+
current.entryKeys.add(entry.key);
|
|
263
256
|
}
|
|
264
257
|
|
|
265
258
|
const metadataMismatch =
|
|
@@ -271,6 +264,7 @@ export const computePreview = (
|
|
|
271
264
|
overwriteKeys,
|
|
272
265
|
skippedKeys,
|
|
273
266
|
unsupportedTypes,
|
|
267
|
+
acceptedEntryIndexes,
|
|
274
268
|
metadataMismatch,
|
|
275
269
|
};
|
|
276
270
|
};
|
package/src/shared/types.ts
CHANGED
|
@@ -7,10 +7,27 @@ export type StorageEntry =
|
|
|
7
7
|
export type StorageEntryType = StorageEntry['type'];
|
|
8
8
|
export type StorageEntryValue = StorageEntry['value'];
|
|
9
9
|
|
|
10
|
+
export type StorageEntryPreview = {
|
|
11
|
+
key: string;
|
|
12
|
+
type: StorageEntryType;
|
|
13
|
+
preview: string;
|
|
14
|
+
valueSize: number;
|
|
15
|
+
isTruncated: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
10
18
|
export type StorageCapabilities = {
|
|
11
19
|
supportedTypes: StorageEntryType[];
|
|
12
20
|
};
|
|
13
21
|
|
|
22
|
+
export type StorageDescriptor = {
|
|
23
|
+
target: StorageTarget;
|
|
24
|
+
adapterName: string;
|
|
25
|
+
storageName: string;
|
|
26
|
+
entryCount: number;
|
|
27
|
+
capabilities: StorageCapabilities;
|
|
28
|
+
supportsSubscriptions: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
14
31
|
export type StorageSubscription = { remove: () => void };
|
|
15
32
|
|
|
16
33
|
export type SyncStorage = {
|
|
@@ -19,6 +36,7 @@ export type SyncStorage = {
|
|
|
19
36
|
get: (key: string) => StorageEntry | undefined;
|
|
20
37
|
set: (entry: StorageEntry) => void;
|
|
21
38
|
delete: (key: string) => void;
|
|
39
|
+
clear: () => void;
|
|
22
40
|
subscribe?: (callback: (key: string) => void) => StorageSubscription;
|
|
23
41
|
};
|
|
24
42
|
|
|
@@ -28,6 +46,7 @@ export type AsyncStorage = {
|
|
|
28
46
|
get: (key: string) => Promise<StorageEntry | undefined>;
|
|
29
47
|
set: (entry: StorageEntry) => Promise<void>;
|
|
30
48
|
delete: (key: string) => Promise<void>;
|
|
49
|
+
clear: () => Promise<void>;
|
|
31
50
|
subscribe?: (callback: (key: string) => void) => StorageSubscription;
|
|
32
51
|
};
|
|
33
52
|
|
|
@@ -61,7 +80,5 @@ export const DEFAULT_SUPPORTED_TYPES: StorageEntryType[] = [
|
|
|
61
80
|
export const getStorageViewId = ({ adapterId, storageId }: StorageTarget) =>
|
|
62
81
|
`${adapterId}:${storageId}`;
|
|
63
82
|
|
|
64
|
-
export const supportsType = (
|
|
65
|
-
capabilities
|
|
66
|
-
type: StorageEntryType
|
|
67
|
-
) => capabilities.supportedTypes.includes(type);
|
|
83
|
+
export const supportsType = (capabilities: StorageCapabilities, type: StorageEntryType) =>
|
|
84
|
+
capabilities.supportedTypes.includes(type);
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
initialState,
|
|
4
|
-
reduce,
|
|
5
|
-
validate,
|
|
6
|
-
type EditorState,
|
|
7
|
-
} from '../binary-value-editor-state';
|
|
2
|
+
import { initialState, reduce, validate, type EditorState } from '../binary-value-editor-state';
|
|
8
3
|
|
|
9
4
|
const hexState = (bytes?: number[]): EditorState =>
|
|
10
5
|
initialState({ initialBytes: bytes, mode: 'hex' });
|
|
@@ -29,9 +24,7 @@ describe('initialState', () => {
|
|
|
29
24
|
});
|
|
30
25
|
|
|
31
26
|
it('encodes initial bytes as base64 when mode is base64', () => {
|
|
32
|
-
expect(
|
|
33
|
-
initialState({ initialBytes: [0x48, 0x69], mode: 'base64' }),
|
|
34
|
-
).toEqual({
|
|
27
|
+
expect(initialState({ initialBytes: [0x48, 0x69], mode: 'base64' })).toEqual({
|
|
35
28
|
mode: 'base64',
|
|
36
29
|
text: 'SGk=',
|
|
37
30
|
bytes: [0x48, 0x69],
|
|
@@ -101,13 +94,12 @@ describe('reduce: set-text', () => {
|
|
|
101
94
|
|
|
102
95
|
describe('reduce: normalize-paste', () => {
|
|
103
96
|
it('rewrites pasted hexdump into canonical grouped hex', () => {
|
|
104
|
-
const pasted =
|
|
105
|
-
'00000000 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 |.PNG........IHDR|';
|
|
97
|
+
const pasted = '00000000 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 |.PNG........IHDR|';
|
|
106
98
|
const next = reduce(hexState(), { type: 'normalize-paste', text: pasted });
|
|
107
99
|
expect(next.text).toBe('89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52');
|
|
108
100
|
expect(next.bytes).toEqual([
|
|
109
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
|
110
|
-
|
|
101
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
|
|
102
|
+
0x52,
|
|
111
103
|
]);
|
|
112
104
|
expect(next.error).toBeNull();
|
|
113
105
|
});
|
|
@@ -176,6 +168,17 @@ describe('reduce: switch-mode', () => {
|
|
|
176
168
|
});
|
|
177
169
|
});
|
|
178
170
|
|
|
171
|
+
describe('reduce: replace-bytes', () => {
|
|
172
|
+
it('formats fetched bytes after the editor has had a chance to paint', () => {
|
|
173
|
+
expect(reduce(hexState(), { type: 'replace-bytes', bytes: [0x48, 0x69] })).toEqual({
|
|
174
|
+
mode: 'hex',
|
|
175
|
+
text: '48 69',
|
|
176
|
+
bytes: [0x48, 0x69],
|
|
177
|
+
error: null,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
179
182
|
describe('validate', () => {
|
|
180
183
|
it('passes when bytes are valid and non-empty', () => {
|
|
181
184
|
const state = reduce(hexState(), { type: 'set-text', text: '89 50' });
|