Skip to content

Commit

Permalink
Merge pull request Expensify#485 from margelo/feat/split-up-in-memory…
Browse files Browse the repository at this point in the history
…-pr-part-3

feat: fallback to `NoopProvider` if OOM happens
  • Loading branch information
marcaaron authored Mar 26, 2024
2 parents d041f22 + 8254d59 commit 879d653
Show file tree
Hide file tree
Showing 19 changed files with 641 additions and 234 deletions.
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 @@ -39,6 +39,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 @@ -51,14 +61,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

0 comments on commit 879d653

Please sign in to comment.