react-native-nitro-storage 0.10.3 → 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 +14 -0
- package/README.md +23 -2
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +47 -29
- package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +7 -4
- package/cpp/core/SqliteDiskStore.cpp +10 -6
- package/docs/benchmarks.md +37 -0
- package/ios/IOSStorageAdapterCpp.mm +53 -39
- package/lib/commonjs/storage-core.js +26 -11
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/module/storage-core.js +26 -11
- package/lib/module/storage-core.js.map +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/storage-core.ts +32 -14
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
Breaking changes are always listed first in each release section.
|
|
8
8
|
|
|
9
|
+
## [0.10.4] - 2026-09-27
|
|
10
|
+
|
|
11
|
+
### Breaking changes
|
|
12
|
+
|
|
13
|
+
- None.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Disk prefix queries and namespace removal now distinguish ASCII case, so clearing `user` does not remove `User` keys.
|
|
18
|
+
- Android Disk and Secure storage preserve embedded NUL characters in keys and values across scalar and batch operations, preventing truncated-key collisions.
|
|
19
|
+
- iOS Secure storage preserves embedded NUL keys and values, and legacy Disk migration no longer aliases a shorter host-app defaults key when a key contains NUL.
|
|
20
|
+
- Memory prefix reads return the original raw string, including values beginning with the reserved internal prefix.
|
|
21
|
+
- Raw enumeration, export, and set items preserve arbitrary keys such as `__proto__`, `constructor`, and `toString` as own properties.
|
|
22
|
+
|
|
9
23
|
## [0.10.3] - 2026-09-18
|
|
10
24
|
|
|
11
25
|
### Breaking changes
|
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ pagination, conflict resolution, or remote synchronization.
|
|
|
35
35
|
- [Legacy Key Migration And Secure Resilience](#legacy-key-migration-and-secure-resilience)
|
|
36
36
|
- [React Hooks](#react-hooks)
|
|
37
37
|
- [Storage Scopes](#storage-scopes)
|
|
38
|
+
- [Prefix Queries](#prefix-queries)
|
|
38
39
|
- [Secure Storage](#secure-storage)
|
|
39
40
|
- [Batch Operations](#batch-operations)
|
|
40
41
|
- [Events And Observability](#events-and-observability)
|
|
@@ -68,7 +69,7 @@ Nitro peer requirement: `react-native-nitro-modules >=0.37.0 <0.38.0`.
|
|
|
68
69
|
The package gate uses React Native `0.86.3` and the Strict TypeScript API.
|
|
69
70
|
`check:ci` also compiles the public source against React Native `0.87.0`'s
|
|
70
71
|
Strict TypeScript API; this does not change the runtime baseline. The Expo
|
|
71
|
-
example uses Expo SDK `57.0.
|
|
72
|
+
example uses Expo SDK `57.0.25`, React Native
|
|
72
73
|
`0.86.3`, React `19.2.3`, and Nitro Modules `0.37.1`, which is the React Native
|
|
73
74
|
version supported by that Expo SDK. Do not override Expo's React Native version.
|
|
74
75
|
|
|
@@ -77,7 +78,7 @@ before installing this package, then rebuild the native app so the generated
|
|
|
77
78
|
Nitro bindings and native runtime use the same major-minor version:
|
|
78
79
|
|
|
79
80
|
```sh
|
|
80
|
-
bun add react-native-nitro-modules@0.37.1 react-native-nitro-storage@0.10.
|
|
81
|
+
bun add react-native-nitro-modules@0.37.1 react-native-nitro-storage@0.10.4
|
|
81
82
|
bunx expo prebuild
|
|
82
83
|
```
|
|
83
84
|
|
|
@@ -393,6 +394,26 @@ const tokenActions = useStorageActions(tokenItem); // { set, merge, reset, remov
|
|
|
393
394
|
| `StorageScope.Disk` | SQLite WAL on iOS/Android (imports UserDefaults / SharedPreferences once); configured web backend | Preferences, feature flags, onboarding state, and non-secret persisted data. |
|
|
394
395
|
| `StorageScope.Secure` | Keychain on iOS, Android Keystore-backed preferences | Refresh tokens, credentials, API tokens, and biometric-protected values. |
|
|
395
396
|
|
|
397
|
+
## Prefix Queries
|
|
398
|
+
|
|
399
|
+
Prefix queries use literal, case-sensitive matching on every backend. `User::`
|
|
400
|
+
and `user::` are separate namespaces; `%`, `_`, and `\\` are literal characters.
|
|
401
|
+
Raw enumeration returns the original strings and preserves arbitrary keys as own
|
|
402
|
+
properties on an ordinary object, including `__proto__`.
|
|
403
|
+
|
|
404
|
+
Disk and Secure strings preserve embedded NUL characters in keys and values,
|
|
405
|
+
including batch operations. Earlier versions could truncate strings at Android
|
|
406
|
+
JNI or iOS Foundation boundaries; this fix cannot reconstruct previously lost
|
|
407
|
+
suffixes. iOS legacy Disk migration also preserves full keys without aliasing
|
|
408
|
+
shorter host-app defaults keys.
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
storage.setString("User::theme", "dark", StorageScope.Disk);
|
|
412
|
+
storage.setString("user::theme", "light", StorageScope.Disk);
|
|
413
|
+
const settings = storage.getByPrefix("User::", StorageScope.Disk);
|
|
414
|
+
// settings["User::theme"] === "dark"; no lowercase namespace entries.
|
|
415
|
+
```
|
|
416
|
+
|
|
396
417
|
## Secure Storage
|
|
397
418
|
|
|
398
419
|
```ts
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#include "AndroidStorageAdapterCpp.hpp"
|
|
2
2
|
|
|
3
|
+
#include <limits>
|
|
4
|
+
#include <stdexcept>
|
|
5
|
+
|
|
3
6
|
namespace NitroStorage {
|
|
4
7
|
|
|
5
8
|
using namespace facebook::jni;
|
|
@@ -7,10 +10,25 @@ using JavaStringArray = JArrayClass<jstring>;
|
|
|
7
10
|
|
|
8
11
|
namespace {
|
|
9
12
|
|
|
13
|
+
local_ref<JString> toJavaString(const std::string& value) {
|
|
14
|
+
// fbjni's std::string overload uses c_str(), which truncates embedded NUL.
|
|
15
|
+
if (value.find('\0') == std::string::npos) return make_jstring(value);
|
|
16
|
+
if (value.size() > static_cast<size_t>(std::numeric_limits<jsize>::max())) {
|
|
17
|
+
throw std::length_error("Storage string exceeds the Java array limit");
|
|
18
|
+
}
|
|
19
|
+
const auto size = static_cast<jsize>(value.size());
|
|
20
|
+
auto bytes = JArrayByte::newArray(size);
|
|
21
|
+
bytes->setRegion(0, size, reinterpret_cast<const jbyte*>(value.data()));
|
|
22
|
+
static auto constructor = JString::javaClassStatic()->getConstructor<
|
|
23
|
+
jstring(jbyteArray, jstring)>();
|
|
24
|
+
auto charset = make_jstring("UTF-8");
|
|
25
|
+
return JString::javaClassStatic()->newObject(constructor, bytes.get(), charset.get());
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
local_ref<JavaStringArray> toJavaStringArray(const std::vector<std::string>& values) {
|
|
11
29
|
auto javaArray = JavaStringArray::newArray(static_cast<jsize>(values.size()));
|
|
12
30
|
for (size_t i = 0; i < values.size(); ++i) {
|
|
13
|
-
auto javaValue =
|
|
31
|
+
auto javaValue = toJavaString(values[i]);
|
|
14
32
|
javaArray->setElement(static_cast<jsize>(i), javaValue.get());
|
|
15
33
|
}
|
|
16
34
|
return javaArray;
|
|
@@ -61,25 +79,25 @@ AndroidStorageAdapterCpp::~AndroidStorageAdapterCpp() = default;
|
|
|
61
79
|
// --- Disk ---
|
|
62
80
|
|
|
63
81
|
void AndroidStorageAdapterCpp::setDisk(const std::string& key, const std::string& value) {
|
|
64
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
65
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key, value);
|
|
82
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>, alias_ref<JString>)>("setDisk");
|
|
83
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key), toJavaString(value));
|
|
66
84
|
}
|
|
67
85
|
|
|
68
86
|
std::optional<std::string> AndroidStorageAdapterCpp::getDisk(const std::string& key) {
|
|
69
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(
|
|
70
|
-
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
87
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(alias_ref<JString>)>("getDisk");
|
|
88
|
+
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
71
89
|
if (!result) return std::nullopt;
|
|
72
90
|
return result->toStdString();
|
|
73
91
|
}
|
|
74
92
|
|
|
75
93
|
void AndroidStorageAdapterCpp::deleteDisk(const std::string& key) {
|
|
76
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
77
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
94
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>)>("deleteDisk");
|
|
95
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
bool AndroidStorageAdapterCpp::hasDisk(const std::string& key) {
|
|
81
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(
|
|
82
|
-
return method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
99
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(alias_ref<JString>)>("hasDisk");
|
|
100
|
+
return method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysDisk() {
|
|
@@ -92,9 +110,9 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysDisk() {
|
|
|
92
110
|
|
|
93
111
|
std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixDisk(const std::string& prefix) {
|
|
94
112
|
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
|
|
95
|
-
local_ref<JavaStringArray>(
|
|
113
|
+
local_ref<JavaStringArray>(alias_ref<JString>)
|
|
96
114
|
>("getKeysByPrefixDisk");
|
|
97
|
-
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
|
|
115
|
+
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(prefix));
|
|
98
116
|
return fromJavaStringArray(keys);
|
|
99
117
|
}
|
|
100
118
|
|
|
@@ -145,25 +163,25 @@ void AndroidStorageAdapterCpp::clearDisk() {
|
|
|
145
163
|
// --- Secure ---
|
|
146
164
|
|
|
147
165
|
void AndroidStorageAdapterCpp::setSecure(const std::string& key, const std::string& value) {
|
|
148
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
149
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key, value);
|
|
166
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>, alias_ref<JString>)>("setSecure");
|
|
167
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key), toJavaString(value));
|
|
150
168
|
}
|
|
151
169
|
|
|
152
170
|
std::optional<std::string> AndroidStorageAdapterCpp::getSecure(const std::string& key) {
|
|
153
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(
|
|
154
|
-
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
171
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(alias_ref<JString>)>("getSecure");
|
|
172
|
+
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
155
173
|
if (!result) return std::nullopt;
|
|
156
174
|
return result->toStdString();
|
|
157
175
|
}
|
|
158
176
|
|
|
159
177
|
void AndroidStorageAdapterCpp::deleteSecure(const std::string& key) {
|
|
160
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
161
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
178
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>)>("deleteSecure");
|
|
179
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
162
180
|
}
|
|
163
181
|
|
|
164
182
|
bool AndroidStorageAdapterCpp::hasSecure(const std::string& key) {
|
|
165
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(
|
|
166
|
-
return method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
183
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(alias_ref<JString>)>("hasSecure");
|
|
184
|
+
return method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
167
185
|
}
|
|
168
186
|
|
|
169
187
|
std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysSecure() {
|
|
@@ -176,9 +194,9 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysSecure() {
|
|
|
176
194
|
|
|
177
195
|
std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixSecure(const std::string& prefix) {
|
|
178
196
|
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
|
|
179
|
-
local_ref<JavaStringArray>(
|
|
197
|
+
local_ref<JavaStringArray>(alias_ref<JString>)
|
|
180
198
|
>("getKeysByPrefixSecure");
|
|
181
|
-
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
|
|
199
|
+
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(prefix));
|
|
182
200
|
return fromJavaStringArray(keys);
|
|
183
201
|
}
|
|
184
202
|
|
|
@@ -242,25 +260,25 @@ void AndroidStorageAdapterCpp::setSecureBiometric(const std::string& key, const
|
|
|
242
260
|
}
|
|
243
261
|
|
|
244
262
|
void AndroidStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, const std::string& value, int level) {
|
|
245
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
246
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key, value, level);
|
|
263
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>, alias_ref<JString>, jint)>("setSecureBiometricWithLevel");
|
|
264
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key), toJavaString(value), level);
|
|
247
265
|
}
|
|
248
266
|
|
|
249
267
|
std::optional<std::string> AndroidStorageAdapterCpp::getSecureBiometric(const std::string& key) {
|
|
250
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(
|
|
251
|
-
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
268
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(alias_ref<JString>)>("getSecureBiometric");
|
|
269
|
+
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
252
270
|
if (!result) return std::nullopt;
|
|
253
271
|
return result->toStdString();
|
|
254
272
|
}
|
|
255
273
|
|
|
256
274
|
void AndroidStorageAdapterCpp::deleteSecureBiometric(const std::string& key) {
|
|
257
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
258
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
275
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>)>("deleteSecureBiometric");
|
|
276
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
259
277
|
}
|
|
260
278
|
|
|
261
279
|
bool AndroidStorageAdapterCpp::hasSecureBiometric(const std::string& key) {
|
|
262
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(
|
|
263
|
-
return method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
280
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(alias_ref<JString>)>("hasSecureBiometric");
|
|
281
|
+
return method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
264
282
|
}
|
|
265
283
|
|
|
266
284
|
void AndroidStorageAdapterCpp::clearSecureBiometric() {
|
|
@@ -103,9 +103,9 @@ internal class DiskSqliteStore(
|
|
|
103
103
|
fun getKeysByPrefix(prefix: String): Array<String> {
|
|
104
104
|
val pattern = buildString {
|
|
105
105
|
for (character in prefix) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
// LIKE stops at NUL; startsWith below checks the complete prefix.
|
|
107
|
+
if (character == '\u0000') break
|
|
108
|
+
if (character == '%' || character == '_' || character == '\\') append('\\')
|
|
109
109
|
append(character)
|
|
110
110
|
}
|
|
111
111
|
append('%')
|
|
@@ -116,7 +116,10 @@ internal class DiskSqliteStore(
|
|
|
116
116
|
).use { cursor ->
|
|
117
117
|
val keys = ArrayList<String>(cursor.count)
|
|
118
118
|
while (cursor.moveToNext()) {
|
|
119
|
-
|
|
119
|
+
val key = cursor.getString(0)
|
|
120
|
+
if (key.startsWith(prefix)) {
|
|
121
|
+
keys.add(key)
|
|
122
|
+
}
|
|
120
123
|
}
|
|
121
124
|
return keys.toTypedArray()
|
|
122
125
|
}
|
|
@@ -44,12 +44,14 @@ void bindText(sqlite3_stmt* stmt, int index, const std::string& value) {
|
|
|
44
44
|
|
|
45
45
|
std::string escapeLikePrefix(const std::string& prefix) {
|
|
46
46
|
std::string escaped;
|
|
47
|
-
escaped.reserve(prefix.size());
|
|
48
|
-
for (const
|
|
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;
|
|
49
51
|
if (character == '%' || character == '_' || character == '\\') {
|
|
50
52
|
escaped.push_back('\\');
|
|
51
53
|
}
|
|
52
|
-
escaped.push_back(
|
|
54
|
+
escaped.push_back(character);
|
|
53
55
|
}
|
|
54
56
|
escaped.push_back('%');
|
|
55
57
|
return escaped;
|
|
@@ -336,8 +338,7 @@ std::vector<std::string> SqliteDiskStore::getKeysByPrefix(const std::string& pre
|
|
|
336
338
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
337
339
|
sqlite3_reset(prefixStmt_);
|
|
338
340
|
sqlite3_clear_bindings(prefixStmt_);
|
|
339
|
-
|
|
340
|
-
bindText(prefixStmt_, 1, pattern);
|
|
341
|
+
bindText(prefixStmt_, 1, escapeLikePrefix(prefix));
|
|
341
342
|
std::vector<std::string> keys;
|
|
342
343
|
while (true) {
|
|
343
344
|
const int rc = sqlite3_step(prefixStmt_);
|
|
@@ -350,10 +351,13 @@ std::vector<std::string> SqliteDiskStore::getKeysByPrefix(const std::string& pre
|
|
|
350
351
|
}
|
|
351
352
|
const unsigned char* text = sqlite3_column_text(prefixStmt_, 0);
|
|
352
353
|
const int bytes = sqlite3_column_bytes(prefixStmt_, 0);
|
|
353
|
-
|
|
354
|
+
std::string key(
|
|
354
355
|
text != nullptr ? reinterpret_cast<const char*>(text) : "",
|
|
355
356
|
static_cast<size_t>(bytes)
|
|
356
357
|
);
|
|
358
|
+
if (key.compare(0, prefix.size(), prefix) == 0) {
|
|
359
|
+
keys.push_back(std::move(key));
|
|
360
|
+
}
|
|
357
361
|
}
|
|
358
362
|
sqlite3_reset(prefixStmt_);
|
|
359
363
|
return keys;
|
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.
|