@rozenite/storage-plugin 1.12.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +6 -1
  3. package/dist/devtools/assets/panel-B6Wp0eie.css +1 -0
  4. package/dist/devtools/assets/panel-D7MFc4HG.js +32 -0
  5. package/dist/devtools/panel.html +2 -2
  6. package/dist/react-native/chunks/async-storage.require.cjs +1 -1
  7. package/dist/react-native/chunks/async-storage.require.js +8 -7
  8. package/dist/react-native/chunks/index.require.cjs +1 -1
  9. package/dist/react-native/chunks/index.require.js +70 -64
  10. package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
  11. package/dist/react-native/chunks/secure-storage.require.js +22 -14
  12. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  13. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -315
  14. package/dist/react-native/index.d.ts +154 -18
  15. package/dist/react-native/index.js +10 -2
  16. package/dist/rozenite.json +1 -1
  17. package/dist/sdk/index.cjs +1 -1
  18. package/dist/sdk/index.d.ts +13 -5
  19. package/dist/sdk/index.js +21 -12
  20. package/package.json +13 -15
  21. package/react-native.ts +29 -14
  22. package/rozenite.config.ts +78 -0
  23. package/src/react-native/__tests__/entry-preview-pagination.test.ts +262 -0
  24. package/src/react-native/__tests__/export-snapshot.test.ts +142 -0
  25. package/src/react-native/__tests__/full-entry-request.test.ts +155 -0
  26. package/src/react-native/__tests__/import.test.ts +172 -10
  27. package/src/react-native/__tests__/storage-discovery.test.ts +155 -0
  28. package/src/react-native/__tests__/storage-view.test.ts +172 -0
  29. package/src/react-native/__tests__/stress-verification.test.ts +153 -0
  30. package/src/react-native/__tests__/use-storage-agent-tools.test.ts +231 -0
  31. package/src/react-native/adapters/async-storage.ts +9 -7
  32. package/src/react-native/adapters/mmkv.ts +5 -0
  33. package/src/react-native/adapters/secure-storage.ts +9 -1
  34. package/src/react-native/entry-preview-pagination.ts +277 -0
  35. package/src/react-native/export-snapshot.ts +94 -0
  36. package/src/react-native/full-entry-request.ts +96 -0
  37. package/src/react-native/import.ts +190 -16
  38. package/src/react-native/storage-discovery.ts +96 -0
  39. package/src/react-native/storage-view.ts +45 -103
  40. package/src/react-native/useRozeniteStoragePlugin.ts +108 -72
  41. package/src/react-native/useStorageAgentTools.ts +207 -86
  42. package/src/shared/__tests__/entry-preview.test.ts +117 -0
  43. package/src/shared/__tests__/snapshot.test.ts +21 -0
  44. package/src/shared/agent-tools.ts +27 -11
  45. package/src/shared/entry-preview.ts +59 -0
  46. package/src/shared/messaging.ts +136 -21
  47. package/src/shared/snapshot.ts +8 -1
  48. package/src/shared/types.ts +20 -1
  49. package/src/ui/__tests__/binary-value-editor-state.test.ts +13 -0
  50. package/src/ui/__tests__/binary.test.ts +18 -0
  51. package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
  52. package/src/ui/__tests__/large-value-viewer.test.tsx +63 -0
  53. package/src/ui/__tests__/panel.test.tsx +409 -0
  54. package/src/ui/__tests__/storage-groups.test.ts +91 -0
  55. package/src/ui/__tests__/use-storage-entries.test.tsx +353 -0
  56. package/src/ui/add-entry-dialog.tsx +65 -105
  57. package/src/ui/binary-value-editor-state.ts +10 -1
  58. package/src/ui/binary-value-editor.tsx +57 -33
  59. package/src/ui/binary.ts +56 -4
  60. package/src/ui/edit-entry-dialog.tsx +71 -99
  61. package/src/ui/editor-switcher.tsx +7 -5
  62. package/src/ui/entry-detail-dialog.tsx +69 -179
  63. package/src/ui/format-value.tsx +32 -0
  64. package/src/ui/globals.css +1 -123
  65. package/src/ui/import-dialog.tsx +66 -91
  66. package/src/ui/large-value-viewer-state.ts +32 -0
  67. package/src/ui/large-value-viewer.tsx +71 -0
  68. package/src/ui/panel.tsx +724 -496
  69. package/src/ui/query-client.tsx +34 -0
  70. package/src/ui/storage-groups.ts +66 -0
  71. package/src/ui/typed-value-editor.tsx +14 -15
  72. package/src/ui/use-storage-entries.ts +207 -0
  73. package/tsconfig.json +5 -1
  74. package/dist/devtools/assets/panel-Bm-SWF7d.js +0 -33
  75. package/dist/devtools/assets/panel-DIqI4WSp.css +0 -1
  76. package/postcss.config.js +0 -6
  77. package/src/ui/confirm-dialog.tsx +0 -100
  78. package/src/ui/editable-table.tsx +0 -300
  79. 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 = CreateAsyncStorageAdapterOptions;
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('Expo SecureStore adapter supports only string values.');
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