Skip to content

Commit a68a5ba

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

2 files changed

Lines changed: 89 additions & 5 deletions

File tree

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

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)