Skip to content

Commit

Permalink
Revert "Revert Expensify#475"
Browse files Browse the repository at this point in the history
This reverts commit 577e611.
  • Loading branch information
kirillzyusko committed Mar 25, 2024
1 parent bee53ad commit b859465
Show file tree
Hide file tree
Showing 15 changed files with 286 additions and 126 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 @@ -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
16 changes: 16 additions & 0 deletions lib/storage/InstanceSync/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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 = {
init: NOOP,
setItem: NOOP,
removeItem: NOOP,
removeItems: NOOP,
mergeItem: NOOP,
clear: <T extends () => void>(callback: T) => Promise.resolve(callback()),
};

export default InstanceSync;
66 changes: 66 additions & 0 deletions lib/storage/InstanceSync/index.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* eslint-disable no-invalid-this */
/**
* 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, OnyxValue} from '../../types';
import type {KeyList, OnStorageKeyChanged} 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);
});
}

const InstanceSync = {
/**
* @param {Function} onStorageKeyChanged Storage synchronization mechanism keeping all opened tabs in sync
*/
init: (onStorageKeyChanged: OnStorageKeyChanged) => {
// 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;
// @ts-expect-error `this` will be substituted later in actual function call
this.getItem(onyxKey).then((value: OnyxValue) => 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
// @ts-expect-error `this` will be substituted later in actual function call
return this.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.

3 changes: 0 additions & 3 deletions lib/storage/index.native.ts

This file was deleted.

138 changes: 136 additions & 2 deletions lib/storage/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,137 @@
import WebStorage from './WebStorage';
import PlatformStorage from './platforms';
import InstanceSync from './InstanceSync';
import type StorageProvider from './providers/types';

export default WebStorage;
const provider = PlatformStorage;
let shouldKeepInstancesSync = false;

type Storage = {
getStorageProvider: () => StorageProvider;
} & StorageProvider;

const Storage: Storage = {
/**
* Returns the storage provider currently in use
*/
getStorageProvider() {
return provider;
},

/**
* Initializes all providers in the list of storage providers
* and enables fallback providers if necessary
*/
init() {
provider.init();
},

/**
* Get the value of a given key or return `null` if it's not available
*/
getItem: (key) => provider.getItem(key),

/**
* Get multiple key-value pairs for the give array of keys in a batch
*/
multiGet: (keys) => provider.multiGet(keys),

/**
* Sets the value for a given key. The only requirement is that the value should be serializable to JSON string
*/
setItem: (key, value) => {
const promise = provider.setItem(key, value);

if (shouldKeepInstancesSync) {
return promise.then(() => InstanceSync.setItem(key));
}

return promise;
},

/**
* Stores multiple key-value pairs in a batch
*/
multiSet: (pairs) => provider.multiSet(pairs),

/**
* Merging an existing value with a new one
*/
mergeItem: (key, changes, modifiedData) => {
const promise = provider.mergeItem(key, changes, modifiedData);

if (shouldKeepInstancesSync) {
return promise.then(() => InstanceSync.mergeItem(key));
}

return promise;
},

/**
* Multiple merging of existing and new values in a batch
* This function also removes all nested null values from an object.
*/
multiMerge: (pairs) => provider.multiMerge(pairs),

/**
* Removes given key and its value
*/
removeItem: (key) => {
const promise = provider.removeItem(key);

if (shouldKeepInstancesSync) {
return promise.then(() => InstanceSync.removeItem(key));
}

return promise;
},

/**
* Remove given keys and their values
*/
removeItems: (keys) => {
const promise = provider.removeItems(keys);

if (shouldKeepInstancesSync) {
return promise.then(() => InstanceSync.removeItems(keys));
}

return promise;
},

/**
* Clears everything
*/
clear: () => {
if (shouldKeepInstancesSync) {
return InstanceSync.clear(() => provider.clear());
}

return provider.clear();
},

// This is a noop for now in order to keep clients from crashing see https://github.com/Expensify/Expensify/issues/312438
setMemoryOnlyKeys: () => provider.setMemoryOnlyKeys(),

/**
* Returns all available keys
*/
getAllKeys: () => provider.getAllKeys(),

/**
* Gets the total bytes of the store
*/
getDatabaseSize: () => provider.getDatabaseSize(),

/**
* @param onStorageKeyChanged - Storage synchronization mechanism keeping all opened tabs in sync (web only)
*/
keepInstancesSync(onStorageKeyChanged) {
// If InstanceSync is null, it means we're on a native platform and we don't need to keep instances in sync
if (InstanceSync == null) return;

shouldKeepInstancesSync = true;
InstanceSync.init(onStorageKeyChanged);
},
};

export default Storage;
3 changes: 3 additions & 0 deletions lib/storage/platforms/index.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import NativeStorage from '../providers/SQLiteProvider';

export default NativeStorage;
3 changes: 3 additions & 0 deletions lib/storage/platforms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import WebStorage from '../providers/IDBKeyValProvider';

export default WebStorage;
Loading

0 comments on commit b859465

Please sign in to comment.