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