@rozenite/storage-plugin 2.0.0 → 2.2.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 +36 -0
- package/dist/devtools/assets/panel-D5z1BY8U.js +32 -0
- package/dist/devtools/assets/panel-DVlWRg6d.css +1 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/index.require.js +1 -3
- package/dist/react-native/chunks/secure-storage.require.js +2 -6
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +266 -295
- package/dist/react-native/index.d.ts +14 -2
- package/dist/react-native/index.js +2 -10
- package/dist/rozenite.json +1 -1
- package/package.json +29 -29
- package/react-native.ts +11 -36
- package/rozenite.config.ts +2 -6
- package/sdk.ts +1 -5
- package/src/react-native/__tests__/entry-preview-pagination.test.ts +12 -40
- package/src/react-native/__tests__/export-snapshot.test.ts +2 -6
- package/src/react-native/__tests__/full-entry-request.test.ts +5 -17
- package/src/react-native/__tests__/import.test.ts +9 -8
- package/src/react-native/__tests__/storage-discovery.test.ts +7 -18
- package/src/react-native/__tests__/stress-verification.test.ts +5 -18
- package/src/react-native/__tests__/use-storage-agent-tools.test.ts +25 -35
- package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
- package/src/react-native/adapters/async-storage.ts +2 -5
- package/src/react-native/adapters/index.ts +2 -8
- package/src/react-native/adapters/mmkv.ts +7 -24
- package/src/react-native/adapters/secure-storage.ts +2 -6
- package/src/react-native/entry-preview-pagination.ts +11 -27
- package/src/react-native/export-snapshot.ts +3 -9
- package/src/react-native/full-entry-request.ts +3 -8
- package/src/react-native/import.ts +10 -21
- package/src/react-native/storage-discovery.ts +2 -6
- package/src/react-native/storage-view.ts +2 -6
- package/src/react-native/useRozeniteStoragePlugin.ts +107 -119
- package/src/react-native/useStorageAgentTools.ts +8 -24
- package/src/shared/__tests__/entry-preview.test.ts +9 -27
- package/src/shared/__tests__/snapshot.test.ts +10 -32
- package/src/shared/agent-tools.ts +9 -30
- package/src/shared/entry-preview.ts +3 -8
- package/src/shared/messaging.ts +15 -11
- package/src/shared/snapshot.ts +4 -17
- package/src/shared/types.ts +2 -4
- package/src/ui/__tests__/binary-value-editor-state.test.ts +6 -16
- package/src/ui/__tests__/binary.test.ts +13 -33
- package/src/ui/__tests__/large-value-viewer.test.tsx +4 -13
- package/src/ui/__tests__/panel.test.tsx +50 -63
- package/src/ui/__tests__/storage-groups.test.ts +1 -4
- package/src/ui/__tests__/type-conversion.test.ts +8 -24
- package/src/ui/__tests__/use-storage-entries.test.tsx +18 -34
- package/src/ui/add-entry-dialog.tsx +66 -87
- package/src/ui/binary-value-editor-state.ts +6 -24
- package/src/ui/binary-value-editor.tsx +6 -18
- package/src/ui/binary.ts +6 -19
- package/src/ui/edit-entry-dialog.tsx +59 -78
- package/src/ui/editor-switcher.tsx +2 -11
- package/src/ui/entry-detail-dialog.tsx +9 -16
- package/src/ui/format-value.tsx +2 -9
- package/src/ui/import-dialog.tsx +12 -24
- package/src/ui/large-value-viewer-state.ts +2 -9
- package/src/ui/large-value-viewer.tsx +3 -9
- package/src/ui/panel.tsx +129 -235
- package/src/ui/query-client.tsx +4 -15
- package/src/ui/type-conversion.ts +2 -5
- package/src/ui/typed-value-editor.tsx +1 -5
- package/src/ui/use-storage-entries.ts +6 -21
- package/src/ui/utils.ts +1 -4
- package/tsconfig.json +4 -7
- package/dist/devtools/assets/panel-B6Wp0eie.css +0 -1
- package/dist/devtools/assets/panel-D7MFc4HG.js +0 -32
|
@@ -26,9 +26,7 @@ export type RozeniteStoragePluginOptions = {
|
|
|
26
26
|
storages: StorageAdapter[];
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
export const useRozeniteStoragePlugin = ({
|
|
30
|
-
storages,
|
|
31
|
-
}: RozeniteStoragePluginOptions) => {
|
|
29
|
+
export const useRozeniteStoragePlugin = ({ storages }: RozeniteStoragePluginOptions) => {
|
|
32
30
|
const views = useMemo(() => createStorageViews(storages), [storages]);
|
|
33
31
|
|
|
34
32
|
useStorageAgentTools(views);
|
|
@@ -45,16 +43,23 @@ export const useRozeniteStoragePlugin = ({
|
|
|
45
43
|
const viewSubscriptions: { remove: () => void }[] = [];
|
|
46
44
|
let disposed = false;
|
|
47
45
|
|
|
48
|
-
const sendInvalidation = (
|
|
46
|
+
const sendInvalidation = async (
|
|
49
47
|
target: StorageSetEntryEvent['target'],
|
|
50
48
|
key?: string,
|
|
51
49
|
operation?: StorageInvalidationOperation,
|
|
52
50
|
) => {
|
|
51
|
+
const view = views.find(
|
|
52
|
+
(candidate) =>
|
|
53
|
+
candidate.target.adapterId === target.adapterId &&
|
|
54
|
+
candidate.target.storageId === target.storageId,
|
|
55
|
+
);
|
|
56
|
+
const entryCount = view ? (await view.getAllKeys()).length : 0;
|
|
53
57
|
client.send('storage-invalidated', {
|
|
54
58
|
type: 'storage-invalidated',
|
|
55
59
|
target,
|
|
56
60
|
key,
|
|
57
61
|
operation,
|
|
62
|
+
entryCount,
|
|
58
63
|
});
|
|
59
64
|
};
|
|
60
65
|
|
|
@@ -70,7 +75,7 @@ export const useRozeniteStoragePlugin = ({
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
const subscription = await watch((key) => {
|
|
73
|
-
sendInvalidation(view.target, key);
|
|
78
|
+
void sendInvalidation(view.target, key);
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
if (disposed) {
|
|
@@ -89,94 +94,82 @@ export const useRozeniteStoragePlugin = ({
|
|
|
89
94
|
);
|
|
90
95
|
|
|
91
96
|
const messageSubscriptions = [
|
|
92
|
-
client.onMessage(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
client.onMessage('set-entry', async ({ target, entry }: StorageSetEntryEvent) => {
|
|
98
|
+
const view = views.find(
|
|
99
|
+
(candidate) =>
|
|
100
|
+
candidate.target.adapterId === target.adapterId &&
|
|
101
|
+
candidate.target.storageId === target.storageId,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (!view) {
|
|
105
|
+
console.warn(
|
|
106
|
+
`[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
|
|
99
107
|
);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
sendInvalidation(view.target, entry.key, 'set');
|
|
111
|
-
} catch (error) {
|
|
112
|
-
console.warn(
|
|
113
|
-
`[Rozenite] Storage Plugin: Failed to set entry in ${target.adapterId}/${target.storageId}.`,
|
|
114
|
-
error,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
),
|
|
119
|
-
client.onMessage(
|
|
120
|
-
'delete-entry',
|
|
121
|
-
async ({ target, key }: StorageDeleteEntryEvent) => {
|
|
122
|
-
const view = views.find(
|
|
123
|
-
(candidate) =>
|
|
124
|
-
candidate.target.adapterId === target.adapterId &&
|
|
125
|
-
candidate.target.storageId === target.storageId,
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await view.set(entry);
|
|
113
|
+
await sendInvalidation(view.target, entry.key, 'set');
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.warn(
|
|
116
|
+
`[Rozenite] Storage Plugin: Failed to set entry in ${target.adapterId}/${target.storageId}.`,
|
|
117
|
+
error,
|
|
126
118
|
);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
console.warn(
|
|
140
|
-
`[Rozenite] Storage Plugin: Failed to delete entry in ${target.adapterId}/${target.storageId}.`,
|
|
141
|
-
error,
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
},
|
|
145
|
-
),
|
|
146
|
-
client.onMessage(
|
|
147
|
-
'purge-storage',
|
|
148
|
-
async ({ target }: StoragePurgeEvent) => {
|
|
149
|
-
const view = views.find(
|
|
150
|
-
(candidate) =>
|
|
151
|
-
candidate.target.adapterId === target.adapterId &&
|
|
152
|
-
candidate.target.storageId === target.storageId,
|
|
119
|
+
}
|
|
120
|
+
}),
|
|
121
|
+
client.onMessage('delete-entry', async ({ target, key }: StorageDeleteEntryEvent) => {
|
|
122
|
+
const view = views.find(
|
|
123
|
+
(candidate) =>
|
|
124
|
+
candidate.target.adapterId === target.adapterId &&
|
|
125
|
+
candidate.target.storageId === target.storageId,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (!view) {
|
|
129
|
+
console.warn(
|
|
130
|
+
`[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
|
|
153
131
|
);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await view.delete(key);
|
|
137
|
+
await sendInvalidation(view.target, key, 'delete');
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.warn(
|
|
140
|
+
`[Rozenite] Storage Plugin: Failed to delete entry in ${target.adapterId}/${target.storageId}.`,
|
|
141
|
+
error,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}),
|
|
145
|
+
client.onMessage('purge-storage', async ({ target }: StoragePurgeEvent) => {
|
|
146
|
+
const view = views.find(
|
|
147
|
+
(candidate) =>
|
|
148
|
+
candidate.target.adapterId === target.adapterId &&
|
|
149
|
+
candidate.target.storageId === target.storageId,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (!view) {
|
|
153
|
+
console.warn(
|
|
154
|
+
`[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
await view.purge();
|
|
161
|
+
await sendInvalidation(view.target, undefined, 'purge');
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.warn(
|
|
164
|
+
`[Rozenite] Storage Plugin: Failed to purge storage in ${target.adapterId}/${target.storageId}.`,
|
|
165
|
+
error,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}),
|
|
169
|
+
client.onMessage('discover-storages', async (event: StorageDiscoverStoragesRequestEvent) => {
|
|
170
|
+
const response = await handleStorageDiscoveryRequest(views, event);
|
|
171
|
+
client.send(response.type, response);
|
|
172
|
+
}),
|
|
180
173
|
client.onMessage(
|
|
181
174
|
'list-entry-previews',
|
|
182
175
|
async (event: StorageListEntryPreviewsRequestEvent) => {
|
|
@@ -184,37 +177,32 @@ export const useRozeniteStoragePlugin = ({
|
|
|
184
177
|
client.send(response.type, response);
|
|
185
178
|
},
|
|
186
179
|
),
|
|
187
|
-
client.onMessage(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const response = await handleImportPreviewRequest(views, event);
|
|
205
|
-
client.send(response.type, response);
|
|
206
|
-
},
|
|
207
|
-
),
|
|
208
|
-
client.onMessage(
|
|
209
|
-
'import-entries',
|
|
210
|
-
async (event: StorageImportEntriesEvent) => {
|
|
211
|
-
await handleImportEntries(views, event, (out) => {
|
|
212
|
-
client.send(out.type, out);
|
|
213
|
-
});
|
|
214
|
-
},
|
|
215
|
-
),
|
|
180
|
+
client.onMessage('get-entry', async (event: StorageGetEntryRequestEvent) => {
|
|
181
|
+
const response = await handleFullEntryRequest(views, event);
|
|
182
|
+
client.send(response.type, response);
|
|
183
|
+
}),
|
|
184
|
+
client.onMessage('export-snapshot', async (event: StorageExportSnapshotRequestEvent) => {
|
|
185
|
+
const response = await handleExportSnapshotRequest(views, event);
|
|
186
|
+
client.send(response.type, response);
|
|
187
|
+
}),
|
|
188
|
+
client.onMessage('preview-import', async (event: StorageImportPreviewRequestEvent) => {
|
|
189
|
+
const response = await handleImportPreviewRequest(views, event);
|
|
190
|
+
client.send(response.type, response);
|
|
191
|
+
}),
|
|
192
|
+
client.onMessage('import-entries', async (event: StorageImportEntriesEvent) => {
|
|
193
|
+
await handleImportEntries(views, event, (out) => {
|
|
194
|
+
client.send(out.type, out);
|
|
195
|
+
});
|
|
196
|
+
}),
|
|
216
197
|
];
|
|
217
198
|
|
|
199
|
+
// Sent last, so a panel that reacts to it is guaranteed to find the
|
|
200
|
+
// handlers above already listening. The panel is recreated on every app
|
|
201
|
+
// reload and asks for storages as soon as it boots, which usually beats the
|
|
202
|
+
// app's React tree to the punch; without this the panel's only request is
|
|
203
|
+
// dropped and it waits for storages forever.
|
|
204
|
+
client.send('device-ready', { type: 'device-ready' });
|
|
205
|
+
|
|
218
206
|
return () => {
|
|
219
207
|
disposed = true;
|
|
220
208
|
viewSubscriptions.forEach((subscription) => subscription.remove());
|
|
@@ -13,10 +13,7 @@ import type {
|
|
|
13
13
|
} from '../shared/types';
|
|
14
14
|
import type { StorageView } from './storage-view';
|
|
15
15
|
|
|
16
|
-
const parseValueForType = (
|
|
17
|
-
type: StorageEntryType,
|
|
18
|
-
value: unknown,
|
|
19
|
-
): StorageEntryValue => {
|
|
16
|
+
const parseValueForType = (type: StorageEntryType, value: unknown): StorageEntryValue => {
|
|
20
17
|
if (type === 'string') {
|
|
21
18
|
if (typeof value !== 'string') {
|
|
22
19
|
throw new Error('Expected string value for type=string');
|
|
@@ -45,8 +42,7 @@ const parseValueForType = (
|
|
|
45
42
|
return value as number[];
|
|
46
43
|
};
|
|
47
44
|
|
|
48
|
-
const formatViewRef = (view: StorageView) =>
|
|
49
|
-
`${view.target.adapterId}/${view.target.storageId}`;
|
|
45
|
+
const formatViewRef = (view: StorageView) => `${view.target.adapterId}/${view.target.storageId}`;
|
|
50
46
|
|
|
51
47
|
const isSameTarget = (left: StorageTarget, right: StorageTarget) =>
|
|
52
48
|
left.adapterId === right.adapterId && left.storageId === right.storageId;
|
|
@@ -68,10 +64,8 @@ const decodeCursor = (value: string): ListEntriesCursor => {
|
|
|
68
64
|
cursor == null ||
|
|
69
65
|
Array.isArray(cursor) ||
|
|
70
66
|
(cursor as { version?: unknown }).version !== 1 ||
|
|
71
|
-
typeof (cursor as { target?: { adapterId?: unknown } }).target
|
|
72
|
-
|
|
73
|
-
typeof (cursor as { target?: { storageId?: unknown } }).target
|
|
74
|
-
?.storageId !== 'string' ||
|
|
67
|
+
typeof (cursor as { target?: { adapterId?: unknown } }).target?.adapterId !== 'string' ||
|
|
68
|
+
typeof (cursor as { target?: { storageId?: unknown } }).target?.storageId !== 'string' ||
|
|
75
69
|
!Number.isSafeInteger((cursor as { offset?: unknown }).offset) ||
|
|
76
70
|
(cursor as { offset: number }).offset < 0
|
|
77
71
|
) {
|
|
@@ -112,11 +106,7 @@ const sanitizeOffset = (offset: number | undefined) => {
|
|
|
112
106
|
return offset;
|
|
113
107
|
};
|
|
114
108
|
|
|
115
|
-
const resolveStorage = (
|
|
116
|
-
views: readonly StorageView[],
|
|
117
|
-
adapterId?: string,
|
|
118
|
-
storageId?: string,
|
|
119
|
-
) => {
|
|
109
|
+
const resolveStorage = (views: readonly StorageView[], adapterId?: string, storageId?: string) => {
|
|
120
110
|
if (views.length === 0) {
|
|
121
111
|
throw new Error('No storages are registered.');
|
|
122
112
|
}
|
|
@@ -158,16 +148,12 @@ const resolveStorage = (
|
|
|
158
148
|
return views[0]!;
|
|
159
149
|
};
|
|
160
150
|
|
|
161
|
-
const listStorageKeys = async (
|
|
162
|
-
view: StorageView,
|
|
163
|
-
input: StorageListEntriesArgs,
|
|
164
|
-
) => {
|
|
151
|
+
const listStorageKeys = async (view: StorageView, input: StorageListEntriesArgs) => {
|
|
165
152
|
const limit = sanitizeLimit(input.limit);
|
|
166
153
|
if (input.cursor !== undefined && input.offset !== undefined) {
|
|
167
154
|
throw new Error('Provide either cursor or offset, not both.');
|
|
168
155
|
}
|
|
169
|
-
const cursor =
|
|
170
|
-
input.cursor === undefined ? undefined : decodeCursor(input.cursor);
|
|
156
|
+
const cursor = input.cursor === undefined ? undefined : decodeCursor(input.cursor);
|
|
171
157
|
if (cursor && !isSameTarget(cursor.target, view.target)) {
|
|
172
158
|
throw new Error(
|
|
173
159
|
'Cursor does not match the requested storage. Run list-entries again without a cursor to restart pagination.',
|
|
@@ -228,9 +214,7 @@ export const createStorageAgentHandlers = (views: readonly StorageView[]) => ({
|
|
|
228
214
|
const entry = await view.get(key);
|
|
229
215
|
|
|
230
216
|
if (!entry) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`Key "${key}" not found in storage "${formatViewRef(view)}".`,
|
|
233
|
-
);
|
|
217
|
+
throw new Error(`Key "${key}" not found in storage "${formatViewRef(view)}".`);
|
|
234
218
|
}
|
|
235
219
|
|
|
236
220
|
return {
|
|
@@ -7,9 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
describe('toStorageEntryPreview', () => {
|
|
9
9
|
it('represents empty and boundary-sized strings completely', () => {
|
|
10
|
-
expect(
|
|
11
|
-
toStorageEntryPreview({ key: 'empty', type: 'string', value: '' }),
|
|
12
|
-
).toEqual({
|
|
10
|
+
expect(toStorageEntryPreview({ key: 'empty', type: 'string', value: '' })).toEqual({
|
|
13
11
|
key: 'empty',
|
|
14
12
|
type: 'string',
|
|
15
13
|
preview: '',
|
|
@@ -18,9 +16,7 @@ describe('toStorageEntryPreview', () => {
|
|
|
18
16
|
});
|
|
19
17
|
|
|
20
18
|
const value = 'a'.repeat(MAX_STRING_PREVIEW_CODE_POINTS);
|
|
21
|
-
expect(
|
|
22
|
-
toStorageEntryPreview({ key: 'boundary', type: 'string', value }),
|
|
23
|
-
).toEqual({
|
|
19
|
+
expect(toStorageEntryPreview({ key: 'boundary', type: 'string', value })).toEqual({
|
|
24
20
|
key: 'boundary',
|
|
25
21
|
type: 'string',
|
|
26
22
|
preview: value,
|
|
@@ -32,9 +28,7 @@ describe('toStorageEntryPreview', () => {
|
|
|
32
28
|
it('truncates an oversized string at the fixed code-point boundary', () => {
|
|
33
29
|
const value = `${'a'.repeat(MAX_STRING_PREVIEW_CODE_POINTS)}z`;
|
|
34
30
|
|
|
35
|
-
expect(
|
|
36
|
-
toStorageEntryPreview({ key: 'long', type: 'string', value }),
|
|
37
|
-
).toEqual({
|
|
31
|
+
expect(toStorageEntryPreview({ key: 'long', type: 'string', value })).toEqual({
|
|
38
32
|
key: 'long',
|
|
39
33
|
type: 'string',
|
|
40
34
|
preview: 'a'.repeat(MAX_STRING_PREVIEW_CODE_POINTS),
|
|
@@ -46,9 +40,7 @@ describe('toStorageEntryPreview', () => {
|
|
|
46
40
|
it('does not split Unicode code points while preserving string length', () => {
|
|
47
41
|
const value = `${'😀'.repeat(MAX_STRING_PREVIEW_CODE_POINTS)}!`;
|
|
48
42
|
|
|
49
|
-
expect(
|
|
50
|
-
toStorageEntryPreview({ key: 'emoji', type: 'string', value }),
|
|
51
|
-
).toEqual({
|
|
43
|
+
expect(toStorageEntryPreview({ key: 'emoji', type: 'string', value })).toEqual({
|
|
52
44
|
key: 'emoji',
|
|
53
45
|
type: 'string',
|
|
54
46
|
preview: '😀'.repeat(MAX_STRING_PREVIEW_CODE_POINTS),
|
|
@@ -58,18 +50,14 @@ describe('toStorageEntryPreview', () => {
|
|
|
58
50
|
});
|
|
59
51
|
|
|
60
52
|
it('formats primitive values completely', () => {
|
|
61
|
-
expect(
|
|
62
|
-
toStorageEntryPreview({ key: 'count', type: 'number', value: 42 }),
|
|
63
|
-
).toEqual({
|
|
53
|
+
expect(toStorageEntryPreview({ key: 'count', type: 'number', value: 42 })).toEqual({
|
|
64
54
|
key: 'count',
|
|
65
55
|
type: 'number',
|
|
66
56
|
preview: '42',
|
|
67
57
|
valueSize: 2,
|
|
68
58
|
isTruncated: false,
|
|
69
59
|
});
|
|
70
|
-
expect(
|
|
71
|
-
toStorageEntryPreview({ key: 'enabled', type: 'boolean', value: false }),
|
|
72
|
-
).toEqual({
|
|
60
|
+
expect(toStorageEntryPreview({ key: 'enabled', type: 'boolean', value: false })).toEqual({
|
|
73
61
|
key: 'enabled',
|
|
74
62
|
type: 'boolean',
|
|
75
63
|
preview: 'false',
|
|
@@ -79,9 +67,7 @@ describe('toStorageEntryPreview', () => {
|
|
|
79
67
|
});
|
|
80
68
|
|
|
81
69
|
it('formats empty and boundary-sized buffers as hexadecimal', () => {
|
|
82
|
-
expect(
|
|
83
|
-
toStorageEntryPreview({ key: 'empty', type: 'buffer', value: [] }),
|
|
84
|
-
).toEqual({
|
|
70
|
+
expect(toStorageEntryPreview({ key: 'empty', type: 'buffer', value: [] })).toEqual({
|
|
85
71
|
key: 'empty',
|
|
86
72
|
type: 'buffer',
|
|
87
73
|
preview: '',
|
|
@@ -90,9 +76,7 @@ describe('toStorageEntryPreview', () => {
|
|
|
90
76
|
});
|
|
91
77
|
|
|
92
78
|
const value = new Array(MAX_BUFFER_PREVIEW_BYTES).fill(0xab);
|
|
93
|
-
expect(
|
|
94
|
-
toStorageEntryPreview({ key: 'boundary', type: 'buffer', value }),
|
|
95
|
-
).toEqual({
|
|
79
|
+
expect(toStorageEntryPreview({ key: 'boundary', type: 'buffer', value })).toEqual({
|
|
96
80
|
key: 'boundary',
|
|
97
81
|
type: 'buffer',
|
|
98
82
|
preview: new Array(MAX_BUFFER_PREVIEW_BYTES).fill('AB').join(' '),
|
|
@@ -104,9 +88,7 @@ describe('toStorageEntryPreview', () => {
|
|
|
104
88
|
it('truncates oversized buffers at the fixed byte boundary', () => {
|
|
105
89
|
const value = [...new Array(MAX_BUFFER_PREVIEW_BYTES).fill(0xab), 0xcd];
|
|
106
90
|
|
|
107
|
-
expect(
|
|
108
|
-
toStorageEntryPreview({ key: 'long', type: 'buffer', value }),
|
|
109
|
-
).toEqual({
|
|
91
|
+
expect(toStorageEntryPreview({ key: 'long', type: 'buffer', value })).toEqual({
|
|
110
92
|
key: 'long',
|
|
111
93
|
type: 'buffer',
|
|
112
94
|
preview: new Array(MAX_BUFFER_PREVIEW_BYTES).fill('AB').join(' '),
|
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type StorageSnapshotV1,
|
|
7
|
-
} from '../snapshot';
|
|
8
|
-
import type {
|
|
9
|
-
StorageCapabilities,
|
|
10
|
-
StorageEntry,
|
|
11
|
-
StorageTarget,
|
|
12
|
-
} from '../types';
|
|
13
|
-
|
|
14
|
-
const validSnapshot = (
|
|
15
|
-
overrides: Partial<StorageSnapshotV1> = {},
|
|
16
|
-
): StorageSnapshotV1 => ({
|
|
2
|
+
import { buildSnapshot, computePreview, parseSnapshot, type StorageSnapshotV1 } from '../snapshot';
|
|
3
|
+
import type { StorageCapabilities, StorageEntry, StorageTarget } from '../types';
|
|
4
|
+
|
|
5
|
+
const validSnapshot = (overrides: Partial<StorageSnapshotV1> = {}): StorageSnapshotV1 => ({
|
|
17
6
|
version: 1,
|
|
18
7
|
plugin: '@rozenite/storage-plugin',
|
|
19
8
|
createdAt: '2026-05-11T12:00:00.000Z',
|
|
@@ -125,16 +114,13 @@ describe('parseSnapshot', () => {
|
|
|
125
114
|
const result = parseSnapshot(input);
|
|
126
115
|
expect(result.ok).toBe(false);
|
|
127
116
|
if (!result.ok) {
|
|
128
|
-
expect(result.error.path).toBe(
|
|
129
|
-
'storage.capabilities.supportedTypes[1]',
|
|
130
|
-
);
|
|
117
|
+
expect(result.error.path).toBe('storage.capabilities.supportedTypes[1]');
|
|
131
118
|
}
|
|
132
119
|
});
|
|
133
120
|
});
|
|
134
121
|
|
|
135
122
|
describe('per-entry rejections', () => {
|
|
136
|
-
const withEntry = (entry: unknown) =>
|
|
137
|
-
parseSnapshot({ ...validSnapshot(), entries: [entry] });
|
|
123
|
+
const withEntry = (entry: unknown) => parseSnapshot({ ...validSnapshot(), entries: [entry] });
|
|
138
124
|
|
|
139
125
|
it('rejects entry missing key', () => {
|
|
140
126
|
const result = withEntry({ type: 'string', value: 'x' });
|
|
@@ -217,9 +203,7 @@ describe('buildSnapshot', () => {
|
|
|
217
203
|
const capabilities: StorageCapabilities = {
|
|
218
204
|
supportedTypes: ['string', 'number'],
|
|
219
205
|
};
|
|
220
|
-
const entries: StorageEntry[] = [
|
|
221
|
-
{ key: 'token', type: 'string', value: 'abc' },
|
|
222
|
-
];
|
|
206
|
+
const entries: StorageEntry[] = [{ key: 'token', type: 'string', value: 'abc' }];
|
|
223
207
|
|
|
224
208
|
const result = buildSnapshot({
|
|
225
209
|
target,
|
|
@@ -280,9 +264,7 @@ describe('computePreview', () => {
|
|
|
280
264
|
entryKeys: new Set(),
|
|
281
265
|
isBlacklisted: (key) => key.startsWith('__'),
|
|
282
266
|
});
|
|
283
|
-
expect(preview.skippedKeys).toEqual([
|
|
284
|
-
{ key: '__internal', reason: 'blacklist' },
|
|
285
|
-
]);
|
|
267
|
+
expect(preview.skippedKeys).toEqual([{ key: '__internal', reason: 'blacklist' }]);
|
|
286
268
|
expect(preview.newKeys).toEqual(['visible']);
|
|
287
269
|
});
|
|
288
270
|
|
|
@@ -320,12 +302,8 @@ describe('computePreview', () => {
|
|
|
320
302
|
entryKeys: new Set(['existing']),
|
|
321
303
|
isBlacklisted: (key) => key.startsWith('__'),
|
|
322
304
|
});
|
|
323
|
-
expect(preview.unsupportedTypes).toEqual([
|
|
324
|
-
|
|
325
|
-
]);
|
|
326
|
-
expect(preview.skippedKeys).toEqual([
|
|
327
|
-
{ key: '__skipped', reason: 'blacklist' },
|
|
328
|
-
]);
|
|
305
|
+
expect(preview.unsupportedTypes).toEqual([{ key: '__blocked', type: 'buffer' }]);
|
|
306
|
+
expect(preview.skippedKeys).toEqual([{ key: '__skipped', reason: 'blacklist' }]);
|
|
329
307
|
expect(preview.overwriteKeys).toEqual(['existing']);
|
|
330
308
|
expect(preview.newKeys).toEqual(['fresh']);
|
|
331
309
|
});
|
|
@@ -11,8 +11,7 @@ type StorageSelection = Partial<StorageTarget>;
|
|
|
11
11
|
const sharedStorageProperties = {
|
|
12
12
|
adapterId: {
|
|
13
13
|
type: 'string',
|
|
14
|
-
description:
|
|
15
|
-
'Storage adapter ID. Required when multiple adapters are configured.',
|
|
14
|
+
description: 'Storage adapter ID. Required when multiple adapters are configured.',
|
|
16
15
|
},
|
|
17
16
|
storageId: {
|
|
18
17
|
type: 'string',
|
|
@@ -105,19 +104,13 @@ export type StorageRemoveEntryResult = {
|
|
|
105
104
|
};
|
|
106
105
|
|
|
107
106
|
export const storageToolDefinitions = {
|
|
108
|
-
listStorages: defineAgentToolContract<
|
|
109
|
-
StorageListStoragesArgs,
|
|
110
|
-
StorageListStoragesResult
|
|
111
|
-
>({
|
|
107
|
+
listStorages: defineAgentToolContract<StorageListStoragesArgs, StorageListStoragesResult>({
|
|
112
108
|
name: 'list-storages',
|
|
113
109
|
description:
|
|
114
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.',
|
|
115
111
|
inputSchema: { type: 'object', properties: {} },
|
|
116
112
|
}),
|
|
117
|
-
listEntries: definePaginatedAgentToolContract<
|
|
118
|
-
StorageListEntriesArgs,
|
|
119
|
-
StorageListEntriesResult
|
|
120
|
-
>({
|
|
113
|
+
listEntries: definePaginatedAgentToolContract<StorageListEntriesArgs, StorageListEntriesResult>({
|
|
121
114
|
name: 'list-entries',
|
|
122
115
|
description:
|
|
123
116
|
'List keys in a storage. This call intentionally does not return entry values to keep responses token-efficient.',
|
|
@@ -131,8 +124,7 @@ export const storageToolDefinitions = {
|
|
|
131
124
|
},
|
|
132
125
|
cursor: {
|
|
133
126
|
type: 'string',
|
|
134
|
-
description:
|
|
135
|
-
'Opaque pagination cursor from a previous list-entries call.',
|
|
127
|
+
description: 'Opaque pagination cursor from a previous list-entries call.',
|
|
136
128
|
},
|
|
137
129
|
offset: {
|
|
138
130
|
type: 'number',
|
|
@@ -147,10 +139,7 @@ export const storageToolDefinitions = {
|
|
|
147
139
|
defaultFields: ['key'],
|
|
148
140
|
},
|
|
149
141
|
}),
|
|
150
|
-
readEntry: defineAgentToolContract<
|
|
151
|
-
StorageReadEntryArgs,
|
|
152
|
-
StorageReadEntryResult
|
|
153
|
-
>({
|
|
142
|
+
readEntry: defineAgentToolContract<StorageReadEntryArgs, StorageReadEntryResult>({
|
|
154
143
|
name: 'read-entry',
|
|
155
144
|
description: 'Read a single storage entry value by key.',
|
|
156
145
|
inputSchema: {
|
|
@@ -162,10 +151,7 @@ export const storageToolDefinitions = {
|
|
|
162
151
|
required: ['key'],
|
|
163
152
|
},
|
|
164
153
|
}),
|
|
165
|
-
createEntry: defineAgentToolContract<
|
|
166
|
-
StorageCreateEntryArgs,
|
|
167
|
-
StorageCreateEntryResult
|
|
168
|
-
>({
|
|
154
|
+
createEntry: defineAgentToolContract<StorageCreateEntryArgs, StorageCreateEntryResult>({
|
|
169
155
|
name: 'create-entry',
|
|
170
156
|
description: 'Create a new storage entry. Fails if the key already exists.',
|
|
171
157
|
inputSchema: {
|
|
@@ -186,13 +172,9 @@ export const storageToolDefinitions = {
|
|
|
186
172
|
required: ['key', 'type', 'value'],
|
|
187
173
|
},
|
|
188
174
|
}),
|
|
189
|
-
editEntry: defineAgentToolContract<
|
|
190
|
-
StorageEditEntryArgs,
|
|
191
|
-
StorageEditEntryResult
|
|
192
|
-
>({
|
|
175
|
+
editEntry: defineAgentToolContract<StorageEditEntryArgs, StorageEditEntryResult>({
|
|
193
176
|
name: 'edit-entry',
|
|
194
|
-
description:
|
|
195
|
-
'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.',
|
|
196
178
|
inputSchema: {
|
|
197
179
|
type: 'object',
|
|
198
180
|
properties: {
|
|
@@ -211,10 +193,7 @@ export const storageToolDefinitions = {
|
|
|
211
193
|
required: ['key', 'type', 'value'],
|
|
212
194
|
},
|
|
213
195
|
}),
|
|
214
|
-
removeEntry: defineAgentToolContract<
|
|
215
|
-
StorageRemoveEntryArgs,
|
|
216
|
-
StorageRemoveEntryResult
|
|
217
|
-
>({
|
|
196
|
+
removeEntry: defineAgentToolContract<StorageRemoveEntryArgs, StorageRemoveEntryResult>({
|
|
218
197
|
name: 'remove-entry',
|
|
219
198
|
description: 'Remove a storage entry by key.',
|
|
220
199
|
inputSchema: {
|
|
@@ -3,12 +3,9 @@ import type { StorageEntry, StorageEntryPreview } from './types';
|
|
|
3
3
|
export const MAX_STRING_PREVIEW_CODE_POINTS = 256;
|
|
4
4
|
export const MAX_BUFFER_PREVIEW_BYTES = 16;
|
|
5
5
|
|
|
6
|
-
const toHexPair = (byte: number): string =>
|
|
7
|
-
byte.toString(16).toUpperCase().padStart(2, '0');
|
|
6
|
+
const toHexPair = (byte: number): string => byte.toString(16).toUpperCase().padStart(2, '0');
|
|
8
7
|
|
|
9
|
-
const truncateString = (
|
|
10
|
-
value: string,
|
|
11
|
-
): Pick<StorageEntryPreview, 'preview' | 'isTruncated'> => {
|
|
8
|
+
const truncateString = (value: string): Pick<StorageEntryPreview, 'preview' | 'isTruncated'> => {
|
|
12
9
|
let preview = '';
|
|
13
10
|
let codePointCount = 0;
|
|
14
11
|
|
|
@@ -24,9 +21,7 @@ const truncateString = (
|
|
|
24
21
|
return { preview, isTruncated: false };
|
|
25
22
|
};
|
|
26
23
|
|
|
27
|
-
export const toStorageEntryPreview = (
|
|
28
|
-
entry: StorageEntry,
|
|
29
|
-
): StorageEntryPreview => {
|
|
24
|
+
export const toStorageEntryPreview = (entry: StorageEntry): StorageEntryPreview => {
|
|
30
25
|
if (entry.type === 'string') {
|
|
31
26
|
return {
|
|
32
27
|
key: entry.key,
|