react-native-nitro-storage 0.10.2 → 0.10.3
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 +31 -0
- package/README.md +17 -8
- package/android/CMakeLists.txt +2 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +12 -25
- package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +171 -0
- package/cpp/core/SqliteDiskStore.cpp +412 -0
- package/cpp/core/SqliteDiskStore.hpp +69 -0
- package/docs/api-reference.md +4 -1
- package/docs/native-libraries.md +30 -0
- package/docs/secure-storage.md +5 -0
- package/ios/IOSStorageAdapterCpp.mm +98 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +31 -0
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +31 -0
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/testing.js.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +2 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +6 -0
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +2 -1
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +3 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/package.json +1 -1
- package/react-native-nitro-storage.podspec +2 -0
- package/src/index.ts +1 -0
- package/src/index.web.ts +1 -0
- package/src/shared.ts +7 -0
- package/src/storage-core.ts +33 -0
- package/src/testing.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
Breaking changes are always listed first in each release section.
|
|
8
8
|
|
|
9
|
+
## [0.10.3] - 2026-09-18
|
|
10
|
+
|
|
11
|
+
### Breaking changes
|
|
12
|
+
|
|
13
|
+
- None.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `storage.getCacheMetrics()` reports live Disk/Secure raw-cache hits, misses,
|
|
18
|
+
entries, and estimated bytes. The cache stays unbounded; reset zeros the
|
|
19
|
+
counters only.
|
|
20
|
+
- Example integrity, keychain, and Disk/Secure stress labs at
|
|
21
|
+
`nitrostorage://e2e-integrity`, `nitrostorage://e2e-keychain`, and
|
|
22
|
+
`nitrostorage://e2e-stress`.
|
|
23
|
+
- Native Disk now uses SQLite WAL (`SqliteDiskStore` on iOS, `DiskSqliteStore`
|
|
24
|
+
on Android). Existing UserDefaults suite keys and `NitroStorage`
|
|
25
|
+
SharedPreferences values are imported once. See
|
|
26
|
+
[docs/native-libraries.md](docs/native-libraries.md).
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- The example Expo pin follows SDK 57.0.24 (`expo-doctor` / `expo install --check`).
|
|
31
|
+
React Native stays `0.86.3`.
|
|
32
|
+
- The example iOS host uses a `SceneDelegate` so the app can present a window on
|
|
33
|
+
iOS 27 physical devices.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Android Disk no longer crashes on first open when enabling SQLite WAL:
|
|
38
|
+
`PRAGMA journal_mode=WAL` is applied with `rawQuery` instead of `execSQL`.
|
|
39
|
+
|
|
9
40
|
## [0.10.2] - 2026-09-10
|
|
10
41
|
|
|
11
42
|
### Breaking changes
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ Nitro peer requirement: `react-native-nitro-modules >=0.37.0 <0.38.0`.
|
|
|
68
68
|
The package gate uses React Native `0.86.3` and the Strict TypeScript API.
|
|
69
69
|
`check:ci` also compiles the public source against React Native `0.87.0`'s
|
|
70
70
|
Strict TypeScript API; this does not change the runtime baseline. The Expo
|
|
71
|
-
example uses Expo SDK `57.0.
|
|
71
|
+
example uses Expo SDK `57.0.24`, React Native
|
|
72
72
|
`0.86.3`, React `19.2.3`, and Nitro Modules `0.37.1`, which is the React Native
|
|
73
73
|
version supported by that Expo SDK. Do not override Expo's React Native version.
|
|
74
74
|
|
|
@@ -77,7 +77,7 @@ before installing this package, then rebuild the native app so the generated
|
|
|
77
77
|
Nitro bindings and native runtime use the same major-minor version:
|
|
78
78
|
|
|
79
79
|
```sh
|
|
80
|
-
bun add react-native-nitro-modules@0.37.1 react-native-nitro-storage@0.10.
|
|
80
|
+
bun add react-native-nitro-modules@0.37.1 react-native-nitro-storage@0.10.3
|
|
81
81
|
bunx expo prebuild
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -387,11 +387,11 @@ const tokenActions = useStorageActions(tokenItem); // { set, merge, reset, remov
|
|
|
387
387
|
|
|
388
388
|
## Storage Scopes
|
|
389
389
|
|
|
390
|
-
| Scope | Backing store
|
|
391
|
-
| --------------------- |
|
|
392
|
-
| `StorageScope.Memory` | In-process memory
|
|
393
|
-
| `StorageScope.Disk` |
|
|
394
|
-
| `StorageScope.Secure` | Keychain on iOS, Android Keystore-backed preferences
|
|
390
|
+
| Scope | Backing store | Use it for |
|
|
391
|
+
| --------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
392
|
+
| `StorageScope.Memory` | In-process memory | Session-only state, fast counters, and render-time caches. |
|
|
393
|
+
| `StorageScope.Disk` | SQLite WAL on iOS/Android (imports UserDefaults / SharedPreferences once); configured web backend | Preferences, feature flags, onboarding state, and non-secret persisted data. |
|
|
394
|
+
| `StorageScope.Secure` | Keychain on iOS, Android Keystore-backed preferences | Refresh tokens, credentials, API tokens, and biometric-protected values. |
|
|
395
395
|
|
|
396
396
|
## Secure Storage
|
|
397
397
|
|
|
@@ -520,13 +520,21 @@ storage.setMetricsObserver((event) => {
|
|
|
520
520
|
|
|
521
521
|
const metrics = storage.getMetricsSnapshot();
|
|
522
522
|
const scopedMetrics = storage.getScopedMetricsSnapshot();
|
|
523
|
+
const cacheMetrics = storage.getCacheMetrics();
|
|
523
524
|
storage.resetMetrics();
|
|
524
525
|
unsubscribe();
|
|
525
526
|
```
|
|
526
527
|
|
|
527
528
|
`getMetricsSnapshot()` aggregates each operation across scopes for backward
|
|
528
529
|
compatibility. `getScopedMetricsSnapshot()` adds the numeric scope suffix for
|
|
529
|
-
per-scope analysis, for example `item:set:1`.
|
|
530
|
+
per-scope analysis, for example `item:set:1`. `getCacheMetrics()` reports live
|
|
531
|
+
Disk/Secure raw-cache hits, misses, entries, and estimated bytes. The cache is
|
|
532
|
+
unbounded; `resetMetrics()` zeros the hit/miss counters and leaves entries in
|
|
533
|
+
place.
|
|
534
|
+
|
|
535
|
+
The example app includes hidden integrity, keychain, and Disk/Secure stress
|
|
536
|
+
labs at `nitrostorage://e2e-integrity`, `nitrostorage://e2e-keychain`, and
|
|
537
|
+
`nitrostorage://e2e-stress`.
|
|
530
538
|
|
|
531
539
|
Secure event observer values are redacted by default. Pass
|
|
532
540
|
`{ redactSecureValues: false }` only in trusted debug tooling where raw values
|
|
@@ -674,6 +682,7 @@ the error for diagnostics.
|
|
|
674
682
|
| Web backends | [docs/web-backends.md](docs/web-backends.md) |
|
|
675
683
|
| Batch, transactions, and migrations | [docs/batch-transactions-migrations.md](docs/batch-transactions-migrations.md) |
|
|
676
684
|
| MMKV migration | [docs/mmkv-migration.md](docs/mmkv-migration.md) |
|
|
685
|
+
| Native libraries | [docs/native-libraries.md](docs/native-libraries.md) |
|
|
677
686
|
| Recipes | [docs/recipes.md](docs/recipes.md) |
|
|
678
687
|
| Benchmarks | [docs/benchmarks.md](docs/benchmarks.md) |
|
|
679
688
|
| Security policy | [SECURITY.md](SECURITY.md) |
|
package/android/CMakeLists.txt
CHANGED
|
@@ -13,6 +13,8 @@ file(GLOB SOURCES
|
|
|
13
13
|
)
|
|
14
14
|
# Unit/integration C++ tests define `main()` and must never be linked into the Android shared library.
|
|
15
15
|
list(FILTER SOURCES EXCLUDE REGEX ".*/[^/]*Test\\.cpp$")
|
|
16
|
+
# iOS/macOS Disk SQLite lives in this translation unit; Android uses DiskSqliteStore.kt.
|
|
17
|
+
list(FILTER SOURCES EXCLUDE REGEX ".*/SqliteDiskStore\\.cpp$")
|
|
16
18
|
|
|
17
19
|
# 2. Create the library target
|
|
18
20
|
add_library(
|
|
@@ -61,6 +61,7 @@ private fun Throwable.wrapStorageException(
|
|
|
61
61
|
class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
62
62
|
private val sharedPreferences: SharedPreferences =
|
|
63
63
|
context.getSharedPreferences("NitroStorage", Context.MODE_PRIVATE)
|
|
64
|
+
private val diskStore: DiskSqliteStore = DiskSqliteStore(context, sharedPreferences)
|
|
64
65
|
|
|
65
66
|
private val masterKeyAlias = "${context.packageName}.nitro_storage.master_key"
|
|
66
67
|
|
|
@@ -519,71 +520,57 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
519
520
|
|
|
520
521
|
@JvmStatic
|
|
521
522
|
fun setDisk(key: String, value: String) {
|
|
522
|
-
getInstanceOrThrow().
|
|
523
|
+
getInstanceOrThrow().diskStore.set(key, value)
|
|
523
524
|
}
|
|
524
525
|
|
|
525
526
|
@JvmStatic
|
|
526
527
|
fun setDiskBatch(keys: Array<String>, values: Array<String>) {
|
|
527
|
-
|
|
528
|
-
val count = minOf(keys.size, values.size)
|
|
529
|
-
for (index in 0 until count) {
|
|
530
|
-
editor.putString(keys[index], values[index])
|
|
531
|
-
}
|
|
532
|
-
editor.apply()
|
|
528
|
+
getInstanceOrThrow().diskStore.setBatch(keys, values)
|
|
533
529
|
}
|
|
534
530
|
|
|
535
531
|
@JvmStatic
|
|
536
532
|
fun getDisk(key: String): String? {
|
|
537
|
-
return getInstanceOrThrow().
|
|
533
|
+
return getInstanceOrThrow().diskStore.get(key)
|
|
538
534
|
}
|
|
539
535
|
|
|
540
536
|
@JvmStatic
|
|
541
537
|
fun getDiskBatch(keys: Array<String>): Array<String?> {
|
|
542
|
-
|
|
543
|
-
return Array(keys.size) { index ->
|
|
544
|
-
prefs.getString(keys[index], null)
|
|
545
|
-
}
|
|
538
|
+
return getInstanceOrThrow().diskStore.getBatch(keys)
|
|
546
539
|
}
|
|
547
540
|
|
|
548
541
|
@JvmStatic
|
|
549
542
|
fun deleteDisk(key: String) {
|
|
550
|
-
getInstanceOrThrow().
|
|
543
|
+
getInstanceOrThrow().diskStore.remove(key)
|
|
551
544
|
}
|
|
552
545
|
|
|
553
546
|
@JvmStatic
|
|
554
547
|
fun deleteDiskBatch(keys: Array<String>) {
|
|
555
|
-
|
|
556
|
-
for (key in keys) {
|
|
557
|
-
editor.remove(key)
|
|
558
|
-
}
|
|
559
|
-
editor.apply()
|
|
548
|
+
getInstanceOrThrow().diskStore.removeBatch(keys)
|
|
560
549
|
}
|
|
561
550
|
|
|
562
551
|
@JvmStatic
|
|
563
552
|
fun hasDisk(key: String): Boolean {
|
|
564
|
-
return getInstanceOrThrow().
|
|
553
|
+
return getInstanceOrThrow().diskStore.has(key)
|
|
565
554
|
}
|
|
566
555
|
|
|
567
556
|
@JvmStatic
|
|
568
557
|
fun getAllKeysDisk(): Array<String> {
|
|
569
|
-
return getInstanceOrThrow().
|
|
558
|
+
return getInstanceOrThrow().diskStore.getAllKeys()
|
|
570
559
|
}
|
|
571
560
|
|
|
572
561
|
@JvmStatic
|
|
573
562
|
fun getKeysByPrefixDisk(prefix: String): Array<String> {
|
|
574
|
-
return getInstanceOrThrow().
|
|
575
|
-
.filter { it.startsWith(prefix) }
|
|
576
|
-
.toTypedArray()
|
|
563
|
+
return getInstanceOrThrow().diskStore.getKeysByPrefix(prefix)
|
|
577
564
|
}
|
|
578
565
|
|
|
579
566
|
@JvmStatic
|
|
580
567
|
fun sizeDisk(): Int {
|
|
581
|
-
return getInstanceOrThrow().
|
|
568
|
+
return getInstanceOrThrow().diskStore.size()
|
|
582
569
|
}
|
|
583
570
|
|
|
584
571
|
@JvmStatic
|
|
585
572
|
fun clearDisk() {
|
|
586
|
-
getInstanceOrThrow().
|
|
573
|
+
getInstanceOrThrow().diskStore.clear()
|
|
587
574
|
}
|
|
588
575
|
|
|
589
576
|
// --- Secure (async apply by default, sync commit when requested) ---
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
package com.nitrostorage
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import android.database.sqlite.SQLiteDatabase
|
|
6
|
+
import java.io.File
|
|
7
|
+
|
|
8
|
+
internal class DiskSqliteStore(
|
|
9
|
+
context: Context,
|
|
10
|
+
private val legacyPreferences: SharedPreferences,
|
|
11
|
+
) {
|
|
12
|
+
private val db: SQLiteDatabase
|
|
13
|
+
|
|
14
|
+
init {
|
|
15
|
+
val file = File(context.filesDir, DATABASE_NAME)
|
|
16
|
+
db = SQLiteDatabase.openOrCreateDatabase(file, null)
|
|
17
|
+
// PRAGMA journal_mode returns a row; Android forbids result-bearing SQL on execSQL.
|
|
18
|
+
db.rawQuery("PRAGMA journal_mode=WAL", null).close()
|
|
19
|
+
db.execSQL("PRAGMA synchronous=NORMAL")
|
|
20
|
+
db.execSQL(
|
|
21
|
+
"CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL)",
|
|
22
|
+
)
|
|
23
|
+
db.execSQL(
|
|
24
|
+
"CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY NOT NULL, v TEXT NOT NULL)",
|
|
25
|
+
)
|
|
26
|
+
migrateLegacyPreferences()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Synchronized
|
|
30
|
+
fun set(key: String, value: String) {
|
|
31
|
+
db.execSQL(
|
|
32
|
+
"INSERT OR REPLACE INTO kv(key, value) VALUES(?, ?)",
|
|
33
|
+
arrayOf(key, value),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Synchronized
|
|
38
|
+
fun get(key: String): String? {
|
|
39
|
+
db.rawQuery("SELECT value FROM kv WHERE key = ? LIMIT 1", arrayOf(key)).use { cursor ->
|
|
40
|
+
return if (cursor.moveToFirst()) cursor.getString(0) else null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Synchronized
|
|
45
|
+
fun remove(key: String) {
|
|
46
|
+
db.execSQL("DELETE FROM kv WHERE key = ?", arrayOf(key))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Synchronized
|
|
50
|
+
fun has(key: String): Boolean {
|
|
51
|
+
db.rawQuery("SELECT 1 FROM kv WHERE key = ? LIMIT 1", arrayOf(key)).use { cursor ->
|
|
52
|
+
return cursor.moveToFirst()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Synchronized
|
|
57
|
+
fun setBatch(keys: Array<String>, values: Array<String>) {
|
|
58
|
+
val count = minOf(keys.size, values.size)
|
|
59
|
+
db.beginTransaction()
|
|
60
|
+
try {
|
|
61
|
+
for (index in 0 until count) {
|
|
62
|
+
db.execSQL(
|
|
63
|
+
"INSERT OR REPLACE INTO kv(key, value) VALUES(?, ?)",
|
|
64
|
+
arrayOf(keys[index], values[index]),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
db.setTransactionSuccessful()
|
|
68
|
+
} finally {
|
|
69
|
+
db.endTransaction()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Synchronized
|
|
74
|
+
fun getBatch(keys: Array<String>): Array<String?> {
|
|
75
|
+
return Array(keys.size) { index -> get(keys[index]) }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Synchronized
|
|
79
|
+
fun removeBatch(keys: Array<String>) {
|
|
80
|
+
db.beginTransaction()
|
|
81
|
+
try {
|
|
82
|
+
for (key in keys) {
|
|
83
|
+
db.execSQL("DELETE FROM kv WHERE key = ?", arrayOf(key))
|
|
84
|
+
}
|
|
85
|
+
db.setTransactionSuccessful()
|
|
86
|
+
} finally {
|
|
87
|
+
db.endTransaction()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Synchronized
|
|
92
|
+
fun getAllKeys(): Array<String> {
|
|
93
|
+
db.rawQuery("SELECT key FROM kv", null).use { cursor ->
|
|
94
|
+
val keys = ArrayList<String>(cursor.count)
|
|
95
|
+
while (cursor.moveToNext()) {
|
|
96
|
+
keys.add(cursor.getString(0))
|
|
97
|
+
}
|
|
98
|
+
return keys.toTypedArray()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Synchronized
|
|
103
|
+
fun getKeysByPrefix(prefix: String): Array<String> {
|
|
104
|
+
val pattern = buildString {
|
|
105
|
+
for (character in prefix) {
|
|
106
|
+
if (character == '%' || character == '_' || character == '\\') {
|
|
107
|
+
append('\\')
|
|
108
|
+
}
|
|
109
|
+
append(character)
|
|
110
|
+
}
|
|
111
|
+
append('%')
|
|
112
|
+
}
|
|
113
|
+
db.rawQuery(
|
|
114
|
+
"SELECT key FROM kv WHERE key LIKE ? ESCAPE '\\'",
|
|
115
|
+
arrayOf(pattern),
|
|
116
|
+
).use { cursor ->
|
|
117
|
+
val keys = ArrayList<String>(cursor.count)
|
|
118
|
+
while (cursor.moveToNext()) {
|
|
119
|
+
keys.add(cursor.getString(0))
|
|
120
|
+
}
|
|
121
|
+
return keys.toTypedArray()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@Synchronized
|
|
126
|
+
fun size(): Int {
|
|
127
|
+
db.rawQuery("SELECT COUNT(*) FROM kv", null).use { cursor ->
|
|
128
|
+
return if (cursor.moveToFirst()) cursor.getInt(0) else 0
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Synchronized
|
|
133
|
+
fun clear() {
|
|
134
|
+
db.execSQL("DELETE FROM kv")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private fun migrateLegacyPreferences() {
|
|
138
|
+
db.rawQuery(
|
|
139
|
+
"SELECT v FROM meta WHERE k = ? LIMIT 1",
|
|
140
|
+
arrayOf(PREFS_MIGRATION_KEY),
|
|
141
|
+
).use { cursor ->
|
|
142
|
+
if (cursor.moveToFirst() && cursor.getString(0) == "1") {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
db.beginTransaction()
|
|
148
|
+
try {
|
|
149
|
+
for ((key, value) in legacyPreferences.all) {
|
|
150
|
+
if (value is String) {
|
|
151
|
+
db.execSQL(
|
|
152
|
+
"INSERT OR IGNORE INTO kv(key, value) VALUES(?, ?)",
|
|
153
|
+
arrayOf(key, value),
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
db.execSQL(
|
|
158
|
+
"INSERT OR REPLACE INTO meta(k, v) VALUES(?, ?)",
|
|
159
|
+
arrayOf(PREFS_MIGRATION_KEY, "1"),
|
|
160
|
+
)
|
|
161
|
+
db.setTransactionSuccessful()
|
|
162
|
+
} finally {
|
|
163
|
+
db.endTransaction()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private companion object {
|
|
168
|
+
const val DATABASE_NAME = "nitro-storage-disk.sqlite"
|
|
169
|
+
const val PREFS_MIGRATION_KEY = "prefs_v1"
|
|
170
|
+
}
|
|
171
|
+
}
|