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
|
@@ -0,0 +1,412 @@
|
|
|
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());
|
|
48
|
+
for (const unsigned char character : prefix) {
|
|
49
|
+
if (character == '%' || character == '_' || character == '\\') {
|
|
50
|
+
escaped.push_back('\\');
|
|
51
|
+
}
|
|
52
|
+
escaped.push_back(static_cast<char>(character));
|
|
53
|
+
}
|
|
54
|
+
escaped.push_back('%');
|
|
55
|
+
return escaped;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
} // namespace
|
|
59
|
+
|
|
60
|
+
SqliteDiskStore::SqliteDiskStore(std::string path) : path_(std::move(path)) {
|
|
61
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
62
|
+
openLocked();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
SqliteDiskStore::~SqliteDiskStore() {
|
|
66
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
67
|
+
closeLocked();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
SqliteDiskStore& SqliteDiskStore::shared(const std::string& path) {
|
|
71
|
+
std::lock_guard<std::mutex> lock(sharedMutex());
|
|
72
|
+
auto& store = sharedStore();
|
|
73
|
+
if (!store || store->path() != path) {
|
|
74
|
+
store = std::make_unique<SqliteDiskStore>(path);
|
|
75
|
+
}
|
|
76
|
+
return *store;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
void SqliteDiskStore::resetShared() {
|
|
80
|
+
std::lock_guard<std::mutex> lock(sharedMutex());
|
|
81
|
+
sharedStore().reset();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
std::string SqliteDiskStore::defaultPath() {
|
|
85
|
+
#ifdef __APPLE__
|
|
86
|
+
const char* home = std::getenv("HOME");
|
|
87
|
+
if (home == nullptr || home[0] == '\0') {
|
|
88
|
+
return "nitro-storage-disk.sqlite";
|
|
89
|
+
}
|
|
90
|
+
return std::string(home) + "/Library/Application Support/nitro-storage-disk.sqlite";
|
|
91
|
+
#else
|
|
92
|
+
return "nitro-storage-disk.sqlite";
|
|
93
|
+
#endif
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
void SqliteDiskStore::openLocked() {
|
|
97
|
+
const auto parent = std::filesystem::path(path_).parent_path();
|
|
98
|
+
if (!parent.empty()) {
|
|
99
|
+
std::filesystem::create_directories(parent);
|
|
100
|
+
}
|
|
101
|
+
sqlite3* db = nullptr;
|
|
102
|
+
const int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX;
|
|
103
|
+
const int openRc = sqlite3_open_v2(path_.c_str(), &db, flags, nullptr);
|
|
104
|
+
if (openRc != SQLITE_OK) {
|
|
105
|
+
const int code = openRc;
|
|
106
|
+
if (db != nullptr) {
|
|
107
|
+
sqlite3_close(db);
|
|
108
|
+
}
|
|
109
|
+
throwSqlite(nullptr, "open", code);
|
|
110
|
+
}
|
|
111
|
+
db_ = db;
|
|
112
|
+
sqlite3_busy_timeout(db_, 5000);
|
|
113
|
+
execLocked("PRAGMA journal_mode=WAL;");
|
|
114
|
+
execLocked("PRAGMA synchronous=NORMAL;");
|
|
115
|
+
execLocked("PRAGMA temp_store=MEMORY;");
|
|
116
|
+
execLocked(
|
|
117
|
+
"CREATE TABLE IF NOT EXISTS kv ("
|
|
118
|
+
"key TEXT PRIMARY KEY NOT NULL,"
|
|
119
|
+
"value TEXT NOT NULL"
|
|
120
|
+
");"
|
|
121
|
+
);
|
|
122
|
+
setStmt_ = prepareLocked("INSERT OR REPLACE INTO kv(key, value) VALUES(?1, ?2);");
|
|
123
|
+
getStmt_ = prepareLocked("SELECT value FROM kv WHERE key = ?1;");
|
|
124
|
+
removeStmt_ = prepareLocked("DELETE FROM kv WHERE key = ?1;");
|
|
125
|
+
hasStmt_ = prepareLocked("SELECT 1 FROM kv WHERE key = ?1 LIMIT 1;");
|
|
126
|
+
keysStmt_ = prepareLocked("SELECT key FROM kv;");
|
|
127
|
+
prefixStmt_ = prepareLocked("SELECT key FROM kv WHERE key LIKE ?1 ESCAPE '\\';");
|
|
128
|
+
sizeStmt_ = prepareLocked("SELECT COUNT(*) FROM kv;");
|
|
129
|
+
clearStmt_ = prepareLocked("DELETE FROM kv;");
|
|
130
|
+
insertAbsentStmt_ = prepareLocked(
|
|
131
|
+
"INSERT OR IGNORE INTO kv(key, value) VALUES(?1, ?2);"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
void SqliteDiskStore::closeLocked() {
|
|
136
|
+
auto finalize = [](sqlite3_stmt*& stmt) {
|
|
137
|
+
if (stmt != nullptr) {
|
|
138
|
+
sqlite3_finalize(stmt);
|
|
139
|
+
stmt = nullptr;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
finalize(setStmt_);
|
|
143
|
+
finalize(getStmt_);
|
|
144
|
+
finalize(removeStmt_);
|
|
145
|
+
finalize(hasStmt_);
|
|
146
|
+
finalize(keysStmt_);
|
|
147
|
+
finalize(prefixStmt_);
|
|
148
|
+
finalize(sizeStmt_);
|
|
149
|
+
finalize(clearStmt_);
|
|
150
|
+
finalize(insertAbsentStmt_);
|
|
151
|
+
if (db_ != nullptr) {
|
|
152
|
+
sqlite3_close(db_);
|
|
153
|
+
db_ = nullptr;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
void SqliteDiskStore::execLocked(const char* sql) {
|
|
158
|
+
char* error = nullptr;
|
|
159
|
+
const int rc = sqlite3_exec(db_, sql, nullptr, nullptr, &error);
|
|
160
|
+
if (rc != SQLITE_OK) {
|
|
161
|
+
const std::string message = error != nullptr ? error : "unknown error";
|
|
162
|
+
sqlite3_free(error);
|
|
163
|
+
throw std::runtime_error(
|
|
164
|
+
std::string("NitroStorage: Disk SQLite exec failed: ") + message
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
void SqliteDiskStore::beginLocked() {
|
|
170
|
+
execLocked("BEGIN IMMEDIATE;");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
void SqliteDiskStore::commitLocked() {
|
|
174
|
+
execLocked("COMMIT;");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
void SqliteDiskStore::rollbackLocked() {
|
|
178
|
+
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
sqlite3_stmt* SqliteDiskStore::prepareLocked(const char* sql) {
|
|
182
|
+
sqlite3_stmt* stmt = nullptr;
|
|
183
|
+
const int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
|
|
184
|
+
if (rc != SQLITE_OK) {
|
|
185
|
+
throwSqlite(db_, "prepare", rc);
|
|
186
|
+
}
|
|
187
|
+
return stmt;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
void SqliteDiskStore::setLocked(const std::string& key, const std::string& value) {
|
|
191
|
+
sqlite3_reset(setStmt_);
|
|
192
|
+
sqlite3_clear_bindings(setStmt_);
|
|
193
|
+
bindText(setStmt_, 1, key);
|
|
194
|
+
bindText(setStmt_, 2, value);
|
|
195
|
+
const int rc = sqlite3_step(setStmt_);
|
|
196
|
+
sqlite3_reset(setStmt_);
|
|
197
|
+
if (rc != SQLITE_DONE) {
|
|
198
|
+
throwSqlite(db_, "set", rc);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
std::optional<std::string> SqliteDiskStore::getLocked(const std::string& key) {
|
|
203
|
+
sqlite3_reset(getStmt_);
|
|
204
|
+
sqlite3_clear_bindings(getStmt_);
|
|
205
|
+
bindText(getStmt_, 1, key);
|
|
206
|
+
const int rc = sqlite3_step(getStmt_);
|
|
207
|
+
if (rc == SQLITE_DONE) {
|
|
208
|
+
sqlite3_reset(getStmt_);
|
|
209
|
+
return std::nullopt;
|
|
210
|
+
}
|
|
211
|
+
if (rc != SQLITE_ROW) {
|
|
212
|
+
sqlite3_reset(getStmt_);
|
|
213
|
+
throwSqlite(db_, "get", rc);
|
|
214
|
+
}
|
|
215
|
+
const unsigned char* text = sqlite3_column_text(getStmt_, 0);
|
|
216
|
+
const int bytes = sqlite3_column_bytes(getStmt_, 0);
|
|
217
|
+
std::string value(
|
|
218
|
+
text != nullptr ? reinterpret_cast<const char*>(text) : "",
|
|
219
|
+
static_cast<size_t>(bytes)
|
|
220
|
+
);
|
|
221
|
+
sqlite3_reset(getStmt_);
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
void SqliteDiskStore::removeLocked(const std::string& key) {
|
|
226
|
+
sqlite3_reset(removeStmt_);
|
|
227
|
+
sqlite3_clear_bindings(removeStmt_);
|
|
228
|
+
bindText(removeStmt_, 1, key);
|
|
229
|
+
const int rc = sqlite3_step(removeStmt_);
|
|
230
|
+
sqlite3_reset(removeStmt_);
|
|
231
|
+
if (rc != SQLITE_DONE) {
|
|
232
|
+
throwSqlite(db_, "remove", rc);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
void SqliteDiskStore::set(const std::string& key, const std::string& value) {
|
|
237
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
238
|
+
setLocked(key, value);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
std::optional<std::string> SqliteDiskStore::get(const std::string& key) {
|
|
242
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
243
|
+
return getLocked(key);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
void SqliteDiskStore::remove(const std::string& key) {
|
|
247
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
248
|
+
removeLocked(key);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
bool SqliteDiskStore::has(const std::string& key) {
|
|
252
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
253
|
+
sqlite3_reset(hasStmt_);
|
|
254
|
+
sqlite3_clear_bindings(hasStmt_);
|
|
255
|
+
bindText(hasStmt_, 1, key);
|
|
256
|
+
const int rc = sqlite3_step(hasStmt_);
|
|
257
|
+
sqlite3_reset(hasStmt_);
|
|
258
|
+
if (rc == SQLITE_ROW) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (rc == SQLITE_DONE) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
throwSqlite(db_, "has", rc);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
void SqliteDiskStore::setBatch(
|
|
268
|
+
const std::vector<std::string>& keys,
|
|
269
|
+
const std::vector<std::string>& values
|
|
270
|
+
) {
|
|
271
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
272
|
+
beginLocked();
|
|
273
|
+
try {
|
|
274
|
+
const size_t count = std::min(keys.size(), values.size());
|
|
275
|
+
for (size_t index = 0; index < count; ++index) {
|
|
276
|
+
setLocked(keys[index], values[index]);
|
|
277
|
+
}
|
|
278
|
+
commitLocked();
|
|
279
|
+
} catch (...) {
|
|
280
|
+
rollbackLocked();
|
|
281
|
+
throw;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
std::vector<std::optional<std::string>> SqliteDiskStore::getBatch(
|
|
286
|
+
const std::vector<std::string>& keys
|
|
287
|
+
) {
|
|
288
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
289
|
+
std::vector<std::optional<std::string>> results;
|
|
290
|
+
results.reserve(keys.size());
|
|
291
|
+
for (const auto& key : keys) {
|
|
292
|
+
results.push_back(getLocked(key));
|
|
293
|
+
}
|
|
294
|
+
return results;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
void SqliteDiskStore::removeBatch(const std::vector<std::string>& keys) {
|
|
298
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
299
|
+
beginLocked();
|
|
300
|
+
try {
|
|
301
|
+
for (const auto& key : keys) {
|
|
302
|
+
removeLocked(key);
|
|
303
|
+
}
|
|
304
|
+
commitLocked();
|
|
305
|
+
} catch (...) {
|
|
306
|
+
rollbackLocked();
|
|
307
|
+
throw;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
std::vector<std::string> SqliteDiskStore::getAllKeys() {
|
|
312
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
313
|
+
sqlite3_reset(keysStmt_);
|
|
314
|
+
std::vector<std::string> keys;
|
|
315
|
+
while (true) {
|
|
316
|
+
const int rc = sqlite3_step(keysStmt_);
|
|
317
|
+
if (rc == SQLITE_DONE) {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
if (rc != SQLITE_ROW) {
|
|
321
|
+
sqlite3_reset(keysStmt_);
|
|
322
|
+
throwSqlite(db_, "getAllKeys", rc);
|
|
323
|
+
}
|
|
324
|
+
const unsigned char* text = sqlite3_column_text(keysStmt_, 0);
|
|
325
|
+
const int bytes = sqlite3_column_bytes(keysStmt_, 0);
|
|
326
|
+
keys.emplace_back(
|
|
327
|
+
text != nullptr ? reinterpret_cast<const char*>(text) : "",
|
|
328
|
+
static_cast<size_t>(bytes)
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
sqlite3_reset(keysStmt_);
|
|
332
|
+
return keys;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
std::vector<std::string> SqliteDiskStore::getKeysByPrefix(const std::string& prefix) {
|
|
336
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
337
|
+
sqlite3_reset(prefixStmt_);
|
|
338
|
+
sqlite3_clear_bindings(prefixStmt_);
|
|
339
|
+
const std::string pattern = escapeLikePrefix(prefix);
|
|
340
|
+
bindText(prefixStmt_, 1, pattern);
|
|
341
|
+
std::vector<std::string> keys;
|
|
342
|
+
while (true) {
|
|
343
|
+
const int rc = sqlite3_step(prefixStmt_);
|
|
344
|
+
if (rc == SQLITE_DONE) {
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
if (rc != SQLITE_ROW) {
|
|
348
|
+
sqlite3_reset(prefixStmt_);
|
|
349
|
+
throwSqlite(db_, "getKeysByPrefix", rc);
|
|
350
|
+
}
|
|
351
|
+
const unsigned char* text = sqlite3_column_text(prefixStmt_, 0);
|
|
352
|
+
const int bytes = sqlite3_column_bytes(prefixStmt_, 0);
|
|
353
|
+
keys.emplace_back(
|
|
354
|
+
text != nullptr ? reinterpret_cast<const char*>(text) : "",
|
|
355
|
+
static_cast<size_t>(bytes)
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
sqlite3_reset(prefixStmt_);
|
|
359
|
+
return keys;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
size_t SqliteDiskStore::size() {
|
|
363
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
364
|
+
sqlite3_reset(sizeStmt_);
|
|
365
|
+
const int rc = sqlite3_step(sizeStmt_);
|
|
366
|
+
if (rc != SQLITE_ROW) {
|
|
367
|
+
sqlite3_reset(sizeStmt_);
|
|
368
|
+
throwSqlite(db_, "size", rc);
|
|
369
|
+
}
|
|
370
|
+
const size_t count = static_cast<size_t>(sqlite3_column_int64(sizeStmt_, 0));
|
|
371
|
+
sqlite3_reset(sizeStmt_);
|
|
372
|
+
return count;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
void SqliteDiskStore::clear() {
|
|
376
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
377
|
+
sqlite3_reset(clearStmt_);
|
|
378
|
+
const int rc = sqlite3_step(clearStmt_);
|
|
379
|
+
sqlite3_reset(clearStmt_);
|
|
380
|
+
if (rc != SQLITE_DONE) {
|
|
381
|
+
throwSqlite(db_, "clear", rc);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
void SqliteDiskStore::migrateIfAbsent(
|
|
386
|
+
const std::vector<std::pair<std::string, std::string>>& entries
|
|
387
|
+
) {
|
|
388
|
+
if (entries.empty()) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
392
|
+
beginLocked();
|
|
393
|
+
try {
|
|
394
|
+
for (const auto& entry : entries) {
|
|
395
|
+
sqlite3_reset(insertAbsentStmt_);
|
|
396
|
+
sqlite3_clear_bindings(insertAbsentStmt_);
|
|
397
|
+
bindText(insertAbsentStmt_, 1, entry.first);
|
|
398
|
+
bindText(insertAbsentStmt_, 2, entry.second);
|
|
399
|
+
const int rc = sqlite3_step(insertAbsentStmt_);
|
|
400
|
+
sqlite3_reset(insertAbsentStmt_);
|
|
401
|
+
if (rc != SQLITE_DONE) {
|
|
402
|
+
throwSqlite(db_, "migrate", rc);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
commitLocked();
|
|
406
|
+
} catch (...) {
|
|
407
|
+
rollbackLocked();
|
|
408
|
+
throw;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
} // 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.
|
|
@@ -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.
|