@rozenite/storage-plugin 1.13.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +6 -1
- package/dist/devtools/assets/panel-B6Wp0eie.css +1 -0
- package/dist/devtools/assets/panel-D7MFc4HG.js +32 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/async-storage.require.cjs +1 -1
- package/dist/react-native/chunks/async-storage.require.js +8 -7
- package/dist/react-native/chunks/index.require.cjs +1 -1
- package/dist/react-native/chunks/index.require.js +70 -64
- package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
- package/dist/react-native/chunks/secure-storage.require.js +22 -14
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -315
- package/dist/react-native/index.d.ts +154 -18
- package/dist/react-native/index.js +10 -2
- package/dist/rozenite.json +1 -1
- package/dist/sdk/index.cjs +1 -1
- package/dist/sdk/index.d.ts +13 -5
- package/dist/sdk/index.js +21 -12
- package/package.json +13 -15
- package/react-native.ts +29 -14
- package/rozenite.config.ts +78 -0
- package/src/react-native/__tests__/entry-preview-pagination.test.ts +262 -0
- package/src/react-native/__tests__/export-snapshot.test.ts +142 -0
- package/src/react-native/__tests__/full-entry-request.test.ts +155 -0
- package/src/react-native/__tests__/import.test.ts +172 -10
- package/src/react-native/__tests__/storage-discovery.test.ts +155 -0
- package/src/react-native/__tests__/storage-view.test.ts +172 -0
- package/src/react-native/__tests__/stress-verification.test.ts +153 -0
- package/src/react-native/__tests__/use-storage-agent-tools.test.ts +231 -0
- package/src/react-native/adapters/async-storage.ts +9 -7
- package/src/react-native/adapters/mmkv.ts +5 -0
- package/src/react-native/adapters/secure-storage.ts +9 -1
- package/src/react-native/entry-preview-pagination.ts +277 -0
- package/src/react-native/export-snapshot.ts +94 -0
- package/src/react-native/full-entry-request.ts +96 -0
- package/src/react-native/import.ts +190 -16
- package/src/react-native/storage-discovery.ts +96 -0
- package/src/react-native/storage-view.ts +45 -103
- package/src/react-native/useRozeniteStoragePlugin.ts +108 -72
- package/src/react-native/useStorageAgentTools.ts +207 -86
- package/src/shared/__tests__/entry-preview.test.ts +117 -0
- package/src/shared/__tests__/snapshot.test.ts +21 -0
- package/src/shared/agent-tools.ts +27 -11
- package/src/shared/entry-preview.ts +59 -0
- package/src/shared/messaging.ts +136 -21
- package/src/shared/snapshot.ts +8 -1
- package/src/shared/types.ts +20 -1
- package/src/ui/__tests__/binary-value-editor-state.test.ts +13 -0
- package/src/ui/__tests__/binary.test.ts +18 -0
- package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
- package/src/ui/__tests__/large-value-viewer.test.tsx +63 -0
- package/src/ui/__tests__/panel.test.tsx +409 -0
- package/src/ui/__tests__/storage-groups.test.ts +91 -0
- package/src/ui/__tests__/use-storage-entries.test.tsx +353 -0
- package/src/ui/add-entry-dialog.tsx +65 -105
- package/src/ui/binary-value-editor-state.ts +10 -1
- package/src/ui/binary-value-editor.tsx +57 -33
- package/src/ui/binary.ts +56 -4
- package/src/ui/edit-entry-dialog.tsx +71 -99
- package/src/ui/editor-switcher.tsx +7 -5
- package/src/ui/entry-detail-dialog.tsx +69 -179
- package/src/ui/format-value.tsx +32 -0
- package/src/ui/globals.css +1 -123
- package/src/ui/import-dialog.tsx +66 -91
- package/src/ui/large-value-viewer-state.ts +32 -0
- package/src/ui/large-value-viewer.tsx +71 -0
- package/src/ui/panel.tsx +724 -496
- package/src/ui/query-client.tsx +34 -0
- package/src/ui/storage-groups.ts +66 -0
- package/src/ui/typed-value-editor.tsx +14 -15
- package/src/ui/use-storage-entries.ts +207 -0
- package/tsconfig.json +5 -1
- package/dist/devtools/assets/panel-Bm-SWF7d.js +0 -33
- package/dist/devtools/assets/panel-DIqI4WSp.css +0 -1
- package/postcss.config.js +0 -6
- package/src/ui/confirm-dialog.tsx +0 -100
- package/src/ui/editable-table.tsx +0 -300
- package/tailwind.config.ts +0 -94
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
MAX_BUFFER_PREVIEW_BYTES,
|
|
4
|
+
MAX_STRING_PREVIEW_CODE_POINTS,
|
|
5
|
+
toStorageEntryPreview,
|
|
6
|
+
} from '../entry-preview';
|
|
7
|
+
|
|
8
|
+
describe('toStorageEntryPreview', () => {
|
|
9
|
+
it('represents empty and boundary-sized strings completely', () => {
|
|
10
|
+
expect(
|
|
11
|
+
toStorageEntryPreview({ key: 'empty', type: 'string', value: '' }),
|
|
12
|
+
).toEqual({
|
|
13
|
+
key: 'empty',
|
|
14
|
+
type: 'string',
|
|
15
|
+
preview: '',
|
|
16
|
+
valueSize: 0,
|
|
17
|
+
isTruncated: false,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const value = 'a'.repeat(MAX_STRING_PREVIEW_CODE_POINTS);
|
|
21
|
+
expect(
|
|
22
|
+
toStorageEntryPreview({ key: 'boundary', type: 'string', value }),
|
|
23
|
+
).toEqual({
|
|
24
|
+
key: 'boundary',
|
|
25
|
+
type: 'string',
|
|
26
|
+
preview: value,
|
|
27
|
+
valueSize: MAX_STRING_PREVIEW_CODE_POINTS,
|
|
28
|
+
isTruncated: false,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('truncates an oversized string at the fixed code-point boundary', () => {
|
|
33
|
+
const value = `${'a'.repeat(MAX_STRING_PREVIEW_CODE_POINTS)}z`;
|
|
34
|
+
|
|
35
|
+
expect(
|
|
36
|
+
toStorageEntryPreview({ key: 'long', type: 'string', value }),
|
|
37
|
+
).toEqual({
|
|
38
|
+
key: 'long',
|
|
39
|
+
type: 'string',
|
|
40
|
+
preview: 'a'.repeat(MAX_STRING_PREVIEW_CODE_POINTS),
|
|
41
|
+
valueSize: MAX_STRING_PREVIEW_CODE_POINTS + 1,
|
|
42
|
+
isTruncated: true,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does not split Unicode code points while preserving string length', () => {
|
|
47
|
+
const value = `${'😀'.repeat(MAX_STRING_PREVIEW_CODE_POINTS)}!`;
|
|
48
|
+
|
|
49
|
+
expect(
|
|
50
|
+
toStorageEntryPreview({ key: 'emoji', type: 'string', value }),
|
|
51
|
+
).toEqual({
|
|
52
|
+
key: 'emoji',
|
|
53
|
+
type: 'string',
|
|
54
|
+
preview: '😀'.repeat(MAX_STRING_PREVIEW_CODE_POINTS),
|
|
55
|
+
valueSize: value.length,
|
|
56
|
+
isTruncated: true,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('formats primitive values completely', () => {
|
|
61
|
+
expect(
|
|
62
|
+
toStorageEntryPreview({ key: 'count', type: 'number', value: 42 }),
|
|
63
|
+
).toEqual({
|
|
64
|
+
key: 'count',
|
|
65
|
+
type: 'number',
|
|
66
|
+
preview: '42',
|
|
67
|
+
valueSize: 2,
|
|
68
|
+
isTruncated: false,
|
|
69
|
+
});
|
|
70
|
+
expect(
|
|
71
|
+
toStorageEntryPreview({ key: 'enabled', type: 'boolean', value: false }),
|
|
72
|
+
).toEqual({
|
|
73
|
+
key: 'enabled',
|
|
74
|
+
type: 'boolean',
|
|
75
|
+
preview: 'false',
|
|
76
|
+
valueSize: 5,
|
|
77
|
+
isTruncated: false,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('formats empty and boundary-sized buffers as hexadecimal', () => {
|
|
82
|
+
expect(
|
|
83
|
+
toStorageEntryPreview({ key: 'empty', type: 'buffer', value: [] }),
|
|
84
|
+
).toEqual({
|
|
85
|
+
key: 'empty',
|
|
86
|
+
type: 'buffer',
|
|
87
|
+
preview: '',
|
|
88
|
+
valueSize: 0,
|
|
89
|
+
isTruncated: false,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const value = new Array(MAX_BUFFER_PREVIEW_BYTES).fill(0xab);
|
|
93
|
+
expect(
|
|
94
|
+
toStorageEntryPreview({ key: 'boundary', type: 'buffer', value }),
|
|
95
|
+
).toEqual({
|
|
96
|
+
key: 'boundary',
|
|
97
|
+
type: 'buffer',
|
|
98
|
+
preview: new Array(MAX_BUFFER_PREVIEW_BYTES).fill('AB').join(' '),
|
|
99
|
+
valueSize: MAX_BUFFER_PREVIEW_BYTES,
|
|
100
|
+
isTruncated: false,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('truncates oversized buffers at the fixed byte boundary', () => {
|
|
105
|
+
const value = [...new Array(MAX_BUFFER_PREVIEW_BYTES).fill(0xab), 0xcd];
|
|
106
|
+
|
|
107
|
+
expect(
|
|
108
|
+
toStorageEntryPreview({ key: 'long', type: 'buffer', value }),
|
|
109
|
+
).toEqual({
|
|
110
|
+
key: 'long',
|
|
111
|
+
type: 'buffer',
|
|
112
|
+
preview: new Array(MAX_BUFFER_PREVIEW_BYTES).fill('AB').join(' '),
|
|
113
|
+
valueSize: MAX_BUFFER_PREVIEW_BYTES + 1,
|
|
114
|
+
isTruncated: true,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -358,4 +358,25 @@ describe('computePreview', () => {
|
|
|
358
358
|
});
|
|
359
359
|
expect(preview.metadataMismatch).toBe(false);
|
|
360
360
|
});
|
|
361
|
+
|
|
362
|
+
it('treats accepted duplicate keys as sequential overwrites', () => {
|
|
363
|
+
const preview = computePreview(
|
|
364
|
+
validSnapshot({
|
|
365
|
+
entries: [
|
|
366
|
+
{ key: 'duplicate', type: 'string', value: 'first' },
|
|
367
|
+
{ key: 'duplicate', type: 'string', value: 'second' },
|
|
368
|
+
],
|
|
369
|
+
}),
|
|
370
|
+
{
|
|
371
|
+
target,
|
|
372
|
+
capabilities: allTypes,
|
|
373
|
+
entryKeys: new Set(),
|
|
374
|
+
isBlacklisted: noBlacklist,
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
expect(preview.newKeys).toEqual(['duplicate']);
|
|
379
|
+
expect(preview.overwriteKeys).toEqual(['duplicate']);
|
|
380
|
+
expect(preview.acceptedEntryIndexes).toEqual([0, 1]);
|
|
381
|
+
});
|
|
361
382
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineAgentToolContract,
|
|
3
|
+
definePaginatedAgentToolContract,
|
|
3
4
|
type AgentToolContract,
|
|
4
5
|
} from '@rozenite/agent-shared';
|
|
5
6
|
|
|
@@ -36,7 +37,6 @@ export type StorageListStoragesItem = {
|
|
|
36
37
|
storageId: string;
|
|
37
38
|
adapterName: string;
|
|
38
39
|
storageName: string;
|
|
39
|
-
entryCount: number;
|
|
40
40
|
supportedTypes: StorageEntryType[];
|
|
41
41
|
};
|
|
42
42
|
|
|
@@ -45,17 +45,22 @@ export type StorageListStoragesResult = {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
export type StorageListEntriesArgs = StorageSelection & {
|
|
48
|
-
offset?: number;
|
|
49
48
|
limit?: number;
|
|
49
|
+
cursor?: string;
|
|
50
|
+
/** @deprecated Use the opaque cursor returned by a previous page. */
|
|
51
|
+
offset?: number;
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
export type StorageListEntriesResult = {
|
|
53
55
|
adapterId: string;
|
|
54
56
|
storageId: string;
|
|
55
57
|
total: number;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
items: Array<{ key: string }>;
|
|
59
|
+
page: {
|
|
60
|
+
limit: number;
|
|
61
|
+
hasMore: boolean;
|
|
62
|
+
nextCursor?: string;
|
|
63
|
+
};
|
|
59
64
|
};
|
|
60
65
|
|
|
61
66
|
export type StorageReadEntryArgs = StorageSelection & {
|
|
@@ -106,10 +111,10 @@ export const storageToolDefinitions = {
|
|
|
106
111
|
>({
|
|
107
112
|
name: 'list-storages',
|
|
108
113
|
description:
|
|
109
|
-
'List all storage adapters and their storage nodes currently available on the device, including supported entry types
|
|
114
|
+
'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
115
|
inputSchema: { type: 'object', properties: {} },
|
|
111
116
|
}),
|
|
112
|
-
listEntries:
|
|
117
|
+
listEntries: definePaginatedAgentToolContract<
|
|
113
118
|
StorageListEntriesArgs,
|
|
114
119
|
StorageListEntriesResult
|
|
115
120
|
>({
|
|
@@ -120,16 +125,27 @@ export const storageToolDefinitions = {
|
|
|
120
125
|
type: 'object',
|
|
121
126
|
properties: {
|
|
122
127
|
...sharedStorageProperties,
|
|
123
|
-
|
|
128
|
+
limit: {
|
|
124
129
|
type: 'number',
|
|
125
|
-
description: 'Pagination
|
|
130
|
+
description: 'Pagination size. Defaults to 20 and is capped at 100.',
|
|
126
131
|
},
|
|
127
|
-
|
|
132
|
+
cursor: {
|
|
133
|
+
type: 'string',
|
|
134
|
+
description:
|
|
135
|
+
'Opaque pagination cursor from a previous list-entries call.',
|
|
136
|
+
},
|
|
137
|
+
offset: {
|
|
128
138
|
type: 'number',
|
|
129
|
-
description:
|
|
139
|
+
description:
|
|
140
|
+
'Deprecated initial pagination offset. Must be a non-negative integer and cannot be combined with cursor.',
|
|
130
141
|
},
|
|
131
142
|
},
|
|
132
143
|
},
|
|
144
|
+
pagination: {
|
|
145
|
+
kind: 'cursor',
|
|
146
|
+
fields: ['key'],
|
|
147
|
+
defaultFields: ['key'],
|
|
148
|
+
},
|
|
133
149
|
}),
|
|
134
150
|
readEntry: defineAgentToolContract<
|
|
135
151
|
StorageReadEntryArgs,
|
|
@@ -0,0 +1,59 @@
|
|
|
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 =>
|
|
7
|
+
byte.toString(16).toUpperCase().padStart(2, '0');
|
|
8
|
+
|
|
9
|
+
const truncateString = (
|
|
10
|
+
value: string,
|
|
11
|
+
): Pick<StorageEntryPreview, 'preview' | 'isTruncated'> => {
|
|
12
|
+
let preview = '';
|
|
13
|
+
let codePointCount = 0;
|
|
14
|
+
|
|
15
|
+
for (const codePoint of value) {
|
|
16
|
+
if (codePointCount === MAX_STRING_PREVIEW_CODE_POINTS) {
|
|
17
|
+
return { preview, isTruncated: true };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
preview += codePoint;
|
|
21
|
+
codePointCount += 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { preview, isTruncated: false };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const toStorageEntryPreview = (
|
|
28
|
+
entry: StorageEntry,
|
|
29
|
+
): StorageEntryPreview => {
|
|
30
|
+
if (entry.type === 'string') {
|
|
31
|
+
return {
|
|
32
|
+
key: entry.key,
|
|
33
|
+
type: entry.type,
|
|
34
|
+
...truncateString(entry.value),
|
|
35
|
+
// JavaScript strings expose their length in UTF-16 code units.
|
|
36
|
+
valueSize: entry.value.length,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (entry.type === 'buffer') {
|
|
41
|
+
const shownBytes = entry.value.slice(0, MAX_BUFFER_PREVIEW_BYTES);
|
|
42
|
+
return {
|
|
43
|
+
key: entry.key,
|
|
44
|
+
type: entry.type,
|
|
45
|
+
preview: shownBytes.map(toHexPair).join(' '),
|
|
46
|
+
valueSize: entry.value.length,
|
|
47
|
+
isTruncated: entry.value.length > MAX_BUFFER_PREVIEW_BYTES,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const preview = String(entry.value);
|
|
52
|
+
return {
|
|
53
|
+
key: entry.key,
|
|
54
|
+
type: entry.type,
|
|
55
|
+
preview,
|
|
56
|
+
valueSize: preview.length,
|
|
57
|
+
isTruncated: false,
|
|
58
|
+
};
|
|
59
|
+
};
|
package/src/shared/messaging.ts
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 {
|
|
2
|
+
StorageDescriptor,
|
|
3
|
+
StorageEntry,
|
|
4
|
+
StorageEntryPreview,
|
|
5
|
+
StorageTarget,
|
|
6
|
+
} from './types';
|
|
7
|
+
import type { ImportPreview, StorageSnapshotV1 } from './snapshot';
|
|
17
8
|
|
|
18
9
|
export type StorageSetEntryEvent = {
|
|
19
10
|
type: 'set-entry';
|
|
@@ -27,19 +18,131 @@ export type StorageDeleteEntryEvent = {
|
|
|
27
18
|
key: string;
|
|
28
19
|
};
|
|
29
20
|
|
|
30
|
-
export type
|
|
31
|
-
type: '
|
|
32
|
-
target: StorageTarget
|
|
21
|
+
export type StoragePurgeEvent = {
|
|
22
|
+
type: 'purge-storage';
|
|
23
|
+
target: StorageTarget;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type StorageInvalidationOperation =
|
|
27
|
+
| 'set'
|
|
28
|
+
| 'delete'
|
|
29
|
+
| 'import'
|
|
30
|
+
| 'purge';
|
|
31
|
+
|
|
32
|
+
export type StorageInvalidatedEvent = {
|
|
33
|
+
type: 'storage-invalidated';
|
|
34
|
+
target: StorageTarget;
|
|
35
|
+
/** Native subscriptions only expose a key, not the operation. */
|
|
36
|
+
key?: string;
|
|
37
|
+
operation?: StorageInvalidationOperation;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type StorageDiscoverStoragesRequestEvent = {
|
|
41
|
+
type: 'discover-storages';
|
|
42
|
+
requestId: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type StorageDiscoverStoragesResponseEvent = {
|
|
46
|
+
type: 'storage-descriptors';
|
|
47
|
+
requestId: string;
|
|
48
|
+
storages: StorageDescriptor[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type StorageListEntryPreviewsRequestEvent = {
|
|
52
|
+
type: 'list-entry-previews';
|
|
53
|
+
requestId: string;
|
|
54
|
+
target: StorageTarget;
|
|
55
|
+
search?: string;
|
|
56
|
+
keySortDirection?: 'ascending' | 'descending';
|
|
57
|
+
cursor?: string;
|
|
58
|
+
limit: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type StorageListEntryPreviewsResponseEvent = {
|
|
62
|
+
type: 'entry-previews';
|
|
63
|
+
requestId: string;
|
|
64
|
+
target: StorageTarget;
|
|
65
|
+
items: StorageEntryPreview[];
|
|
66
|
+
nextCursor?: string;
|
|
67
|
+
previousCursor?: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type StorageGetEntryRequestEvent = {
|
|
71
|
+
type: 'get-entry';
|
|
72
|
+
requestId: string;
|
|
73
|
+
target: StorageTarget;
|
|
74
|
+
key: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type StorageGetEntryResponseEvent = {
|
|
78
|
+
type: 'entry';
|
|
79
|
+
requestId: string;
|
|
80
|
+
target: StorageTarget;
|
|
81
|
+
entry: StorageEntry;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type StorageExportSnapshotRequestEvent = {
|
|
85
|
+
type: 'export-snapshot';
|
|
86
|
+
requestId: string;
|
|
87
|
+
target: StorageTarget;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type StorageExportSnapshotResponseEvent = {
|
|
91
|
+
type: 'export-snapshot-result';
|
|
92
|
+
requestId: string;
|
|
93
|
+
target: StorageTarget;
|
|
94
|
+
snapshot: StorageSnapshotV1;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type StorageImportPreviewRequestEvent = {
|
|
98
|
+
type: 'preview-import';
|
|
99
|
+
requestId: string;
|
|
100
|
+
target: StorageTarget;
|
|
101
|
+
snapshot: StorageSnapshotV1;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type StorageImportPreviewResponseEvent = {
|
|
105
|
+
type: 'import-preview';
|
|
106
|
+
requestId: string;
|
|
107
|
+
target: StorageTarget;
|
|
108
|
+
preview: ImportPreview;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export type StorageRequestError = {
|
|
112
|
+
requestId: string;
|
|
113
|
+
code:
|
|
114
|
+
| 'TARGET_NOT_FOUND'
|
|
115
|
+
| 'ENTRY_NOT_FOUND'
|
|
116
|
+
| 'INVALID_CURSOR'
|
|
117
|
+
| 'INVALID_REQUEST'
|
|
118
|
+
| 'READ_FAILED'
|
|
119
|
+
| 'WRITE_FAILED';
|
|
120
|
+
message: string;
|
|
121
|
+
resetPagination?: boolean;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type StorageRequestErrorEvent = StorageRequestError & {
|
|
125
|
+
type: 'storage-request-error';
|
|
33
126
|
};
|
|
34
127
|
|
|
35
128
|
export type StorageImportEntriesEvent = {
|
|
36
129
|
type: 'import-entries';
|
|
130
|
+
requestId: string;
|
|
37
131
|
target: StorageTarget;
|
|
38
132
|
entries: StorageEntry[];
|
|
39
133
|
};
|
|
40
134
|
|
|
135
|
+
export type StorageImportProgressEvent = {
|
|
136
|
+
type: 'import-progress';
|
|
137
|
+
requestId: string;
|
|
138
|
+
target: StorageTarget;
|
|
139
|
+
written: number;
|
|
140
|
+
total: number;
|
|
141
|
+
};
|
|
142
|
+
|
|
41
143
|
export type StorageImportResultEvent = {
|
|
42
144
|
type: 'import-result';
|
|
145
|
+
requestId: string;
|
|
43
146
|
target: StorageTarget;
|
|
44
147
|
ok: boolean;
|
|
45
148
|
written: number;
|
|
@@ -49,11 +152,23 @@ export type StorageImportResultEvent = {
|
|
|
49
152
|
};
|
|
50
153
|
|
|
51
154
|
export type StorageEvent =
|
|
52
|
-
| StorageSnapshotEvent
|
|
53
155
|
| StorageSetEntryEvent
|
|
54
156
|
| StorageDeleteEntryEvent
|
|
55
|
-
|
|
|
157
|
+
| StoragePurgeEvent
|
|
158
|
+
| StorageInvalidatedEvent
|
|
159
|
+
| StorageDiscoverStoragesRequestEvent
|
|
160
|
+
| StorageDiscoverStoragesResponseEvent
|
|
161
|
+
| StorageListEntryPreviewsRequestEvent
|
|
162
|
+
| StorageListEntryPreviewsResponseEvent
|
|
163
|
+
| StorageGetEntryRequestEvent
|
|
164
|
+
| StorageGetEntryResponseEvent
|
|
165
|
+
| StorageExportSnapshotRequestEvent
|
|
166
|
+
| StorageExportSnapshotResponseEvent
|
|
167
|
+
| StorageImportPreviewRequestEvent
|
|
168
|
+
| StorageImportPreviewResponseEvent
|
|
169
|
+
| StorageRequestErrorEvent
|
|
56
170
|
| StorageImportEntriesEvent
|
|
171
|
+
| StorageImportProgressEvent
|
|
57
172
|
| StorageImportResultEvent;
|
|
58
173
|
|
|
59
174
|
export type StorageEventMap = {
|
package/src/shared/snapshot.ts
CHANGED
|
@@ -229,6 +229,7 @@ export type ImportPreview = {
|
|
|
229
229
|
overwriteKeys: string[];
|
|
230
230
|
skippedKeys: { key: string; reason: 'blacklist' }[];
|
|
231
231
|
unsupportedTypes: { key: string; type: StorageEntryType }[];
|
|
232
|
+
acceptedEntryIndexes: number[];
|
|
232
233
|
metadataMismatch: boolean;
|
|
233
234
|
};
|
|
234
235
|
|
|
@@ -245,8 +246,9 @@ export const computePreview = (
|
|
|
245
246
|
const overwriteKeys: string[] = [];
|
|
246
247
|
const skippedKeys: { key: string; reason: 'blacklist' }[] = [];
|
|
247
248
|
const unsupportedTypes: { key: string; type: StorageEntryType }[] = [];
|
|
249
|
+
const acceptedEntryIndexes: number[] = [];
|
|
248
250
|
|
|
249
|
-
for (const entry of snapshot.entries) {
|
|
251
|
+
for (const [index, entry] of snapshot.entries.entries()) {
|
|
250
252
|
if (!supportsType(current.capabilities, entry.type)) {
|
|
251
253
|
unsupportedTypes.push({ key: entry.key, type: entry.type });
|
|
252
254
|
continue;
|
|
@@ -260,6 +262,10 @@ export const computePreview = (
|
|
|
260
262
|
} else {
|
|
261
263
|
newKeys.push(entry.key);
|
|
262
264
|
}
|
|
265
|
+
acceptedEntryIndexes.push(index);
|
|
266
|
+
// Duplicate keys are applied in source order, so every accepted duplicate
|
|
267
|
+
// after the first overwrites the value that preceded it in this import.
|
|
268
|
+
current.entryKeys.add(entry.key);
|
|
263
269
|
}
|
|
264
270
|
|
|
265
271
|
const metadataMismatch =
|
|
@@ -271,6 +277,7 @@ export const computePreview = (
|
|
|
271
277
|
overwriteKeys,
|
|
272
278
|
skippedKeys,
|
|
273
279
|
unsupportedTypes,
|
|
280
|
+
acceptedEntryIndexes,
|
|
274
281
|
metadataMismatch,
|
|
275
282
|
};
|
|
276
283
|
};
|
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
|
|
|
@@ -63,5 +82,5 @@ export const getStorageViewId = ({ adapterId, storageId }: StorageTarget) =>
|
|
|
63
82
|
|
|
64
83
|
export const supportsType = (
|
|
65
84
|
capabilities: StorageCapabilities,
|
|
66
|
-
type: StorageEntryType
|
|
85
|
+
type: StorageEntryType,
|
|
67
86
|
) => capabilities.supportedTypes.includes(type);
|
|
@@ -176,6 +176,19 @@ describe('reduce: switch-mode', () => {
|
|
|
176
176
|
});
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
+
describe('reduce: replace-bytes', () => {
|
|
180
|
+
it('formats fetched bytes after the editor has had a chance to paint', () => {
|
|
181
|
+
expect(
|
|
182
|
+
reduce(hexState(), { type: 'replace-bytes', bytes: [0x48, 0x69] }),
|
|
183
|
+
).toEqual({
|
|
184
|
+
mode: 'hex',
|
|
185
|
+
text: '48 69',
|
|
186
|
+
bytes: [0x48, 0x69],
|
|
187
|
+
error: null,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
179
192
|
describe('validate', () => {
|
|
180
193
|
it('passes when bytes are valid and non-empty', () => {
|
|
181
194
|
const state = reduce(hexState(), { type: 'set-text', text: '89 50' });
|
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
bytesToBase64,
|
|
6
6
|
bytesToGroupedHex,
|
|
7
7
|
bytesToHexdump,
|
|
8
|
+
compactAsciiPreview,
|
|
8
9
|
compactBufferPreview,
|
|
10
|
+
formatHexdumpRow,
|
|
9
11
|
hexInputToBytes,
|
|
10
12
|
} from '../binary';
|
|
11
13
|
|
|
@@ -80,6 +82,16 @@ describe('bytesToHexdump', () => {
|
|
|
80
82
|
});
|
|
81
83
|
});
|
|
82
84
|
|
|
85
|
+
describe('formatHexdumpRow', () => {
|
|
86
|
+
it('formats one requested row without requiring a complete hexdump', () => {
|
|
87
|
+
expect(formatHexdumpRow(new Array(32).fill(0x41), 16)).toEqual({
|
|
88
|
+
offset: '00000010',
|
|
89
|
+
hex: '41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41',
|
|
90
|
+
ascii: 'AAAAAAAAAAAAAAAA',
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
83
95
|
describe('bytesToAsciiPreview', () => {
|
|
84
96
|
it('maps printable bytes to characters', () => {
|
|
85
97
|
expect(bytesToAsciiPreview([0x48, 0x69])).toBe('Hi');
|
|
@@ -94,6 +106,12 @@ describe('bytesToAsciiPreview', () => {
|
|
|
94
106
|
});
|
|
95
107
|
});
|
|
96
108
|
|
|
109
|
+
describe('compactAsciiPreview', () => {
|
|
110
|
+
it('only converts the visible prefix of a large buffer', () => {
|
|
111
|
+
expect(compactAsciiPreview(new Array(128).fill(0x41), 4)).toBe('AAAA…');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
97
115
|
describe('bytesToBase64 / base64ToBytes round-trip', () => {
|
|
98
116
|
it('encodes simple ASCII to base64 and back', () => {
|
|
99
117
|
const bytes = [0x48, 0x65, 0x6c, 0x6c, 0x6f]; // "Hello"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { TEXT_CHUNK_SIZE, textRowRanges } from '../large-value-viewer-state';
|
|
3
|
+
|
|
4
|
+
describe('textRowRanges', () => {
|
|
5
|
+
it('indexes chunks without splitting a pathological single line', () => {
|
|
6
|
+
const value = 'x'.repeat(TEXT_CHUNK_SIZE * 2 + 7);
|
|
7
|
+
|
|
8
|
+
expect(textRowRanges(value)).toEqual([
|
|
9
|
+
[0, TEXT_CHUNK_SIZE],
|
|
10
|
+
[TEXT_CHUNK_SIZE, TEXT_CHUNK_SIZE * 2],
|
|
11
|
+
[TEXT_CHUNK_SIZE * 2, value.length],
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('keeps newline boundaries in their displayed row', () => {
|
|
16
|
+
expect(textRowRanges('first\nsecond')).toEqual([
|
|
17
|
+
[0, 6],
|
|
18
|
+
[6, 12],
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
});
|