@rozenite/storage-plugin 1.8.0 → 1.9.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 +23 -0
- package/dist/devtools/assets/panel-DVxtKLI9.css +1 -0
- package/dist/devtools/assets/panel-DnjbFMGo.js +22 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/index.require.cjs +1 -1
- package/dist/react-native/chunks/index.require.js +9 -5
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +253 -191
- package/dist/react-native/index.d.ts +29 -6
- package/dist/rozenite.json +1 -1
- package/dist/sdk/index.d.ts +1 -0
- package/package.json +6 -6
- package/src/react-native/__tests__/import.test.ts +182 -0
- package/src/react-native/adapters/__tests__/mmkv.test.ts +141 -0
- package/src/react-native/adapters/mmkv.ts +15 -0
- package/src/react-native/import.ts +67 -0
- package/src/react-native/storage-view.ts +16 -8
- package/src/react-native/useRozeniteStoragePlugin.ts +61 -36
- package/src/shared/__tests__/snapshot.test.ts +361 -0
- package/src/shared/messaging.ts +25 -1
- package/src/shared/snapshot.ts +276 -0
- package/src/shared/types.ts +1 -0
- package/src/ui/import-dialog.tsx +261 -0
- package/src/ui/panel.tsx +257 -57
- package/src/ui/utils.ts +30 -0
- package/dist/devtools/assets/panel-DMhXYHH4.css +0 -1
- package/dist/devtools/assets/panel-eGOuOVos.js +0 -22
|
@@ -24,13 +24,13 @@ export declare type AsyncStorageLike = {
|
|
|
24
24
|
removeItem: (key: string) => Promise<void>;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
export declare let createAsyncStorageAdapter: createAsyncStorageAdapter_2;
|
|
27
|
+
export declare let createAsyncStorageAdapter: typeof createAsyncStorageAdapter_2;
|
|
28
28
|
|
|
29
29
|
declare const createAsyncStorageAdapter_2: (options: CreateAsyncStorageAdapterOptions) => StorageAdapter;
|
|
30
30
|
|
|
31
31
|
export declare type CreateAsyncStorageAdapterOptions = SingleStorageOptions | MultiStorageOptions;
|
|
32
32
|
|
|
33
|
-
export declare let createExpoAsyncStorageAdapter: createExpoAsyncStorageAdapter_2;
|
|
33
|
+
export declare let createExpoAsyncStorageAdapter: typeof createExpoAsyncStorageAdapter_2;
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* @deprecated Use createAsyncStorageAdapter instead.
|
|
@@ -39,7 +39,7 @@ declare const createExpoAsyncStorageAdapter_2: (options: CreateAsyncStorageAdapt
|
|
|
39
39
|
|
|
40
40
|
export declare type CreateExpoAsyncStorageAdapterOptions = CreateAsyncStorageAdapterOptions;
|
|
41
41
|
|
|
42
|
-
export declare let createExpoSecureStorageAdapter: createExpoSecureStorageAdapter_2;
|
|
42
|
+
export declare let createExpoSecureStorageAdapter: typeof createExpoSecureStorageAdapter_2;
|
|
43
43
|
|
|
44
44
|
declare const createExpoSecureStorageAdapter_2: ({ storage, keys, adapterId, adapterName, storageId, storageName, blacklist, }: CreateExpoSecureStorageAdapterOptions) => StorageAdapter;
|
|
45
45
|
|
|
@@ -53,7 +53,7 @@ export declare type CreateExpoSecureStorageAdapterOptions = {
|
|
|
53
53
|
blacklist?: RegExp;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
export declare let createMMKVStorageAdapter: createMMKVStorageAdapter_2;
|
|
56
|
+
export declare let createMMKVStorageAdapter: typeof createMMKVStorageAdapter_2;
|
|
57
57
|
|
|
58
58
|
declare const createMMKVStorageAdapter_2: ({ adapterId, adapterName, storages, blacklist, }: CreateMMKVStorageAdapterOptions) => StorageAdapter;
|
|
59
59
|
|
|
@@ -86,6 +86,11 @@ export declare type SecureStorageLike = {
|
|
|
86
86
|
deleteItemAsync: (key: string) => Promise<void>;
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
+
declare type SerializedBlacklist = {
|
|
90
|
+
source: string;
|
|
91
|
+
flags: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
89
94
|
declare type SingleStorageOptions = {
|
|
90
95
|
storage: AsyncStorageLike;
|
|
91
96
|
adapterId?: string;
|
|
@@ -133,7 +138,7 @@ export declare type StorageEntryType = StorageEntry['type'];
|
|
|
133
138
|
|
|
134
139
|
export declare type StorageEntryValue = StorageEntry['value'];
|
|
135
140
|
|
|
136
|
-
declare type StorageEvent_2 = StorageSnapshotEvent | StorageSetEntryEvent | StorageDeleteEntryEvent | StorageGetSnapshotEvent;
|
|
141
|
+
declare type StorageEvent_2 = StorageSnapshotEvent | StorageSetEntryEvent | StorageDeleteEntryEvent | StorageGetSnapshotEvent | StorageImportEntriesEvent | StorageImportResultEvent;
|
|
137
142
|
|
|
138
143
|
declare type StorageEventMap = {
|
|
139
144
|
[K in StorageEvent_2['type']]: Extract<StorageEvent_2, {
|
|
@@ -146,12 +151,29 @@ declare type StorageGetSnapshotEvent = {
|
|
|
146
151
|
target: StorageTarget | 'all';
|
|
147
152
|
};
|
|
148
153
|
|
|
154
|
+
declare type StorageImportEntriesEvent = {
|
|
155
|
+
type: 'import-entries';
|
|
156
|
+
target: StorageTarget;
|
|
157
|
+
entries: StorageEntry[];
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
declare type StorageImportResultEvent = {
|
|
161
|
+
type: 'import-result';
|
|
162
|
+
target: StorageTarget;
|
|
163
|
+
ok: boolean;
|
|
164
|
+
written: number;
|
|
165
|
+
total: number;
|
|
166
|
+
failedKey?: string;
|
|
167
|
+
error?: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
149
170
|
export declare type StorageNode = {
|
|
150
171
|
id: string;
|
|
151
172
|
name: string;
|
|
152
173
|
storage: SyncStorage | AsyncStorage;
|
|
153
174
|
capabilities: StorageCapabilities;
|
|
154
175
|
blacklist?: RegExp;
|
|
176
|
+
shouldFilterKey?: (key: string) => boolean;
|
|
155
177
|
};
|
|
156
178
|
|
|
157
179
|
declare type StorageSetEntryEvent = {
|
|
@@ -166,6 +188,7 @@ declare type StorageSnapshotEvent = {
|
|
|
166
188
|
adapterName: string;
|
|
167
189
|
storageName: string;
|
|
168
190
|
capabilities: StorageCapabilities;
|
|
191
|
+
blacklist?: SerializedBlacklist;
|
|
169
192
|
entries: StorageEntry[];
|
|
170
193
|
};
|
|
171
194
|
|
|
@@ -187,7 +210,7 @@ export declare type SyncStorage = {
|
|
|
187
210
|
subscribe?: (callback: (key: string) => void) => StorageSubscription;
|
|
188
211
|
};
|
|
189
212
|
|
|
190
|
-
export declare let useRozeniteStoragePlugin: useRozeniteStoragePlugin_2;
|
|
213
|
+
export declare let useRozeniteStoragePlugin: typeof useRozeniteStoragePlugin_2;
|
|
191
214
|
|
|
192
215
|
declare const useRozeniteStoragePlugin_2: ({ storages, }: RozeniteStoragePluginOptions) => RozeniteDevToolsClient<StorageEventMap> | null;
|
|
193
216
|
|
package/dist/rozenite.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@rozenite/storage-plugin","version":"1.
|
|
1
|
+
{"name":"@rozenite/storage-plugin","version":"1.9.0","description":"Generic Storage Inspector for Rozenite.","panels":[{"name":"Storage","source":"/devtools/panel.html"}]}
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -99,6 +99,7 @@ export declare type StorageNode = {
|
|
|
99
99
|
storage: SyncStorage | AsyncStorage;
|
|
100
100
|
capabilities: StorageCapabilities;
|
|
101
101
|
blacklist?: RegExp;
|
|
102
|
+
shouldFilterKey?: (key: string) => boolean;
|
|
102
103
|
};
|
|
103
104
|
|
|
104
105
|
export declare type StorageReadEntryArgs = StorageSelection & {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rozenite/storage-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Generic Storage Inspector for Rozenite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/react-native/index.cjs",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"url": "https://github.com/callstackincubator/rozenite.git"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@rozenite/
|
|
19
|
-
"@rozenite/agent-
|
|
20
|
-
"@rozenite/
|
|
18
|
+
"@rozenite/plugin-bridge": "1.9.0",
|
|
19
|
+
"@rozenite/agent-shared": "1.9.0",
|
|
20
|
+
"@rozenite/agent-bridge": "1.9.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@tanstack/react-table": "^8.21.3",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"tailwindcss-animate": "^1.0.7",
|
|
39
39
|
"typescript": "~5.9.3",
|
|
40
40
|
"vite": "^7.3.1",
|
|
41
|
-
"@rozenite/vite-plugin": "1.
|
|
42
|
-
"rozenite": "1.
|
|
41
|
+
"@rozenite/vite-plugin": "1.9.0",
|
|
42
|
+
"rozenite": "1.9.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@react-native-async-storage/async-storage": "*",
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { handleImportEntries, type ImportEmittedEvent } from '../import';
|
|
3
|
+
import type { StorageView } from '../storage-view';
|
|
4
|
+
import { getStorageViewId } from '../../shared/types';
|
|
5
|
+
import type { StorageEntry, StorageTarget } from '../../shared/types';
|
|
6
|
+
import type { StorageImportEntriesEvent } from '../../shared/messaging';
|
|
7
|
+
|
|
8
|
+
const target: StorageTarget = { adapterId: 'mmkv', storageId: 'user' };
|
|
9
|
+
|
|
10
|
+
const buildView = (overrides?: Partial<StorageView>): StorageView => {
|
|
11
|
+
const unexpected = (method: string) =>
|
|
12
|
+
vi.fn(() => {
|
|
13
|
+
throw new Error(`Unexpected call to ${method}`);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
id: getStorageViewId(target),
|
|
18
|
+
target,
|
|
19
|
+
adapterName: 'MMKV',
|
|
20
|
+
storageName: 'user',
|
|
21
|
+
capabilities: {
|
|
22
|
+
supportedTypes: ['string', 'number', 'boolean', 'buffer'],
|
|
23
|
+
},
|
|
24
|
+
get: unexpected('get'),
|
|
25
|
+
set: vi.fn(async () => {}),
|
|
26
|
+
delete: unexpected('delete'),
|
|
27
|
+
getAllKeys: unexpected('getAllKeys'),
|
|
28
|
+
getAllEntries: unexpected('getAllEntries'),
|
|
29
|
+
watch: unexpected('watch'),
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const buildEntries = (count: number): StorageEntry[] =>
|
|
35
|
+
Array.from({ length: count }, (_, index) => ({
|
|
36
|
+
key: `k${index}`,
|
|
37
|
+
type: 'string' as const,
|
|
38
|
+
value: `v${index}`,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
describe('handleImportEntries', () => {
|
|
42
|
+
it('emits a set-entry echo per write followed by a success import-result', async () => {
|
|
43
|
+
const view = buildView();
|
|
44
|
+
const entries = buildEntries(5);
|
|
45
|
+
const event: StorageImportEntriesEvent = {
|
|
46
|
+
type: 'import-entries',
|
|
47
|
+
target,
|
|
48
|
+
entries,
|
|
49
|
+
};
|
|
50
|
+
const emitted: ImportEmittedEvent[] = [];
|
|
51
|
+
|
|
52
|
+
await handleImportEntries([view], event, (out) => emitted.push(out));
|
|
53
|
+
|
|
54
|
+
expect(view.set).toHaveBeenCalledTimes(5);
|
|
55
|
+
|
|
56
|
+
expect(emitted).toHaveLength(6);
|
|
57
|
+
expect(emitted.slice(0, 5)).toEqual(
|
|
58
|
+
entries.map((entry) => ({
|
|
59
|
+
type: 'set-entry',
|
|
60
|
+
target,
|
|
61
|
+
entry,
|
|
62
|
+
})),
|
|
63
|
+
);
|
|
64
|
+
expect(emitted[5]).toEqual({
|
|
65
|
+
type: 'import-result',
|
|
66
|
+
target,
|
|
67
|
+
ok: true,
|
|
68
|
+
written: 5,
|
|
69
|
+
total: 5,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('stops at the first write that throws and reports the failed key', async () => {
|
|
74
|
+
const failingKey = 'k2';
|
|
75
|
+
const view = buildView({
|
|
76
|
+
set: vi.fn(async (entry: StorageEntry) => {
|
|
77
|
+
if (entry.key === failingKey) {
|
|
78
|
+
throw new Error('boom');
|
|
79
|
+
}
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
const entries = buildEntries(5);
|
|
83
|
+
const event: StorageImportEntriesEvent = {
|
|
84
|
+
type: 'import-entries',
|
|
85
|
+
target,
|
|
86
|
+
entries,
|
|
87
|
+
};
|
|
88
|
+
const emitted: ImportEmittedEvent[] = [];
|
|
89
|
+
|
|
90
|
+
await handleImportEntries([view], event, (out) => emitted.push(out));
|
|
91
|
+
|
|
92
|
+
// 2 set-entry echoes (k0, k1), then the failure result. No echoes for k2+.
|
|
93
|
+
expect(view.set).toHaveBeenCalledTimes(3);
|
|
94
|
+
expect(emitted).toHaveLength(3);
|
|
95
|
+
expect(emitted[0]).toMatchObject({ type: 'set-entry', entry: entries[0] });
|
|
96
|
+
expect(emitted[1]).toMatchObject({ type: 'set-entry', entry: entries[1] });
|
|
97
|
+
expect(emitted[2]).toEqual({
|
|
98
|
+
type: 'import-result',
|
|
99
|
+
target,
|
|
100
|
+
ok: false,
|
|
101
|
+
written: 2,
|
|
102
|
+
total: 5,
|
|
103
|
+
failedKey: failingKey,
|
|
104
|
+
error: 'boom',
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('coerces non-Error throws to a string error message', async () => {
|
|
109
|
+
const view = buildView({
|
|
110
|
+
set: vi.fn(async () => {
|
|
111
|
+
throw 'plain string failure';
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
const event: StorageImportEntriesEvent = {
|
|
115
|
+
type: 'import-entries',
|
|
116
|
+
target,
|
|
117
|
+
entries: buildEntries(1),
|
|
118
|
+
};
|
|
119
|
+
const emitted: ImportEmittedEvent[] = [];
|
|
120
|
+
|
|
121
|
+
await handleImportEntries([view], event, (out) => emitted.push(out));
|
|
122
|
+
|
|
123
|
+
expect(emitted).toHaveLength(1);
|
|
124
|
+
expect(emitted[0]).toMatchObject({
|
|
125
|
+
type: 'import-result',
|
|
126
|
+
ok: false,
|
|
127
|
+
error: 'plain string failure',
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('emits an unknown-target import-result without calling set', async () => {
|
|
132
|
+
const view = buildView();
|
|
133
|
+
const unknownTarget: StorageTarget = {
|
|
134
|
+
adapterId: 'async-storage',
|
|
135
|
+
storageId: 'default',
|
|
136
|
+
};
|
|
137
|
+
const entries = buildEntries(3);
|
|
138
|
+
const event: StorageImportEntriesEvent = {
|
|
139
|
+
type: 'import-entries',
|
|
140
|
+
target: unknownTarget,
|
|
141
|
+
entries,
|
|
142
|
+
};
|
|
143
|
+
const emitted: ImportEmittedEvent[] = [];
|
|
144
|
+
|
|
145
|
+
await handleImportEntries([view], event, (out) => emitted.push(out));
|
|
146
|
+
|
|
147
|
+
expect(view.set).not.toHaveBeenCalled();
|
|
148
|
+
expect(emitted).toEqual([
|
|
149
|
+
{
|
|
150
|
+
type: 'import-result',
|
|
151
|
+
target: unknownTarget,
|
|
152
|
+
ok: false,
|
|
153
|
+
written: 0,
|
|
154
|
+
total: 3,
|
|
155
|
+
error: 'Target storage not found',
|
|
156
|
+
},
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('handles empty entries as a successful no-op', async () => {
|
|
161
|
+
const view = buildView();
|
|
162
|
+
const event: StorageImportEntriesEvent = {
|
|
163
|
+
type: 'import-entries',
|
|
164
|
+
target,
|
|
165
|
+
entries: [],
|
|
166
|
+
};
|
|
167
|
+
const emitted: ImportEmittedEvent[] = [];
|
|
168
|
+
|
|
169
|
+
await handleImportEntries([view], event, (out) => emitted.push(out));
|
|
170
|
+
|
|
171
|
+
expect(view.set).not.toHaveBeenCalled();
|
|
172
|
+
expect(emitted).toEqual([
|
|
173
|
+
{
|
|
174
|
+
type: 'import-result',
|
|
175
|
+
target,
|
|
176
|
+
ok: true,
|
|
177
|
+
written: 0,
|
|
178
|
+
total: 0,
|
|
179
|
+
},
|
|
180
|
+
]);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
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(() => createMMKVStorageAdapter({ storages: [storage as any] })).toThrow(
|
|
113
|
+
/MMKV arrays are not supported for v4 storages/
|
|
114
|
+
);
|
|
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((view) => view.target.storageId === 'user-storage');
|
|
133
|
+
const cacheView = views.find((view) => view.target.storageId === 'cache-storage');
|
|
134
|
+
|
|
135
|
+
expect(userView).toBeDefined();
|
|
136
|
+
expect(cacheView).toBeDefined();
|
|
137
|
+
|
|
138
|
+
await expect(userView?.getAllKeys()).resolves.toEqual(['visible']);
|
|
139
|
+
await expect(cacheView?.getAllKeys()).resolves.toEqual(['persisted']);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -148,6 +148,20 @@ const getStorageBlacklist = (
|
|
|
148
148
|
return config[storageId];
|
|
149
149
|
};
|
|
150
150
|
|
|
151
|
+
const createStorageBlacklistMatcher = (
|
|
152
|
+
config: MMKVBlacklistConfig | undefined,
|
|
153
|
+
storageId: string
|
|
154
|
+
) => {
|
|
155
|
+
if (!(config instanceof RegExp)) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (key: string) => {
|
|
160
|
+
config.lastIndex = 0;
|
|
161
|
+
return config.test(`${storageId}:${key}`);
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
151
165
|
export const createMMKVStorageAdapter = ({
|
|
152
166
|
adapterId = 'mmkv',
|
|
153
167
|
adapterName = 'MMKV',
|
|
@@ -164,6 +178,7 @@ export const createMMKVStorageAdapter = ({
|
|
|
164
178
|
id: storageId,
|
|
165
179
|
name: storageId,
|
|
166
180
|
blacklist: getStorageBlacklist(blacklist, storageId),
|
|
181
|
+
shouldFilterKey: createStorageBlacklistMatcher(blacklist, storageId),
|
|
167
182
|
capabilities: {
|
|
168
183
|
supportedTypes: DEFAULT_SUPPORTED_TYPES,
|
|
169
184
|
},
|
|
@@ -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
|
+
};
|
|
@@ -34,6 +34,10 @@ const toSnapshotMap = (entries: StorageEntry[]) => {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
const shouldFilterKey = (storage: StorageNode, key: string) => {
|
|
37
|
+
if (storage.shouldFilterKey?.(key)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
if (!storage.blacklist) {
|
|
38
42
|
return false;
|
|
39
43
|
}
|
|
@@ -45,14 +49,14 @@ const shouldFilterKey = (storage: StorageNode, key: string) => {
|
|
|
45
49
|
const checkTypeSupport = (
|
|
46
50
|
capabilities: StorageCapabilities,
|
|
47
51
|
type: StorageEntryType,
|
|
48
|
-
target: StorageTarget
|
|
52
|
+
target: StorageTarget,
|
|
49
53
|
) => {
|
|
50
54
|
if (supportsType(capabilities, type)) {
|
|
51
55
|
return;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
throw new Error(
|
|
55
|
-
`Type "${type}" is not supported by storage "${target.storageId}" in adapter "${target.adapterId}"
|
|
59
|
+
`Type "${type}" is not supported by storage "${target.storageId}" in adapter "${target.adapterId}".`,
|
|
56
60
|
);
|
|
57
61
|
};
|
|
58
62
|
|
|
@@ -96,6 +100,7 @@ export type StorageView = {
|
|
|
96
100
|
adapterName: string;
|
|
97
101
|
storageName: string;
|
|
98
102
|
capabilities: StorageCapabilities;
|
|
103
|
+
blacklist?: RegExp;
|
|
99
104
|
get: (key: string) => Promise<StorageEntry | undefined>;
|
|
100
105
|
set: (entry: StorageEntry) => Promise<void>;
|
|
101
106
|
delete: (key: string) => Promise<void>;
|
|
@@ -108,7 +113,7 @@ export type StorageView = {
|
|
|
108
113
|
};
|
|
109
114
|
|
|
110
115
|
const buildSnapshotMap = async (
|
|
111
|
-
getAllEntries: () => Promise<StorageEntry[]
|
|
116
|
+
getAllEntries: () => Promise<StorageEntry[]>,
|
|
112
117
|
): Promise<StorageSnapshotMap> => {
|
|
113
118
|
const entries = await getAllEntries();
|
|
114
119
|
return toSnapshotMap(entries);
|
|
@@ -120,7 +125,7 @@ const diffSnapshots = (
|
|
|
120
125
|
handlers: {
|
|
121
126
|
onSet: (entry: StorageEntry) => void;
|
|
122
127
|
onDelete: (key: string) => void;
|
|
123
|
-
}
|
|
128
|
+
},
|
|
124
129
|
) => {
|
|
125
130
|
next.forEach((nextEntry, key) => {
|
|
126
131
|
const previousEntry = previous.get(key);
|
|
@@ -147,7 +152,7 @@ const createPollingSubscription = async (
|
|
|
147
152
|
handlers: {
|
|
148
153
|
onSet: (entry: StorageEntry) => void;
|
|
149
154
|
onDelete: (key: string) => void;
|
|
150
|
-
}
|
|
155
|
+
},
|
|
151
156
|
): Promise<StorageSubscription> => {
|
|
152
157
|
let previousSnapshot = await buildSnapshotMap(getAllEntries);
|
|
153
158
|
|
|
@@ -170,7 +175,7 @@ const createPollingSubscription = async (
|
|
|
170
175
|
|
|
171
176
|
export const createStorageView = (
|
|
172
177
|
adapter: StorageAdapter,
|
|
173
|
-
storageNode: StorageNode
|
|
178
|
+
storageNode: StorageNode,
|
|
174
179
|
): StorageView => {
|
|
175
180
|
const storage = storageNode.storage;
|
|
176
181
|
const target: StorageTarget = {
|
|
@@ -191,7 +196,7 @@ export const createStorageView = (
|
|
|
191
196
|
const visibleEntries = await Promise.all(
|
|
192
197
|
keys
|
|
193
198
|
.filter((key) => !shouldFilterKey(storageNode, key))
|
|
194
|
-
.map((key) => getEntry(storage, key))
|
|
199
|
+
.map((key) => getEntry(storage, key)),
|
|
195
200
|
);
|
|
196
201
|
|
|
197
202
|
return visibleEntries.filter((entry): entry is StorageEntry => !!entry);
|
|
@@ -203,6 +208,7 @@ export const createStorageView = (
|
|
|
203
208
|
adapterName: adapter.name,
|
|
204
209
|
storageName: storageNode.name,
|
|
205
210
|
capabilities: storageNode.capabilities,
|
|
211
|
+
blacklist: storageNode.blacklist,
|
|
206
212
|
get,
|
|
207
213
|
set: async (entry) => {
|
|
208
214
|
checkTypeSupport(storageNode.capabilities, entry.type, target);
|
|
@@ -241,5 +247,7 @@ export const createStorageView = (
|
|
|
241
247
|
|
|
242
248
|
export const createStorageViews = (storages: StorageAdapter[]) =>
|
|
243
249
|
storages.flatMap((adapter) =>
|
|
244
|
-
adapter.storages.map((storageNode) =>
|
|
250
|
+
adapter.storages.map((storageNode) =>
|
|
251
|
+
createStorageView(adapter, storageNode),
|
|
252
|
+
),
|
|
245
253
|
);
|