Skip to content

Commit 138b467

Browse files
committed
feat(react-native): no storage fallback to in-memory map
1 parent 7ed2c08 commit 138b467

2 files changed

Lines changed: 90 additions & 5 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { LDLogger } from '@launchdarkly/js-client-sdk-common';
2+
3+
// eslint-disable-next-line import/first, import/order
4+
import getAsyncStorage from '../../src/platform/ConditionalAsyncStorage';
5+
6+
// Do NOT use the global jest setup mock for async-storage in this file.
7+
// We need to test what happens when the require fails.
8+
jest.mock('@react-native-async-storage/async-storage', () => {
9+
throw new Error('Cannot find module @react-native-async-storage/async-storage');
10+
});
11+
12+
describe('ConditionalAsyncStorage in-memory fallback', () => {
13+
let logger: LDLogger;
14+
let storage: any;
15+
16+
beforeEach(() => {
17+
logger = {
18+
error: jest.fn(),
19+
warn: jest.fn(),
20+
info: jest.fn(),
21+
debug: jest.fn(),
22+
};
23+
storage = getAsyncStorage(logger);
24+
});
25+
26+
it('logs a warning when AsyncStorage is unavailable', () => {
27+
expect(logger.warn).toHaveBeenCalledWith(
28+
expect.stringContaining('AsyncStorage is not available'),
29+
);
30+
expect(logger.warn).toHaveBeenCalledWith(
31+
expect.stringContaining('in-memory storage as a fallback'),
32+
);
33+
});
34+
35+
it('returns null for keys that have not been set', async () => {
36+
const value = await storage.getItem('nonexistent');
37+
expect(value).toBeNull();
38+
});
39+
40+
it('stores and retrieves values within the session', async () => {
41+
await storage.setItem('flag-key', '{"flagA":true}');
42+
const value = await storage.getItem('flag-key');
43+
expect(value).toBe('{"flagA":true}');
44+
});
45+
46+
it('overwrites existing values', async () => {
47+
await storage.setItem('key', 'first');
48+
await storage.setItem('key', 'second');
49+
const value = await storage.getItem('key');
50+
expect(value).toBe('second');
51+
});
52+
53+
it('removes values', async () => {
54+
await storage.setItem('key', 'value');
55+
await storage.removeItem('key');
56+
const value = await storage.getItem('key');
57+
expect(value).toBeNull();
58+
});
59+
60+
it('removing a nonexistent key does not throw', async () => {
61+
await expect(storage.removeItem('nonexistent')).resolves.toBeUndefined();
62+
});
63+
64+
it('isolates storage between separate fallback instances', async () => {
65+
const otherLogger: LDLogger = {
66+
error: jest.fn(),
67+
warn: jest.fn(),
68+
info: jest.fn(),
69+
debug: jest.fn(),
70+
};
71+
const otherStorage = getAsyncStorage(otherLogger);
72+
73+
await storage.setItem('key', 'from-first');
74+
await expect(otherStorage.getItem('key')).resolves.toBeNull();
75+
});
76+
});

packages/sdk/react-native/src/platform/ConditionalAsyncStorage.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,23 @@ export default function getAsyncStorage(logger: LDLogger): any {
2121
try {
2222
return require('@react-native-async-storage/async-storage').default;
2323
} catch (e) {
24-
// Use a mock if async-storage is unavailable
24+
// Use an in-memory fallback if async-storage is unavailable.
25+
// This preserves session-level persistence (flag caching, generated keys,
26+
// context index) but data will not survive app restarts.
2527
logger.warn(
26-
'AsyncStorage is not available, generated keys and context caches will not be persisted. Please see https://launchdarkly.github.io/js-core/packages/sdk/react-native/docs/interfaces/LDOptions.html#storage for more information.',
28+
'AsyncStorage is not available. Using in-memory storage as a fallback — cached flags, generated keys, and context data will not persist across app restarts. Please see https://launchdarkly.github.io/js-core/packages/sdk/react-native/docs/interfaces/LDOptions.html#storage for more information.',
2729
);
30+
const memoryStore = new Map<string, string>();
2831
return {
29-
getItem: (_key: string) => Promise.resolve(null),
30-
setItem: (_key: string, _value: string) => Promise.resolve(),
31-
removeItem: (_key: string) => Promise.resolve(),
32+
getItem: (key: string) => Promise.resolve(memoryStore.get(key) ?? null),
33+
setItem: (key: string, value: string) => {
34+
memoryStore.set(key, value);
35+
return Promise.resolve();
36+
},
37+
removeItem: (key: string) => {
38+
memoryStore.delete(key);
39+
return Promise.resolve();
40+
},
3241
};
3342
}
3443
}

0 commit comments

Comments
 (0)