react-native-nitro-storage 0.10.2 → 0.10.4
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 +45 -0
- package/README.md +38 -8
- package/android/CMakeLists.txt +2 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +47 -29
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +12 -25
- package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +174 -0
- package/cpp/core/SqliteDiskStore.cpp +416 -0
- package/cpp/core/SqliteDiskStore.hpp +69 -0
- package/docs/api-reference.md +4 -1
- package/docs/benchmarks.md +37 -0
- package/docs/native-libraries.md +30 -0
- package/docs/secure-storage.md +5 -0
- package/ios/IOSStorageAdapterCpp.mm +146 -52
- 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 +57 -11
- 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 +57 -11
- 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 +65 -14
- package/src/testing.ts +1 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#include "SqliteDiskStore.hpp"
|
|
2
|
+
|
|
3
|
+
#include <sqlite3.h>
|
|
4
|
+
|
|
5
|
+
#include <algorithm>
|
|
6
|
+
#include <cstdlib>
|
|
7
|
+
#include <filesystem>
|
|
8
|
+
#include <memory>
|
|
9
|
+
#include <stdexcept>
|
|
10
|
+
|
|
11
|
+
namespace NitroStorage {
|
|
12
|
+
namespace {
|
|
13
|
+
|
|
14
|
+
std::unique_ptr<SqliteDiskStore>& sharedStore() {
|
|
15
|
+
static std::unique_ptr<SqliteDiskStore> store;
|
|
16
|
+
return store;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
std::mutex& sharedMutex() {
|
|
20
|
+
static std::mutex mutex;
|
|
21
|
+
return mutex;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
[[noreturn]] void throwSqlite(sqlite3* db, const char* operation, int code) {
|
|
25
|
+
const char* message = db != nullptr ? sqlite3_errmsg(db) : sqlite3_errstr(code);
|
|
26
|
+
throw std::runtime_error(
|
|
27
|
+
std::string("NitroStorage: Disk SQLite ") + operation + " failed: " +
|
|
28
|
+
(message != nullptr ? message : "unknown error")
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
void bindText(sqlite3_stmt* stmt, int index, const std::string& value) {
|
|
33
|
+
const int rc = sqlite3_bind_text(
|
|
34
|
+
stmt,
|
|
35
|
+
index,
|
|
36
|
+
value.data(),
|
|
37
|
+
static_cast<int>(value.size()),
|
|
38
|
+
SQLITE_TRANSIENT
|
|
39
|
+
);
|
|
40
|
+
if (rc != SQLITE_OK) {
|
|
41
|
+
throw std::runtime_error("NitroStorage: Disk SQLite bind failed");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
std::string escapeLikePrefix(const std::string& prefix) {
|
|
46
|
+
std::string escaped;
|
|
47
|
+
escaped.reserve(prefix.size() + 1);
|
|
48
|
+
for (const char character : prefix) {
|
|
49
|
+
// SQLite LIKE stops at NUL; exact filtering below handles the full key.
|
|
50
|
+
if (character == '\0') break;
|
|
51
|
+
if (character == '%' || character == '_' || character == '\\') {
|
|
52
|
+
escaped.push_back('\\');
|
|
53
|
+
}
|
|
54
|
+
escaped.push_back(character);
|
|
55
|
+
}
|
|
56
|
+
escaped.push_back('%');
|
|
57
|
+
return escaped;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
} // namespace
|
|
61
|
+
|
|
62
|
+
SqliteDiskStore::SqliteDiskStore(std::string path) : path_(std::move(path)) {
|
|
63
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
64
|
+
openLocked();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
SqliteDiskStore::~SqliteDiskStore() {
|
|
68
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
69
|
+
closeLocked();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
SqliteDiskStore& SqliteDiskStore::shared(const std::string& path) {
|
|
73
|
+
std::lock_guard<std::mutex> lock(sharedMutex());
|
|
74
|
+
auto& store = sharedStore();
|
|
75
|
+
if (!store || store->path() != path) {
|
|
76
|
+
store = std::make_unique<SqliteDiskStore>(path);
|
|
77
|
+
}
|
|
78
|
+
return *store;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
void SqliteDiskStore::resetShared() {
|
|
82
|
+
std::lock_guard<std::mutex> lock(sharedMutex());
|
|
83
|
+
sharedStore().reset();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
std::string SqliteDiskStore::defaultPath() {
|
|
87
|
+
#ifdef __APPLE__
|
|
88
|
+
const char* home = std::getenv("HOME");
|
|
89
|
+
if (home == nullptr || home[0] == '\0') {
|
|
90
|
+
return "nitro-storage-disk.sqlite";
|
|
91
|
+
}
|
|
92
|
+
return std::string(home) + "/Library/Application Support/nitro-storage-disk.sqlite";
|
|
93
|
+
#else
|
|
94
|
+
return "nitro-storage-disk.sqlite";
|
|
95
|
+
#endif
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
void SqliteDiskStore::openLocked() {
|
|
99
|
+
const auto parent = std::filesystem::path(path_).parent_path();
|
|
100
|
+
if (!parent.empty()) {
|
|
101
|
+
std::filesystem::create_directories(parent);
|
|
102
|
+
}
|
|
103
|
+
sqlite3* db = nullptr;
|
|
104
|
+
const int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX;
|
|
105
|
+
const int openRc = sqlite3_open_v2(path_.c_str(), &db, flags, nullptr);
|
|
106
|
+
if (openRc != SQLITE_OK) {
|
|
107
|
+
const int code = openRc;
|
|
108
|
+
if (db != nullptr) {
|
|
109
|
+
sqlite3_close(db);
|
|
110
|
+
}
|
|
111
|
+
throwSqlite(nullptr, "open", code);
|
|
112
|
+
}
|
|
113
|
+
db_ = db;
|
|
114
|
+
sqlite3_busy_timeout(db_, 5000);
|
|
115
|
+
execLocked("PRAGMA journal_mode=WAL;");
|
|
116
|
+
execLocked("PRAGMA synchronous=NORMAL;");
|
|
117
|
+
execLocked("PRAGMA temp_store=MEMORY;");
|
|
118
|
+
execLocked(
|
|
119
|
+
"CREATE TABLE IF NOT EXISTS kv ("
|
|
120
|
+
"key TEXT PRIMARY KEY NOT NULL,"
|
|
121
|
+
"value TEXT NOT NULL"
|
|
122
|
+
");"
|
|
123
|
+
);
|
|
124
|
+
setStmt_ = prepareLocked("INSERT OR REPLACE INTO kv(key, value) VALUES(?1, ?2);");
|
|
125
|
+
getStmt_ = prepareLocked("SELECT value FROM kv WHERE key = ?1;");
|
|
126
|
+
removeStmt_ = prepareLocked("DELETE FROM kv WHERE key = ?1;");
|
|
127
|
+
hasStmt_ = prepareLocked("SELECT 1 FROM kv WHERE key = ?1 LIMIT 1;");
|
|
128
|
+
keysStmt_ = prepareLocked("SELECT key FROM kv;");
|
|
129
|
+
prefixStmt_ = prepareLocked("SELECT key FROM kv WHERE key LIKE ?1 ESCAPE '\\';");
|
|
130
|
+
sizeStmt_ = prepareLocked("SELECT COUNT(*) FROM kv;");
|
|
131
|
+
clearStmt_ = prepareLocked("DELETE FROM kv;");
|
|
132
|
+
insertAbsentStmt_ = prepareLocked(
|
|
133
|
+
"INSERT OR IGNORE INTO kv(key, value) VALUES(?1, ?2);"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
void SqliteDiskStore::closeLocked() {
|
|
138
|
+
auto finalize = [](sqlite3_stmt*& stmt) {
|
|
139
|
+
if (stmt != nullptr) {
|
|
140
|
+
sqlite3_finalize(stmt);
|
|
141
|
+
stmt = nullptr;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
finalize(setStmt_);
|
|
145
|
+
finalize(getStmt_);
|
|
146
|
+
finalize(removeStmt_);
|
|
147
|
+
finalize(hasStmt_);
|
|
148
|
+
finalize(keysStmt_);
|
|
149
|
+
finalize(prefixStmt_);
|
|
150
|
+
finalize(sizeStmt_);
|
|
151
|
+
finalize(clearStmt_);
|
|
152
|
+
finalize(insertAbsentStmt_);
|
|
153
|
+
if (db_ != nullptr) {
|
|
154
|
+
sqlite3_close(db_);
|
|
155
|
+
db_ = nullptr;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
void SqliteDiskStore::execLocked(const char* sql) {
|
|
160
|
+
char* error = nullptr;
|
|
161
|
+
const int rc = sqlite3_exec(db_, sql, nullptr, nullptr, &error);
|
|
162
|
+
if (rc != SQLITE_OK) {
|
|
163
|
+
const std::string message = error != nullptr ? error : "unknown error";
|
|
164
|
+
sqlite3_free(error);
|
|
165
|
+
throw std::runtime_error(
|
|
166
|
+
std::string("NitroStorage: Disk SQLite exec failed: ") + message
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
void SqliteDiskStore::beginLocked() {
|
|
172
|
+
execLocked("BEGIN IMMEDIATE;");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
void SqliteDiskStore::commitLocked() {
|
|
176
|
+
execLocked("COMMIT;");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
void SqliteDiskStore::rollbackLocked() {
|
|
180
|
+
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
sqlite3_stmt* SqliteDiskStore::prepareLocked(const char* sql) {
|
|
184
|
+
sqlite3_stmt* stmt = nullptr;
|
|
185
|
+
const int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
|
|
186
|
+
if (rc != SQLITE_OK) {
|
|
187
|
+
throwSqlite(db_, "prepare", rc);
|
|
188
|
+
}
|
|
189
|
+
return stmt;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
void SqliteDiskStore::setLocked(const std::string& key, const std::string& value) {
|
|
193
|
+
sqlite3_reset(setStmt_);
|
|
194
|
+
sqlite3_clear_bindings(setStmt_);
|
|
195
|
+
bindText(setStmt_, 1, key);
|
|
196
|
+
bindText(setStmt_, 2, value);
|
|
197
|
+
const int rc = sqlite3_step(setStmt_);
|
|
198
|
+
sqlite3_reset(setStmt_);
|
|
199
|
+
if (rc != SQLITE_DONE) {
|
|
200
|
+
throwSqlite(db_, "set", rc);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
std::optional<std::string> SqliteDiskStore::getLocked(const std::string& key) {
|
|
205
|
+
sqlite3_reset(getStmt_);
|
|
206
|
+
sqlite3_clear_bindings(getStmt_);
|
|
207
|
+
bindText(getStmt_, 1, key);
|
|
208
|
+
const int rc = sqlite3_step(getStmt_);
|
|
209
|
+
if (rc == SQLITE_DONE) {
|
|
210
|
+
sqlite3_reset(getStmt_);
|
|
211
|
+
return std::nullopt;
|
|
212
|
+
}
|
|
213
|
+
if (rc != SQLITE_ROW) {
|
|
214
|
+
sqlite3_reset(getStmt_);
|
|
215
|
+
throwSqlite(db_, "get", rc);
|
|
216
|
+
}
|
|
217
|
+
const unsigned char* text = sqlite3_column_text(getStmt_, 0);
|
|
218
|
+
const int bytes = sqlite3_column_bytes(getStmt_, 0);
|
|
219
|
+
std::string value(
|
|
220
|
+
text != nullptr ? reinterpret_cast<const char*>(text) : "",
|
|
221
|
+
static_cast<size_t>(bytes)
|
|
222
|
+
);
|
|
223
|
+
sqlite3_reset(getStmt_);
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
void SqliteDiskStore::removeLocked(const std::string& key) {
|
|
228
|
+
sqlite3_reset(removeStmt_);
|
|
229
|
+
sqlite3_clear_bindings(removeStmt_);
|
|
230
|
+
bindText(removeStmt_, 1, key);
|
|
231
|
+
const int rc = sqlite3_step(removeStmt_);
|
|
232
|
+
sqlite3_reset(removeStmt_);
|
|
233
|
+
if (rc != SQLITE_DONE) {
|
|
234
|
+
throwSqlite(db_, "remove", rc);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
void SqliteDiskStore::set(const std::string& key, const std::string& value) {
|
|
239
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
240
|
+
setLocked(key, value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
std::optional<std::string> SqliteDiskStore::get(const std::string& key) {
|
|
244
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
245
|
+
return getLocked(key);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
void SqliteDiskStore::remove(const std::string& key) {
|
|
249
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
250
|
+
removeLocked(key);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
bool SqliteDiskStore::has(const std::string& key) {
|
|
254
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
255
|
+
sqlite3_reset(hasStmt_);
|
|
256
|
+
sqlite3_clear_bindings(hasStmt_);
|
|
257
|
+
bindText(hasStmt_, 1, key);
|
|
258
|
+
const int rc = sqlite3_step(hasStmt_);
|
|
259
|
+
sqlite3_reset(hasStmt_);
|
|
260
|
+
if (rc == SQLITE_ROW) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
if (rc == SQLITE_DONE) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
throwSqlite(db_, "has", rc);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
void SqliteDiskStore::setBatch(
|
|
270
|
+
const std::vector<std::string>& keys,
|
|
271
|
+
const std::vector<std::string>& values
|
|
272
|
+
) {
|
|
273
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
274
|
+
beginLocked();
|
|
275
|
+
try {
|
|
276
|
+
const size_t count = std::min(keys.size(), values.size());
|
|
277
|
+
for (size_t index = 0; index < count; ++index) {
|
|
278
|
+
setLocked(keys[index], values[index]);
|
|
279
|
+
}
|
|
280
|
+
commitLocked();
|
|
281
|
+
} catch (...) {
|
|
282
|
+
rollbackLocked();
|
|
283
|
+
throw;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
std::vector<std::optional<std::string>> SqliteDiskStore::getBatch(
|
|
288
|
+
const std::vector<std::string>& keys
|
|
289
|
+
) {
|
|
290
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
291
|
+
std::vector<std::optional<std::string>> results;
|
|
292
|
+
results.reserve(keys.size());
|
|
293
|
+
for (const auto& key : keys) {
|
|
294
|
+
results.push_back(getLocked(key));
|
|
295
|
+
}
|
|
296
|
+
return results;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
void SqliteDiskStore::removeBatch(const std::vector<std::string>& keys) {
|
|
300
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
301
|
+
beginLocked();
|
|
302
|
+
try {
|
|
303
|
+
for (const auto& key : keys) {
|
|
304
|
+
removeLocked(key);
|
|
305
|
+
}
|
|
306
|
+
commitLocked();
|
|
307
|
+
} catch (...) {
|
|
308
|
+
rollbackLocked();
|
|
309
|
+
throw;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
std::vector<std::string> SqliteDiskStore::getAllKeys() {
|
|
314
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
315
|
+
sqlite3_reset(keysStmt_);
|
|
316
|
+
std::vector<std::string> keys;
|
|
317
|
+
while (true) {
|
|
318
|
+
const int rc = sqlite3_step(keysStmt_);
|
|
319
|
+
if (rc == SQLITE_DONE) {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
if (rc != SQLITE_ROW) {
|
|
323
|
+
sqlite3_reset(keysStmt_);
|
|
324
|
+
throwSqlite(db_, "getAllKeys", rc);
|
|
325
|
+
}
|
|
326
|
+
const unsigned char* text = sqlite3_column_text(keysStmt_, 0);
|
|
327
|
+
const int bytes = sqlite3_column_bytes(keysStmt_, 0);
|
|
328
|
+
keys.emplace_back(
|
|
329
|
+
text != nullptr ? reinterpret_cast<const char*>(text) : "",
|
|
330
|
+
static_cast<size_t>(bytes)
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
sqlite3_reset(keysStmt_);
|
|
334
|
+
return keys;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
std::vector<std::string> SqliteDiskStore::getKeysByPrefix(const std::string& prefix) {
|
|
338
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
339
|
+
sqlite3_reset(prefixStmt_);
|
|
340
|
+
sqlite3_clear_bindings(prefixStmt_);
|
|
341
|
+
bindText(prefixStmt_, 1, escapeLikePrefix(prefix));
|
|
342
|
+
std::vector<std::string> keys;
|
|
343
|
+
while (true) {
|
|
344
|
+
const int rc = sqlite3_step(prefixStmt_);
|
|
345
|
+
if (rc == SQLITE_DONE) {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
if (rc != SQLITE_ROW) {
|
|
349
|
+
sqlite3_reset(prefixStmt_);
|
|
350
|
+
throwSqlite(db_, "getKeysByPrefix", rc);
|
|
351
|
+
}
|
|
352
|
+
const unsigned char* text = sqlite3_column_text(prefixStmt_, 0);
|
|
353
|
+
const int bytes = sqlite3_column_bytes(prefixStmt_, 0);
|
|
354
|
+
std::string key(
|
|
355
|
+
text != nullptr ? reinterpret_cast<const char*>(text) : "",
|
|
356
|
+
static_cast<size_t>(bytes)
|
|
357
|
+
);
|
|
358
|
+
if (key.compare(0, prefix.size(), prefix) == 0) {
|
|
359
|
+
keys.push_back(std::move(key));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
sqlite3_reset(prefixStmt_);
|
|
363
|
+
return keys;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
size_t SqliteDiskStore::size() {
|
|
367
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
368
|
+
sqlite3_reset(sizeStmt_);
|
|
369
|
+
const int rc = sqlite3_step(sizeStmt_);
|
|
370
|
+
if (rc != SQLITE_ROW) {
|
|
371
|
+
sqlite3_reset(sizeStmt_);
|
|
372
|
+
throwSqlite(db_, "size", rc);
|
|
373
|
+
}
|
|
374
|
+
const size_t count = static_cast<size_t>(sqlite3_column_int64(sizeStmt_, 0));
|
|
375
|
+
sqlite3_reset(sizeStmt_);
|
|
376
|
+
return count;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
void SqliteDiskStore::clear() {
|
|
380
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
381
|
+
sqlite3_reset(clearStmt_);
|
|
382
|
+
const int rc = sqlite3_step(clearStmt_);
|
|
383
|
+
sqlite3_reset(clearStmt_);
|
|
384
|
+
if (rc != SQLITE_DONE) {
|
|
385
|
+
throwSqlite(db_, "clear", rc);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
void SqliteDiskStore::migrateIfAbsent(
|
|
390
|
+
const std::vector<std::pair<std::string, std::string>>& entries
|
|
391
|
+
) {
|
|
392
|
+
if (entries.empty()) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
396
|
+
beginLocked();
|
|
397
|
+
try {
|
|
398
|
+
for (const auto& entry : entries) {
|
|
399
|
+
sqlite3_reset(insertAbsentStmt_);
|
|
400
|
+
sqlite3_clear_bindings(insertAbsentStmt_);
|
|
401
|
+
bindText(insertAbsentStmt_, 1, entry.first);
|
|
402
|
+
bindText(insertAbsentStmt_, 2, entry.second);
|
|
403
|
+
const int rc = sqlite3_step(insertAbsentStmt_);
|
|
404
|
+
sqlite3_reset(insertAbsentStmt_);
|
|
405
|
+
if (rc != SQLITE_DONE) {
|
|
406
|
+
throwSqlite(db_, "migrate", rc);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
commitLocked();
|
|
410
|
+
} catch (...) {
|
|
411
|
+
rollbackLocked();
|
|
412
|
+
throw;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
} // namespace NitroStorage
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <mutex>
|
|
4
|
+
#include <optional>
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <utility>
|
|
7
|
+
#include <vector>
|
|
8
|
+
|
|
9
|
+
struct sqlite3;
|
|
10
|
+
struct sqlite3_stmt;
|
|
11
|
+
|
|
12
|
+
namespace NitroStorage {
|
|
13
|
+
|
|
14
|
+
class SqliteDiskStore {
|
|
15
|
+
public:
|
|
16
|
+
explicit SqliteDiskStore(std::string path);
|
|
17
|
+
SqliteDiskStore(const SqliteDiskStore&) = delete;
|
|
18
|
+
SqliteDiskStore& operator=(const SqliteDiskStore&) = delete;
|
|
19
|
+
~SqliteDiskStore();
|
|
20
|
+
|
|
21
|
+
static SqliteDiskStore& shared(const std::string& path);
|
|
22
|
+
static void resetShared();
|
|
23
|
+
static std::string defaultPath();
|
|
24
|
+
|
|
25
|
+
const std::string& path() const { return path_; }
|
|
26
|
+
|
|
27
|
+
void set(const std::string& key, const std::string& value);
|
|
28
|
+
std::optional<std::string> get(const std::string& key);
|
|
29
|
+
void remove(const std::string& key);
|
|
30
|
+
bool has(const std::string& key);
|
|
31
|
+
void setBatch(
|
|
32
|
+
const std::vector<std::string>& keys,
|
|
33
|
+
const std::vector<std::string>& values
|
|
34
|
+
);
|
|
35
|
+
std::vector<std::optional<std::string>> getBatch(const std::vector<std::string>& keys);
|
|
36
|
+
void removeBatch(const std::vector<std::string>& keys);
|
|
37
|
+
std::vector<std::string> getAllKeys();
|
|
38
|
+
std::vector<std::string> getKeysByPrefix(const std::string& prefix);
|
|
39
|
+
size_t size();
|
|
40
|
+
void clear();
|
|
41
|
+
void migrateIfAbsent(const std::vector<std::pair<std::string, std::string>>& entries);
|
|
42
|
+
|
|
43
|
+
private:
|
|
44
|
+
void openLocked();
|
|
45
|
+
void closeLocked();
|
|
46
|
+
void execLocked(const char* sql);
|
|
47
|
+
void beginLocked();
|
|
48
|
+
void commitLocked();
|
|
49
|
+
void rollbackLocked();
|
|
50
|
+
void setLocked(const std::string& key, const std::string& value);
|
|
51
|
+
std::optional<std::string> getLocked(const std::string& key);
|
|
52
|
+
void removeLocked(const std::string& key);
|
|
53
|
+
sqlite3_stmt* prepareLocked(const char* sql);
|
|
54
|
+
|
|
55
|
+
std::string path_;
|
|
56
|
+
sqlite3* db_ = nullptr;
|
|
57
|
+
sqlite3_stmt* setStmt_ = nullptr;
|
|
58
|
+
sqlite3_stmt* getStmt_ = nullptr;
|
|
59
|
+
sqlite3_stmt* removeStmt_ = nullptr;
|
|
60
|
+
sqlite3_stmt* hasStmt_ = nullptr;
|
|
61
|
+
sqlite3_stmt* keysStmt_ = nullptr;
|
|
62
|
+
sqlite3_stmt* prefixStmt_ = nullptr;
|
|
63
|
+
sqlite3_stmt* sizeStmt_ = nullptr;
|
|
64
|
+
sqlite3_stmt* clearStmt_ = nullptr;
|
|
65
|
+
sqlite3_stmt* insertAbsentStmt_ = nullptr;
|
|
66
|
+
std::mutex mutex_;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
} // namespace NitroStorage
|
package/docs/api-reference.md
CHANGED
|
@@ -342,7 +342,10 @@ Common public types:
|
|
|
342
342
|
|
|
343
343
|
`getCapabilities().writeBuffering` describes real per-mode durability:
|
|
344
344
|
|
|
345
|
-
- Native: Disk writes
|
|
345
|
+
- Native: Disk writes commit in a SQLite WAL transaction. JavaScript can still
|
|
346
|
+
coalesce Disk writes until `flushDiskWrites()`. Secure writes are buffered
|
|
347
|
+
only when `setSecureWritesAsync(true)` is active on Android; iOS Keychain
|
|
348
|
+
writes are synchronous.
|
|
346
349
|
- Web: buffering follows the configured backend; IndexedDB backends are buffered, localStorage backends are synchronous.
|
|
347
350
|
|
|
348
351
|
`describeWebBackendCapabilities(backend)` reports a backend's `buffered`, `flushable`, `closable`, and `subscribable` capabilities from the same typed contract used by the built-in backends.
|
package/docs/benchmarks.md
CHANGED
|
@@ -44,3 +44,40 @@ bun run --cwd packages/react-native-nitro-storage check:pack
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Keep the dry-publish output in the release notes when validating a version locally.
|
|
47
|
+
|
|
48
|
+
## 2026-09-27 Memory enumeration experiment
|
|
49
|
+
|
|
50
|
+
Command: `bun run benchmark:enumeration`. The smoke form, `bun run benchmark:enumeration -- --smoke`, validates fixture execution only.
|
|
51
|
+
|
|
52
|
+
Host: Apple M4 Pro, darwin 27.0.0, Bun 1.4.2. This uses the actual web Memory implementation, not native SQLite or JSI. Each run contains five batches of 30 warm samples per operation and size. Every output key/value is checked outside the timed interval.
|
|
53
|
+
|
|
54
|
+
The comparison starts from the correctness-fixed implementation, not the original release, whose arbitrary-key behavior failed the fixture. It therefore does not establish an overall speedup over the published version.
|
|
55
|
+
|
|
56
|
+
The accepted iteration change removes the repeated Map lookup in `getAll` and the intermediate prefix-key array. Two separate process runs produced these median changes versus the corrected baseline (negative is faster):
|
|
57
|
+
|
|
58
|
+
| Keys | Operation | Run 1 | Run 2 |
|
|
59
|
+
| ------- | ------------- | ------ | ------ |
|
|
60
|
+
| 1,000 | `getAll` | -10.6% | -14.2% |
|
|
61
|
+
| 1,000 | `getByPrefix` | -8.4% | -3.7% |
|
|
62
|
+
| 10,000 | `getAll` | -10.4% | -7.4% |
|
|
63
|
+
| 10,000 | `getByPrefix` | -10.2% | -9.7% |
|
|
64
|
+
| 100,000 | `getAll` | -47.5% | -47.8% |
|
|
65
|
+
| 100,000 | `getByPrefix` | -12.2% | -6.9% |
|
|
66
|
+
|
|
67
|
+
Only the 100k-key `getAll` result clearly and repeatedly exceeds the proposed 10% target: median improved about 47%, with median batch p95 improving 43–48%. Smaller results vary and do not establish repeatable gains above the target.
|
|
68
|
+
|
|
69
|
+
Peak process RSS was 318/339 MB for the corrected baseline and 400/340 MB for the candidate (decimal MB). This high-water metric was noisy and triggers review; source review found no added retained collection or cache. It does not prove equal allocation cost. Native/device performance and retained-memory profiling remain separate acceptance evidence.
|
|
70
|
+
|
|
71
|
+
An earlier ordinary-property shortcut was reverted after a 10k-key regression. An entry-tuple iteration candidate was replaced after elevated peak RSS.
|
|
72
|
+
|
|
73
|
+
Fixture SHA-256: `1ec2feb7ff55d5c61f33e4f70948336ca97d0a7746a77d9c295d7ae4417463f6`. Lockfile SHA-256: `1a6a54f48038afa313fb8a8316611f2091edf8a133bcc16173836599f1831492`. Corrected core SHA-256: `f7e8687b69d93a0fec8318877e6227b8b42ee617b68a257a17e33f83f4492b17`. Candidate core SHA-256: `e8b68c5bb3305b07eeaaff609381ec28ab564424a8fdbb53a8014f6460b6c86e`.
|
|
74
|
+
|
|
75
|
+
Raw reports and environment: `/tmp/nitro-implementation.GbjgqhYj/storage-enumeration-v2-{corrected,candidate3}{,-repeat}.json` and `storage-enumeration-environment.json` on the execution host. These temporary receipts are not portable release artifacts; the table above preserves the selected results. No device or universal speed claim is made.
|
|
76
|
+
|
|
77
|
+
### Original-release control and rejected prefix experiment
|
|
78
|
+
|
|
79
|
+
The additional `--ordinary` mode omits the special prototype key so the original 0.10.3 source and the corrected source can produce identical valid output. Two quiet process comparisons against the archived original revision measured 100k-key `getAll` median improvement of about 42%, with batch p95 improving 43–45%. This is a host web-Memory result only. The correctness-fixed `getByPrefix` was 17–20% slower at 100k keys and 15–23% slower at 1k; no overall prefix speedup is claimed.
|
|
80
|
+
|
|
81
|
+
A subsequent direct prefix scan preserved the observer path and passed independent source review, but was rejected: although 100k prefix medians improved 26–28% against the original, 1k medians regressed 66–67%. The final implementation retains the prior iteration change. The observer mutation/order regression test remains as coverage. Correctness and predictable small-workload behavior take priority over the large-workload result.
|
|
82
|
+
|
|
83
|
+
Original-control receipts: `storage-ordinary-{original,final}{,-repeat}.json` in the evidence directory above (`final` denotes retained candidate3). Rejected-experiment receipts: `storage-ordinary-candidate4{,-repeat}.json`. The ordinary-mode harness extends the fixture; its hash differs from the default-only fixture recorded above. Neither these comparisons nor peak process RSS establish native latency, retained-memory equivalence, or a universal improvement.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Native libraries
|
|
2
|
+
|
|
3
|
+
Disk scope is a SQLite WAL key-value store. Secure scope stays on Keychain and
|
|
4
|
+
EncryptedSharedPreferences. Web Disk stays on the configured web backend
|
|
5
|
+
(IndexedDB or `localStorage`).
|
|
6
|
+
|
|
7
|
+
## Kept
|
|
8
|
+
|
|
9
|
+
| Library | Where | Why |
|
|
10
|
+
| ---------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| SQLite WAL | iOS `SqliteDiskStore`, Android `DiskSqliteStore` | Transactional batch writes, prefix queries without loading a plist/XML map, and crash-safe persistence. Closest Disk engine to a dedicated mmap KV without adding MMKV. |
|
|
12
|
+
|
|
13
|
+
Existing UserDefaults suite keys and Android `NitroStorage` preferences are
|
|
14
|
+
copied into SQLite on first open. Later Disk reads and writes use SQLite.
|
|
15
|
+
|
|
16
|
+
## Evaluated and not shipped
|
|
17
|
+
|
|
18
|
+
| Library | Decision |
|
|
19
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| MMKV | Skipped. Disk still needs a first-party engine; MMKV remains a one-way migration source only. |
|
|
21
|
+
| LMDB | Rejected. Extra vendored C, mmap file-handle limits on mobile, and no system copy on Android/iOS. SQLite is already on both platforms. |
|
|
22
|
+
| RocksDB / LevelDB | Rejected. Heavy native footprint for small preference-sized values. |
|
|
23
|
+
| yyjson | Rejected for Disk. Typed TTL/JSON envelopes stay in JavaScript. |
|
|
24
|
+
|
|
25
|
+
## Compatibility
|
|
26
|
+
|
|
27
|
+
- Public Disk APIs (`get`/`set`/`setBatch`/`getKeysByPrefix`/`import`) are
|
|
28
|
+
unchanged.
|
|
29
|
+
- iOS still runs the suite-domain legacy cutover before SQLite import.
|
|
30
|
+
- Android still uses EncryptedSharedPreferences for Secure scope.
|
package/docs/secure-storage.md
CHANGED
|
@@ -213,6 +213,11 @@ values, and failed persistence therefore remain available for recovery. The
|
|
|
213
213
|
migration is retryable on a later initialization; do not delete the registry
|
|
214
214
|
manually while an upgrade is in progress.
|
|
215
215
|
|
|
216
|
+
After that cutover, suite string keys are imported into the SQLite WAL Disk
|
|
217
|
+
database. New Disk writes go to SQLite. The v1 suite marker is preserved so
|
|
218
|
+
the UserDefaults cutover stays retryable. See
|
|
219
|
+
[native-libraries.md](native-libraries.md).
|
|
220
|
+
|
|
216
221
|
## Web Secure Backend
|
|
217
222
|
|
|
218
223
|
Browsers cannot provide iOS Keychain or Android Keystore guarantees. On web, Secure scope is only as strong as the backend you configure.
|