@multiplatform.one/store 6.6.0 → 7.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/dist/cjs/storage/index.native.js +97 -34
- package/dist/cjs/storage/index.native.js.map +1 -1
- package/dist/esm/storage/index.native.js +96 -35
- package/dist/esm/storage/index.native.js.map +1 -1
- package/package.json +8 -8
- package/src/storage/index.native.ts +144 -23
- package/src/storage/nativeStorage.spec.ts +206 -0
- package/types/storage/index.native.d.ts +38 -0
- package/types/storage/index.native.d.ts.map +1 -1
|
@@ -24,46 +24,90 @@ var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
|
|
|
24
24
|
}), mod);
|
|
25
25
|
var index_native_exports = {};
|
|
26
26
|
__export(index_native_exports, {
|
|
27
|
+
logNativeStorageResolution: () => logNativeStorageResolution,
|
|
28
|
+
resolveNativeStorage: () => resolveNativeStorage,
|
|
27
29
|
storage: () => storage
|
|
28
30
|
});
|
|
29
31
|
module.exports = __toCommonJS(index_native_exports);
|
|
30
|
-
function
|
|
32
|
+
function _instanceof(left, right) {
|
|
33
|
+
"@swc/helpers - instanceof";
|
|
34
|
+
|
|
35
|
+
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
|
|
36
|
+
return !!right[Symbol.hasInstance](left);
|
|
37
|
+
} else {
|
|
38
|
+
return left instanceof right;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
var defaultLoaders = {
|
|
42
|
+
// Literal require(...) calls on purpose: metro resolves them natively and
|
|
43
|
+
// vxrn/rolldown rewrites literal requires into bundled module inits (a
|
|
44
|
+
// passed-around `require` reference would survive as a bare identifier and
|
|
45
|
+
// crash). Lazy so a genuinely missing native module throws HERE — inside
|
|
46
|
+
// the try/catch — instead of at bundle evaluation.
|
|
47
|
+
loadMMKV: function loadMMKV() {
|
|
48
|
+
return require("react-native-mmkv");
|
|
49
|
+
},
|
|
50
|
+
loadAsyncStorage: function loadAsyncStorage() {
|
|
51
|
+
return require("@react-native-async-storage/async-storage");
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function resolveNativeStorage() {
|
|
55
|
+
var loaders = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : defaultLoaders;
|
|
56
|
+
var errors = {};
|
|
31
57
|
try {
|
|
32
|
-
var
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
var mmkvModule = loaders.loadMMKV();
|
|
59
|
+
if (typeof (mmkvModule === null || mmkvModule === void 0 ? void 0 : mmkvModule.createMMKV) !== "function") {
|
|
60
|
+
throw new Error("no createMMKV() export (react-native-mmkv v4 API expected)");
|
|
61
|
+
}
|
|
62
|
+
var mmkv = mmkvModule.createMMKV();
|
|
36
63
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
backend: "mmkv",
|
|
65
|
+
errors,
|
|
66
|
+
storage: {
|
|
67
|
+
getItem: function getItem(name) {
|
|
68
|
+
var _mmkv_getString;
|
|
69
|
+
return (_mmkv_getString = mmkv.getString(name)) !== null && _mmkv_getString !== void 0 ? _mmkv_getString : null;
|
|
70
|
+
},
|
|
71
|
+
setItem: function setItem(name, value) {
|
|
72
|
+
mmkv.set(name, value);
|
|
73
|
+
},
|
|
74
|
+
removeItem: function removeItem(name) {
|
|
75
|
+
mmkv.remove(name);
|
|
76
|
+
}
|
|
46
77
|
}
|
|
47
78
|
};
|
|
48
|
-
} catch (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return AsyncStorage.setItem(name, value);
|
|
58
|
-
},
|
|
59
|
-
removeItem: function removeItem(name) {
|
|
60
|
-
return AsyncStorage.removeItem(name);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
} catch (unused2) {}
|
|
65
|
-
var map = /* @__PURE__ */new Map();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
errors.mmkv = err;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
var asyncStorageModule = loaders.loadAsyncStorage();
|
|
84
|
+
var asyncStorage = asyncStorageModule === null || asyncStorageModule === void 0 ? void 0 : asyncStorageModule.default;
|
|
85
|
+
if (!asyncStorage || typeof asyncStorage.getItem !== "function") {
|
|
86
|
+
throw new Error("no usable default export on @react-native-async-storage/async-storage");
|
|
87
|
+
}
|
|
66
88
|
return {
|
|
89
|
+
backend: "async-storage",
|
|
90
|
+
errors,
|
|
91
|
+
storage: {
|
|
92
|
+
getItem: function getItem(name) {
|
|
93
|
+
return asyncStorage.getItem(name);
|
|
94
|
+
},
|
|
95
|
+
setItem: function setItem(name, value) {
|
|
96
|
+
return asyncStorage.setItem(name, value);
|
|
97
|
+
},
|
|
98
|
+
removeItem: function removeItem(name) {
|
|
99
|
+
return asyncStorage.removeItem(name);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
} catch (err) {
|
|
104
|
+
errors.asyncStorage = err;
|
|
105
|
+
}
|
|
106
|
+
var map = /* @__PURE__ */new Map();
|
|
107
|
+
return {
|
|
108
|
+
backend: "memory",
|
|
109
|
+
errors,
|
|
110
|
+
storage: {
|
|
67
111
|
getItem: function getItem(name) {
|
|
68
112
|
var _map_get;
|
|
69
113
|
return (_map_get = map.get(name)) !== null && _map_get !== void 0 ? _map_get : null;
|
|
@@ -74,8 +118,27 @@ function createStorage() {
|
|
|
74
118
|
removeItem: function removeItem(name) {
|
|
75
119
|
map.delete(name);
|
|
76
120
|
}
|
|
77
|
-
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function describeError(err) {
|
|
125
|
+
return _instanceof(err, Error) ? err.message : String(err);
|
|
126
|
+
}
|
|
127
|
+
function logNativeStorageResolution(resolution2) {
|
|
128
|
+
var logger = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : console;
|
|
129
|
+
if (resolution2.backend === "mmkv") {
|
|
130
|
+
logger.info("[@multiplatform.one/store] native storage backend: mmkv");
|
|
131
|
+
return;
|
|
78
132
|
}
|
|
133
|
+
var causes = [`react-native-mmkv unavailable: ${describeError(resolution2.errors.mmkv)}`];
|
|
134
|
+
if (resolution2.backend === "memory") {
|
|
135
|
+
causes.push(`async-storage unavailable: ${describeError(resolution2.errors.asyncStorage)}`);
|
|
136
|
+
}
|
|
137
|
+
logger.warn(`[@multiplatform.one/store] native storage fell back to ${resolution2.backend}` + (resolution2.backend === "memory" ? " \u2014 persisted state will NOT survive app restarts" : "") + ` (${causes.join("; ")})`);
|
|
138
|
+
}
|
|
139
|
+
var resolution = resolveNativeStorage();
|
|
140
|
+
if (process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test") {
|
|
141
|
+
logNativeStorageResolution(resolution);
|
|
79
142
|
}
|
|
80
|
-
var storage =
|
|
143
|
+
var storage = resolution.storage;
|
|
81
144
|
//# sourceMappingURL=index.native.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["index_native_exports","__export","storage","module","exports","__toCommonJS","
|
|
1
|
+
{"version":3,"names":["index_native_exports","__export","logNativeStorageResolution","resolveNativeStorage","storage","module","exports","__toCommonJS","_instanceof","left","right","Symbol","hasInstance","defaultLoaders","loadMMKV","require","loadAsyncStorage","loaders","arguments","length","errors","mmkvModule","createMMKV","Error","mmkv","backend","getItem","name","_mmkv_getString","getString","setItem","value","set","removeItem","remove","err","asyncStorageModule","asyncStorage","default","map","Map","_map_get","get","delete","describeError","message","String","resolution2","logger","console","info","causes","push","warn","join","resolution","process","env","NODE_ENV"],"sources":["../../../src/storage/index.native.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
|
@@ -1,40 +1,82 @@
|
|
|
1
|
-
function
|
|
1
|
+
function _instanceof(left, right) {
|
|
2
|
+
"@swc/helpers - instanceof";
|
|
3
|
+
|
|
4
|
+
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
|
|
5
|
+
return !!right[Symbol.hasInstance](left);
|
|
6
|
+
} else {
|
|
7
|
+
return left instanceof right;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
var defaultLoaders = {
|
|
11
|
+
// Literal require(...) calls on purpose: metro resolves them natively and
|
|
12
|
+
// vxrn/rolldown rewrites literal requires into bundled module inits (a
|
|
13
|
+
// passed-around `require` reference would survive as a bare identifier and
|
|
14
|
+
// crash). Lazy so a genuinely missing native module throws HERE — inside
|
|
15
|
+
// the try/catch — instead of at bundle evaluation.
|
|
16
|
+
loadMMKV: function loadMMKV() {
|
|
17
|
+
return require("react-native-mmkv");
|
|
18
|
+
},
|
|
19
|
+
loadAsyncStorage: function loadAsyncStorage() {
|
|
20
|
+
return require("@react-native-async-storage/async-storage");
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
function resolveNativeStorage() {
|
|
24
|
+
var loaders = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : defaultLoaders;
|
|
25
|
+
var errors = {};
|
|
2
26
|
try {
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
27
|
+
var mmkvModule = loaders.loadMMKV();
|
|
28
|
+
if (typeof (mmkvModule === null || mmkvModule === void 0 ? void 0 : mmkvModule.createMMKV) !== "function") {
|
|
29
|
+
throw new Error("no createMMKV() export (react-native-mmkv v4 API expected)");
|
|
30
|
+
}
|
|
31
|
+
var mmkv = mmkvModule.createMMKV();
|
|
7
32
|
return {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
33
|
+
backend: "mmkv",
|
|
34
|
+
errors,
|
|
35
|
+
storage: {
|
|
36
|
+
getItem: function getItem(name) {
|
|
37
|
+
var _mmkv_getString;
|
|
38
|
+
return (_mmkv_getString = mmkv.getString(name)) !== null && _mmkv_getString !== void 0 ? _mmkv_getString : null;
|
|
39
|
+
},
|
|
40
|
+
setItem: function setItem(name, value) {
|
|
41
|
+
mmkv.set(name, value);
|
|
42
|
+
},
|
|
43
|
+
removeItem: function removeItem(name) {
|
|
44
|
+
mmkv.remove(name);
|
|
45
|
+
}
|
|
17
46
|
}
|
|
18
47
|
};
|
|
19
|
-
} catch (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return AsyncStorage.setItem(name, value);
|
|
29
|
-
},
|
|
30
|
-
removeItem: function removeItem(name) {
|
|
31
|
-
return AsyncStorage.removeItem(name);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
} catch (unused2) {}
|
|
36
|
-
var map = /* @__PURE__ */new Map();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
errors.mmkv = err;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
var asyncStorageModule = loaders.loadAsyncStorage();
|
|
53
|
+
var asyncStorage = asyncStorageModule === null || asyncStorageModule === void 0 ? void 0 : asyncStorageModule.default;
|
|
54
|
+
if (!asyncStorage || typeof asyncStorage.getItem !== "function") {
|
|
55
|
+
throw new Error("no usable default export on @react-native-async-storage/async-storage");
|
|
56
|
+
}
|
|
37
57
|
return {
|
|
58
|
+
backend: "async-storage",
|
|
59
|
+
errors,
|
|
60
|
+
storage: {
|
|
61
|
+
getItem: function getItem(name) {
|
|
62
|
+
return asyncStorage.getItem(name);
|
|
63
|
+
},
|
|
64
|
+
setItem: function setItem(name, value) {
|
|
65
|
+
return asyncStorage.setItem(name, value);
|
|
66
|
+
},
|
|
67
|
+
removeItem: function removeItem(name) {
|
|
68
|
+
return asyncStorage.removeItem(name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
} catch (err) {
|
|
73
|
+
errors.asyncStorage = err;
|
|
74
|
+
}
|
|
75
|
+
var map = /* @__PURE__ */new Map();
|
|
76
|
+
return {
|
|
77
|
+
backend: "memory",
|
|
78
|
+
errors,
|
|
79
|
+
storage: {
|
|
38
80
|
getItem: function getItem(name) {
|
|
39
81
|
var _map_get;
|
|
40
82
|
return (_map_get = map.get(name)) !== null && _map_get !== void 0 ? _map_get : null;
|
|
@@ -45,9 +87,28 @@ function createStorage() {
|
|
|
45
87
|
removeItem: function removeItem(name) {
|
|
46
88
|
map.delete(name);
|
|
47
89
|
}
|
|
48
|
-
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function describeError(err) {
|
|
94
|
+
return _instanceof(err, Error) ? err.message : String(err);
|
|
95
|
+
}
|
|
96
|
+
function logNativeStorageResolution(resolution2) {
|
|
97
|
+
var logger = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : console;
|
|
98
|
+
if (resolution2.backend === "mmkv") {
|
|
99
|
+
logger.info("[@multiplatform.one/store] native storage backend: mmkv");
|
|
100
|
+
return;
|
|
49
101
|
}
|
|
102
|
+
var causes = [`react-native-mmkv unavailable: ${describeError(resolution2.errors.mmkv)}`];
|
|
103
|
+
if (resolution2.backend === "memory") {
|
|
104
|
+
causes.push(`async-storage unavailable: ${describeError(resolution2.errors.asyncStorage)}`);
|
|
105
|
+
}
|
|
106
|
+
logger.warn(`[@multiplatform.one/store] native storage fell back to ${resolution2.backend}` + (resolution2.backend === "memory" ? " \u2014 persisted state will NOT survive app restarts" : "") + ` (${causes.join("; ")})`);
|
|
107
|
+
}
|
|
108
|
+
var resolution = resolveNativeStorage();
|
|
109
|
+
if (process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test") {
|
|
110
|
+
logNativeStorageResolution(resolution);
|
|
50
111
|
}
|
|
51
|
-
var storage =
|
|
52
|
-
export { storage };
|
|
112
|
+
var storage = resolution.storage;
|
|
113
|
+
export { logNativeStorageResolution, resolveNativeStorage, storage };
|
|
53
114
|
//# sourceMappingURL=index.native.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["
|
|
1
|
+
{"version":3,"names":["_instanceof","left","right","Symbol","hasInstance","defaultLoaders","loadMMKV","require","loadAsyncStorage","resolveNativeStorage","loaders","arguments","length","errors","mmkvModule","createMMKV","Error","mmkv","backend","storage","getItem","name","_mmkv_getString","getString","setItem","value","set","removeItem","remove","err","asyncStorageModule","asyncStorage","default","map","Map","_map_get","get","delete","describeError","message","String","logNativeStorageResolution","resolution2","logger","console","info","causes","push","warn","join","resolution","process","env","NODE_ENV"],"sources":["../../../src/storage/index.native.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/store",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "multiplatform.one TanStack Store with cross-platform persistent storage",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"react-native-mmkv": "^4.3.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@tamagui/build": "2.
|
|
60
|
+
"@tamagui/build": "2.7.6",
|
|
61
61
|
"@testing-library/jest-dom": "^6.9.1",
|
|
62
62
|
"@testing-library/react": "^16.3.2",
|
|
63
63
|
"@vitejs/plugin-react": "^6.0.1",
|
|
@@ -67,22 +67,22 @@
|
|
|
67
67
|
"typescript": "~5.9.3",
|
|
68
68
|
"vite": "^8.0.10",
|
|
69
69
|
"vitest": "^4.1.5",
|
|
70
|
-
"@multiplatform.one/
|
|
71
|
-
"@multiplatform.one/
|
|
70
|
+
"@multiplatform.one/config": "7.0.0",
|
|
71
|
+
"@multiplatform.one/test-utils": "7.0.0"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"react": "^19.1.0"
|
|
75
75
|
},
|
|
76
|
-
"optionalDependencies": {
|
|
77
|
-
"@tauri-apps/plugin-store": "^2.4.2"
|
|
78
|
-
},
|
|
79
76
|
"engines": {
|
|
80
77
|
"node": ">=18.0.0"
|
|
81
78
|
},
|
|
79
|
+
"optionalDependencies": {
|
|
80
|
+
"@tauri-apps/plugin-store": "^2.4.2"
|
|
81
|
+
},
|
|
82
82
|
"scripts": {
|
|
83
83
|
"build": "rm -rf dist types 2>/dev/null && tamagui-build --ts-project ./tsconfig.build.json --exclude '\\.spec\\.|\\.test\\.|\\.stories\\.'",
|
|
84
84
|
"pretest": "node -e \"const p=require('path').resolve(__dirname,'../../node_modules/@testing-library/react/node_modules');['react','react-dom','scheduler'].forEach(d=>{try{require('fs').rmSync(p+'/'+d,{recursive:true,force:true})}catch{}})\"",
|
|
85
85
|
"test": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=text --coverage.reporter=lcov --coverage.reporter=html",
|
|
86
|
-
"typecheck": "
|
|
86
|
+
"typecheck": "tsgo --noEmit"
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -1,30 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native storage twin — backend priority: MMKV (react-native-mmkv v4,
|
|
3
|
+
* synchronous JSI/Nitro) → AsyncStorage (environments without Nitro, e.g.
|
|
4
|
+
* Expo Go) → in-memory Map (nothing survives a restart).
|
|
5
|
+
*
|
|
6
|
+
* react-native-mmkv v4 exports `MMKV` as a TYPE only — the runtime API is
|
|
7
|
+
* the `createMMKV()` factory. The previous implementation destructured the
|
|
8
|
+
* non-existent `MMKV` class (v3 API) and threw `undefined is not a
|
|
9
|
+
* constructor` on every launch, silently landing all native persistence on
|
|
10
|
+
* the in-memory fallback (favorites lost on restart — 2026-08-13 iOS smoke
|
|
11
|
+
* test, p-results/native-smoke/REPORT.md #2). Resolution is therefore
|
|
12
|
+
* factored into a pure, loader-injectable function (spec-covered) and the
|
|
13
|
+
* chosen backend is logged once in dev — a degraded backend can never be
|
|
14
|
+
* silent again.
|
|
15
|
+
*/
|
|
16
|
+
|
|
1
17
|
import type { Storage } from "../persist";
|
|
2
18
|
|
|
3
|
-
|
|
19
|
+
export type NativeStorageBackend = "mmkv" | "async-storage" | "memory";
|
|
20
|
+
|
|
21
|
+
/** Runtime subset of react-native-mmkv v4's MMKV HybridObject. */
|
|
22
|
+
interface MMKVLike {
|
|
23
|
+
getString(key: string): string | undefined;
|
|
24
|
+
set(key: string, value: string): void;
|
|
25
|
+
remove(key: string): boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface AsyncStorageLike {
|
|
29
|
+
getItem(key: string): Promise<string | null>;
|
|
30
|
+
setItem(key: string, value: string): Promise<void>;
|
|
31
|
+
removeItem(key: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Module loaders, injectable so specs can drive every resolution branch. */
|
|
35
|
+
export interface NativeStorageLoaders {
|
|
36
|
+
/** Load react-native-mmkv (throws when unavailable). */
|
|
37
|
+
loadMMKV(): unknown;
|
|
38
|
+
/** Load @react-native-async-storage/async-storage (throws when unavailable). */
|
|
39
|
+
loadAsyncStorage(): unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NativeStorageResolution {
|
|
43
|
+
storage: Storage;
|
|
44
|
+
backend: NativeStorageBackend;
|
|
45
|
+
/** Why higher-priority backends were skipped (surfaced by the dev log). */
|
|
46
|
+
errors: { mmkv?: unknown; asyncStorage?: unknown };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const defaultLoaders: NativeStorageLoaders = {
|
|
50
|
+
// Literal require(...) calls on purpose: metro resolves them natively and
|
|
51
|
+
// vxrn/rolldown rewrites literal requires into bundled module inits (a
|
|
52
|
+
// passed-around `require` reference would survive as a bare identifier and
|
|
53
|
+
// crash). Lazy so a genuinely missing native module throws HERE — inside
|
|
54
|
+
// the try/catch — instead of at bundle evaluation.
|
|
55
|
+
loadMMKV: () => require("react-native-mmkv"),
|
|
56
|
+
loadAsyncStorage: () => require("@react-native-async-storage/async-storage"),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function resolveNativeStorage(
|
|
60
|
+
loaders: NativeStorageLoaders = defaultLoaders,
|
|
61
|
+
): NativeStorageResolution {
|
|
62
|
+
const errors: NativeStorageResolution["errors"] = {};
|
|
63
|
+
|
|
4
64
|
try {
|
|
5
|
-
const
|
|
6
|
-
|
|
65
|
+
const mmkvModule = loaders.loadMMKV() as {
|
|
66
|
+
createMMKV?: (configuration?: unknown) => MMKVLike;
|
|
67
|
+
};
|
|
68
|
+
if (typeof mmkvModule?.createMMKV !== "function") {
|
|
69
|
+
// v3 shipped an `MMKV` class; v4 only ships the factory. Anything
|
|
70
|
+
// without createMMKV() is not a usable react-native-mmkv module.
|
|
71
|
+
throw new Error("no createMMKV() export (react-native-mmkv v4 API expected)");
|
|
72
|
+
}
|
|
73
|
+
const mmkv = mmkvModule.createMMKV();
|
|
7
74
|
return {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
75
|
+
backend: "mmkv",
|
|
76
|
+
errors,
|
|
77
|
+
storage: {
|
|
78
|
+
getItem: (name: string) => mmkv.getString(name) ?? null,
|
|
79
|
+
setItem: (name: string, value: string) => {
|
|
80
|
+
mmkv.set(name, value);
|
|
81
|
+
},
|
|
82
|
+
removeItem: (name: string) => {
|
|
83
|
+
mmkv.remove(name);
|
|
84
|
+
},
|
|
85
|
+
},
|
|
11
86
|
};
|
|
12
|
-
} catch {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
} catch {}
|
|
25
|
-
// Final fallback: in-memory storage
|
|
26
|
-
const map = new Map<string, string>();
|
|
87
|
+
} catch (err) {
|
|
88
|
+
errors.mmkv = err;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const asyncStorageModule = loaders.loadAsyncStorage() as { default?: AsyncStorageLike };
|
|
93
|
+
const asyncStorage = asyncStorageModule?.default;
|
|
94
|
+
if (!asyncStorage || typeof asyncStorage.getItem !== "function") {
|
|
95
|
+
throw new Error("no usable default export on @react-native-async-storage/async-storage");
|
|
96
|
+
}
|
|
27
97
|
return {
|
|
98
|
+
backend: "async-storage",
|
|
99
|
+
errors,
|
|
100
|
+
storage: {
|
|
101
|
+
getItem: (name: string) => asyncStorage.getItem(name),
|
|
102
|
+
setItem: (name: string, value: string) => asyncStorage.setItem(name, value),
|
|
103
|
+
removeItem: (name: string) => asyncStorage.removeItem(name),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
} catch (err) {
|
|
107
|
+
errors.asyncStorage = err;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const map = new Map<string, string>();
|
|
111
|
+
return {
|
|
112
|
+
backend: "memory",
|
|
113
|
+
errors,
|
|
114
|
+
storage: {
|
|
28
115
|
getItem: (name: string) => map.get(name) ?? null,
|
|
29
116
|
setItem: (name: string, value: string) => {
|
|
30
117
|
map.set(name, value);
|
|
@@ -32,8 +119,42 @@ function createStorage(): Storage {
|
|
|
32
119
|
removeItem: (name: string) => {
|
|
33
120
|
map.delete(name);
|
|
34
121
|
},
|
|
35
|
-
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function describeError(err: unknown): string {
|
|
127
|
+
return err instanceof Error ? err.message : String(err);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* One dev-mode line naming the winning backend — a console.warn when
|
|
132
|
+
* persistence degraded, so a broken storage twin is loud instead of silent.
|
|
133
|
+
*/
|
|
134
|
+
export function logNativeStorageResolution(
|
|
135
|
+
resolution: NativeStorageResolution,
|
|
136
|
+
logger: Pick<Console, "info" | "warn"> = console,
|
|
137
|
+
): void {
|
|
138
|
+
if (resolution.backend === "mmkv") {
|
|
139
|
+
logger.info("[@multiplatform.one/store] native storage backend: mmkv");
|
|
140
|
+
return;
|
|
36
141
|
}
|
|
142
|
+
const causes = [`react-native-mmkv unavailable: ${describeError(resolution.errors.mmkv)}`];
|
|
143
|
+
if (resolution.backend === "memory") {
|
|
144
|
+
causes.push(`async-storage unavailable: ${describeError(resolution.errors.asyncStorage)}`);
|
|
145
|
+
}
|
|
146
|
+
logger.warn(
|
|
147
|
+
`[@multiplatform.one/store] native storage fell back to ${resolution.backend}` +
|
|
148
|
+
(resolution.backend === "memory" ? " — persisted state will NOT survive app restarts" : "") +
|
|
149
|
+
` (${causes.join("; ")})`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const resolution = resolveNativeStorage();
|
|
154
|
+
// NODE_ENV !== "test" keeps vitest imports of this module quiet; specs cover
|
|
155
|
+
// the log contract by calling logNativeStorageResolution directly.
|
|
156
|
+
if (process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test") {
|
|
157
|
+
logNativeStorageResolution(resolution);
|
|
37
158
|
}
|
|
38
159
|
|
|
39
|
-
export const storage: Storage =
|
|
160
|
+
export const storage: Storage = resolution.storage;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native storage resolution specs — lock down the backend-priority contract
|
|
3
|
+
* (mmkv → async-storage → memory), the react-native-mmkv v4 API usage
|
|
4
|
+
* (`createMMKV()` factory; `MMKV` is a type-only export in v4, and
|
|
5
|
+
* destructuring it was the silent-in-memory persistence bug on iOS), and
|
|
6
|
+
* the loud dev-mode fallback log.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it, vi } from "vitest";
|
|
10
|
+
import type { Storage } from "../persist";
|
|
11
|
+
import { logNativeStorageResolution, resolveNativeStorage } from "./index.native";
|
|
12
|
+
|
|
13
|
+
function createMMKVv4Module() {
|
|
14
|
+
const data = new Map<string, string>();
|
|
15
|
+
const instance = {
|
|
16
|
+
getString: (key: string) => data.get(key),
|
|
17
|
+
set: (key: string, value: string) => {
|
|
18
|
+
data.set(key, value);
|
|
19
|
+
},
|
|
20
|
+
remove: (key: string) => data.delete(key),
|
|
21
|
+
};
|
|
22
|
+
const createMMKV = vi.fn(() => instance);
|
|
23
|
+
return { module: { createMMKV }, createMMKV, data };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createAsyncStorageModule() {
|
|
27
|
+
const data = new Map<string, string>();
|
|
28
|
+
return {
|
|
29
|
+
module: {
|
|
30
|
+
default: {
|
|
31
|
+
getItem: async (key: string) => data.get(key) ?? null,
|
|
32
|
+
setItem: async (key: string, value: string) => {
|
|
33
|
+
data.set(key, value);
|
|
34
|
+
},
|
|
35
|
+
removeItem: async (key: string) => {
|
|
36
|
+
data.delete(key);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
data,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const throwing = (name: string) => () => {
|
|
45
|
+
throw new Error(`${name} unavailable`);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
async function roundTrip(storage: Storage): Promise<{
|
|
49
|
+
afterSet: string | null;
|
|
50
|
+
afterRemove: string | null;
|
|
51
|
+
missing: string | null;
|
|
52
|
+
}> {
|
|
53
|
+
await storage.setItem("k", "v");
|
|
54
|
+
const afterSet = await storage.getItem("k");
|
|
55
|
+
await storage.removeItem("k");
|
|
56
|
+
const afterRemove = await storage.getItem("k");
|
|
57
|
+
const missing = await storage.getItem("never-written");
|
|
58
|
+
return { afterSet, afterRemove, missing };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("resolveNativeStorage", () => {
|
|
62
|
+
it("picks MMKV via the v4 createMMKV() factory and round-trips values", async () => {
|
|
63
|
+
const mmkv = createMMKVv4Module();
|
|
64
|
+
const resolution = resolveNativeStorage({
|
|
65
|
+
loadMMKV: () => mmkv.module,
|
|
66
|
+
loadAsyncStorage: throwing("async-storage"),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(resolution.backend).toBe("mmkv");
|
|
70
|
+
expect(mmkv.createMMKV).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(resolution.errors.mmkv).toBeUndefined();
|
|
72
|
+
await expect(roundTrip(resolution.storage)).resolves.toEqual({
|
|
73
|
+
afterSet: "v",
|
|
74
|
+
afterRemove: null,
|
|
75
|
+
missing: null,
|
|
76
|
+
});
|
|
77
|
+
expect(mmkv.data.size).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("regression: a v3-shaped module (MMKV class, no createMMKV) is not treated as usable", () => {
|
|
81
|
+
// react-native-mmkv v4 exports `MMKV` as a type only; the old
|
|
82
|
+
// `new MMKV()` code threw at runtime and silently fell to memory.
|
|
83
|
+
class FakeV3MMKV {}
|
|
84
|
+
const asyncStorage = createAsyncStorageModule();
|
|
85
|
+
const resolution = resolveNativeStorage({
|
|
86
|
+
loadMMKV: () => ({ MMKV: FakeV3MMKV }),
|
|
87
|
+
loadAsyncStorage: () => asyncStorage.module,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(resolution.backend).toBe("async-storage");
|
|
91
|
+
expect(String(resolution.errors.mmkv)).toContain("createMMKV");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("falls back to AsyncStorage when the MMKV module fails to load", async () => {
|
|
95
|
+
const asyncStorage = createAsyncStorageModule();
|
|
96
|
+
const resolution = resolveNativeStorage({
|
|
97
|
+
loadMMKV: throwing("react-native-mmkv"),
|
|
98
|
+
loadAsyncStorage: () => asyncStorage.module,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(resolution.backend).toBe("async-storage");
|
|
102
|
+
expect(String(resolution.errors.mmkv)).toContain("react-native-mmkv unavailable");
|
|
103
|
+
await expect(roundTrip(resolution.storage)).resolves.toEqual({
|
|
104
|
+
afterSet: "v",
|
|
105
|
+
afterRemove: null,
|
|
106
|
+
missing: null,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("falls back to memory when AsyncStorage has no usable default export", async () => {
|
|
111
|
+
const resolution = resolveNativeStorage({
|
|
112
|
+
loadMMKV: throwing("react-native-mmkv"),
|
|
113
|
+
loadAsyncStorage: () => ({ default: undefined }),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(resolution.backend).toBe("memory");
|
|
117
|
+
expect(resolution.errors.asyncStorage).toBeDefined();
|
|
118
|
+
// Memory storage still honors the Storage contract.
|
|
119
|
+
await expect(roundTrip(resolution.storage)).resolves.toEqual({
|
|
120
|
+
afterSet: "v",
|
|
121
|
+
afterRemove: null,
|
|
122
|
+
missing: null,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("records both loader errors when everything fails", () => {
|
|
127
|
+
const resolution = resolveNativeStorage({
|
|
128
|
+
loadMMKV: throwing("react-native-mmkv"),
|
|
129
|
+
loadAsyncStorage: throwing("async-storage"),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(resolution.backend).toBe("memory");
|
|
133
|
+
expect(String(resolution.errors.mmkv)).toContain("react-native-mmkv unavailable");
|
|
134
|
+
expect(String(resolution.errors.asyncStorage)).toContain("async-storage unavailable");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("logNativeStorageResolution", () => {
|
|
139
|
+
it("logs a single info line when MMKV won", () => {
|
|
140
|
+
const info = vi.fn();
|
|
141
|
+
const warn = vi.fn();
|
|
142
|
+
const mmkv = createMMKVv4Module();
|
|
143
|
+
const resolution = resolveNativeStorage({
|
|
144
|
+
loadMMKV: () => mmkv.module,
|
|
145
|
+
loadAsyncStorage: throwing("async-storage"),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
logNativeStorageResolution(resolution, { info, warn });
|
|
149
|
+
|
|
150
|
+
expect(info).toHaveBeenCalledTimes(1);
|
|
151
|
+
expect(info.mock.calls[0]?.[0]).toContain("backend: mmkv");
|
|
152
|
+
expect(warn).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("warns loudly (once) when persistence degraded to async-storage", () => {
|
|
156
|
+
const info = vi.fn();
|
|
157
|
+
const warn = vi.fn();
|
|
158
|
+
const asyncStorage = createAsyncStorageModule();
|
|
159
|
+
const resolution = resolveNativeStorage({
|
|
160
|
+
loadMMKV: throwing("react-native-mmkv"),
|
|
161
|
+
loadAsyncStorage: () => asyncStorage.module,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
logNativeStorageResolution(resolution, { info, warn });
|
|
165
|
+
|
|
166
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
167
|
+
const message = String(warn.mock.calls[0]?.[0]);
|
|
168
|
+
expect(message).toContain("fell back to async-storage");
|
|
169
|
+
expect(message).toContain("react-native-mmkv unavailable");
|
|
170
|
+
expect(info).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("warns that state will not survive restarts when memory won", () => {
|
|
174
|
+
const info = vi.fn();
|
|
175
|
+
const warn = vi.fn();
|
|
176
|
+
const resolution = resolveNativeStorage({
|
|
177
|
+
loadMMKV: throwing("react-native-mmkv"),
|
|
178
|
+
loadAsyncStorage: throwing("async-storage"),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
logNativeStorageResolution(resolution, { info, warn });
|
|
182
|
+
|
|
183
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
184
|
+
const message = String(warn.mock.calls[0]?.[0]);
|
|
185
|
+
expect(message).toContain("fell back to memory");
|
|
186
|
+
expect(message).toContain("NOT survive app restarts");
|
|
187
|
+
expect(message).toContain("react-native-mmkv unavailable");
|
|
188
|
+
expect(message).toContain("async-storage unavailable");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("module-level storage export", () => {
|
|
193
|
+
it("resolves without crashing outside React Native", async () => {
|
|
194
|
+
// Outside metro/vxrn the default loader chain must settle on SOME backend
|
|
195
|
+
// instead of throwing at import time (in this vitest environment it lands
|
|
196
|
+
// on AsyncStorage's web build over the test setup's no-op localStorage
|
|
197
|
+
// stub, so a round-trip cannot be asserted here — the injected-loader
|
|
198
|
+
// specs above cover the round-trip contract per backend).
|
|
199
|
+
const { storage } = await import("./index.native");
|
|
200
|
+
expect(typeof storage.getItem).toBe("function");
|
|
201
|
+
expect(typeof storage.setItem).toBe("function");
|
|
202
|
+
expect(typeof storage.removeItem).toBe("function");
|
|
203
|
+
await expect(Promise.resolve(storage.setItem("spec-key", "spec-value"))).resolves.not.toThrow();
|
|
204
|
+
await expect(Promise.resolve(storage.removeItem("spec-key"))).resolves.not.toThrow();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native storage twin — backend priority: MMKV (react-native-mmkv v4,
|
|
3
|
+
* synchronous JSI/Nitro) → AsyncStorage (environments without Nitro, e.g.
|
|
4
|
+
* Expo Go) → in-memory Map (nothing survives a restart).
|
|
5
|
+
*
|
|
6
|
+
* react-native-mmkv v4 exports `MMKV` as a TYPE only — the runtime API is
|
|
7
|
+
* the `createMMKV()` factory. The previous implementation destructured the
|
|
8
|
+
* non-existent `MMKV` class (v3 API) and threw `undefined is not a
|
|
9
|
+
* constructor` on every launch, silently landing all native persistence on
|
|
10
|
+
* the in-memory fallback (favorites lost on restart — 2026-08-13 iOS smoke
|
|
11
|
+
* test, p-results/native-smoke/REPORT.md #2). Resolution is therefore
|
|
12
|
+
* factored into a pure, loader-injectable function (spec-covered) and the
|
|
13
|
+
* chosen backend is logged once in dev — a degraded backend can never be
|
|
14
|
+
* silent again.
|
|
15
|
+
*/
|
|
1
16
|
import type { Storage } from "../persist";
|
|
17
|
+
export type NativeStorageBackend = "mmkv" | "async-storage" | "memory";
|
|
18
|
+
/** Module loaders, injectable so specs can drive every resolution branch. */
|
|
19
|
+
export interface NativeStorageLoaders {
|
|
20
|
+
/** Load react-native-mmkv (throws when unavailable). */
|
|
21
|
+
loadMMKV(): unknown;
|
|
22
|
+
/** Load @react-native-async-storage/async-storage (throws when unavailable). */
|
|
23
|
+
loadAsyncStorage(): unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface NativeStorageResolution {
|
|
26
|
+
storage: Storage;
|
|
27
|
+
backend: NativeStorageBackend;
|
|
28
|
+
/** Why higher-priority backends were skipped (surfaced by the dev log). */
|
|
29
|
+
errors: {
|
|
30
|
+
mmkv?: unknown;
|
|
31
|
+
asyncStorage?: unknown;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export declare function resolveNativeStorage(loaders?: NativeStorageLoaders): NativeStorageResolution;
|
|
35
|
+
/**
|
|
36
|
+
* One dev-mode line naming the winning backend — a console.warn when
|
|
37
|
+
* persistence degraded, so a broken storage twin is loud instead of silent.
|
|
38
|
+
*/
|
|
39
|
+
export declare function logNativeStorageResolution(resolution: NativeStorageResolution, logger?: Pick<Console, "info" | "warn">): void;
|
|
2
40
|
export declare const storage: Storage;
|
|
3
41
|
//# sourceMappingURL=index.native.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.native.d.ts","sourceRoot":"","sources":["../../src/storage/index.native.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.native.d.ts","sourceRoot":"","sources":["../../src/storage/index.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,eAAe,GAAG,QAAQ,CAAC;AAevE,6EAA6E;AAC7E,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,QAAQ,IAAI,OAAO,CAAC;IACpB,gFAAgF;IAChF,gBAAgB,IAAI,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,oBAAoB,CAAC;IAC9B,2EAA2E;IAC3E,MAAM,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACpD;AAYD,wBAAgB,oBAAoB,CAClC,OAAO,GAAE,oBAAqC,GAC7C,uBAAuB,CA+DzB;AAMD;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,uBAAuB,EACnC,MAAM,GAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAW,GAC/C,IAAI,CAcN;AASD,eAAO,MAAM,OAAO,EAAE,OAA4B,CAAC"}
|