react-native-nitro-storage 0.11.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +71 -11
  3. package/SECURITY.md +2 -2
  4. package/android/build.gradle +5 -0
  5. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +12 -13
  6. package/android/src/main/cpp/JniSize.hpp +21 -0
  7. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +124 -16
  8. package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +160 -43
  9. package/cpp/bindings/HybridStorage.cpp +12 -4
  10. package/cpp/core/SqliteDiskStore.cpp +191 -33
  11. package/cpp/core/SqliteDiskStore.hpp +11 -1
  12. package/docs/api-reference.md +11 -10
  13. package/docs/native-libraries.md +28 -0
  14. package/ios/IOSStorageAdapterCpp.mm +89 -22
  15. package/lib/commonjs/index.web.js +2 -1
  16. package/lib/commonjs/index.web.js.map +1 -1
  17. package/lib/commonjs/storage-core.js +26 -6
  18. package/lib/commonjs/storage-core.js.map +1 -1
  19. package/lib/commonjs/storage-runtime.js +1 -1
  20. package/lib/commonjs/storage-runtime.js.map +1 -1
  21. package/lib/module/index.web.js +2 -1
  22. package/lib/module/index.web.js.map +1 -1
  23. package/lib/module/storage-core.js +26 -6
  24. package/lib/module/storage-core.js.map +1 -1
  25. package/lib/module/storage-runtime.js +1 -1
  26. package/lib/module/storage-runtime.js.map +1 -1
  27. package/lib/typescript/index.d.ts.map +1 -1
  28. package/lib/typescript/index.web.d.ts.map +1 -1
  29. package/lib/typescript/storage-core.d.ts.map +1 -1
  30. package/lib/typescript/storage-runtime.d.ts +1 -1
  31. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  32. package/lib/typescript/testing.d.ts.map +1 -1
  33. package/package.json +2 -1
  34. package/src/index.web.ts +5 -1
  35. package/src/storage-core.ts +26 -6
  36. package/src/storage-runtime.ts +2 -0
@@ -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,24 +23,43 @@ std::mutex& sharedMutex() {
21
23
  return mutex;
22
24
  }
23
25
 
26
+ const char* storageErrorTag(sqlite3* db, int code) {
27
+ switch (code & 0xff) {
28
+ case SQLITE_FULL:
29
+ return "[nitro-error:storage_full] ";
30
+ case SQLITE_CORRUPT:
31
+ case SQLITE_NOTADB:
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
+ }
39
+ default:
40
+ return "";
41
+ }
42
+ }
43
+
24
44
  [[noreturn]] void throwSqlite(sqlite3* db, const char* operation, int code) {
25
45
  const char* message = db != nullptr ? sqlite3_errmsg(db) : sqlite3_errstr(code);
26
46
  throw std::runtime_error(
27
- std::string("NitroStorage: Disk SQLite ") + operation + " failed: " +
28
- (message != nullptr ? message : "unknown error")
47
+ std::string(storageErrorTag(db, code)) + "NitroStorage: Disk SQLite " + operation +
48
+ " failed: " + (message != nullptr ? message : "unknown error")
29
49
  );
30
50
  }
31
51
 
32
52
  void bindText(sqlite3_stmt* stmt, int index, const std::string& value) {
33
- const int rc = sqlite3_bind_text(
53
+ const int rc = sqlite3_bind_text64(
34
54
  stmt,
35
55
  index,
36
56
  value.data(),
37
- static_cast<int>(value.size()),
38
- SQLITE_TRANSIENT
57
+ static_cast<sqlite3_uint64>(value.size()),
58
+ SQLITE_TRANSIENT,
59
+ SQLITE_UTF8
39
60
  );
40
61
  if (rc != SQLITE_OK) {
41
- throw std::runtime_error("NitroStorage: Disk SQLite bind failed");
62
+ throwSqlite(sqlite3_db_handle(stmt), "bind", rc);
42
63
  }
43
64
  }
44
65
 
@@ -57,16 +78,23 @@ std::string escapeLikePrefix(const std::string& prefix) {
57
78
  return escaped;
58
79
  }
59
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
+
60
93
  } // namespace
61
94
 
62
95
  SqliteDiskStore::SqliteDiskStore(std::string path) : path_(std::move(path)) {
63
96
  std::lock_guard<std::mutex> lock(mutex_);
64
- try {
65
- openLocked();
66
- } catch (...) {
67
- closeLocked();
68
- throw;
69
- }
97
+ reopenLocked();
70
98
  }
71
99
 
72
100
  SqliteDiskStore::~SqliteDiskStore() {
@@ -83,6 +111,98 @@ SqliteDiskStore& SqliteDiskStore::shared(const std::string& path) {
83
111
  return *store;
84
112
  }
85
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
+
86
206
  void SqliteDiskStore::resetShared() {
87
207
  std::lock_guard<std::mutex> lock(sharedMutex());
88
208
  sharedStore().reset();
@@ -109,17 +229,22 @@ void SqliteDiskStore::openLocked() {
109
229
  const int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX;
110
230
  const int openRc = sqlite3_open_v2(path_.c_str(), &db, flags, nullptr);
111
231
  if (openRc != SQLITE_OK) {
112
- const int code = openRc;
113
- if (db != nullptr) {
114
- sqlite3_close(db);
232
+ try {
233
+ throwSqlite(db, "open", openRc);
234
+ } catch (...) {
235
+ if (db != nullptr) {
236
+ sqlite3_close(db);
237
+ }
238
+ throw;
115
239
  }
116
- throwSqlite(nullptr, "open", code);
117
240
  }
118
241
  db_ = db;
119
242
  sqlite3_busy_timeout(db_, 5000);
120
243
  execLocked("PRAGMA journal_mode=WAL;");
121
244
  execLocked("PRAGMA synchronous=NORMAL;");
122
245
  execLocked("PRAGMA temp_store=MEMORY;");
246
+ execLocked("PRAGMA wal_autocheckpoint=1000;");
247
+ execLocked("PRAGMA journal_size_limit=32768;");
123
248
  execLocked(
124
249
  "CREATE TABLE IF NOT EXISTS kv ("
125
250
  "key TEXT PRIMARY KEY NOT NULL,"
@@ -137,7 +262,9 @@ void SqliteDiskStore::openLocked() {
137
262
  removeStmt_ = prepareLocked("DELETE FROM kv WHERE key = ?1;");
138
263
  hasStmt_ = prepareLocked("SELECT 1 FROM kv WHERE key = ?1 LIMIT 1;");
139
264
  keysStmt_ = prepareLocked("SELECT key FROM kv;");
140
- 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 '\\';");
141
268
  sizeStmt_ = prepareLocked("SELECT COUNT(*) FROM kv;");
142
269
  clearStmt_ = prepareLocked("DELETE FROM kv;");
143
270
  insertAbsentStmt_ = prepareLocked(
@@ -159,7 +286,9 @@ void SqliteDiskStore::closeLocked() {
159
286
  finalize(removeStmt_);
160
287
  finalize(hasStmt_);
161
288
  finalize(keysStmt_);
162
- finalize(prefixStmt_);
289
+ finalize(prefixRangeStmt_);
290
+ finalize(prefixFromStmt_);
291
+ finalize(prefixLikeStmt_);
163
292
  finalize(sizeStmt_);
164
293
  finalize(clearStmt_);
165
294
  finalize(insertAbsentStmt_);
@@ -172,17 +301,24 @@ void SqliteDiskStore::closeLocked() {
172
301
  }
173
302
 
174
303
  void SqliteDiskStore::execLocked(const char* sql) {
175
- char* error = nullptr;
176
- const int rc = sqlite3_exec(db_, sql, nullptr, nullptr, &error);
304
+ const int rc = sqlite3_exec(db_, sql, nullptr, nullptr, nullptr);
177
305
  if (rc != SQLITE_OK) {
178
- const std::string message = error != nullptr ? error : "unknown error";
179
- sqlite3_free(error);
180
- throw std::runtime_error(
181
- std::string("NitroStorage: Disk SQLite exec failed: ") + message
182
- );
306
+ throwSqlite(db_, "exec", rc);
183
307
  }
184
308
  }
185
309
 
310
+ void SqliteDiskStore::limitPageCountForTesting(int pages) {
311
+ std::lock_guard<std::mutex> lock(mutex_);
312
+ ensureOpenLocked();
313
+ execLocked(("PRAGMA max_page_count=" + std::to_string(pages) + ";").c_str());
314
+ }
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
+
186
322
  void SqliteDiskStore::beginLocked() {
187
323
  execLocked("BEGIN IMMEDIATE;");
188
324
  }
@@ -252,21 +388,25 @@ void SqliteDiskStore::removeLocked(const std::string& key) {
252
388
 
253
389
  void SqliteDiskStore::set(const std::string& key, const std::string& value) {
254
390
  std::lock_guard<std::mutex> lock(mutex_);
391
+ ensureOpenLocked();
255
392
  setLocked(key, value);
256
393
  }
257
394
 
258
395
  std::optional<std::string> SqliteDiskStore::get(const std::string& key) {
259
396
  std::lock_guard<std::mutex> lock(mutex_);
397
+ ensureOpenLocked();
260
398
  return getLocked(key);
261
399
  }
262
400
 
263
401
  void SqliteDiskStore::remove(const std::string& key) {
264
402
  std::lock_guard<std::mutex> lock(mutex_);
403
+ ensureOpenLocked();
265
404
  removeLocked(key);
266
405
  }
267
406
 
268
407
  bool SqliteDiskStore::has(const std::string& key) {
269
408
  std::lock_guard<std::mutex> lock(mutex_);
409
+ ensureOpenLocked();
270
410
  sqlite3_reset(hasStmt_);
271
411
  sqlite3_clear_bindings(hasStmt_);
272
412
  bindText(hasStmt_, 1, key);
@@ -286,6 +426,7 @@ void SqliteDiskStore::setBatch(
286
426
  const std::vector<std::string>& values
287
427
  ) {
288
428
  std::lock_guard<std::mutex> lock(mutex_);
429
+ ensureOpenLocked();
289
430
  beginLocked();
290
431
  try {
291
432
  const size_t count = std::min(keys.size(), values.size());
@@ -303,6 +444,7 @@ std::vector<std::optional<std::string>> SqliteDiskStore::getBatch(
303
444
  const std::vector<std::string>& keys
304
445
  ) {
305
446
  std::lock_guard<std::mutex> lock(mutex_);
447
+ ensureOpenLocked();
306
448
  std::vector<std::optional<std::string>> results;
307
449
  results.reserve(keys.size());
308
450
  for (const auto& key : keys) {
@@ -313,6 +455,7 @@ std::vector<std::optional<std::string>> SqliteDiskStore::getBatch(
313
455
 
314
456
  void SqliteDiskStore::removeBatch(const std::vector<std::string>& keys) {
315
457
  std::lock_guard<std::mutex> lock(mutex_);
458
+ ensureOpenLocked();
316
459
  beginLocked();
317
460
  try {
318
461
  for (const auto& key : keys) {
@@ -327,6 +470,7 @@ void SqliteDiskStore::removeBatch(const std::vector<std::string>& keys) {
327
470
 
328
471
  std::vector<std::string> SqliteDiskStore::getAllKeys() {
329
472
  std::lock_guard<std::mutex> lock(mutex_);
473
+ ensureOpenLocked();
330
474
  sqlite3_reset(keysStmt_);
331
475
  std::vector<std::string> keys;
332
476
  while (true) {
@@ -351,21 +495,30 @@ std::vector<std::string> SqliteDiskStore::getAllKeys() {
351
495
 
352
496
  std::vector<std::string> SqliteDiskStore::getKeysByPrefix(const std::string& prefix) {
353
497
  std::lock_guard<std::mutex> lock(mutex_);
354
- sqlite3_reset(prefixStmt_);
355
- sqlite3_clear_bindings(prefixStmt_);
356
- 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
+ }
357
510
  std::vector<std::string> keys;
358
511
  while (true) {
359
- const int rc = sqlite3_step(prefixStmt_);
512
+ const int rc = sqlite3_step(stmt);
360
513
  if (rc == SQLITE_DONE) {
361
514
  break;
362
515
  }
363
516
  if (rc != SQLITE_ROW) {
364
- sqlite3_reset(prefixStmt_);
517
+ sqlite3_reset(stmt);
365
518
  throwSqlite(db_, "getKeysByPrefix", rc);
366
519
  }
367
- const unsigned char* text = sqlite3_column_text(prefixStmt_, 0);
368
- 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);
369
522
  std::string key(
370
523
  text != nullptr ? reinterpret_cast<const char*>(text) : "",
371
524
  static_cast<size_t>(bytes)
@@ -374,12 +527,13 @@ std::vector<std::string> SqliteDiskStore::getKeysByPrefix(const std::string& pre
374
527
  keys.push_back(std::move(key));
375
528
  }
376
529
  }
377
- sqlite3_reset(prefixStmt_);
530
+ sqlite3_reset(stmt);
378
531
  return keys;
379
532
  }
380
533
 
381
534
  size_t SqliteDiskStore::size() {
382
535
  std::lock_guard<std::mutex> lock(mutex_);
536
+ ensureOpenLocked();
383
537
  sqlite3_reset(sizeStmt_);
384
538
  const int rc = sqlite3_step(sizeStmt_);
385
539
  if (rc != SQLITE_ROW) {
@@ -393,6 +547,7 @@ size_t SqliteDiskStore::size() {
393
547
 
394
548
  void SqliteDiskStore::clear() {
395
549
  std::lock_guard<std::mutex> lock(mutex_);
550
+ ensureOpenLocked();
396
551
  sqlite3_reset(clearStmt_);
397
552
  const int rc = sqlite3_step(clearStmt_);
398
553
  sqlite3_reset(clearStmt_);
@@ -451,6 +606,7 @@ void SqliteDiskStore::migrateIfAbsent(
451
606
  return;
452
607
  }
453
608
  std::lock_guard<std::mutex> lock(mutex_);
609
+ ensureOpenLocked();
454
610
  beginLocked();
455
611
  try {
456
612
  insertAbsentLocked(entries);
@@ -463,6 +619,7 @@ void SqliteDiskStore::migrateIfAbsent(
463
619
 
464
620
  bool SqliteDiskStore::hasMigrationMarker(const std::string& name) {
465
621
  std::lock_guard<std::mutex> lock(mutex_);
622
+ ensureOpenLocked();
466
623
  return hasMigrationMarkerLocked(name);
467
624
  }
468
625
 
@@ -471,6 +628,7 @@ void SqliteDiskStore::migrateOnce(
471
628
  const std::vector<std::pair<std::string, std::string>>& entries
472
629
  ) {
473
630
  std::lock_guard<std::mutex> lock(mutex_);
631
+ ensureOpenLocked();
474
632
  beginLocked();
475
633
  try {
476
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,9 +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();
51
+ void limitPageCountForTesting(int pages);
52
+ void limitValueLengthForTesting(int bytes);
47
53
 
48
54
  private:
49
55
  void openLocked();
56
+ void reopenLocked();
57
+ void ensureOpenLocked();
50
58
  void closeLocked();
51
59
  void execLocked(const char* sql);
52
60
  void beginLocked();
@@ -67,7 +75,9 @@ private:
67
75
  sqlite3_stmt* removeStmt_ = nullptr;
68
76
  sqlite3_stmt* hasStmt_ = nullptr;
69
77
  sqlite3_stmt* keysStmt_ = nullptr;
70
- sqlite3_stmt* prefixStmt_ = nullptr;
78
+ sqlite3_stmt* prefixRangeStmt_ = nullptr;
79
+ sqlite3_stmt* prefixFromStmt_ = nullptr;
80
+ sqlite3_stmt* prefixLikeStmt_ = nullptr;
71
81
  sqlite3_stmt* sizeStmt_ = nullptr;
72
82
  sqlite3_stmt* clearStmt_ = nullptr;
73
83
  sqlite3_stmt* insertAbsentStmt_ = nullptr;
@@ -276,16 +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, 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
- | `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. |
289
290
 
290
291
  `StorageCompositeError` and `StorageCompensationError` describe errors that
291
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