Skip to content

Commit 2cb1a93

Browse files
fix: resolve keychain locked migration crash and improve storage functionality
1 parent 1bd1262 commit 2cb1a93

6 files changed

Lines changed: 134 additions & 94 deletions

File tree

apps/example/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"userInterfaceStyle": "automatic",
1010
"ios": {
1111
"supportsTablet": true,
12-
"bundleIdentifier": "com.nitrostorage.example"
12+
"bundleIdentifier": "com.nitrostorage.example",
1313
},
1414
"android": {
1515
"package": "com.nitrostorage.example"

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
"test:coverage": "turbo run test:coverage --filter=react-native-nitro-storage",
2121
"benchmark": "turbo run benchmark --filter=react-native-nitro-storage",
2222
"test:cpp": "turbo run test:cpp --filter=react-native-nitro-storage",
23-
"start": "turbo run start --filter=example --",
24-
"example:ios": "turbo run ios --filter=example",
25-
"example:android": "turbo run android --filter=example",
26-
"example:prebuild": "turbo run prebuild --filter=example",
27-
"example:prebuild:clean": "turbo run prebuild:clean --filter=example"
23+
"example:start": "bun run --cwd apps/example start",
24+
"example:ios": "bun run --cwd apps/example ios",
25+
"example:android": "bun run --cwd apps/example android",
26+
"example:prebuild": "bun run --cwd apps/example prebuild",
27+
"example:prebuild:clean": "bun run --cwd apps/example prebuild:clean"
2828
},
2929
"devDependencies": {
3030
"@swc/core": "^1.15.18",

packages/react-native-nitro-storage/src/index.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ function measureOperation<T>(
173173
fn: () => T,
174174
keysCount = 1,
175175
): T {
176+
if (!metricsObserver) {
177+
return fn();
178+
}
176179
const start = Date.now();
177180
try {
178181
return fn();
@@ -215,13 +218,20 @@ function clearScopeRawCache(scope: NonMemoryScope): void {
215218
}
216219

217220
function notifyKeyListeners(registry: KeyListenerRegistry, key: string): void {
218-
registry.get(key)?.forEach((listener) => listener());
221+
const listeners = registry.get(key);
222+
if (listeners) {
223+
for (const listener of listeners) {
224+
listener();
225+
}
226+
}
219227
}
220228

221229
function notifyAllListeners(registry: KeyListenerRegistry): void {
222-
registry.forEach((listeners) => {
223-
listeners.forEach((listener) => listener());
224-
});
230+
for (const listeners of registry.values()) {
231+
for (const listener of listeners) {
232+
listener();
233+
}
234+
}
225235
}
226236

227237
function addKeyListener(
@@ -842,17 +852,18 @@ export function createStorageItem<T = undefined>(
842852
return memoryStore.get(storageKey);
843853
}
844854

845-
if (
846-
nonMemoryScope === StorageScope.Secure &&
847-
!isBiometric &&
848-
hasPendingSecureWrite(storageKey)
849-
) {
850-
return readPendingSecureWrite(storageKey);
855+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
856+
const pending = pendingSecureWrites.get(storageKey);
857+
if (pending !== undefined) {
858+
return pending.value;
859+
}
851860
}
852861

853862
if (readCache) {
854-
if (hasCachedRawValue(nonMemoryScope!, storageKey)) {
855-
return readCachedRawValue(nonMemoryScope!, storageKey);
863+
const cache = getScopeRawCache(nonMemoryScope!);
864+
const cached = cache.get(storageKey);
865+
if (cached !== undefined || cache.has(storageKey)) {
866+
return cached;
856867
}
857868
}
858869

@@ -1069,14 +1080,13 @@ export function createStorageItem<T = undefined>(
10691080
? valueOrFn(getInternal())
10701081
: valueOrFn;
10711082

1072-
invalidateParsedCache();
1073-
10741083
if (validate && !validate(newValue)) {
10751084
throw new Error(
10761085
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
10771086
);
10781087
}
10791088

1089+
invalidateParsedCache();
10801090
writeValueWithoutValidation(newValue);
10811091
});
10821092
};
@@ -1215,15 +1225,18 @@ export function getBatch(
12151225

12161226
items.forEach((item, index) => {
12171227
if (scope === StorageScope.Secure) {
1218-
if (hasPendingSecureWrite(item.key)) {
1219-
rawValues[index] = readPendingSecureWrite(item.key);
1228+
const pending = pendingSecureWrites.get(item.key);
1229+
if (pending !== undefined) {
1230+
rawValues[index] = pending.value;
12201231
return;
12211232
}
12221233
}
12231234

12241235
if (item._readCacheEnabled === true) {
1225-
if (hasCachedRawValue(scope, item.key)) {
1226-
rawValues[index] = readCachedRawValue(scope, item.key);
1236+
const cache = getScopeRawCache(scope);
1237+
const cached = cache.get(item.key);
1238+
if (cached !== undefined || cache.has(item.key)) {
1239+
rawValues[index] = cached;
12271240
return;
12281241
}
12291242
}

packages/react-native-nitro-storage/src/index.web.ts

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ function measureOperation<T>(
202202
fn: () => T,
203203
keysCount = 1,
204204
): T {
205+
if (!metricsObserver) {
206+
return fn();
207+
}
205208
const start = Date.now();
206209
try {
207210
return fn();
@@ -234,6 +237,9 @@ function createLocalStorageWebSecureBackend(): WebSecureStorageBackend {
234237
let webSecureStorageBackend: WebSecureStorageBackend | undefined =
235238
createLocalStorageWebSecureBackend();
236239

240+
let cachedSecureBrowserStorage: BrowserStorageLike | undefined;
241+
let cachedSecureBackendRef: WebSecureStorageBackend | undefined;
242+
237243
function getBrowserStorage(scope: number): BrowserStorageLike | undefined {
238244
if (scope === StorageScope.Disk) {
239245
return globalThis.localStorage;
@@ -242,16 +248,24 @@ function getBrowserStorage(scope: number): BrowserStorageLike | undefined {
242248
if (!webSecureStorageBackend) {
243249
return undefined;
244250
}
245-
return {
246-
setItem: (key, value) => webSecureStorageBackend?.setItem(key, value),
247-
getItem: (key) => webSecureStorageBackend?.getItem(key) ?? null,
248-
removeItem: (key) => webSecureStorageBackend?.removeItem(key),
249-
clear: () => webSecureStorageBackend?.clear(),
250-
key: (index) => webSecureStorageBackend?.getAllKeys()[index] ?? null,
251+
if (
252+
cachedSecureBackendRef === webSecureStorageBackend &&
253+
cachedSecureBrowserStorage
254+
) {
255+
return cachedSecureBrowserStorage;
256+
}
257+
cachedSecureBackendRef = webSecureStorageBackend;
258+
cachedSecureBrowserStorage = {
259+
setItem: (key, value) => webSecureStorageBackend!.setItem(key, value),
260+
getItem: (key) => webSecureStorageBackend!.getItem(key) ?? null,
261+
removeItem: (key) => webSecureStorageBackend!.removeItem(key),
262+
clear: () => webSecureStorageBackend!.clear(),
263+
key: (index) => webSecureStorageBackend!.getAllKeys()[index] ?? null,
251264
get length() {
252-
return webSecureStorageBackend?.getAllKeys().length ?? 0;
265+
return webSecureStorageBackend!.getAllKeys().length;
253266
},
254267
};
268+
return cachedSecureBrowserStorage;
255269
}
256270
return undefined;
257271
}
@@ -429,13 +443,20 @@ function clearScopeRawCache(scope: NonMemoryScope): void {
429443
}
430444

431445
function notifyKeyListeners(registry: KeyListenerRegistry, key: string): void {
432-
registry.get(key)?.forEach((listener) => listener());
446+
const listeners = registry.get(key);
447+
if (listeners) {
448+
for (const listener of listeners) {
449+
listener();
450+
}
451+
}
433452
}
434453

435454
function notifyAllListeners(registry: KeyListenerRegistry): void {
436-
registry.forEach((listeners) => {
437-
listeners.forEach((listener) => listener());
438-
});
455+
for (const listeners of registry.values()) {
456+
for (const listener of listeners) {
457+
listener();
458+
}
459+
}
439460
}
440461

441462
function addKeyListener(
@@ -1077,6 +1098,8 @@ export function setWebSecureStorageBackend(
10771098
backend?: WebSecureStorageBackend,
10781099
): void {
10791100
webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
1101+
cachedSecureBrowserStorage = undefined;
1102+
cachedSecureBackendRef = undefined;
10801103
hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
10811104
clearScopeRawCache(StorageScope.Secure);
10821105
}
@@ -1250,17 +1273,18 @@ export function createStorageItem<T = undefined>(
12501273
return memoryStore.get(storageKey);
12511274
}
12521275

1253-
if (
1254-
nonMemoryScope === StorageScope.Secure &&
1255-
!isBiometric &&
1256-
hasPendingSecureWrite(storageKey)
1257-
) {
1258-
return readPendingSecureWrite(storageKey);
1276+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1277+
const pending = pendingSecureWrites.get(storageKey);
1278+
if (pending !== undefined) {
1279+
return pending.value;
1280+
}
12591281
}
12601282

12611283
if (readCache) {
1262-
if (hasCachedRawValue(nonMemoryScope!, storageKey)) {
1263-
return readCachedRawValue(nonMemoryScope!, storageKey);
1284+
const cache = getScopeRawCache(nonMemoryScope!);
1285+
const cached = cache.get(storageKey);
1286+
if (cached !== undefined || cache.has(storageKey)) {
1287+
return cached;
12641288
}
12651289
}
12661290

@@ -1474,14 +1498,13 @@ export function createStorageItem<T = undefined>(
14741498
? valueOrFn(getInternal())
14751499
: valueOrFn;
14761500

1477-
invalidateParsedCache();
1478-
14791501
if (validate && !validate(newValue)) {
14801502
throw new Error(
14811503
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
14821504
);
14831505
}
14841506

1507+
invalidateParsedCache();
14851508
writeValueWithoutValidation(newValue);
14861509
});
14871510
};
@@ -1620,15 +1643,18 @@ export function getBatch(
16201643

16211644
items.forEach((item, index) => {
16221645
if (scope === StorageScope.Secure) {
1623-
if (hasPendingSecureWrite(item.key)) {
1624-
rawValues[index] = readPendingSecureWrite(item.key);
1646+
const pending = pendingSecureWrites.get(item.key);
1647+
if (pending !== undefined) {
1648+
rawValues[index] = pending.value;
16251649
return;
16261650
}
16271651
}
16281652

16291653
if (item._readCacheEnabled === true) {
1630-
if (hasCachedRawValue(scope, item.key)) {
1631-
rawValues[index] = readCachedRawValue(scope, item.key);
1654+
const cache = getScopeRawCache(scope);
1655+
const cached = cache.get(item.key);
1656+
if (cached !== undefined || cache.has(item.key)) {
1657+
rawValues[index] = cached;
16321658
return;
16331659
}
16341660
}

packages/react-native-nitro-storage/src/internal.ts

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ export const MIGRATION_VERSION_KEY = "__nitro_storage_migration_version__";
44
export const NATIVE_BATCH_MISSING_SENTINEL =
55
"__nitro_storage_batch_missing__::v1";
66
const PRIMITIVE_FAST_PATH_PREFIX = "__nitro_storage_primitive__:";
7+
const PRIM_NULL = "__nitro_storage_primitive__:l";
8+
const PRIM_UNDEFINED = "__nitro_storage_primitive__:u";
9+
const PRIM_TRUE = "__nitro_storage_primitive__:b:1";
10+
const PRIM_FALSE = "__nitro_storage_primitive__:b:0";
11+
const PRIM_STRING_PREFIX = "__nitro_storage_primitive__:s:";
12+
const PRIM_NUMBER_PREFIX = "__nitro_storage_primitive__:n:";
13+
const PRIM_INFINITY = "__nitro_storage_primitive__:n:Infinity";
14+
const PRIM_NEG_INFINITY = "__nitro_storage_primitive__:n:-Infinity";
15+
const PRIM_NAN = "__nitro_storage_primitive__:n:NaN";
716
const NAMESPACE_SEPARATOR = ":";
817
const VERSION_TOKEN_PREFIX = "__nitro_storage_version__:";
918

@@ -80,30 +89,30 @@ export function isNamespaced(key: string, namespace: string): boolean {
8089

8190
export function serializeWithPrimitiveFastPath<T>(value: T): string {
8291
if (value === null) {
83-
return `${PRIMITIVE_FAST_PATH_PREFIX}l`;
92+
return PRIM_NULL;
8493
}
8594

8695
switch (typeof value) {
8796
case "string":
88-
return `${PRIMITIVE_FAST_PATH_PREFIX}s:${value}`;
97+
return PRIM_STRING_PREFIX + (value as string);
8998
case "number":
9099
if (Number.isFinite(value)) {
91-
return `${PRIMITIVE_FAST_PATH_PREFIX}n:${value}`;
100+
return PRIM_NUMBER_PREFIX + String(value);
92101
}
93102
if (Number.isNaN(value as number)) {
94-
return `${PRIMITIVE_FAST_PATH_PREFIX}n:NaN`;
103+
return PRIM_NAN;
95104
}
96105
if (value === Infinity) {
97-
return `${PRIMITIVE_FAST_PATH_PREFIX}n:Infinity`;
106+
return PRIM_INFINITY;
98107
}
99108
if (value === -Infinity) {
100-
return `${PRIMITIVE_FAST_PATH_PREFIX}n:-Infinity`;
109+
return PRIM_NEG_INFINITY;
101110
}
102111
break;
103112
case "boolean":
104-
return `${PRIMITIVE_FAST_PATH_PREFIX}b:${value ? "1" : "0"}`;
113+
return value ? PRIM_TRUE : PRIM_FALSE;
105114
case "undefined":
106-
return `${PRIMITIVE_FAST_PATH_PREFIX}u`;
115+
return PRIM_UNDEFINED;
107116
default:
108117
break;
109118
}
@@ -117,34 +126,41 @@ export function serializeWithPrimitiveFastPath<T>(value: T): string {
117126
return serialized;
118127
}
119128

129+
// charCode constants for fast tag dispatch
130+
const CHAR_U = 117; // 'u'
131+
const CHAR_L = 108; // 'l'
132+
const CHAR_S = 115; // 's'
133+
const CHAR_B = 98; // 'b'
134+
const CHAR_N = 110; // 'n'
135+
120136
export function deserializeWithPrimitiveFastPath<T>(value: string): T {
121137
if (value.startsWith(PRIMITIVE_FAST_PATH_PREFIX)) {
122-
const encodedValue = value.slice(PRIMITIVE_FAST_PATH_PREFIX.length);
123-
if (encodedValue === "u") {
138+
const prefixLen = PRIMITIVE_FAST_PATH_PREFIX.length;
139+
const tagChar = value.charCodeAt(prefixLen);
140+
141+
if (tagChar === CHAR_U) {
124142
return undefined as T;
125143
}
126-
if (encodedValue === "l") {
144+
if (tagChar === CHAR_L) {
127145
return null as T;
128146
}
129147

130-
const separatorIndex = encodedValue.indexOf(":");
131-
if (separatorIndex > 0) {
132-
const tag = encodedValue.slice(0, separatorIndex);
133-
const payload = encodedValue.slice(separatorIndex + 1);
134-
if (tag === "s") {
135-
return payload as T;
136-
}
137-
if (tag === "b") {
138-
return (payload === "1") as T;
139-
}
140-
if (tag === "n") {
141-
if (payload === "NaN") return NaN as T;
142-
if (payload === "Infinity") return Infinity as T;
143-
if (payload === "-Infinity") return -Infinity as T;
144-
const parsed = Number(payload);
145-
if (Number.isFinite(parsed)) {
146-
return parsed as T;
147-
}
148+
// Tagged values have format: prefix + tag + ':' + payload
149+
const payload = value.slice(prefixLen + 2);
150+
151+
if (tagChar === CHAR_S) {
152+
return payload as T;
153+
}
154+
if (tagChar === CHAR_B) {
155+
return (payload === "1") as T;
156+
}
157+
if (tagChar === CHAR_N) {
158+
if (payload === "NaN") return NaN as T;
159+
if (payload === "Infinity") return Infinity as T;
160+
if (payload === "-Infinity") return -Infinity as T;
161+
const parsed = Number(payload);
162+
if (Number.isFinite(parsed)) {
163+
return parsed as T;
148164
}
149165
}
150166
}

0 commit comments

Comments
 (0)