Skip to content

Commit 879d653

Browse files
authored
Merge pull request #485 from margelo/feat/split-up-in-memory-pr-part-3
feat: fallback to `NoopProvider` if OOM happens
2 parents d041f22 + 8254d59 commit 879d653

19 files changed

+641
-234
lines changed

jestSetup.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
jest.mock('./lib/storage');
2-
jest.mock('./lib/storage/NativeStorage', () => require('./lib/storage/__mocks__'));
3-
jest.mock('./lib/storage/WebStorage', () => require('./lib/storage/__mocks__'));
4-
jest.mock('./lib/storage/providers/IDBKeyVal', () => require('./lib/storage/__mocks__'));
2+
jest.mock('./lib/storage/platforms/index.native', () => require('./lib/storage/__mocks__'));
3+
jest.mock('./lib/storage/platforms/index', () => require('./lib/storage/__mocks__'));
4+
jest.mock('./lib/storage/providers/IDBKeyValProvider', () => require('./lib/storage/__mocks__'));
55

66
jest.mock('react-native-device-info', () => ({getFreeDiskStorage: () => {}}));
77
jest.mock('react-native-quick-sqlite', () => ({

lib/Onyx.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ function init({
3939
shouldSyncMultipleInstances = Boolean(global.localStorage),
4040
debugSetState = false,
4141
}: InitOptions): void {
42+
Storage.init();
43+
44+
if (shouldSyncMultipleInstances) {
45+
Storage.keepInstancesSync?.((key, value) => {
46+
const prevValue = cache.getValue(key, false);
47+
cache.set(key, value);
48+
OnyxUtils.keyChanged(key, value, prevValue);
49+
});
50+
}
51+
4252
if (debugSetState) {
4353
PerformanceUtils.setShouldDebugSetState(true);
4454
}
@@ -51,14 +61,6 @@ function init({
5161

5262
// Initialize all of our keys with data provided then give green light to any pending connections
5363
Promise.all([OnyxUtils.addAllSafeEvictionKeysToRecentlyAccessedList(), OnyxUtils.initializeWithDefaultKeyStates()]).then(deferredInitTask.resolve);
54-
55-
if (shouldSyncMultipleInstances) {
56-
Storage.keepInstancesSync?.((key, value) => {
57-
const prevValue = cache.getValue(key, false);
58-
cache.set(key, value);
59-
OnyxUtils.keyChanged(key, value, prevValue);
60-
});
61-
}
6264
}
6365

6466
/**

lib/storage/InstanceSync/index.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import NOOP from 'lodash/noop';
2+
3+
/**
4+
* This is used to keep multiple browser tabs in sync, therefore only needed on web
5+
* On native platforms, we omit this syncing logic by setting this to mock implementation.
6+
*/
7+
const InstanceSync = {
8+
shouldBeUsed: false,
9+
init: NOOP,
10+
setItem: NOOP,
11+
removeItem: NOOP,
12+
removeItems: NOOP,
13+
mergeItem: NOOP,
14+
clear: <T extends () => void>(callback: T) => Promise.resolve(callback()),
15+
};
16+
17+
export default InstanceSync;

lib/storage/InstanceSync/index.web.ts

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* The InstancesSync object provides data-changed events like the ones that exist
3+
* when using LocalStorage APIs in the browser. These events are great because multiple tabs can listen for when
4+
* data changes and then stay up-to-date with everything happening in Onyx.
5+
*/
6+
import type {OnyxKey} from '../../types';
7+
import NoopProvider from '../providers/NoopProvider';
8+
import type {KeyList, OnStorageKeyChanged} from '../providers/types';
9+
import type StorageProvider from '../providers/types';
10+
11+
const SYNC_ONYX = 'SYNC_ONYX';
12+
13+
/**
14+
* Raise an event through `localStorage` to let other tabs know a value changed
15+
* @param {String} onyxKey
16+
*/
17+
function raiseStorageSyncEvent(onyxKey: OnyxKey) {
18+
global.localStorage.setItem(SYNC_ONYX, onyxKey);
19+
global.localStorage.removeItem(SYNC_ONYX);
20+
}
21+
22+
function raiseStorageSyncManyKeysEvent(onyxKeys: KeyList) {
23+
onyxKeys.forEach((onyxKey) => {
24+
raiseStorageSyncEvent(onyxKey);
25+
});
26+
}
27+
28+
let storage = NoopProvider;
29+
30+
const InstanceSync = {
31+
shouldBeUsed: true,
32+
/**
33+
* @param {Function} onStorageKeyChanged Storage synchronization mechanism keeping all opened tabs in sync
34+
*/
35+
init: (onStorageKeyChanged: OnStorageKeyChanged, store: StorageProvider) => {
36+
storage = store;
37+
38+
// This listener will only be triggered by events coming from other tabs
39+
global.addEventListener('storage', (event) => {
40+
// Ignore events that don't originate from the SYNC_ONYX logic
41+
if (event.key !== SYNC_ONYX || !event.newValue) {
42+
return;
43+
}
44+
45+
const onyxKey = event.newValue;
46+
47+
storage.getItem(onyxKey).then((value) => onStorageKeyChanged(onyxKey, value));
48+
});
49+
},
50+
setItem: raiseStorageSyncEvent,
51+
removeItem: raiseStorageSyncEvent,
52+
removeItems: raiseStorageSyncManyKeysEvent,
53+
mergeItem: raiseStorageSyncEvent,
54+
clear: (clearImplementation: () => void) => {
55+
let allKeys: KeyList;
56+
57+
// The keys must be retrieved before storage is cleared or else the list of keys would be empty
58+
return storage
59+
.getAllKeys()
60+
.then((keys: KeyList) => {
61+
allKeys = keys;
62+
})
63+
.then(() => clearImplementation())
64+
.then(() => {
65+
// Now that storage is cleared, the storage sync event can happen which is a more atomic action
66+
// for other browser tabs
67+
raiseStorageSyncManyKeysEvent(allKeys);
68+
});
69+
},
70+
};
71+
72+
export default InstanceSync;

lib/storage/NativeStorage.ts

-3
This file was deleted.

lib/storage/WebStorage.ts

-74
This file was deleted.

lib/storage/__mocks__/index.ts

+21-84
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,26 @@
1-
import type {OnyxKey, OnyxValue} from '../../types';
2-
import utils from '../../utils';
3-
import type {KeyValuePairList} from '../providers/types';
4-
import type StorageProvider from '../providers/types';
1+
import MemoryOnlyProvider, {mockStore, mockSet, setMockStore} from '../providers/MemoryOnlyProvider';
52

6-
let storageMapInternal: Record<OnyxKey, OnyxValue<OnyxKey>> = {};
3+
const init = jest.fn(MemoryOnlyProvider.init);
74

8-
const set = jest.fn((key, value) => {
9-
storageMapInternal[key] = value;
10-
return Promise.resolve(value);
11-
});
5+
init();
126

13-
const idbKeyvalMock: StorageProvider = {
14-
setItem(key, value) {
15-
return set(key, value);
16-
},
17-
multiSet(pairs) {
18-
const setPromises = pairs.map(([key, value]) => this.setItem(key, value));
19-
return new Promise((resolve) => {
20-
Promise.all(setPromises).then(() => resolve(storageMapInternal));
21-
});
22-
},
23-
getItem<TKey extends OnyxKey>(key: TKey) {
24-
return Promise.resolve(storageMapInternal[key] as OnyxValue<TKey>);
25-
},
26-
multiGet(keys) {
27-
const getPromises = keys.map(
28-
(key) =>
29-
new Promise((resolve) => {
30-
this.getItem(key).then((value) => resolve([key, value]));
31-
}),
32-
);
33-
return Promise.all(getPromises) as Promise<KeyValuePairList>;
34-
},
35-
multiMerge(pairs) {
36-
pairs.forEach(([key, value]) => {
37-
const existingValue = storageMapInternal[key];
38-
const newValue = utils.fastMerge(existingValue as Record<string, unknown>, value as Record<string, unknown>);
39-
40-
set(key, newValue);
41-
});
42-
43-
return Promise.resolve(storageMapInternal);
44-
},
45-
mergeItem(key, _changes, modifiedData) {
46-
return this.setItem(key, modifiedData);
47-
},
48-
removeItem(key) {
49-
delete storageMapInternal[key];
50-
return Promise.resolve();
51-
},
52-
removeItems(keys) {
53-
keys.forEach((key) => {
54-
delete storageMapInternal[key];
55-
});
56-
return Promise.resolve();
57-
},
58-
clear() {
59-
storageMapInternal = {};
60-
return Promise.resolve();
61-
},
62-
getAllKeys() {
63-
return Promise.resolve(Object.keys(storageMapInternal));
64-
},
65-
getDatabaseSize() {
66-
return Promise.resolve({bytesRemaining: 0, bytesUsed: 99999});
67-
},
68-
};
69-
70-
const idbKeyvalMockSpy = {
71-
idbKeyvalSet: set,
72-
setItem: jest.fn(idbKeyvalMock.setItem),
73-
getItem: jest.fn(idbKeyvalMock.getItem),
74-
removeItem: jest.fn(idbKeyvalMock.removeItem),
75-
removeItems: jest.fn(idbKeyvalMock.removeItems),
76-
clear: jest.fn(idbKeyvalMock.clear),
77-
getAllKeys: jest.fn(idbKeyvalMock.getAllKeys),
78-
multiGet: jest.fn(idbKeyvalMock.multiGet),
79-
multiSet: jest.fn(idbKeyvalMock.multiSet),
80-
multiMerge: jest.fn(idbKeyvalMock.multiMerge),
81-
mergeItem: jest.fn(idbKeyvalMock.mergeItem),
82-
getStorageMap: jest.fn(() => storageMapInternal),
83-
setInitialMockData: jest.fn((data) => {
84-
storageMapInternal = data;
85-
}),
86-
getDatabaseSize: jest.fn(idbKeyvalMock.getDatabaseSize),
7+
const StorageMock = {
8+
init,
9+
getItem: jest.fn(MemoryOnlyProvider.getItem),
10+
multiGet: jest.fn(MemoryOnlyProvider.multiGet),
11+
setItem: jest.fn(MemoryOnlyProvider.setItem),
12+
multiSet: jest.fn(MemoryOnlyProvider.multiSet),
13+
mergeItem: jest.fn(MemoryOnlyProvider.mergeItem),
14+
multiMerge: jest.fn(MemoryOnlyProvider.multiMerge),
15+
removeItem: jest.fn(MemoryOnlyProvider.removeItem),
16+
removeItems: jest.fn(MemoryOnlyProvider.removeItems),
17+
clear: jest.fn(MemoryOnlyProvider.clear),
18+
getAllKeys: jest.fn(MemoryOnlyProvider.getAllKeys),
19+
getDatabaseSize: jest.fn(MemoryOnlyProvider.getDatabaseSize),
20+
keepInstancesSync: jest.fn(),
21+
mockSet,
22+
getMockStore: jest.fn(() => mockStore),
23+
setMockStore: jest.fn((data) => setMockStore(data)),
8724
};
8825

89-
export default idbKeyvalMockSpy;
26+
export default StorageMock;

lib/storage/index.native.ts

-3
This file was deleted.

0 commit comments

Comments
 (0)