@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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { StorageAdapter, StorageEntry, StorageEntryType } from '../../shared/types';
|
|
3
|
+
import {
|
|
4
|
+
ENTRY_PREVIEW_READ_CONCURRENCY,
|
|
5
|
+
MAX_ENTRY_PREVIEW_PAGE_SIZE,
|
|
6
|
+
handleListEntryPreviewsRequest,
|
|
7
|
+
} from '../entry-preview-pagination';
|
|
8
|
+
import { handleStorageDiscoveryRequest } from '../storage-discovery';
|
|
9
|
+
import { createStorageViews } from '../storage-view';
|
|
10
|
+
|
|
11
|
+
const STORAGE_COUNT = 250;
|
|
12
|
+
const ENTRIES_PER_STORAGE = 1_000;
|
|
13
|
+
const MEGABYTE_VALUE = 'x'.repeat(1024 * 1024);
|
|
14
|
+
const selectedTarget = { adapterId: 'stress', storageId: 'storage-000' };
|
|
15
|
+
|
|
16
|
+
const keys = Array.from(
|
|
17
|
+
{ length: ENTRIES_PER_STORAGE },
|
|
18
|
+
(_, index) => `entry-${String(index).padStart(4, '0')}`,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const previewRequest = (overrides: Record<string, unknown> = {}) => ({
|
|
22
|
+
type: 'list-entry-previews' as const,
|
|
23
|
+
requestId: 'stress-request',
|
|
24
|
+
target: selectedTarget,
|
|
25
|
+
limit: MAX_ENTRY_PREVIEW_PAGE_SIZE,
|
|
26
|
+
...overrides,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const createStressAdapter = () => {
|
|
30
|
+
let activeReads = 0;
|
|
31
|
+
let maxActiveReads = 0;
|
|
32
|
+
const storageReads = Array.from({ length: STORAGE_COUNT }, (_, index) => {
|
|
33
|
+
const getAllKeys = vi.fn(async () => keys);
|
|
34
|
+
const get = vi.fn(async (key: string): Promise<StorageEntry> => {
|
|
35
|
+
activeReads += 1;
|
|
36
|
+
maxActiveReads = Math.max(maxActiveReads, activeReads);
|
|
37
|
+
// This fixture is intentionally asynchronous. The assertion below uses
|
|
38
|
+
// counts, rather than duration, so it stays deterministic under load.
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
40
|
+
activeReads -= 1;
|
|
41
|
+
return { key, type: 'string', value: MEGABYTE_VALUE };
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
getAllKeys,
|
|
46
|
+
get,
|
|
47
|
+
node: {
|
|
48
|
+
id: `storage-${String(index).padStart(3, '0')}`,
|
|
49
|
+
name: `Stress storage ${index}`,
|
|
50
|
+
capabilities: { supportedTypes: ['string'] as StorageEntryType[] },
|
|
51
|
+
storage: {
|
|
52
|
+
kind: 'async' as const,
|
|
53
|
+
getAllKeys,
|
|
54
|
+
get,
|
|
55
|
+
set: vi.fn(async () => undefined),
|
|
56
|
+
delete: vi.fn(async () => undefined),
|
|
57
|
+
clear: vi.fn(async () => undefined),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const adapter: StorageAdapter = {
|
|
64
|
+
id: 'stress',
|
|
65
|
+
name: 'Stress adapter',
|
|
66
|
+
storages: storageReads.map(({ node }) => node),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
maxActiveReads: () => maxActiveReads,
|
|
71
|
+
storageReads,
|
|
72
|
+
views: createStorageViews([adapter]),
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
describe('storage scalability stress fixture', () => {
|
|
77
|
+
it('discovers many storages without starting value work', async () => {
|
|
78
|
+
const fixture = createStressAdapter();
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
await handleStorageDiscoveryRequest(fixture.views, {
|
|
82
|
+
type: 'discover-storages',
|
|
83
|
+
requestId: 'discover-stress',
|
|
84
|
+
}),
|
|
85
|
+
).toMatchObject({
|
|
86
|
+
type: 'storage-descriptors',
|
|
87
|
+
storages: expect.any(Array),
|
|
88
|
+
});
|
|
89
|
+
expect(fixture.views).toHaveLength(STORAGE_COUNT);
|
|
90
|
+
for (const { get, getAllKeys } of fixture.storageReads) {
|
|
91
|
+
expect(getAllKeys).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(get).not.toHaveBeenCalled();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('reads only one bounded page from the selected storage when values are slow and large', async () => {
|
|
97
|
+
const fixture = createStressAdapter();
|
|
98
|
+
|
|
99
|
+
const result = await handleListEntryPreviewsRequest(fixture.views, previewRequest());
|
|
100
|
+
|
|
101
|
+
expect(result).toMatchObject({
|
|
102
|
+
type: 'entry-previews',
|
|
103
|
+
items: Array.from({ length: MAX_ENTRY_PREVIEW_PAGE_SIZE }, () =>
|
|
104
|
+
expect.objectContaining({
|
|
105
|
+
preview: 'x'.repeat(256),
|
|
106
|
+
valueSize: MEGABYTE_VALUE.length,
|
|
107
|
+
isTruncated: true,
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
});
|
|
111
|
+
if (result.type === 'entry-previews') {
|
|
112
|
+
expect(result.items.every((item) => !('value' in item))).toBe(true);
|
|
113
|
+
}
|
|
114
|
+
expect(fixture.storageReads[0]?.getAllKeys).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(fixture.storageReads[0]?.get).toHaveBeenCalledTimes(MAX_ENTRY_PREVIEW_PAGE_SIZE);
|
|
116
|
+
expect(fixture.maxActiveReads()).toBeLessThanOrEqual(ENTRY_PREVIEW_READ_CONCURRENCY);
|
|
117
|
+
for (const { get, getAllKeys } of fixture.storageReads.slice(1)) {
|
|
118
|
+
expect(getAllKeys).not.toHaveBeenCalled();
|
|
119
|
+
expect(get).not.toHaveBeenCalled();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('searches keys first and reads only the resulting page', async () => {
|
|
124
|
+
const fixture = createStressAdapter();
|
|
125
|
+
|
|
126
|
+
const result = await handleListEntryPreviewsRequest(
|
|
127
|
+
fixture.views,
|
|
128
|
+
previewRequest({ search: ' 0999 ' }),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(result).toMatchObject({
|
|
132
|
+
type: 'entry-previews',
|
|
133
|
+
items: [expect.objectContaining({ key: 'entry-0999' })],
|
|
134
|
+
});
|
|
135
|
+
expect(fixture.storageReads[0]?.get).toHaveBeenCalledExactlyOnceWith('entry-0999');
|
|
136
|
+
for (const { get } of fixture.storageReads.slice(1)) {
|
|
137
|
+
expect(get).not.toHaveBeenCalled();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { MAX_PAGE_LIMIT } from '@rozenite/agent-shared';
|
|
3
|
+
import { storageToolDefinitions } from '../../shared/agent-tools';
|
|
4
|
+
import type { StorageAdapter, StorageEntry } from '../../shared/types';
|
|
5
|
+
|
|
6
|
+
vi.mock('@rozenite/agent-bridge', () => ({
|
|
7
|
+
useRozenitePluginAgentTool: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import { createStorageAgentHandlers } from '../useStorageAgentTools';
|
|
11
|
+
import { createStorageViews } from '../storage-view';
|
|
12
|
+
|
|
13
|
+
const createViews = (
|
|
14
|
+
storages: Array<{
|
|
15
|
+
adapterId: string;
|
|
16
|
+
storageId: string;
|
|
17
|
+
entries: Record<string, StorageEntry>;
|
|
18
|
+
}>,
|
|
19
|
+
) => {
|
|
20
|
+
const adapters = storages.map(({ adapterId, storageId, entries }): StorageAdapter => ({
|
|
21
|
+
id: adapterId,
|
|
22
|
+
name: `Adapter ${adapterId}`,
|
|
23
|
+
storages: [
|
|
24
|
+
{
|
|
25
|
+
id: storageId,
|
|
26
|
+
name: `Storage ${storageId}`,
|
|
27
|
+
capabilities: { supportedTypes: ['string', 'buffer'] },
|
|
28
|
+
storage: {
|
|
29
|
+
kind: 'sync',
|
|
30
|
+
getAllKeys: vi.fn(() => Object.keys(entries)),
|
|
31
|
+
get: vi.fn((key: string) => entries[key]),
|
|
32
|
+
set: vi.fn(),
|
|
33
|
+
delete: vi.fn(),
|
|
34
|
+
clear: vi.fn(),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
return { adapters, views: createStorageViews(adapters) };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const entries = (...keys: string[]): Record<string, StorageEntry> =>
|
|
44
|
+
Object.fromEntries(keys.map((key) => [key, { key, type: 'string', value: `value:${key}` }]));
|
|
45
|
+
|
|
46
|
+
describe('storage Agent tool handlers', () => {
|
|
47
|
+
it('lists storage descriptors without reading keys or values', async () => {
|
|
48
|
+
const { adapters, views } = createViews([
|
|
49
|
+
{ adapterId: 'one', storageId: 'first', entries: entries('a') },
|
|
50
|
+
{ adapterId: 'two', storageId: 'second', entries: entries('b') },
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
await expect(createStorageAgentHandlers(views).listStorages()).resolves.toEqual({
|
|
54
|
+
storages: [
|
|
55
|
+
{
|
|
56
|
+
adapterId: 'one',
|
|
57
|
+
storageId: 'first',
|
|
58
|
+
adapterName: 'Adapter one',
|
|
59
|
+
storageName: 'Storage first',
|
|
60
|
+
supportedTypes: ['string', 'buffer'],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
adapterId: 'two',
|
|
64
|
+
storageId: 'second',
|
|
65
|
+
adapterName: 'Adapter two',
|
|
66
|
+
storageName: 'Storage second',
|
|
67
|
+
supportedTypes: ['string', 'buffer'],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
for (const adapter of adapters) {
|
|
72
|
+
const storage = adapter.storages[0].storage;
|
|
73
|
+
expect(storage.getAllKeys).not.toHaveBeenCalled();
|
|
74
|
+
expect(storage.get).not.toHaveBeenCalled();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('uses the shared cursor pagination contract and returns keys without values', async () => {
|
|
79
|
+
const { adapters, views } = createViews([
|
|
80
|
+
{
|
|
81
|
+
adapterId: 'adapter',
|
|
82
|
+
storageId: 'storage',
|
|
83
|
+
entries: entries('first', 'second', 'third'),
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
const handlers = createStorageAgentHandlers(views);
|
|
87
|
+
|
|
88
|
+
expect(storageToolDefinitions.listEntries.pagination).toEqual({
|
|
89
|
+
kind: 'cursor',
|
|
90
|
+
fields: ['key'],
|
|
91
|
+
defaultFields: ['key'],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const first = await handlers.listEntries({ limit: 2 });
|
|
95
|
+
expect(first).toEqual({
|
|
96
|
+
adapterId: 'adapter',
|
|
97
|
+
storageId: 'storage',
|
|
98
|
+
total: 3,
|
|
99
|
+
items: [{ key: 'first' }, { key: 'second' }],
|
|
100
|
+
page: {
|
|
101
|
+
limit: 2,
|
|
102
|
+
hasMore: true,
|
|
103
|
+
nextCursor: expect.any(String),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
expect(first.items.every((item) => !('value' in item))).toBe(true);
|
|
107
|
+
|
|
108
|
+
await expect(handlers.listEntries({ cursor: first.page.nextCursor })).resolves.toEqual({
|
|
109
|
+
adapterId: 'adapter',
|
|
110
|
+
storageId: 'storage',
|
|
111
|
+
total: 3,
|
|
112
|
+
items: [{ key: 'third' }],
|
|
113
|
+
page: { limit: 20, hasMore: false },
|
|
114
|
+
});
|
|
115
|
+
expect(adapters[0].storages[0].storage.get).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('defaults invalid limits and offsets and caps oversized finite limits', async () => {
|
|
119
|
+
const { views } = createViews([
|
|
120
|
+
{
|
|
121
|
+
adapterId: 'adapter',
|
|
122
|
+
storageId: 'storage',
|
|
123
|
+
entries: entries(
|
|
124
|
+
...Array.from({ length: MAX_PAGE_LIMIT + 1 }, (_, index) => String(index)),
|
|
125
|
+
),
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
const handlers = createStorageAgentHandlers(views);
|
|
129
|
+
|
|
130
|
+
for (const limit of [-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
131
|
+
await expect(handlers.listEntries({ limit })).resolves.toMatchObject({
|
|
132
|
+
page: { limit: 20 },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
for (const offset of [
|
|
136
|
+
-1,
|
|
137
|
+
1.5,
|
|
138
|
+
Number.NaN,
|
|
139
|
+
Number.POSITIVE_INFINITY,
|
|
140
|
+
Number.MAX_SAFE_INTEGER + 1,
|
|
141
|
+
]) {
|
|
142
|
+
await expect(handlers.listEntries({ offset })).resolves.toMatchObject({
|
|
143
|
+
items: expect.arrayContaining([{ key: '0' }]),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const capped = await handlers.listEntries({ limit: MAX_PAGE_LIMIT + 1 });
|
|
147
|
+
expect(capped.items).toHaveLength(MAX_PAGE_LIMIT);
|
|
148
|
+
expect(capped.page).toMatchObject({
|
|
149
|
+
limit: MAX_PAGE_LIMIT,
|
|
150
|
+
hasMore: true,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('rejects malformed and target-mismatched cursors and ambiguous selections', async () => {
|
|
155
|
+
const { views } = createViews([
|
|
156
|
+
{ adapterId: 'adapter', storageId: 'one', entries: entries('a', 'b') },
|
|
157
|
+
{ adapterId: 'adapter', storageId: 'two', entries: entries('c') },
|
|
158
|
+
{ adapterId: 'other', storageId: 'one', entries: entries('d') },
|
|
159
|
+
]);
|
|
160
|
+
const handlers = createStorageAgentHandlers(views);
|
|
161
|
+
const cursor = (
|
|
162
|
+
await handlers.listEntries({
|
|
163
|
+
adapterId: 'adapter',
|
|
164
|
+
storageId: 'one',
|
|
165
|
+
limit: 1,
|
|
166
|
+
})
|
|
167
|
+
).page.nextCursor!;
|
|
168
|
+
|
|
169
|
+
await expect(
|
|
170
|
+
handlers.listEntries({
|
|
171
|
+
adapterId: 'adapter',
|
|
172
|
+
storageId: 'one',
|
|
173
|
+
cursor: 'bad',
|
|
174
|
+
}),
|
|
175
|
+
).rejects.toThrow('Invalid cursor');
|
|
176
|
+
await expect(
|
|
177
|
+
handlers.listEntries({
|
|
178
|
+
adapterId: 'other',
|
|
179
|
+
storageId: 'one',
|
|
180
|
+
cursor,
|
|
181
|
+
}),
|
|
182
|
+
).rejects.toThrow('Cursor does not match the requested storage');
|
|
183
|
+
await expect(
|
|
184
|
+
handlers.listEntries({
|
|
185
|
+
adapterId: 'adapter',
|
|
186
|
+
storageId: 'one',
|
|
187
|
+
cursor,
|
|
188
|
+
offset: 0,
|
|
189
|
+
}),
|
|
190
|
+
).rejects.toThrow('either cursor or offset');
|
|
191
|
+
await expect(handlers.listEntries({ adapterId: 'adapter' })).rejects.toThrow(
|
|
192
|
+
'Multiple storages matched',
|
|
193
|
+
);
|
|
194
|
+
await expect(handlers.listEntries({ storageId: 'one' })).rejects.toThrow(
|
|
195
|
+
'Multiple storages matched',
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('returns a complete large value only when read-entry explicitly requests it', async () => {
|
|
200
|
+
const value = 'x'.repeat(100_000);
|
|
201
|
+
const { adapters, views } = createViews([
|
|
202
|
+
{
|
|
203
|
+
adapterId: 'adapter',
|
|
204
|
+
storageId: 'storage',
|
|
205
|
+
entries: {
|
|
206
|
+
large: { key: 'large', type: 'string', value },
|
|
207
|
+
other: { key: 'other', type: 'string', value: 'not read' },
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
]);
|
|
211
|
+
const handlers = createStorageAgentHandlers(views);
|
|
212
|
+
|
|
213
|
+
await expect(handlers.readEntry({ key: 'large' })).resolves.toEqual({
|
|
214
|
+
adapterId: 'adapter',
|
|
215
|
+
storageId: 'storage',
|
|
216
|
+
entry: { key: 'large', type: 'string', value },
|
|
217
|
+
});
|
|
218
|
+
expect(adapters[0].storages[0].storage.get).toHaveBeenCalledTimes(1);
|
|
219
|
+
expect(adapters[0].storages[0].storage.get).toHaveBeenCalledWith('large');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -109,9 +109,9 @@ describe('createMMKVStorageAdapter', () => {
|
|
|
109
109
|
it('rejects MMKV v4 storages passed as an array', () => {
|
|
110
110
|
const storage = createFakeMMKVV4();
|
|
111
111
|
|
|
112
|
-
expect(() =>
|
|
113
|
-
|
|
114
|
-
)
|
|
112
|
+
expect(() => createMMKVStorageAdapter({ storages: [storage as any] })).toThrow(
|
|
113
|
+
/MMKV arrays are not supported for v4 storages/,
|
|
114
|
+
);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
it('applies global blacklist patterns against storageId:key', async () => {
|
|
@@ -129,12 +129,8 @@ describe('createMMKVStorageAdapter', () => {
|
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
const views = createStorageViews([adapter]);
|
|
132
|
-
const userView = views.find(
|
|
133
|
-
|
|
134
|
-
);
|
|
135
|
-
const cacheView = views.find(
|
|
136
|
-
(view) => view.target.storageId === 'cache-storage',
|
|
137
|
-
);
|
|
132
|
+
const userView = views.find((view) => view.target.storageId === 'user-storage');
|
|
133
|
+
const cacheView = views.find((view) => view.target.storageId === 'cache-storage');
|
|
138
134
|
|
|
139
135
|
expect(userView).toBeDefined();
|
|
140
136
|
expect(cacheView).toBeDefined();
|
|
@@ -173,22 +169,20 @@ const createAmbiguousFakeMMKVV4 = ({
|
|
|
173
169
|
const values = new Map<string, Stored>();
|
|
174
170
|
|
|
175
171
|
return {
|
|
176
|
-
set: vi.fn(
|
|
177
|
-
(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
},
|
|
191
|
-
),
|
|
172
|
+
set: vi.fn((key: string, value: string | number | boolean | ArrayBuffer) => {
|
|
173
|
+
if (typeof value === 'string') {
|
|
174
|
+
values.set(key, { kind: 'string', value });
|
|
175
|
+
} else if (typeof value === 'number') {
|
|
176
|
+
values.set(key, { kind: 'number', value });
|
|
177
|
+
} else if (typeof value === 'boolean') {
|
|
178
|
+
values.set(key, { kind: 'boolean', value });
|
|
179
|
+
} else {
|
|
180
|
+
values.set(key, {
|
|
181
|
+
kind: 'buffer',
|
|
182
|
+
bytes: new Uint8Array(value),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}),
|
|
192
186
|
getString: vi.fn((key: string) => {
|
|
193
187
|
const stored = values.get(key);
|
|
194
188
|
if (!stored) return undefined;
|
|
@@ -205,11 +199,7 @@ const createAmbiguousFakeMMKVV4 = ({
|
|
|
205
199
|
getNumber: vi.fn((key: string) => {
|
|
206
200
|
const stored = values.get(key);
|
|
207
201
|
if (stored?.kind === 'number') return stored.value;
|
|
208
|
-
if (
|
|
209
|
-
lenientGetNumber &&
|
|
210
|
-
stored?.kind === 'buffer' &&
|
|
211
|
-
stored.bytes.byteLength === 8
|
|
212
|
-
) {
|
|
202
|
+
if (lenientGetNumber && stored?.kind === 'buffer' && stored.bytes.byteLength === 8) {
|
|
213
203
|
return new DataView(stored.bytes.buffer).getFloat64(0, true);
|
|
214
204
|
}
|
|
215
205
|
return undefined;
|
|
@@ -5,6 +5,7 @@ export type AsyncStorageLike = {
|
|
|
5
5
|
getItem: (key: string) => Promise<string | null>;
|
|
6
6
|
setItem: (key: string, value: string) => Promise<void>;
|
|
7
7
|
removeItem: (key: string) => Promise<void>;
|
|
8
|
+
clear: () => Promise<void>;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
export type AsyncStorageInstanceConfig = {
|
|
@@ -28,15 +29,13 @@ type MultiStorageOptions = {
|
|
|
28
29
|
adapterName?: string;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
|
-
export type CreateAsyncStorageAdapterOptions =
|
|
32
|
-
| SingleStorageOptions
|
|
33
|
-
| MultiStorageOptions;
|
|
32
|
+
export type CreateAsyncStorageAdapterOptions = SingleStorageOptions | MultiStorageOptions;
|
|
34
33
|
|
|
35
34
|
const toStorageNode = (
|
|
36
35
|
storageId: string,
|
|
37
36
|
config: AsyncStorageLike | AsyncStorageInstanceConfig,
|
|
38
37
|
fallbackName?: string,
|
|
39
|
-
fallbackBlacklist?: RegExp
|
|
38
|
+
fallbackBlacklist?: RegExp,
|
|
40
39
|
): StorageNode => {
|
|
41
40
|
const resolved =
|
|
42
41
|
'storage' in config
|
|
@@ -77,27 +76,27 @@ const toStorageNode = (
|
|
|
77
76
|
await storage.setItem(entry.key, entry.value);
|
|
78
77
|
},
|
|
79
78
|
delete: (key) => storage.removeItem(key),
|
|
79
|
+
clear: () => storage.clear(),
|
|
80
80
|
},
|
|
81
81
|
};
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
export const createAsyncStorageAdapter = (
|
|
85
|
-
options: CreateAsyncStorageAdapterOptions
|
|
85
|
+
options: CreateAsyncStorageAdapterOptions,
|
|
86
86
|
): StorageAdapter => {
|
|
87
|
-
const { adapterId = 'async-storage', adapterName = 'AsyncStorage' } =
|
|
88
|
-
options;
|
|
87
|
+
const { adapterId = 'async-storage', adapterName = 'AsyncStorage' } = options;
|
|
89
88
|
|
|
90
89
|
const storageNodes: StorageNode[] =
|
|
91
90
|
'storages' in options
|
|
92
91
|
? Object.entries(options.storages).map(([storageId, config]) =>
|
|
93
|
-
toStorageNode(storageId, config)
|
|
92
|
+
toStorageNode(storageId, config),
|
|
94
93
|
)
|
|
95
94
|
: [
|
|
96
95
|
toStorageNode(
|
|
97
96
|
options.storageId ?? 'default',
|
|
98
97
|
options.storage,
|
|
99
98
|
options.storageName ?? 'Default Storage',
|
|
100
|
-
options.blacklist
|
|
99
|
+
options.blacklist,
|
|
101
100
|
),
|
|
102
101
|
];
|
|
103
102
|
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
export { createMMKVStorageAdapter } from './mmkv';
|
|
2
2
|
export type { CreateMMKVStorageAdapterOptions } from './mmkv';
|
|
3
3
|
|
|
4
|
-
export {
|
|
5
|
-
createAsyncStorageAdapter,
|
|
6
|
-
createExpoAsyncStorageAdapter,
|
|
7
|
-
} from './async-storage';
|
|
4
|
+
export { createAsyncStorageAdapter, createExpoAsyncStorageAdapter } from './async-storage';
|
|
8
5
|
export type {
|
|
9
6
|
AsyncStorageInstanceConfig,
|
|
10
7
|
AsyncStorageLike,
|
|
@@ -13,7 +10,4 @@ export type {
|
|
|
13
10
|
} from './async-storage';
|
|
14
11
|
|
|
15
12
|
export { createExpoSecureStorageAdapter } from './secure-storage';
|
|
16
|
-
export type {
|
|
17
|
-
CreateExpoSecureStorageAdapterOptions,
|
|
18
|
-
SecureStorageLike,
|
|
19
|
-
} from './secure-storage';
|
|
13
|
+
export type { CreateExpoSecureStorageAdapterOptions, SecureStorageLike } from './secure-storage';
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type { MMKV as MMKVV3 } from 'react-native-mmkv-v3';
|
|
2
2
|
import type { MMKV as MMKVV4 } from 'react-native-mmkv-v4';
|
|
3
|
-
import type {
|
|
4
|
-
StorageAdapter,
|
|
5
|
-
StorageEntry,
|
|
6
|
-
StorageNode,
|
|
7
|
-
} from '../../shared/types';
|
|
3
|
+
import type { StorageAdapter, StorageEntry, StorageNode } from '../../shared/types';
|
|
8
4
|
import { DEFAULT_SUPPORTED_TYPES } from '../../shared/types';
|
|
9
5
|
|
|
10
6
|
type MMKV = MMKVV3 | MMKVV4;
|
|
@@ -17,6 +13,7 @@ type MMKVAdapter = {
|
|
|
17
13
|
getBuffer: (key: string) => ArrayBuffer | undefined;
|
|
18
14
|
delete: (key: string) => void;
|
|
19
15
|
getAllKeys: () => string[];
|
|
16
|
+
clear: () => void;
|
|
20
17
|
addOnValueChangedListener: (callback: (key: string) => void) => {
|
|
21
18
|
remove: () => void;
|
|
22
19
|
};
|
|
@@ -43,15 +40,14 @@ const normalizeStorages = (storages: MMKV[] | Record<string, MMKV>) => {
|
|
|
43
40
|
);
|
|
44
41
|
}
|
|
45
42
|
|
|
46
|
-
return Object.fromEntries(
|
|
47
|
-
(storages as MMKVV3[]).map((storage) => [storage['id'], storage]),
|
|
48
|
-
);
|
|
43
|
+
return Object.fromEntries((storages as MMKVV3[]).map((storage) => [storage['id'], storage]));
|
|
49
44
|
}
|
|
50
45
|
|
|
51
46
|
return storages;
|
|
52
47
|
};
|
|
53
48
|
|
|
54
49
|
const getMMKVAdapter = (mmkv: MMKV): MMKVAdapter => {
|
|
50
|
+
const clear = () => mmkv.clearAll();
|
|
55
51
|
if (isMMKVV4(mmkv)) {
|
|
56
52
|
return {
|
|
57
53
|
set: (key, value) => mmkv.set(key, value),
|
|
@@ -61,8 +57,8 @@ const getMMKVAdapter = (mmkv: MMKV): MMKVAdapter => {
|
|
|
61
57
|
getBuffer: (key) => mmkv.getBuffer(key),
|
|
62
58
|
delete: (key) => mmkv.remove(key),
|
|
63
59
|
getAllKeys: () => mmkv.getAllKeys(),
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
clear,
|
|
61
|
+
addOnValueChangedListener: (callback) => mmkv.addOnValueChangedListener(callback),
|
|
66
62
|
};
|
|
67
63
|
}
|
|
68
64
|
|
|
@@ -74,8 +70,8 @@ const getMMKVAdapter = (mmkv: MMKV): MMKVAdapter => {
|
|
|
74
70
|
getBuffer: (key) => mmkv.getBuffer(key) as ArrayBuffer | undefined,
|
|
75
71
|
delete: (key) => mmkv.delete(key),
|
|
76
72
|
getAllKeys: () => mmkv.getAllKeys(),
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
clear,
|
|
74
|
+
addOnValueChangedListener: (callback) => mmkv.addOnValueChangedListener(callback),
|
|
79
75
|
};
|
|
80
76
|
};
|
|
81
77
|
|
|
@@ -98,10 +94,7 @@ const getMMKVAdapter = (mmkv: MMKV): MMKVAdapter => {
|
|
|
98
94
|
// those keys, so the chain falls through.
|
|
99
95
|
// 4. Empty string — intentional `setString(key, "")` lands here once
|
|
100
96
|
// we've ruled out a non-empty buffer payload at the same key.
|
|
101
|
-
const getEntry = (
|
|
102
|
-
adapter: MMKVAdapter,
|
|
103
|
-
key: string,
|
|
104
|
-
): StorageEntry | undefined => {
|
|
97
|
+
const getEntry = (adapter: MMKVAdapter, key: string): StorageEntry | undefined => {
|
|
105
98
|
const stringValue = adapter.getString(key);
|
|
106
99
|
if (stringValue !== undefined && stringValue.length > 0) {
|
|
107
100
|
return { key, type: 'string', value: stringValue };
|
|
@@ -141,10 +134,7 @@ const setEntry = (adapter: MMKVAdapter, entry: StorageEntry) => {
|
|
|
141
134
|
}
|
|
142
135
|
};
|
|
143
136
|
|
|
144
|
-
const getStorageBlacklist = (
|
|
145
|
-
config: MMKVBlacklistConfig | undefined,
|
|
146
|
-
storageId: string,
|
|
147
|
-
) => {
|
|
137
|
+
const getStorageBlacklist = (config: MMKVBlacklistConfig | undefined, storageId: string) => {
|
|
148
138
|
if (!config) {
|
|
149
139
|
return undefined;
|
|
150
140
|
}
|
|
@@ -176,10 +166,7 @@ export const createMMKVStorageAdapter = ({
|
|
|
176
166
|
storages,
|
|
177
167
|
blacklist,
|
|
178
168
|
}: CreateMMKVStorageAdapterOptions): StorageAdapter => {
|
|
179
|
-
const normalizedStorages = normalizeStorages(storages) as Record<
|
|
180
|
-
string,
|
|
181
|
-
MMKV
|
|
182
|
-
>;
|
|
169
|
+
const normalizedStorages = normalizeStorages(storages) as Record<string, MMKV>;
|
|
183
170
|
|
|
184
171
|
const storageNodes: StorageNode[] = Object.entries(normalizedStorages).map(
|
|
185
172
|
([storageId, storage]) => {
|
|
@@ -199,6 +186,7 @@ export const createMMKVStorageAdapter = ({
|
|
|
199
186
|
get: (key) => getEntry(mmkv, key),
|
|
200
187
|
set: (entry) => setEntry(mmkv, entry),
|
|
201
188
|
delete: (key) => mmkv.delete(key),
|
|
189
|
+
clear: mmkv.clear,
|
|
202
190
|
subscribe: (callback) => mmkv.addOnValueChangedListener(callback),
|
|
203
191
|
},
|
|
204
192
|
};
|
|
@@ -65,6 +65,10 @@ export const createExpoSecureStorageAdapter = ({
|
|
|
65
65
|
await storage.setItemAsync(entry.key, entry.value);
|
|
66
66
|
},
|
|
67
67
|
delete: (key) => storage.deleteItemAsync(key),
|
|
68
|
+
clear: async () => {
|
|
69
|
+
const storageKeys = await resolveKeys(keys);
|
|
70
|
+
await Promise.all(storageKeys.map((key) => storage.deleteItemAsync(key)));
|
|
71
|
+
},
|
|
68
72
|
},
|
|
69
73
|
};
|
|
70
74
|
|