@rozenite/storage-plugin 1.8.1 → 1.10.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 (42) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +23 -0
  3. package/dist/devtools/assets/panel-Bm-SWF7d.js +33 -0
  4. package/dist/devtools/assets/panel-DIqI4WSp.css +1 -0
  5. package/dist/devtools/panel.html +2 -2
  6. package/dist/react-native/chunks/index.require.cjs +1 -1
  7. package/dist/react-native/chunks/index.require.js +43 -57
  8. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  9. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +253 -191
  10. package/dist/react-native/index.d.ts +29 -6
  11. package/dist/rozenite.json +1 -1
  12. package/dist/sdk/index.d.ts +1 -0
  13. package/package.json +9 -6
  14. package/src/react-native/__tests__/import.test.ts +182 -0
  15. package/src/react-native/adapters/__tests__/mmkv.test.ts +436 -0
  16. package/src/react-native/adapters/mmkv.ts +65 -39
  17. package/src/react-native/import.ts +67 -0
  18. package/src/react-native/storage-view.ts +16 -8
  19. package/src/react-native/useRozeniteStoragePlugin.ts +61 -36
  20. package/src/shared/__tests__/snapshot.test.ts +361 -0
  21. package/src/shared/messaging.ts +25 -1
  22. package/src/shared/snapshot.ts +276 -0
  23. package/src/shared/types.ts +1 -0
  24. package/src/ui/__tests__/binary-value-editor-state.test.ts +199 -0
  25. package/src/ui/__tests__/binary.test.ts +251 -0
  26. package/src/ui/__tests__/type-conversion.test.ts +123 -0
  27. package/src/ui/add-entry-dialog.tsx +83 -173
  28. package/src/ui/binary-value-editor-state.ts +125 -0
  29. package/src/ui/binary-value-editor.tsx +168 -0
  30. package/src/ui/binary.ts +123 -0
  31. package/src/ui/edit-entry-dialog.tsx +64 -161
  32. package/src/ui/editable-table.tsx +12 -10
  33. package/src/ui/editor-switcher.tsx +62 -0
  34. package/src/ui/entry-detail-dialog.tsx +14 -6
  35. package/src/ui/import-dialog.tsx +261 -0
  36. package/src/ui/panel.tsx +257 -57
  37. package/src/ui/type-conversion.ts +105 -0
  38. package/src/ui/typed-value-editor.tsx +96 -0
  39. package/src/ui/utils.ts +30 -0
  40. package/dist/devtools/assets/panel-DMhXYHH4.css +0 -1
  41. package/dist/devtools/assets/panel-eGOuOVos.js +0 -22
  42. package/src/react-native/is-garbled.ts +0 -17
@@ -0,0 +1,436 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createMMKVStorageAdapter } from '../mmkv';
3
+ import { createStorageViews } from '../../storage-view';
4
+
5
+ type StoredValue = string | number | boolean | ArrayBuffer;
6
+
7
+ const createFakeMMKVV3 = (id: string) => {
8
+ const values = new Map<string, StoredValue>();
9
+
10
+ return {
11
+ id,
12
+ set: vi.fn((key: string, value: StoredValue) => {
13
+ values.set(key, value);
14
+ }),
15
+ getBoolean: vi.fn((key: string) => {
16
+ const value = values.get(key);
17
+ return typeof value === 'boolean' ? value : undefined;
18
+ }),
19
+ getString: vi.fn((key: string) => {
20
+ const value = values.get(key);
21
+ return typeof value === 'string' ? value : undefined;
22
+ }),
23
+ getNumber: vi.fn((key: string) => {
24
+ const value = values.get(key);
25
+ return typeof value === 'number' ? value : undefined;
26
+ }),
27
+ getBuffer: vi.fn((key: string) => {
28
+ const value = values.get(key);
29
+ return value instanceof ArrayBuffer ? value : undefined;
30
+ }),
31
+ delete: vi.fn((key: string) => {
32
+ values.delete(key);
33
+ }),
34
+ getAllKeys: vi.fn(() => [...values.keys()]),
35
+ addOnValueChangedListener: vi.fn(() => ({ remove: vi.fn() })),
36
+ };
37
+ };
38
+
39
+ const createFakeMMKVV4 = () => {
40
+ const values = new Map<string, StoredValue>();
41
+
42
+ return {
43
+ set: vi.fn((key: string, value: StoredValue) => {
44
+ values.set(key, value);
45
+ }),
46
+ getBoolean: vi.fn((key: string) => {
47
+ const value = values.get(key);
48
+ return typeof value === 'boolean' ? value : undefined;
49
+ }),
50
+ getString: vi.fn((key: string) => {
51
+ const value = values.get(key);
52
+ return typeof value === 'string' ? value : undefined;
53
+ }),
54
+ getNumber: vi.fn((key: string) => {
55
+ const value = values.get(key);
56
+ return typeof value === 'number' ? value : undefined;
57
+ }),
58
+ getBuffer: vi.fn((key: string) => {
59
+ const value = values.get(key);
60
+ return value instanceof ArrayBuffer ? value : undefined;
61
+ }),
62
+ remove: vi.fn((key: string) => {
63
+ values.delete(key);
64
+ }),
65
+ getAllKeys: vi.fn(() => [...values.keys()]),
66
+ addOnValueChangedListener: vi.fn(() => ({ remove: vi.fn() })),
67
+ };
68
+ };
69
+
70
+ describe('createMMKVStorageAdapter', () => {
71
+ it('supports MMKV v3 storages passed as an array', async () => {
72
+ const storage = createFakeMMKVV3('user-storage');
73
+ const adapter = createMMKVStorageAdapter({ storages: [storage as any] });
74
+ const [view] = createStorageViews([adapter]);
75
+
76
+ await view.set({ key: 'theme', type: 'string', value: 'dark' });
77
+
78
+ expect(view.target).toEqual({
79
+ adapterId: 'mmkv',
80
+ storageId: 'user-storage',
81
+ });
82
+ await expect(view.get('theme')).resolves.toEqual({
83
+ key: 'theme',
84
+ type: 'string',
85
+ value: 'dark',
86
+ });
87
+ });
88
+
89
+ it('supports MMKV v4 storages passed as a record', async () => {
90
+ const storage = createFakeMMKVV4();
91
+ const adapter = createMMKVStorageAdapter({
92
+ storages: { 'user-storage': storage as any },
93
+ });
94
+ const [view] = createStorageViews([adapter]);
95
+
96
+ await view.set({ key: 'logged-in', type: 'boolean', value: true });
97
+ await expect(view.get('logged-in')).resolves.toEqual({
98
+ key: 'logged-in',
99
+ type: 'boolean',
100
+ value: true,
101
+ });
102
+
103
+ await view.delete('logged-in');
104
+
105
+ expect(storage.remove).toHaveBeenCalledWith('logged-in');
106
+ await expect(view.get('logged-in')).resolves.toBeUndefined();
107
+ });
108
+
109
+ it('rejects MMKV v4 storages passed as an array', () => {
110
+ const storage = createFakeMMKVV4();
111
+
112
+ expect(() =>
113
+ createMMKVStorageAdapter({ storages: [storage as any] }),
114
+ ).toThrow(/MMKV arrays are not supported for v4 storages/);
115
+ });
116
+
117
+ it('applies global blacklist patterns against storageId:key', async () => {
118
+ const userStorage = createFakeMMKVV3('user-storage');
119
+ const cacheStorage = createFakeMMKVV3('cache-storage');
120
+
121
+ userStorage.set('visible', 'ok');
122
+ userStorage.set('secret', 'hide');
123
+ cacheStorage.set('temp-file', 'hide');
124
+ cacheStorage.set('persisted', 'ok');
125
+
126
+ const adapter = createMMKVStorageAdapter({
127
+ storages: [userStorage as any, cacheStorage as any],
128
+ blacklist: /user-storage:secret|cache-storage:temp.*/,
129
+ });
130
+
131
+ const views = createStorageViews([adapter]);
132
+ const userView = views.find(
133
+ (view) => view.target.storageId === 'user-storage',
134
+ );
135
+ const cacheView = views.find(
136
+ (view) => view.target.storageId === 'cache-storage',
137
+ );
138
+
139
+ expect(userView).toBeDefined();
140
+ expect(cacheView).toBeDefined();
141
+
142
+ await expect(userView?.getAllKeys()).resolves.toEqual(['visible']);
143
+ await expect(cacheView?.getAllKeys()).resolves.toEqual(['persisted']);
144
+ });
145
+ });
146
+
147
+ // MMKV in production stores raw bytes per key without a type tag, so
148
+ // getString returns data for any stored value whose bytes decode as
149
+ // valid UTF-8 — including values the app wrote via setBuffer. This
150
+ // fake mirrors that behaviour: getString attempts a strict UTF-8 decode
151
+ // and returns undefined when the bytes are not valid UTF-8, exposing
152
+ // the read priority chain that the adapter relies on.
153
+ //
154
+ // `lenientGetString`, when true, returns `""` instead of `undefined`
155
+ // on a failed UTF-8 decode — matching real MMKV builds on certain
156
+ // platforms (notably iOS NSString conversion). The adapter has to
157
+ // handle that case by falling through to getBuffer.
158
+ //
159
+ // `lenientGetNumber`, when true, reinterprets 8-byte buffer payloads
160
+ // as IEEE 754 doubles (instead of returning undefined). Some MMKV
161
+ // builds expose this behavior — without the buffer-before-number
162
+ // priority, the adapter would misreport such buffers as numbers.
163
+ const createAmbiguousFakeMMKVV4 = ({
164
+ lenientGetString = false,
165
+ lenientGetNumber = false,
166
+ }: { lenientGetString?: boolean; lenientGetNumber?: boolean } = {}) => {
167
+ type Stored =
168
+ | { kind: 'string'; value: string }
169
+ | { kind: 'number'; value: number }
170
+ | { kind: 'boolean'; value: boolean }
171
+ | { kind: 'buffer'; bytes: Uint8Array };
172
+
173
+ const values = new Map<string, Stored>();
174
+
175
+ return {
176
+ set: vi.fn(
177
+ (key: string, value: string | number | boolean | ArrayBuffer) => {
178
+ if (typeof value === 'string') {
179
+ values.set(key, { kind: 'string', value });
180
+ } else if (typeof value === 'number') {
181
+ values.set(key, { kind: 'number', value });
182
+ } else if (typeof value === 'boolean') {
183
+ values.set(key, { kind: 'boolean', value });
184
+ } else {
185
+ values.set(key, {
186
+ kind: 'buffer',
187
+ bytes: new Uint8Array(value),
188
+ });
189
+ }
190
+ },
191
+ ),
192
+ getString: vi.fn((key: string) => {
193
+ const stored = values.get(key);
194
+ if (!stored) return undefined;
195
+ if (stored.kind === 'string') return stored.value;
196
+ if (stored.kind === 'buffer') {
197
+ try {
198
+ return new TextDecoder('utf-8', { fatal: true }).decode(stored.bytes);
199
+ } catch {
200
+ return lenientGetString ? '' : undefined;
201
+ }
202
+ }
203
+ return undefined;
204
+ }),
205
+ getNumber: vi.fn((key: string) => {
206
+ const stored = values.get(key);
207
+ if (stored?.kind === 'number') return stored.value;
208
+ if (
209
+ lenientGetNumber &&
210
+ stored?.kind === 'buffer' &&
211
+ stored.bytes.byteLength === 8
212
+ ) {
213
+ return new DataView(stored.bytes.buffer).getFloat64(0, true);
214
+ }
215
+ return undefined;
216
+ }),
217
+ getBoolean: vi.fn((key: string) => {
218
+ const stored = values.get(key);
219
+ return stored?.kind === 'boolean' ? stored.value : undefined;
220
+ }),
221
+ getBuffer: vi.fn((key: string) => {
222
+ const stored = values.get(key);
223
+ return stored?.kind === 'buffer' ? stored.bytes.buffer : undefined;
224
+ }),
225
+ remove: vi.fn((key: string) => {
226
+ values.delete(key);
227
+ }),
228
+ getAllKeys: vi.fn(() => [...values.keys()]),
229
+ addOnValueChangedListener: vi.fn(() => ({ remove: vi.fn() })),
230
+ };
231
+ };
232
+
233
+ describe('createMMKVStorageAdapter read priority', () => {
234
+ it('classifies a buffer of valid UTF-8 bytes as a string (documented limitation)', async () => {
235
+ const storage = createAmbiguousFakeMMKVV4();
236
+ const adapter = createMMKVStorageAdapter({
237
+ storages: { 'user-storage': storage as any },
238
+ });
239
+ const [view] = createStorageViews([adapter]);
240
+
241
+ // 0x68 0x65 0x6c 0x6c 0x6f = "hello" in ASCII. MMKV's getString
242
+ // decodes the bytes as UTF-8 and we trust that report — users who
243
+ // know the value is binary disambiguate at edit time via the Hex
244
+ // editor.
245
+ await view.set({
246
+ key: 'token',
247
+ type: 'buffer',
248
+ value: [0x68, 0x65, 0x6c, 0x6c, 0x6f],
249
+ });
250
+
251
+ await expect(view.get('token')).resolves.toEqual({
252
+ key: 'token',
253
+ type: 'string',
254
+ value: 'hello',
255
+ });
256
+ });
257
+
258
+ it('classifies a buffer of non-UTF-8 bytes as a buffer', async () => {
259
+ const storage = createAmbiguousFakeMMKVV4();
260
+ const adapter = createMMKVStorageAdapter({
261
+ storages: { 'user-storage': storage as any },
262
+ });
263
+ const [view] = createStorageViews([adapter]);
264
+
265
+ // 0xFF 0xFE is invalid UTF-8 — getString returns undefined, the
266
+ // chain falls through to getBuffer.
267
+ await view.set({
268
+ key: 'bin',
269
+ type: 'buffer',
270
+ value: [0xff, 0xfe, 0xfd],
271
+ });
272
+
273
+ await expect(view.get('bin')).resolves.toEqual({
274
+ key: 'bin',
275
+ type: 'buffer',
276
+ value: [0xff, 0xfe, 0xfd],
277
+ });
278
+ });
279
+
280
+ it('overwriting a buffer with a string returns the string on the next read', async () => {
281
+ const storage = createAmbiguousFakeMMKVV4();
282
+ const adapter = createMMKVStorageAdapter({
283
+ storages: { 'user-storage': storage as any },
284
+ });
285
+ const [view] = createStorageViews([adapter]);
286
+
287
+ await view.set({
288
+ key: 'flip',
289
+ type: 'buffer',
290
+ value: [0xff, 0xfe],
291
+ });
292
+ await view.set({
293
+ key: 'flip',
294
+ type: 'string',
295
+ value: 'world',
296
+ });
297
+
298
+ await expect(view.get('flip')).resolves.toEqual({
299
+ key: 'flip',
300
+ type: 'string',
301
+ value: 'world',
302
+ });
303
+ });
304
+
305
+ it('returns numbers via getNumber when no string is present at the key', async () => {
306
+ const storage = createAmbiguousFakeMMKVV4();
307
+ const adapter = createMMKVStorageAdapter({
308
+ storages: { 'user-storage': storage as any },
309
+ });
310
+ const [view] = createStorageViews([adapter]);
311
+
312
+ await view.set({ key: 'count', type: 'number', value: 42 });
313
+
314
+ await expect(view.get('count')).resolves.toEqual({
315
+ key: 'count',
316
+ type: 'number',
317
+ value: 42,
318
+ });
319
+ });
320
+
321
+ it('returns booleans via getBoolean when no string or number is present at the key', async () => {
322
+ const storage = createAmbiguousFakeMMKVV4();
323
+ const adapter = createMMKVStorageAdapter({
324
+ storages: { 'user-storage': storage as any },
325
+ });
326
+ const [view] = createStorageViews([adapter]);
327
+
328
+ await view.set({ key: 'flag', type: 'boolean', value: true });
329
+
330
+ await expect(view.get('flag')).resolves.toEqual({
331
+ key: 'flag',
332
+ type: 'boolean',
333
+ value: true,
334
+ });
335
+ });
336
+
337
+ it('deleting a key removes it from the storage view', async () => {
338
+ const storage = createAmbiguousFakeMMKVV4();
339
+ const adapter = createMMKVStorageAdapter({
340
+ storages: { 'user-storage': storage as any },
341
+ });
342
+ const [view] = createStorageViews([adapter]);
343
+
344
+ await view.set({ key: 'temp', type: 'string', value: 'hi' });
345
+ await view.delete('temp');
346
+
347
+ await expect(view.get('temp')).resolves.toBeUndefined();
348
+ });
349
+ });
350
+
351
+ describe('createMMKVStorageAdapter read priority — lenient getString platforms', () => {
352
+ // Real MMKV on some platforms returns `""` for invalid UTF-8 bytes
353
+ // rather than `undefined`. Without the empty-string carve-out in the
354
+ // priority chain, the adapter would misreport such buffers as empty
355
+ // strings and the bytes would appear lost from the panel's POV.
356
+ it('falls through to getBuffer when getString returns an empty string', async () => {
357
+ const storage = createAmbiguousFakeMMKVV4({ lenientGetString: true });
358
+ const adapter = createMMKVStorageAdapter({
359
+ storages: { 'user-storage': storage as any },
360
+ });
361
+ const [view] = createStorageViews([adapter]);
362
+
363
+ // PNG magic bytes — first byte (0x89) is an invalid UTF-8 lead.
364
+ await view.set({
365
+ key: 'png-magic',
366
+ type: 'buffer',
367
+ value: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
368
+ });
369
+
370
+ await expect(view.get('png-magic')).resolves.toEqual({
371
+ key: 'png-magic',
372
+ type: 'buffer',
373
+ value: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
374
+ });
375
+ });
376
+
377
+ it('preserves an intentional empty string when no buffer is present', async () => {
378
+ const storage = createAmbiguousFakeMMKVV4({ lenientGetString: true });
379
+ const adapter = createMMKVStorageAdapter({
380
+ storages: { 'user-storage': storage as any },
381
+ });
382
+ const [view] = createStorageViews([adapter]);
383
+
384
+ await view.set({ key: 'blank', type: 'string', value: '' });
385
+
386
+ await expect(view.get('blank')).resolves.toEqual({
387
+ key: 'blank',
388
+ type: 'string',
389
+ value: '',
390
+ });
391
+ });
392
+
393
+ it('keeps an 8-byte buffer as buffer when getNumber lenient-decodes it as a double', async () => {
394
+ // Some MMKV builds reinterpret 8-byte setBuffer payloads as IEEE
395
+ // 754 doubles via getNumber. The priority chain has to consult
396
+ // getBuffer before getNumber so the bytes that are actually on
397
+ // disk win — otherwise PNG-magic-shaped buffers would be reported
398
+ // as some arbitrary number.
399
+ const storage = createAmbiguousFakeMMKVV4({
400
+ lenientGetString: true,
401
+ lenientGetNumber: true,
402
+ });
403
+ const adapter = createMMKVStorageAdapter({
404
+ storages: { 'user-storage': storage as any },
405
+ });
406
+ const [view] = createStorageViews([adapter]);
407
+
408
+ const pngMagic = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
409
+ await view.set({ key: 'png-magic', type: 'buffer', value: pngMagic });
410
+
411
+ await expect(view.get('png-magic')).resolves.toEqual({
412
+ key: 'png-magic',
413
+ type: 'buffer',
414
+ value: pngMagic,
415
+ });
416
+ });
417
+
418
+ it('reports an empty buffer as buffer (matching its setBuffer write)', async () => {
419
+ const storage = createAmbiguousFakeMMKVV4({ lenientGetString: true });
420
+ const adapter = createMMKVStorageAdapter({
421
+ storages: { 'user-storage': storage as any },
422
+ });
423
+ const [view] = createStorageViews([adapter]);
424
+
425
+ // 0xFF is invalid UTF-8 — getString returns "" under lenient decode.
426
+ // The bytes are still present via getBuffer; we want the buffer
427
+ // reading to win since byteLength > 0.
428
+ await view.set({ key: 'invalid', type: 'buffer', value: [0xff] });
429
+
430
+ await expect(view.get('invalid')).resolves.toEqual({
431
+ key: 'invalid',
432
+ type: 'buffer',
433
+ value: [0xff],
434
+ });
435
+ });
436
+ });
@@ -1,8 +1,11 @@
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 { StorageAdapter, StorageEntry, StorageNode } from '../../shared/types';
3
+ import type {
4
+ StorageAdapter,
5
+ StorageEntry,
6
+ StorageNode,
7
+ } from '../../shared/types';
4
8
  import { DEFAULT_SUPPORTED_TYPES } from '../../shared/types';
5
- import { looksLikeGarbled } from '../is-garbled';
6
9
 
7
10
  type MMKV = MMKVV3 | MMKVV4;
8
11
 
@@ -36,12 +39,12 @@ const normalizeStorages = (storages: MMKV[] | Record<string, MMKV>) => {
36
39
 
37
40
  if (isAnyStorageV4) {
38
41
  throw new Error(
39
- '[Rozenite] Storage Plugin: MMKV arrays are not supported for v4 storages. Pass a record of storage IDs and MMKV instances.'
42
+ '[Rozenite] Storage Plugin: MMKV arrays are not supported for v4 storages. Pass a record of storage IDs and MMKV instances.',
40
43
  );
41
44
  }
42
45
 
43
46
  return Object.fromEntries(
44
- (storages as MMKVV3[]).map((storage) => [storage['id'], storage])
47
+ (storages as MMKVV3[]).map((storage) => [storage['id'], storage]),
45
48
  );
46
49
  }
47
50
 
@@ -71,54 +74,60 @@ const getMMKVAdapter = (mmkv: MMKV): MMKVAdapter => {
71
74
  getBuffer: (key) => mmkv.getBuffer(key) as ArrayBuffer | undefined,
72
75
  delete: (key) => mmkv.delete(key),
73
76
  getAllKeys: () => mmkv.getAllKeys(),
74
- addOnValueChangedListener: (callback) => mmkv.addOnValueChangedListener(callback),
77
+ addOnValueChangedListener: (callback) =>
78
+ mmkv.addOnValueChangedListener(callback),
75
79
  };
76
80
  };
77
81
 
78
- const getEntry = (adapter: MMKVAdapter, key: string): StorageEntry | undefined => {
82
+ // MMKV's typed getters can disagree on the same key on some platforms
83
+ // `getString` lenient-decodes invalid-UTF-8 buffer bytes to `""`, and
84
+ // `getNumber` reinterprets 8-byte buffers as IEEE 754 doubles without
85
+ // honoring the original `setBuffer` write. `getBuffer` is more reliably
86
+ // strict (only returns for keys actually written via `setBuffer`), so
87
+ // we consult it ahead of the numeric getters to break ties in favour of
88
+ // the bytes that are actually on disk.
89
+ //
90
+ // 1. Non-empty string — the common case. Buffer bytes that happen to
91
+ // be valid UTF-8 surface here too (documented trade-off); users
92
+ // disambiguate at edit time via the Hex editor.
93
+ // 2. Non-empty buffer — catches `setBuffer` payloads regardless of
94
+ // byte count and regardless of whether the other typed getters
95
+ // also return spurious values.
96
+ // 3. Number, then boolean — for keys actually written via `setNumber`
97
+ // / `setBoolean`. `getBuffer` will have returned `undefined` for
98
+ // those keys, so the chain falls through.
99
+ // 4. Empty string — intentional `setString(key, "")` lands here once
100
+ // 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 => {
79
105
  const stringValue = adapter.getString(key);
80
-
81
106
  if (stringValue !== undefined && stringValue.length > 0) {
82
- if (looksLikeGarbled(stringValue)) {
83
- return {
84
- key,
85
- type: 'buffer',
86
- value: Array.from(new TextEncoder().encode(stringValue)),
87
- };
88
- }
107
+ return { key, type: 'string', value: stringValue };
108
+ }
89
109
 
110
+ const bufferValue = adapter.getBuffer(key);
111
+ if (bufferValue !== undefined && bufferValue.byteLength > 0) {
90
112
  return {
91
113
  key,
92
- type: 'string',
93
- value: stringValue,
114
+ type: 'buffer',
115
+ value: Array.from(new Uint8Array(bufferValue)),
94
116
  };
95
117
  }
96
118
 
97
119
  const numberValue = adapter.getNumber(key);
98
120
  if (numberValue !== undefined) {
99
- return {
100
- key,
101
- type: 'number',
102
- value: numberValue,
103
- };
121
+ return { key, type: 'number', value: numberValue };
104
122
  }
105
123
 
106
124
  const booleanValue = adapter.getBoolean(key);
107
125
  if (booleanValue !== undefined) {
108
- return {
109
- key,
110
- type: 'boolean',
111
- value: booleanValue,
112
- };
126
+ return { key, type: 'boolean', value: booleanValue };
113
127
  }
114
128
 
115
- const bufferValue = adapter.getBuffer(key);
116
- if (bufferValue !== undefined) {
117
- return {
118
- key,
119
- type: 'buffer',
120
- value: Array.from(new Uint8Array(bufferValue)),
121
- };
129
+ if (stringValue !== undefined) {
130
+ return { key, type: 'string', value: stringValue };
122
131
  }
123
132
 
124
133
  return undefined;
@@ -127,15 +136,14 @@ const getEntry = (adapter: MMKVAdapter, key: string): StorageEntry | undefined =
127
136
  const setEntry = (adapter: MMKVAdapter, entry: StorageEntry) => {
128
137
  if (entry.type === 'buffer') {
129
138
  adapter.set(entry.key, new Uint8Array(entry.value).buffer);
130
- return;
139
+ } else {
140
+ adapter.set(entry.key, entry.value);
131
141
  }
132
-
133
- adapter.set(entry.key, entry.value);
134
142
  };
135
143
 
136
144
  const getStorageBlacklist = (
137
145
  config: MMKVBlacklistConfig | undefined,
138
- storageId: string
146
+ storageId: string,
139
147
  ) => {
140
148
  if (!config) {
141
149
  return undefined;
@@ -148,13 +156,30 @@ const getStorageBlacklist = (
148
156
  return config[storageId];
149
157
  };
150
158
 
159
+ const createStorageBlacklistMatcher = (
160
+ config: MMKVBlacklistConfig | undefined,
161
+ storageId: string,
162
+ ) => {
163
+ if (!(config instanceof RegExp)) {
164
+ return undefined;
165
+ }
166
+
167
+ return (key: string) => {
168
+ config.lastIndex = 0;
169
+ return config.test(`${storageId}:${key}`);
170
+ };
171
+ };
172
+
151
173
  export const createMMKVStorageAdapter = ({
152
174
  adapterId = 'mmkv',
153
175
  adapterName = 'MMKV',
154
176
  storages,
155
177
  blacklist,
156
178
  }: CreateMMKVStorageAdapterOptions): StorageAdapter => {
157
- const normalizedStorages = normalizeStorages(storages) as Record<string, MMKV>;
179
+ const normalizedStorages = normalizeStorages(storages) as Record<
180
+ string,
181
+ MMKV
182
+ >;
158
183
 
159
184
  const storageNodes: StorageNode[] = Object.entries(normalizedStorages).map(
160
185
  ([storageId, storage]) => {
@@ -164,6 +189,7 @@ export const createMMKVStorageAdapter = ({
164
189
  id: storageId,
165
190
  name: storageId,
166
191
  blacklist: getStorageBlacklist(blacklist, storageId),
192
+ shouldFilterKey: createStorageBlacklistMatcher(blacklist, storageId),
167
193
  capabilities: {
168
194
  supportedTypes: DEFAULT_SUPPORTED_TYPES,
169
195
  },
@@ -176,7 +202,7 @@ export const createMMKVStorageAdapter = ({
176
202
  subscribe: (callback) => mmkv.addOnValueChangedListener(callback),
177
203
  },
178
204
  };
179
- }
205
+ },
180
206
  );
181
207
 
182
208
  return {
@@ -0,0 +1,67 @@
1
+ import type {
2
+ StorageImportEntriesEvent,
3
+ StorageImportResultEvent,
4
+ StorageSetEntryEvent,
5
+ } from '../shared/messaging';
6
+ import type { StorageView } from './storage-view';
7
+
8
+ export type ImportEmittedEvent =
9
+ | StorageSetEntryEvent
10
+ | StorageImportResultEvent;
11
+
12
+ export const handleImportEntries = async (
13
+ views: StorageView[],
14
+ event: StorageImportEntriesEvent,
15
+ emit: (out: ImportEmittedEvent) => void,
16
+ ): Promise<void> => {
17
+ const view = views.find(
18
+ (candidate) =>
19
+ candidate.target.adapterId === event.target.adapterId &&
20
+ candidate.target.storageId === event.target.storageId,
21
+ );
22
+
23
+ if (!view) {
24
+ emit({
25
+ type: 'import-result',
26
+ target: event.target,
27
+ ok: false,
28
+ written: 0,
29
+ total: event.entries.length,
30
+ error: 'Target storage not found',
31
+ });
32
+ return;
33
+ }
34
+
35
+ for (let i = 0; i < event.entries.length; i++) {
36
+ const entry = event.entries[i];
37
+
38
+ try {
39
+ await view.set(entry);
40
+ } catch (error) {
41
+ emit({
42
+ type: 'import-result',
43
+ target: event.target,
44
+ ok: false,
45
+ written: i,
46
+ total: event.entries.length,
47
+ failedKey: entry.key,
48
+ error: error instanceof Error ? error.message : String(error),
49
+ });
50
+ return;
51
+ }
52
+
53
+ emit({
54
+ type: 'set-entry',
55
+ target: event.target,
56
+ entry,
57
+ });
58
+ }
59
+
60
+ emit({
61
+ type: 'import-result',
62
+ target: event.target,
63
+ ok: true,
64
+ written: event.entries.length,
65
+ total: event.entries.length,
66
+ });
67
+ };