@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +6 -1
  3. package/dist/devtools/assets/panel-BlDRXgVW.js +32 -0
  4. package/dist/devtools/assets/panel-DBweZnW5.css +1 -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 -66
  10. package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
  11. package/dist/react-native/chunks/secure-storage.require.js +17 -13
  12. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  13. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -344
  14. package/dist/react-native/index.d.ts +166 -18
  15. package/dist/rozenite.json +1 -1
  16. package/dist/sdk/index.cjs +1 -1
  17. package/dist/sdk/index.d.ts +13 -5
  18. package/dist/sdk/index.js +21 -12
  19. package/package.json +35 -37
  20. package/react-native.ts +12 -22
  21. package/rozenite.config.ts +74 -0
  22. package/sdk.ts +1 -5
  23. package/src/react-native/__tests__/entry-preview-pagination.test.ts +234 -0
  24. package/src/react-native/__tests__/export-snapshot.test.ts +138 -0
  25. package/src/react-native/__tests__/full-entry-request.test.ts +143 -0
  26. package/src/react-native/__tests__/import.test.ts +174 -11
  27. package/src/react-native/__tests__/storage-discovery.test.ts +144 -0
  28. package/src/react-native/__tests__/storage-view.test.ts +172 -0
  29. package/src/react-native/__tests__/stress-verification.test.ts +140 -0
  30. package/src/react-native/__tests__/use-storage-agent-tools.test.ts +221 -0
  31. package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
  32. package/src/react-native/adapters/async-storage.ts +8 -9
  33. package/src/react-native/adapters/index.ts +2 -8
  34. package/src/react-native/adapters/mmkv.ts +12 -24
  35. package/src/react-native/adapters/secure-storage.ts +4 -0
  36. package/src/react-native/entry-preview-pagination.ts +261 -0
  37. package/src/react-native/export-snapshot.ts +88 -0
  38. package/src/react-native/full-entry-request.ts +91 -0
  39. package/src/react-native/import.ts +179 -16
  40. package/src/react-native/storage-discovery.ts +92 -0
  41. package/src/react-native/storage-view.ts +47 -109
  42. package/src/react-native/useRozeniteStoragePlugin.ts +153 -129
  43. package/src/react-native/useStorageAgentTools.ts +196 -91
  44. package/src/shared/__tests__/entry-preview.test.ts +99 -0
  45. package/src/shared/__tests__/snapshot.test.ts +31 -32
  46. package/src/shared/agent-tools.ts +33 -38
  47. package/src/shared/entry-preview.ts +54 -0
  48. package/src/shared/messaging.ts +140 -21
  49. package/src/shared/snapshot.ts +12 -18
  50. package/src/shared/types.ts +21 -4
  51. package/src/ui/__tests__/binary-value-editor-state.test.ts +16 -13
  52. package/src/ui/__tests__/binary.test.ts +31 -33
  53. package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
  54. package/src/ui/__tests__/large-value-viewer.test.tsx +54 -0
  55. package/src/ui/__tests__/panel.test.tsx +382 -0
  56. package/src/ui/__tests__/storage-groups.test.ts +88 -0
  57. package/src/ui/__tests__/type-conversion.test.ts +8 -24
  58. package/src/ui/__tests__/use-storage-entries.test.tsx +337 -0
  59. package/src/ui/add-entry-dialog.tsx +66 -117
  60. package/src/ui/binary-value-editor-state.ts +16 -25
  61. package/src/ui/binary-value-editor.tsx +59 -47
  62. package/src/ui/binary.ts +54 -15
  63. package/src/ui/edit-entry-dialog.tsx +68 -104
  64. package/src/ui/editor-switcher.tsx +9 -16
  65. package/src/ui/entry-detail-dialog.tsx +62 -181
  66. package/src/ui/format-value.tsx +25 -0
  67. package/src/ui/globals.css +1 -123
  68. package/src/ui/import-dialog.tsx +72 -109
  69. package/src/ui/large-value-viewer-state.ts +25 -0
  70. package/src/ui/large-value-viewer.tsx +65 -0
  71. package/src/ui/panel.tsx +649 -509
  72. package/src/ui/query-client.tsx +23 -0
  73. package/src/ui/storage-groups.ts +66 -0
  74. package/src/ui/type-conversion.ts +2 -5
  75. package/src/ui/typed-value-editor.tsx +15 -20
  76. package/src/ui/use-storage-entries.ts +192 -0
  77. package/src/ui/utils.ts +1 -4
  78. package/tsconfig.json +5 -1
  79. package/dist/devtools/assets/panel-Bm-SWF7d.js +0 -33
  80. package/dist/devtools/assets/panel-DIqI4WSp.css +0 -1
  81. package/postcss.config.js +0 -6
  82. package/src/ui/confirm-dialog.tsx +0 -100
  83. package/src/ui/editable-table.tsx +0 -300
  84. 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
- offset: number;
57
- limit: number;
58
- keys: string[];
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 and entry counts.',
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: defineAgentToolContract<
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
- offset: {
121
+ limit: {
124
122
  type: 'number',
125
- description: 'Pagination offset. Defaults to 0.',
123
+ description: 'Pagination size. Defaults to 20 and is capped at 100.',
126
124
  },
127
- limit: {
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: 'Pagination size. Defaults to 100.',
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
+ };
@@ -1,19 +1,5 @@
1
- import type { StorageCapabilities, StorageEntry, StorageTarget } from './types';
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 StorageGetSnapshotEvent = {
31
- type: 'get-snapshot';
32
- target: StorageTarget | 'all';
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
- | StorageGetSnapshotEvent
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 = {
@@ -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
  };
@@ -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: StorageCapabilities,
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
- 0x49, 0x48, 0x44, 0x52,
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' });