react-native-nitro-storage 0.12.0 → 0.13.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.
@@ -434,6 +434,7 @@ void HybridStorage::removeBatch(const std::vector<std::string>& keys, double sco
434
434
  }
435
435
 
436
436
  void HybridStorage::removeByPrefix(const std::string& prefix, double scope) {
437
+ toScope(scope);
437
438
  if (prefix.empty()) {
438
439
  return;
439
440
  }
@@ -462,17 +463,23 @@ void HybridStorage::setSecureAccessControl(double level) {
462
463
  throw std::runtime_error("NitroStorage: Invalid access control level");
463
464
  }
464
465
  ensureAdapter();
465
- nativeAdapter_->setSecureAccessControl(intLevel);
466
+ runAdapterOperation(
467
+ [&] { nativeAdapter_->setSecureAccessControl(intLevel); },
468
+ "Secure access control");
466
469
  }
467
470
 
468
471
  void HybridStorage::setSecureWritesAsync(bool enabled) {
469
472
  ensureAdapter();
470
- nativeAdapter_->setSecureWritesAsync(enabled);
473
+ runAdapterOperation(
474
+ [&] { nativeAdapter_->setSecureWritesAsync(enabled); },
475
+ "Secure write mode");
471
476
  }
472
477
 
473
478
  void HybridStorage::setKeychainAccessGroup(const std::string& group) {
474
479
  ensureAdapter();
475
- nativeAdapter_->setKeychainAccessGroup(group);
480
+ runAdapterOperation(
481
+ [&] { nativeAdapter_->setKeychainAccessGroup(group); },
482
+ "Keychain access group");
476
483
  }
477
484
 
478
485
  // --- Biometric ---
@@ -517,7 +524,8 @@ void HybridStorage::deleteSecureBiometric(const std::string& key) {
517
524
 
518
525
  bool HybridStorage::hasSecureBiometric(const std::string& key) {
519
526
  ensureAdapter();
520
- return nativeAdapter_->hasSecureBiometric(key);
527
+ return runAdapterOperation(
528
+ [&] { return nativeAdapter_->hasSecureBiometric(key); }, "Biometric has");
521
529
  }
522
530
 
523
531
  void HybridStorage::clearSecureBiometric() {
@@ -3,10 +3,12 @@
3
3
  #include <sqlite3.h>
4
4
 
5
5
  #include <algorithm>
6
+ #include <cerrno>
6
7
  #include <cstdlib>
7
8
  #include <filesystem>
8
9
  #include <memory>
9
10
  #include <stdexcept>
11
+ #include <system_error>
10
12
 
11
13
  namespace NitroStorage {
12
14
  namespace {
@@ -21,13 +23,19 @@ std::mutex& sharedMutex() {
21
23
  return mutex;
22
24
  }
23
25
 
24
- const char* storageErrorTag(int code) {
26
+ const char* storageErrorTag(sqlite3* db, int code) {
25
27
  switch (code & 0xff) {
26
28
  case SQLITE_FULL:
27
29
  return "[nitro-error:storage_full] ";
28
30
  case SQLITE_CORRUPT:
29
31
  case SQLITE_NOTADB:
30
32
  return "[nitro-error:storage_corruption] ";
33
+ case SQLITE_IOERR: {
34
+ const int systemError = db != nullptr ? sqlite3_system_errno(db) : 0;
35
+ return systemError == ENOSPC || systemError == EDQUOT
36
+ ? "[nitro-error:storage_full] "
37
+ : "";
38
+ }
31
39
  default:
32
40
  return "";
33
41
  }
@@ -36,21 +44,22 @@ const char* storageErrorTag(int code) {
36
44
  [[noreturn]] void throwSqlite(sqlite3* db, const char* operation, int code) {
37
45
  const char* message = db != nullptr ? sqlite3_errmsg(db) : sqlite3_errstr(code);
38
46
  throw std::runtime_error(
39
- std::string(storageErrorTag(code)) + "NitroStorage: Disk SQLite " + operation +
47
+ std::string(storageErrorTag(db, code)) + "NitroStorage: Disk SQLite " + operation +
40
48
  " failed: " + (message != nullptr ? message : "unknown error")
41
49
  );
42
50
  }
43
51
 
44
52
  void bindText(sqlite3_stmt* stmt, int index, const std::string& value) {
45
- const int rc = sqlite3_bind_text(
53
+ const int rc = sqlite3_bind_text64(
46
54
  stmt,
47
55
  index,
48
56
  value.data(),
49
- static_cast<int>(value.size()),
50
- SQLITE_TRANSIENT
57
+ static_cast<sqlite3_uint64>(value.size()),
58
+ SQLITE_TRANSIENT,
59
+ SQLITE_UTF8
51
60
  );
52
61
  if (rc != SQLITE_OK) {
53
- throw std::runtime_error("NitroStorage: Disk SQLite bind failed");
62
+ throwSqlite(sqlite3_db_handle(stmt), "bind", rc);
54
63
  }
55
64
  }
56
65
 
@@ -69,16 +78,23 @@ std::string escapeLikePrefix(const std::string& prefix) {
69
78
  return escaped;
70
79
  }
71
80
 
81
+ std::optional<std::string> prefixUpperBound(const std::string& prefix) {
82
+ std::string upper = prefix;
83
+ while (!upper.empty() && static_cast<unsigned char>(upper.back()) == 0xFF) {
84
+ upper.pop_back();
85
+ }
86
+ if (upper.empty()) {
87
+ return std::nullopt;
88
+ }
89
+ upper.back() = static_cast<char>(static_cast<unsigned char>(upper.back()) + 1);
90
+ return upper;
91
+ }
92
+
72
93
  } // namespace
73
94
 
74
95
  SqliteDiskStore::SqliteDiskStore(std::string path) : path_(std::move(path)) {
75
96
  std::lock_guard<std::mutex> lock(mutex_);
76
- try {
77
- openLocked();
78
- } catch (...) {
79
- closeLocked();
80
- throw;
81
- }
97
+ reopenLocked();
82
98
  }
83
99
 
84
100
  SqliteDiskStore::~SqliteDiskStore() {
@@ -95,6 +111,98 @@ SqliteDiskStore& SqliteDiskStore::shared(const std::string& path) {
95
111
  return *store;
96
112
  }
97
113
 
114
+ bool SqliteDiskStore::isValidUtf8(const std::string& value) {
115
+ const auto* bytes = reinterpret_cast<const unsigned char*>(value.data());
116
+ const size_t size = value.size();
117
+ size_t index = 0;
118
+ while (index < size) {
119
+ const unsigned char lead = bytes[index];
120
+ size_t length = 0;
121
+ unsigned int minimum = 0;
122
+ unsigned int codePoint = 0;
123
+ if (lead < 0x80) {
124
+ index += 1;
125
+ continue;
126
+ } else if ((lead & 0xE0) == 0xC0) {
127
+ length = 2;
128
+ minimum = 0x80;
129
+ codePoint = lead & 0x1Fu;
130
+ } else if ((lead & 0xF0) == 0xE0) {
131
+ length = 3;
132
+ minimum = 0x800;
133
+ codePoint = lead & 0x0Fu;
134
+ } else if ((lead & 0xF8) == 0xF0) {
135
+ length = 4;
136
+ minimum = 0x10000;
137
+ codePoint = lead & 0x07u;
138
+ } else {
139
+ return false;
140
+ }
141
+ if (size - index < length) {
142
+ return false;
143
+ }
144
+ for (size_t offset = 1; offset < length; ++offset) {
145
+ const unsigned char continuation = bytes[index + offset];
146
+ if ((continuation & 0xC0) != 0x80) {
147
+ return false;
148
+ }
149
+ codePoint = (codePoint << 6) | (continuation & 0x3Fu);
150
+ }
151
+ if (codePoint < minimum || codePoint > 0x10FFFF || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
152
+ return false;
153
+ }
154
+ index += length;
155
+ }
156
+ return true;
157
+ }
158
+
159
+ void SqliteDiskStore::recreateShared(const std::string& path) {
160
+ std::lock_guard<std::mutex> lock(sharedMutex());
161
+ auto& store = sharedStore();
162
+ if (store && store->path() == path) {
163
+ store->recreate();
164
+ return;
165
+ }
166
+ store.reset();
167
+ removeDatabaseFiles(path);
168
+ store = std::make_unique<SqliteDiskStore>(path);
169
+ }
170
+
171
+ void SqliteDiskStore::removeDatabaseFiles(const std::string& path) {
172
+ for (const char* suffix : {"", "-wal", "-shm", "-journal"}) {
173
+ std::error_code error;
174
+ std::filesystem::remove(path + suffix, error);
175
+ if (error) {
176
+ throw std::runtime_error(
177
+ "NitroStorage: Disk SQLite clear failed: cannot delete the database file: " +
178
+ error.message()
179
+ );
180
+ }
181
+ }
182
+ }
183
+
184
+ void SqliteDiskStore::recreate() {
185
+ std::lock_guard<std::mutex> lock(mutex_);
186
+ closeLocked();
187
+ removeDatabaseFiles(path_);
188
+ reopenLocked();
189
+ }
190
+
191
+ void SqliteDiskStore::reopenLocked() {
192
+ try {
193
+ openLocked();
194
+ } catch (...) {
195
+ closeLocked();
196
+ throw;
197
+ }
198
+ }
199
+
200
+ void SqliteDiskStore::ensureOpenLocked() {
201
+ if (db_ == nullptr) {
202
+ reopenLocked();
203
+ }
204
+ }
205
+
98
206
  void SqliteDiskStore::resetShared() {
99
207
  std::lock_guard<std::mutex> lock(sharedMutex());
100
208
  sharedStore().reset();
@@ -121,17 +229,22 @@ void SqliteDiskStore::openLocked() {
121
229
  const int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX;
122
230
  const int openRc = sqlite3_open_v2(path_.c_str(), &db, flags, nullptr);
123
231
  if (openRc != SQLITE_OK) {
124
- const int code = openRc;
125
- if (db != nullptr) {
126
- sqlite3_close(db);
232
+ try {
233
+ throwSqlite(db, "open", openRc);
234
+ } catch (...) {
235
+ if (db != nullptr) {
236
+ sqlite3_close(db);
237
+ }
238
+ throw;
127
239
  }
128
- throwSqlite(nullptr, "open", code);
129
240
  }
130
241
  db_ = db;
131
242
  sqlite3_busy_timeout(db_, 5000);
132
243
  execLocked("PRAGMA journal_mode=WAL;");
133
244
  execLocked("PRAGMA synchronous=NORMAL;");
134
245
  execLocked("PRAGMA temp_store=MEMORY;");
246
+ execLocked("PRAGMA wal_autocheckpoint=1000;");
247
+ execLocked("PRAGMA journal_size_limit=32768;");
135
248
  execLocked(
136
249
  "CREATE TABLE IF NOT EXISTS kv ("
137
250
  "key TEXT PRIMARY KEY NOT NULL,"
@@ -149,7 +262,9 @@ void SqliteDiskStore::openLocked() {
149
262
  removeStmt_ = prepareLocked("DELETE FROM kv WHERE key = ?1;");
150
263
  hasStmt_ = prepareLocked("SELECT 1 FROM kv WHERE key = ?1 LIMIT 1;");
151
264
  keysStmt_ = prepareLocked("SELECT key FROM kv;");
152
- prefixStmt_ = prepareLocked("SELECT key FROM kv WHERE key LIKE ?1 ESCAPE '\\';");
265
+ prefixRangeStmt_ = prepareLocked("SELECT key FROM kv WHERE key >= ?1 AND key < ?2;");
266
+ prefixFromStmt_ = prepareLocked("SELECT key FROM kv WHERE key >= ?1;");
267
+ prefixLikeStmt_ = prepareLocked("SELECT key FROM kv WHERE key LIKE ?1 ESCAPE '\\';");
153
268
  sizeStmt_ = prepareLocked("SELECT COUNT(*) FROM kv;");
154
269
  clearStmt_ = prepareLocked("DELETE FROM kv;");
155
270
  insertAbsentStmt_ = prepareLocked(
@@ -171,7 +286,9 @@ void SqliteDiskStore::closeLocked() {
171
286
  finalize(removeStmt_);
172
287
  finalize(hasStmt_);
173
288
  finalize(keysStmt_);
174
- finalize(prefixStmt_);
289
+ finalize(prefixRangeStmt_);
290
+ finalize(prefixFromStmt_);
291
+ finalize(prefixLikeStmt_);
175
292
  finalize(sizeStmt_);
176
293
  finalize(clearStmt_);
177
294
  finalize(insertAbsentStmt_);
@@ -192,9 +309,16 @@ void SqliteDiskStore::execLocked(const char* sql) {
192
309
 
193
310
  void SqliteDiskStore::limitPageCountForTesting(int pages) {
194
311
  std::lock_guard<std::mutex> lock(mutex_);
312
+ ensureOpenLocked();
195
313
  execLocked(("PRAGMA max_page_count=" + std::to_string(pages) + ";").c_str());
196
314
  }
197
315
 
316
+ void SqliteDiskStore::limitValueLengthForTesting(int bytes) {
317
+ std::lock_guard<std::mutex> lock(mutex_);
318
+ ensureOpenLocked();
319
+ sqlite3_limit(db_, SQLITE_LIMIT_LENGTH, bytes);
320
+ }
321
+
198
322
  void SqliteDiskStore::beginLocked() {
199
323
  execLocked("BEGIN IMMEDIATE;");
200
324
  }
@@ -264,21 +388,25 @@ void SqliteDiskStore::removeLocked(const std::string& key) {
264
388
 
265
389
  void SqliteDiskStore::set(const std::string& key, const std::string& value) {
266
390
  std::lock_guard<std::mutex> lock(mutex_);
391
+ ensureOpenLocked();
267
392
  setLocked(key, value);
268
393
  }
269
394
 
270
395
  std::optional<std::string> SqliteDiskStore::get(const std::string& key) {
271
396
  std::lock_guard<std::mutex> lock(mutex_);
397
+ ensureOpenLocked();
272
398
  return getLocked(key);
273
399
  }
274
400
 
275
401
  void SqliteDiskStore::remove(const std::string& key) {
276
402
  std::lock_guard<std::mutex> lock(mutex_);
403
+ ensureOpenLocked();
277
404
  removeLocked(key);
278
405
  }
279
406
 
280
407
  bool SqliteDiskStore::has(const std::string& key) {
281
408
  std::lock_guard<std::mutex> lock(mutex_);
409
+ ensureOpenLocked();
282
410
  sqlite3_reset(hasStmt_);
283
411
  sqlite3_clear_bindings(hasStmt_);
284
412
  bindText(hasStmt_, 1, key);
@@ -298,6 +426,7 @@ void SqliteDiskStore::setBatch(
298
426
  const std::vector<std::string>& values
299
427
  ) {
300
428
  std::lock_guard<std::mutex> lock(mutex_);
429
+ ensureOpenLocked();
301
430
  beginLocked();
302
431
  try {
303
432
  const size_t count = std::min(keys.size(), values.size());
@@ -315,6 +444,7 @@ std::vector<std::optional<std::string>> SqliteDiskStore::getBatch(
315
444
  const std::vector<std::string>& keys
316
445
  ) {
317
446
  std::lock_guard<std::mutex> lock(mutex_);
447
+ ensureOpenLocked();
318
448
  std::vector<std::optional<std::string>> results;
319
449
  results.reserve(keys.size());
320
450
  for (const auto& key : keys) {
@@ -325,6 +455,7 @@ std::vector<std::optional<std::string>> SqliteDiskStore::getBatch(
325
455
 
326
456
  void SqliteDiskStore::removeBatch(const std::vector<std::string>& keys) {
327
457
  std::lock_guard<std::mutex> lock(mutex_);
458
+ ensureOpenLocked();
328
459
  beginLocked();
329
460
  try {
330
461
  for (const auto& key : keys) {
@@ -339,6 +470,7 @@ void SqliteDiskStore::removeBatch(const std::vector<std::string>& keys) {
339
470
 
340
471
  std::vector<std::string> SqliteDiskStore::getAllKeys() {
341
472
  std::lock_guard<std::mutex> lock(mutex_);
473
+ ensureOpenLocked();
342
474
  sqlite3_reset(keysStmt_);
343
475
  std::vector<std::string> keys;
344
476
  while (true) {
@@ -363,21 +495,30 @@ std::vector<std::string> SqliteDiskStore::getAllKeys() {
363
495
 
364
496
  std::vector<std::string> SqliteDiskStore::getKeysByPrefix(const std::string& prefix) {
365
497
  std::lock_guard<std::mutex> lock(mutex_);
366
- sqlite3_reset(prefixStmt_);
367
- sqlite3_clear_bindings(prefixStmt_);
368
- bindText(prefixStmt_, 1, escapeLikePrefix(prefix));
498
+ ensureOpenLocked();
499
+ const bool useRange = isValidUtf8(prefix);
500
+ const auto upperBound = useRange ? prefixUpperBound(prefix) : std::nullopt;
501
+ sqlite3_stmt* stmt = !useRange
502
+ ? prefixLikeStmt_
503
+ : upperBound.has_value() ? prefixRangeStmt_ : prefixFromStmt_;
504
+ sqlite3_reset(stmt);
505
+ sqlite3_clear_bindings(stmt);
506
+ bindText(stmt, 1, useRange ? prefix : escapeLikePrefix(prefix));
507
+ if (upperBound.has_value()) {
508
+ bindText(stmt, 2, *upperBound);
509
+ }
369
510
  std::vector<std::string> keys;
370
511
  while (true) {
371
- const int rc = sqlite3_step(prefixStmt_);
512
+ const int rc = sqlite3_step(stmt);
372
513
  if (rc == SQLITE_DONE) {
373
514
  break;
374
515
  }
375
516
  if (rc != SQLITE_ROW) {
376
- sqlite3_reset(prefixStmt_);
517
+ sqlite3_reset(stmt);
377
518
  throwSqlite(db_, "getKeysByPrefix", rc);
378
519
  }
379
- const unsigned char* text = sqlite3_column_text(prefixStmt_, 0);
380
- const int bytes = sqlite3_column_bytes(prefixStmt_, 0);
520
+ const unsigned char* text = sqlite3_column_text(stmt, 0);
521
+ const int bytes = sqlite3_column_bytes(stmt, 0);
381
522
  std::string key(
382
523
  text != nullptr ? reinterpret_cast<const char*>(text) : "",
383
524
  static_cast<size_t>(bytes)
@@ -386,12 +527,13 @@ std::vector<std::string> SqliteDiskStore::getKeysByPrefix(const std::string& pre
386
527
  keys.push_back(std::move(key));
387
528
  }
388
529
  }
389
- sqlite3_reset(prefixStmt_);
530
+ sqlite3_reset(stmt);
390
531
  return keys;
391
532
  }
392
533
 
393
534
  size_t SqliteDiskStore::size() {
394
535
  std::lock_guard<std::mutex> lock(mutex_);
536
+ ensureOpenLocked();
395
537
  sqlite3_reset(sizeStmt_);
396
538
  const int rc = sqlite3_step(sizeStmt_);
397
539
  if (rc != SQLITE_ROW) {
@@ -405,6 +547,7 @@ size_t SqliteDiskStore::size() {
405
547
 
406
548
  void SqliteDiskStore::clear() {
407
549
  std::lock_guard<std::mutex> lock(mutex_);
550
+ ensureOpenLocked();
408
551
  sqlite3_reset(clearStmt_);
409
552
  const int rc = sqlite3_step(clearStmt_);
410
553
  sqlite3_reset(clearStmt_);
@@ -463,6 +606,7 @@ void SqliteDiskStore::migrateIfAbsent(
463
606
  return;
464
607
  }
465
608
  std::lock_guard<std::mutex> lock(mutex_);
609
+ ensureOpenLocked();
466
610
  beginLocked();
467
611
  try {
468
612
  insertAbsentLocked(entries);
@@ -475,6 +619,7 @@ void SqliteDiskStore::migrateIfAbsent(
475
619
 
476
620
  bool SqliteDiskStore::hasMigrationMarker(const std::string& name) {
477
621
  std::lock_guard<std::mutex> lock(mutex_);
622
+ ensureOpenLocked();
478
623
  return hasMigrationMarkerLocked(name);
479
624
  }
480
625
 
@@ -483,6 +628,7 @@ void SqliteDiskStore::migrateOnce(
483
628
  const std::vector<std::pair<std::string, std::string>>& entries
484
629
  ) {
485
630
  std::lock_guard<std::mutex> lock(mutex_);
631
+ ensureOpenLocked();
486
632
  beginLocked();
487
633
  try {
488
634
  if (hasMigrationMarkerLocked(name)) {
@@ -20,7 +20,10 @@ public:
20
20
 
21
21
  static SqliteDiskStore& shared(const std::string& path);
22
22
  static void resetShared();
23
+ static void recreateShared(const std::string& path);
24
+ static void removeDatabaseFiles(const std::string& path);
23
25
  static std::string defaultPath();
26
+ static bool isValidUtf8(const std::string& value);
24
27
 
25
28
  const std::string& path() const { return path_; }
26
29
 
@@ -44,10 +47,14 @@ public:
44
47
  const std::string& name,
45
48
  const std::vector<std::pair<std::string, std::string>>& entries
46
49
  );
50
+ void recreate();
47
51
  void limitPageCountForTesting(int pages);
52
+ void limitValueLengthForTesting(int bytes);
48
53
 
49
54
  private:
50
55
  void openLocked();
56
+ void reopenLocked();
57
+ void ensureOpenLocked();
51
58
  void closeLocked();
52
59
  void execLocked(const char* sql);
53
60
  void beginLocked();
@@ -68,7 +75,9 @@ private:
68
75
  sqlite3_stmt* removeStmt_ = nullptr;
69
76
  sqlite3_stmt* hasStmt_ = nullptr;
70
77
  sqlite3_stmt* keysStmt_ = nullptr;
71
- sqlite3_stmt* prefixStmt_ = nullptr;
78
+ sqlite3_stmt* prefixRangeStmt_ = nullptr;
79
+ sqlite3_stmt* prefixFromStmt_ = nullptr;
80
+ sqlite3_stmt* prefixLikeStmt_ = nullptr;
72
81
  sqlite3_stmt* sizeStmt_ = nullptr;
73
82
  sqlite3_stmt* clearStmt_ = nullptr;
74
83
  sqlite3_stmt* insertAbsentStmt_ = nullptr;
@@ -276,17 +276,17 @@ the native or web adapter. `isStorageError(error, code)` matches one exact code
276
276
  without parsing platform message text. See [secure-storage.md](secure-storage.md)
277
277
  for recovery semantics.
278
278
 
279
- | Code | Raised when |
280
- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
281
- | `keychain_locked` | The protected store is locked. |
282
- | `authentication_required` | The item needs user authentication, or the user cancelled the prompt. |
283
- | `key_invalidated` | The protecting key was invalidated, for example by a biometric enrolment change. |
284
- | `biometric_unavailable` | The requested biometric level is not available on this device or OS version. |
285
- | `storage_corruption` | Stored secure data could not be decrypted, the Disk database is corrupt, or (Android) the Secure master key or store cannot be created. Do not retry. |
286
- | `storage_compensation_failed` | A multi-step write failed and restoring the previous state also failed. |
287
- | `unsupported` | The operation is not available on this platform or environment. |
288
- | `storage_full` | The device or database is out of space, or the web storage quota is exceeded. Free space before retrying. |
289
- | `invalid_key` | A storage key is empty. |
279
+ | Code | Raised when |
280
+ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
281
+ | `keychain_locked` | The protected store is locked. |
282
+ | `authentication_required` | The item needs user authentication, or the user cancelled the prompt. |
283
+ | `key_invalidated` | The protecting key was invalidated, for example by a biometric enrolment change. |
284
+ | `biometric_unavailable` | The requested biometric level is not available on this device or OS version. |
285
+ | `storage_corruption` | Stored secure data could not be decoded, the Disk database is corrupt, or (Android) the Secure master key or store cannot be created. Do not retry. |
286
+ | `storage_compensation_failed` | A multi-step write failed and restoring the previous state also failed. |
287
+ | `unsupported` | The operation is not available on this platform or environment. |
288
+ | `storage_full` | The device or database is out of space, or the web storage quota is exceeded. Free space before retrying. |
289
+ | `invalid_key` | A storage key is empty. |
290
290
 
291
291
  `StorageCompositeError` and `StorageCompensationError` describe errors that
292
292
  carry a primary failure plus secondary failures from reconciliation or
@@ -28,6 +28,34 @@ wipe leaves no pre-SQLite copy behind. Later Disk reads and writes use SQLite.
28
28
  | RocksDB / LevelDB | Rejected. Heavy native footprint for small preference-sized values. |
29
29
  | yyjson | Rejected for Disk. Typed TTL/JSON envelopes stay in JavaScript. |
30
30
 
31
+ ## Limits and durability
32
+
33
+ - Disk writes wait up to 5 seconds for another connection that holds the
34
+ SQLite write lock. The library uses one connection per process, so this wait
35
+ only happens when another process (for example an app extension in the same
36
+ container) is writing. Reads do not wait in WAL mode. Five seconds gives the
37
+ other writer time to finish and stays below the iOS and Android
38
+ unresponsive-app limits.
39
+ - If the Disk database file is deleted while the app runs, the open connection
40
+ keeps reading and writing the deleted file, and the next launch starts with
41
+ an empty database. The library does not check for this on each call, because
42
+ the check costs a system call on every write. Do not delete
43
+ `nitro-storage-disk.sqlite` yourself; call
44
+ `storage.clear(StorageScope.Disk)`.
45
+ - Each platform keeps its system SQLite checkpoint settings. iOS checkpoints
46
+ every 1000 pages and truncates the WAL to 32 KiB; these values are set in
47
+ code so they do not depend on the SQLite build. Android checkpoints every
48
+ 100 pages with a 512 KiB limit, applied by the Android framework to every
49
+ pooled connection.
50
+ - Android opens the Disk database in WAL mode. If that open fails for a reason
51
+ other than corruption (for example a full device while an older database
52
+ converts to WAL), it opens once without WAL and uses `synchronous=FULL`, so
53
+ reads keep working; the next launch tries WAL again. If WAL is active and the
54
+ device is so full that the WAL shared-memory file cannot be created, reads
55
+ fail with `storage_full` until space is free. iOS has the same limit.
56
+ - With WAL and `synchronous=NORMAL`, an app kill never loses a committed
57
+ write. A power loss can lose the most recent commits.
58
+
31
59
  ## Compatibility
32
60
 
33
61
  - Public Disk APIs (`get`/`set`/`setBatch`/`getKeysByPrefix`/`import`) are