Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fallback to NoopProvider if OOM happens #485

Merged
merged 15 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions jestSetup.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
jest.mock('./lib/storage');
jest.mock('./lib/storage/NativeStorage', () => require('./lib/storage/__mocks__'));
jest.mock('./lib/storage/WebStorage', () => require('./lib/storage/__mocks__'));
jest.mock('./lib/storage/providers/IDBKeyVal', () => require('./lib/storage/__mocks__'));
jest.mock('./lib/storage/platforms/index.native', () => require('./lib/storage/__mocks__'));
jest.mock('./lib/storage/platforms/index', () => require('./lib/storage/__mocks__'));
jest.mock('./lib/storage/providers/IDBKeyValProvider', () => require('./lib/storage/__mocks__'));

jest.mock('react-native-device-info', () => ({getFreeDiskStorage: () => {}}));
jest.mock('react-native-quick-sqlite', () => ({
Expand Down
18 changes: 10 additions & 8 deletions lib/Onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ function init({
shouldSyncMultipleInstances = Boolean(global.localStorage),
debugSetState = false,
}: InitOptions): void {
Storage.init();

if (shouldSyncMultipleInstances) {
Storage.keepInstancesSync?.((key, value) => {
const prevValue = cache.getValue(key, false);
cache.set(key, value);
OnyxUtils.keyChanged(key, value, prevValue);
});
}

if (debugSetState) {
PerformanceUtils.setShouldDebugSetState(true);
}
Expand All @@ -50,14 +60,6 @@ function init({

// Initialize all of our keys with data provided then give green light to any pending connections
Promise.all([OnyxUtils.addAllSafeEvictionKeysToRecentlyAccessedList(), OnyxUtils.initializeWithDefaultKeyStates()]).then(deferredInitTask.resolve);

if (shouldSyncMultipleInstances) {
Storage.keepInstancesSync?.((key, value) => {
const prevValue = cache.getValue(key, false);
cache.set(key, value);
OnyxUtils.keyChanged(key, value, prevValue);
});
}
}

/**
Expand Down
17 changes: 17 additions & 0 deletions lib/storage/InstanceSync/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import NOOP from 'lodash/noop';

/**
* This is used to keep multiple browser tabs in sync, therefore only needed on web
* On native platforms, we omit this syncing logic by setting this to mock implementation.
*/
const InstanceSync = {
shouldBeUsed: false,
init: NOOP,
setItem: NOOP,
removeItem: NOOP,
removeItems: NOOP,
mergeItem: NOOP,
clear: <T extends () => void>(callback: T) => Promise.resolve(callback()),
};

export default InstanceSync;
72 changes: 72 additions & 0 deletions lib/storage/InstanceSync/index.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* The InstancesSync object provides data-changed events like the ones that exist
* when using LocalStorage APIs in the browser. These events are great because multiple tabs can listen for when
* data changes and then stay up-to-date with everything happening in Onyx.
*/
import type {OnyxKey} from '../../types';
import NoopProvider from '../providers/NoopProvider';
import type {KeyList, OnStorageKeyChanged} from '../providers/types';
import type StorageProvider from '../providers/types';

const SYNC_ONYX = 'SYNC_ONYX';

/**
* Raise an event through `localStorage` to let other tabs know a value changed
* @param {String} onyxKey
*/
function raiseStorageSyncEvent(onyxKey: OnyxKey) {
global.localStorage.setItem(SYNC_ONYX, onyxKey);
global.localStorage.removeItem(SYNC_ONYX);
}

function raiseStorageSyncManyKeysEvent(onyxKeys: KeyList) {
onyxKeys.forEach((onyxKey) => {
raiseStorageSyncEvent(onyxKey);
});
}

let storage = NoopProvider;

const InstanceSync = {
shouldBeUsed: true,
/**
* @param {Function} onStorageKeyChanged Storage synchronization mechanism keeping all opened tabs in sync
*/
init: (onStorageKeyChanged: OnStorageKeyChanged, store: StorageProvider) => {
storage = store;

// This listener will only be triggered by events coming from other tabs
global.addEventListener('storage', (event) => {
// Ignore events that don't originate from the SYNC_ONYX logic
if (event.key !== SYNC_ONYX || !event.newValue) {
return;
}

const onyxKey = event.newValue;

storage.getItem(onyxKey).then((value) => onStorageKeyChanged(onyxKey, value));
});
},
setItem: raiseStorageSyncEvent,
removeItem: raiseStorageSyncEvent,
removeItems: raiseStorageSyncManyKeysEvent,
mergeItem: raiseStorageSyncEvent,
clear: (clearImplementation: () => void) => {
let allKeys: KeyList;

// The keys must be retrieved before storage is cleared or else the list of keys would be empty
return storage
.getAllKeys()
.then((keys: KeyList) => {
allKeys = keys;
})
.then(() => clearImplementation())
.then(() => {
// Now that storage is cleared, the storage sync event can happen which is a more atomic action
// for other browser tabs
raiseStorageSyncManyKeysEvent(allKeys);
});
},
};

export default InstanceSync;
3 changes: 0 additions & 3 deletions lib/storage/NativeStorage.ts

This file was deleted.

74 changes: 0 additions & 74 deletions lib/storage/WebStorage.ts

This file was deleted.

105 changes: 21 additions & 84 deletions lib/storage/__mocks__/index.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,26 @@
import type {OnyxKey, OnyxValue} from '../../types';
import utils from '../../utils';
import type {KeyValuePairList} from '../providers/types';
import type StorageProvider from '../providers/types';
import MemoryOnlyProvider, {mockStore, mockSet, setMockStore} from '../providers/MemoryOnlyProvider';

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

const set = jest.fn((key, value) => {
storageMapInternal[key] = value;
return Promise.resolve(value);
});
init();

const idbKeyvalMock: StorageProvider = {
setItem(key, value) {
return set(key, value);
},
multiSet(pairs) {
const setPromises = pairs.map(([key, value]) => this.setItem(key, value));
return new Promise((resolve) => {
Promise.all(setPromises).then(() => resolve(storageMapInternal));
});
},
getItem<TKey extends OnyxKey>(key: TKey) {
return Promise.resolve(storageMapInternal[key] as OnyxValue<TKey>);
},
multiGet(keys) {
const getPromises = keys.map(
(key) =>
new Promise((resolve) => {
this.getItem(key).then((value) => resolve([key, value]));
}),
);
return Promise.all(getPromises) as Promise<KeyValuePairList>;
},
multiMerge(pairs) {
pairs.forEach(([key, value]) => {
const existingValue = storageMapInternal[key];
const newValue = utils.fastMerge(existingValue as Record<string, unknown>, value as Record<string, unknown>);

set(key, newValue);
});

return Promise.resolve(storageMapInternal);
},
mergeItem(key, _changes, modifiedData) {
return this.setItem(key, modifiedData);
},
removeItem(key) {
delete storageMapInternal[key];
return Promise.resolve();
},
removeItems(keys) {
keys.forEach((key) => {
delete storageMapInternal[key];
});
return Promise.resolve();
},
clear() {
storageMapInternal = {};
return Promise.resolve();
},
getAllKeys() {
return Promise.resolve(Object.keys(storageMapInternal));
},
getDatabaseSize() {
return Promise.resolve({bytesRemaining: 0, bytesUsed: 99999});
},
};

const idbKeyvalMockSpy = {
idbKeyvalSet: set,
setItem: jest.fn(idbKeyvalMock.setItem),
getItem: jest.fn(idbKeyvalMock.getItem),
removeItem: jest.fn(idbKeyvalMock.removeItem),
removeItems: jest.fn(idbKeyvalMock.removeItems),
clear: jest.fn(idbKeyvalMock.clear),
getAllKeys: jest.fn(idbKeyvalMock.getAllKeys),
multiGet: jest.fn(idbKeyvalMock.multiGet),
multiSet: jest.fn(idbKeyvalMock.multiSet),
multiMerge: jest.fn(idbKeyvalMock.multiMerge),
mergeItem: jest.fn(idbKeyvalMock.mergeItem),
getStorageMap: jest.fn(() => storageMapInternal),
setInitialMockData: jest.fn((data) => {
storageMapInternal = data;
}),
getDatabaseSize: jest.fn(idbKeyvalMock.getDatabaseSize),
const StorageMock = {
init,
getItem: jest.fn(MemoryOnlyProvider.getItem),
multiGet: jest.fn(MemoryOnlyProvider.multiGet),
setItem: jest.fn(MemoryOnlyProvider.setItem),
multiSet: jest.fn(MemoryOnlyProvider.multiSet),
mergeItem: jest.fn(MemoryOnlyProvider.mergeItem),
multiMerge: jest.fn(MemoryOnlyProvider.multiMerge),
removeItem: jest.fn(MemoryOnlyProvider.removeItem),
removeItems: jest.fn(MemoryOnlyProvider.removeItems),
clear: jest.fn(MemoryOnlyProvider.clear),
getAllKeys: jest.fn(MemoryOnlyProvider.getAllKeys),
getDatabaseSize: jest.fn(MemoryOnlyProvider.getDatabaseSize),
keepInstancesSync: jest.fn(),
mockSet,
getMockStore: jest.fn(() => mockStore),
setMockStore: jest.fn((data) => setMockStore(data)),
};

export default idbKeyvalMockSpy;
export default StorageMock;
3 changes: 0 additions & 3 deletions lib/storage/index.native.ts

This file was deleted.

Loading
Loading