react-native-onyx
Version:
State management for React Native
545 lines (544 loc) • 26.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const Logger = __importStar(require("./Logger"));
const OnyxCache_1 = __importStar(require("./OnyxCache"));
const storage_1 = __importDefault(require("./storage"));
const utils_1 = __importDefault(require("./utils"));
const DevTools_1 = __importStar(require("./DevTools"));
const OnyxUtils_1 = __importDefault(require("./OnyxUtils"));
const logMessages_1 = __importDefault(require("./logMessages"));
const OnyxConnectionManager_1 = __importDefault(require("./OnyxConnectionManager"));
const GlobalSettings = __importStar(require("./GlobalSettings"));
const metrics_1 = __importDefault(require("./metrics"));
const OnyxMerge_1 = __importDefault(require("./OnyxMerge"));
/** Initialize the store with actions and listening for storage events */
function init({ keys = {}, initialKeyStates = {}, evictableKeys = [], maxCachedKeysCount = 1000, shouldSyncMultipleInstances = !!global.localStorage, enablePerformanceMetrics = false, enableDevTools = true, skippableCollectionMemberIDs = [], }) {
var _a;
if (enablePerformanceMetrics) {
GlobalSettings.setPerformanceMetricsEnabled(true);
applyDecorators();
}
(0, DevTools_1.initDevTools)(enableDevTools);
storage_1.default.init();
OnyxUtils_1.default.setSkippableCollectionMemberIDs(new Set(skippableCollectionMemberIDs));
if (shouldSyncMultipleInstances) {
(_a = storage_1.default.keepInstancesSync) === null || _a === void 0 ? void 0 : _a.call(storage_1.default, (key, value) => {
OnyxCache_1.default.set(key, value);
OnyxUtils_1.default.keyChanged(key, value);
});
}
if (maxCachedKeysCount > 0) {
OnyxCache_1.default.setRecentKeysLimit(maxCachedKeysCount);
}
OnyxUtils_1.default.initStoreValues(keys, initialKeyStates, evictableKeys);
// Initialize all of our keys with data provided then give green light to any pending connections
Promise.all([OnyxCache_1.default.addEvictableKeysToRecentlyAccessedList(OnyxUtils_1.default.isCollectionKey, OnyxUtils_1.default.getAllKeys), OnyxUtils_1.default.initializeWithDefaultKeyStates()]).then(OnyxUtils_1.default.getDeferredInitTask().resolve);
}
/**
* Connects to an Onyx key given the options passed and listens to its changes.
* This method will be deprecated soon. Please use `Onyx.connectWithoutView()` instead.
*
* @example
* ```ts
* const connection = Onyx.connectWithoutView({
* key: ONYXKEYS.SESSION,
* callback: onSessionChange,
* });
* ```
*
* @param connectOptions The options object that will define the behavior of the connection.
* @param connectOptions.key The Onyx key to subscribe to.
* @param connectOptions.callback A function that will be called when the Onyx data we are subscribed changes.
* @param connectOptions.waitForCollectionCallback If set to `true`, it will return the entire collection to the callback as a single object.
* @param connectOptions.selector This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.**
* Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render
* when the subset of data changes. Otherwise, any change of data on any property would normally
* cause the component to re-render (and that can be expensive from a performance standpoint).
* @returns The connection object to use when calling `Onyx.disconnect()`.
*/
function connect(connectOptions) {
return OnyxConnectionManager_1.default.connect(connectOptions);
}
/**
* Connects to an Onyx key given the options passed and listens to its changes.
*
* @example
* ```ts
* const connection = Onyx.connectWithoutView({
* key: ONYXKEYS.SESSION,
* callback: onSessionChange,
* });
* ```
*
* @param connectOptions The options object that will define the behavior of the connection.
* @param connectOptions.key The Onyx key to subscribe to.
* @param connectOptions.callback A function that will be called when the Onyx data we are subscribed changes.
* @param connectOptions.waitForCollectionCallback If set to `true`, it will return the entire collection to the callback as a single object.
* @param connectOptions.selector This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.**
* Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render
* when the subset of data changes. Otherwise, any change of data on any property would normally
* cause the component to re-render (and that can be expensive from a performance standpoint).
* @returns The connection object to use when calling `Onyx.disconnect()`.
*/
function connectWithoutView(connectOptions) {
return OnyxConnectionManager_1.default.connect(connectOptions);
}
/**
* Disconnects and removes the listener from the Onyx key.
*
* @example
* ```ts
* const connection = Onyx.connectWithoutView({
* key: ONYXKEYS.SESSION,
* callback: onSessionChange,
* });
*
* Onyx.disconnect(connection);
* ```
*
* @param connection Connection object returned by calling `Onyx.connect()` or `Onyx.connectWithoutView()`.
*/
function disconnect(connection) {
OnyxConnectionManager_1.default.disconnect(connection);
}
/**
* Write a value to our store with the given key
*
* @param key ONYXKEY to set
* @param value value to store
* @param options optional configuration object
*/
function set(key, value, options) {
return OnyxUtils_1.default.setWithRetry({ key, value, options });
}
/**
* Sets multiple keys and values
*
* @example Onyx.multiSet({'key1': 'a', 'key2': 'b'});
*
* @param data object keyed by ONYXKEYS and the values to set
*/
function multiSet(data) {
return OnyxUtils_1.default.multiSetWithRetry(data);
}
/**
* Merge a new value into an existing value at a key.
*
* The types of values that can be merged are `Object` and `Array`. To set another type of value use `Onyx.set()`.
* Values of type `Object` get merged with the old value, whilst for `Array`'s we simply replace the current value with the new one.
*
* Calls to `Onyx.merge()` are batched so that any calls performed in a single tick will stack in a queue and get
* applied in the order they were called. Note: `Onyx.set()` calls do not work this way so use caution when mixing
* `Onyx.merge()` and `Onyx.set()`.
*
* @example
* Onyx.merge(ONYXKEYS.EMPLOYEE_LIST, ['Joe']); // -> ['Joe']
* Onyx.merge(ONYXKEYS.EMPLOYEE_LIST, ['Jack']); // -> ['Joe', 'Jack']
* Onyx.merge(ONYXKEYS.POLICY, {id: 1}); // -> {id: 1}
* Onyx.merge(ONYXKEYS.POLICY, {name: 'My Workspace'}); // -> {id: 1, name: 'My Workspace'}
*/
function merge(key, changes) {
const skippableCollectionMemberIDs = OnyxUtils_1.default.getSkippableCollectionMemberIDs();
if (skippableCollectionMemberIDs.size) {
try {
const [, collectionMemberID] = OnyxUtils_1.default.splitCollectionMemberKey(key);
if (skippableCollectionMemberIDs.has(collectionMemberID)) {
// The key is a skippable one, so we set the new changes to undefined.
// eslint-disable-next-line no-param-reassign
changes = undefined;
}
}
catch (e) {
// The key is not a collection one or something went wrong during split, so we proceed with the function's logic.
}
}
const mergeQueue = OnyxUtils_1.default.getMergeQueue();
const mergeQueuePromise = OnyxUtils_1.default.getMergeQueuePromise();
// Top-level undefined values are ignored
// Therefore, we need to prevent adding them to the merge queue
if (changes === undefined) {
return mergeQueue[key] ? mergeQueuePromise[key] : Promise.resolve();
}
// Merge attempts are batched together. The delta should be applied after a single call to get() to prevent a race condition.
// Using the initial value from storage in subsequent merge attempts will lead to an incorrect final merged value.
if (mergeQueue[key]) {
mergeQueue[key].push(changes);
return mergeQueuePromise[key];
}
mergeQueue[key] = [changes];
mergeQueuePromise[key] = OnyxUtils_1.default.get(key).then((existingValue) => {
// Calls to Onyx.set after a merge will terminate the current merge process and clear the merge queue
if (mergeQueue[key] == null) {
return Promise.resolve();
}
try {
const validChanges = mergeQueue[key].filter((change) => {
const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(change, existingValue);
if (!isCompatible) {
Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'merge', existingValueType, newValueType));
}
return isCompatible;
});
// Clean up the write queue, so we don't apply these changes again.
delete mergeQueue[key];
delete mergeQueuePromise[key];
if (!validChanges.length) {
return Promise.resolve();
}
// If the last change is null, we can just delete the key.
// Therefore, we don't need to further broadcast and update the value so we can return early.
if (validChanges.at(-1) === null) {
OnyxUtils_1.default.remove(key);
OnyxUtils_1.default.logKeyRemoved(OnyxUtils_1.default.METHOD.MERGE, key);
return Promise.resolve();
}
return OnyxMerge_1.default.applyMerge(key, existingValue, validChanges).then(({ mergedValue, updatePromise }) => {
OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.MERGE, key, changes, mergedValue);
return updatePromise;
});
}
catch (error) {
Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
return Promise.resolve();
}
});
return mergeQueuePromise[key];
}
/**
* Merges a collection based on their keys.
*
* @example
*
* Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, {
* [`${ONYXKEYS.COLLECTION.REPORT}1`]: report1,
* [`${ONYXKEYS.COLLECTION.REPORT}2`]: report2,
* });
*
* @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
* @param collection Object collection keyed by individual collection member keys and values
*/
function mergeCollection(collectionKey, collection) {
return OnyxUtils_1.default.mergeCollectionWithPatches({ collectionKey, collection, isProcessingCollectionUpdate: true });
}
/**
* Clear out all the data in the store
*
* Note that calling Onyx.clear() and then Onyx.set() on a key with a default
* key state may store an unexpected value in Storage.
*
* E.g.
* Onyx.clear();
* Onyx.set(ONYXKEYS.DEFAULT_KEY, 'default');
* Storage.getItem(ONYXKEYS.DEFAULT_KEY)
* .then((storedValue) => console.log(storedValue));
* null is logged instead of the expected 'default'
*
* Onyx.set() might call Storage.setItem() before Onyx.clear() calls
* Storage.setItem(). Use Onyx.merge() instead if possible. Onyx.merge() calls
* Onyx.get(key) before calling Storage.setItem() via Onyx.set().
* Storage.setItem() from Onyx.clear() will have already finished and the merged
* value will be saved to storage after the default value.
*
* @param keysToPreserve is a list of ONYXKEYS that should not be cleared with the rest of the data
*/
function clear(keysToPreserve = []) {
const defaultKeyStates = OnyxUtils_1.default.getDefaultKeyStates();
const initialKeys = Object.keys(defaultKeyStates);
const promise = OnyxUtils_1.default.getAllKeys()
.then((cachedKeys) => {
OnyxCache_1.default.clearNullishStorageKeys();
const keysToBeClearedFromStorage = [];
const keyValuesToResetAsCollection = {};
const keyValuesToResetIndividually = {};
const allKeys = new Set([...cachedKeys, ...initialKeys]);
// The only keys that should not be cleared are:
// 1. Anything specifically passed in keysToPreserve (because some keys like language preferences, offline
// status, or activeClients need to remain in Onyx even when signed out)
// 2. Any keys with a default state (because they need to remain in Onyx as their default, and setting them
// to null would cause unknown behavior)
// 2.1 However, if a default key was explicitly set to null, we need to reset it to the default value
allKeys.forEach((key) => {
var _a;
const isKeyToPreserve = keysToPreserve.includes(key);
const isDefaultKey = key in defaultKeyStates;
// If the key is being removed or reset to default:
// 1. Update it in the cache
// 2. Figure out whether it is a collection key or not,
// since collection key subscribers need to be updated differently
if (!isKeyToPreserve) {
const oldValue = OnyxCache_1.default.get(key);
const newValue = (_a = defaultKeyStates[key]) !== null && _a !== void 0 ? _a : null;
if (newValue !== oldValue) {
OnyxCache_1.default.set(key, newValue);
let collectionKey;
try {
collectionKey = OnyxUtils_1.default.getCollectionKey(key);
}
catch (e) {
// If getCollectionKey() throws an error it means the key is not a collection key.
collectionKey = undefined;
}
if (collectionKey) {
if (!keyValuesToResetAsCollection[collectionKey]) {
keyValuesToResetAsCollection[collectionKey] = {};
}
keyValuesToResetAsCollection[collectionKey][key] = newValue !== null && newValue !== void 0 ? newValue : undefined;
}
else {
keyValuesToResetIndividually[key] = newValue !== null && newValue !== void 0 ? newValue : undefined;
}
}
}
if (isKeyToPreserve || isDefaultKey) {
return;
}
// If it isn't preserved and doesn't have a default, we'll remove it
keysToBeClearedFromStorage.push(key);
});
const updatePromises = [];
// Notify the subscribers for each key/value group so they can receive the new values
Object.entries(keyValuesToResetIndividually).forEach(([key, value]) => {
updatePromises.push(OnyxUtils_1.default.scheduleSubscriberUpdate(key, value));
});
Object.entries(keyValuesToResetAsCollection).forEach(([key, value]) => {
updatePromises.push(OnyxUtils_1.default.scheduleNotifyCollectionSubscribers(key, value));
});
const defaultKeyValuePairs = Object.entries(Object.keys(defaultKeyStates)
.filter((key) => !keysToPreserve.includes(key))
.reduce((obj, key) => {
// eslint-disable-next-line no-param-reassign
obj[key] = defaultKeyStates[key];
return obj;
}, {}));
// Remove only the items that we want cleared from storage, and reset others to default
keysToBeClearedFromStorage.forEach((key) => OnyxCache_1.default.drop(key));
return storage_1.default.removeItems(keysToBeClearedFromStorage)
.then(() => OnyxConnectionManager_1.default.refreshSessionID())
.then(() => storage_1.default.multiSet(defaultKeyValuePairs))
.then(() => {
DevTools_1.default.clearState(keysToPreserve);
return Promise.all(updatePromises);
});
})
.then(() => undefined);
return OnyxCache_1.default.captureTask(OnyxCache_1.TASK.CLEAR, promise);
}
/**
* Insert API responses and lifecycle data into Onyx
*
* @param data An array of objects with update expressions
* @returns resolves when all operations are complete
*/
function update(data) {
// First, validate the Onyx object is in the format we expect
data.forEach(({ onyxMethod, key, value }) => {
if (!Object.values(OnyxUtils_1.default.METHOD).includes(onyxMethod)) {
throw new Error(`Invalid onyxMethod ${onyxMethod} in Onyx update.`);
}
if (onyxMethod === OnyxUtils_1.default.METHOD.MULTI_SET) {
// For multiset, we just expect the value to be an object
if (typeof value !== 'object' || Array.isArray(value) || typeof value === 'function') {
throw new Error('Invalid value provided in Onyx multiSet. Onyx multiSet value must be of type object.');
}
}
else if (onyxMethod !== OnyxUtils_1.default.METHOD.CLEAR && typeof key !== 'string') {
throw new Error(`Invalid ${typeof key} key provided in Onyx update. Onyx key must be of type string.`);
}
});
// The queue of operations within a single `update` call in the format of <item key - list of operations updating the item>.
// This allows us to batch the operations per item and merge them into one operation in the order they were requested.
const updateQueue = {};
const enqueueSetOperation = (key, value) => {
// If a `set` operation is enqueued, we should clear the whole queue.
// Since the `set` operation replaces the value entirely, there's no need to perform any previous operations.
// To do this, we first put `null` in the queue, which removes the existing value, and then merge the new value.
updateQueue[key] = [null, value];
};
const enqueueMergeOperation = (key, value) => {
if (value === null) {
// If we merge `null`, the value is removed and all the previous operations are discarded.
updateQueue[key] = [null];
}
else if (!updateQueue[key]) {
updateQueue[key] = [value];
}
else {
updateQueue[key].push(value);
}
};
const promises = [];
let clearPromise = Promise.resolve();
data.forEach(({ onyxMethod, key, value }) => {
const handlers = {
[OnyxUtils_1.default.METHOD.SET]: enqueueSetOperation,
[OnyxUtils_1.default.METHOD.MERGE]: enqueueMergeOperation,
[OnyxUtils_1.default.METHOD.MERGE_COLLECTION]: () => {
const collection = value;
if (!OnyxUtils_1.default.isValidNonEmptyCollectionForMerge(collection)) {
Logger.logInfo('mergeCollection enqueued within update() with invalid or empty value. Skipping this operation.');
return;
}
// Confirm all the collection keys belong to the same parent
const collectionKeys = Object.keys(collection);
if (OnyxUtils_1.default.doAllCollectionItemsBelongToSameParent(key, collectionKeys)) {
const mergedCollection = collection;
collectionKeys.forEach((collectionKey) => enqueueMergeOperation(collectionKey, mergedCollection[collectionKey]));
}
},
[OnyxUtils_1.default.METHOD.SET_COLLECTION]: (k, v) => promises.push(() => setCollection(k, v)),
[OnyxUtils_1.default.METHOD.MULTI_SET]: (k, v) => Object.entries(v).forEach(([entryKey, entryValue]) => enqueueSetOperation(entryKey, entryValue)),
[OnyxUtils_1.default.METHOD.CLEAR]: () => {
clearPromise = clear();
},
};
handlers[onyxMethod](key, value);
});
// Group all the collection-related keys and update each collection in a single `mergeCollection` call.
// This is needed to prevent multiple `mergeCollection` calls for the same collection and `merge` calls for the individual items of the said collection.
// This way, we ensure there is no race condition in the queued updates of the same key.
OnyxUtils_1.default.getCollectionKeys().forEach((collectionKey) => {
const collectionItemKeys = Object.keys(updateQueue).filter((key) => OnyxUtils_1.default.isKeyMatch(collectionKey, key));
if (collectionItemKeys.length <= 1) {
// If there are no items of this collection in the updateQueue, we should skip it.
// If there is only one item, we should update it individually, therefore retain it in the updateQueue.
return;
}
const batchedCollectionUpdates = collectionItemKeys.reduce((queue, key) => {
const operations = updateQueue[key];
// Remove the collection-related key from the updateQueue so that it won't be processed individually.
delete updateQueue[key];
const batchedChanges = OnyxUtils_1.default.mergeAndMarkChanges(operations);
if (operations[0] === null) {
// eslint-disable-next-line no-param-reassign
queue.set[key] = batchedChanges.result;
}
else {
// eslint-disable-next-line no-param-reassign
queue.merge[key] = batchedChanges.result;
if (batchedChanges.replaceNullPatches.length > 0) {
// eslint-disable-next-line no-param-reassign
queue.mergeReplaceNullPatches[key] = batchedChanges.replaceNullPatches;
}
}
return queue;
}, {
merge: {},
mergeReplaceNullPatches: {},
set: {},
});
if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.merge)) {
promises.push(() => OnyxUtils_1.default.mergeCollectionWithPatches({
collectionKey,
collection: batchedCollectionUpdates.merge,
mergeReplaceNullPatches: batchedCollectionUpdates.mergeReplaceNullPatches,
isProcessingCollectionUpdate: true,
}));
}
if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.set)) {
promises.push(() => OnyxUtils_1.default.partialSetCollection({ collectionKey, collection: batchedCollectionUpdates.set }));
}
});
Object.entries(updateQueue).forEach(([key, operations]) => {
if (operations[0] === null) {
const batchedChanges = OnyxUtils_1.default.mergeChanges(operations).result;
promises.push(() => set(key, batchedChanges));
return;
}
operations.forEach((operation) => {
promises.push(() => merge(key, operation));
});
});
const snapshotPromises = OnyxUtils_1.default.updateSnapshots(data, merge);
// We need to run the snapshot updates before the other updates so the snapshot data can be updated before the loading state in the snapshot
const finalPromises = snapshotPromises.concat(promises);
return clearPromise.then(() => Promise.all(finalPromises.map((p) => p()))).then(() => undefined);
}
/**
* Sets a collection by replacing all existing collection members with new values.
* Any existing collection members not included in the new data will be removed.
*
* @example
* Onyx.setCollection(ONYXKEYS.COLLECTION.REPORT, {
* [`${ONYXKEYS.COLLECTION.REPORT}1`]: report1,
* [`${ONYXKEYS.COLLECTION.REPORT}2`]: report2,
* });
*
* @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
* @param collection Object collection keyed by individual collection member keys and values
*/
function setCollection(collectionKey, collection) {
return OnyxUtils_1.default.setCollectionWithRetry({ collectionKey, collection });
}
const Onyx = {
METHOD: OnyxUtils_1.default.METHOD,
connect,
connectWithoutView,
disconnect,
set,
multiSet,
merge,
mergeCollection,
setCollection,
update,
clear,
init,
registerLogger: Logger.registerLogger,
};
function applyDecorators() {
// We are reassigning the functions directly so that internal function calls are also decorated
/* eslint-disable rulesdir/prefer-actions-set-data */
// @ts-expect-error Reassign
connect = (0, metrics_1.default)(connect, 'Onyx.connect');
// @ts-expect-error Reassign
connectWithoutView = (0, metrics_1.default)(connectWithoutView, 'Onyx.connectWithoutView');
// @ts-expect-error Reassign
set = (0, metrics_1.default)(set, 'Onyx.set');
// @ts-expect-error Reassign
multiSet = (0, metrics_1.default)(multiSet, 'Onyx.multiSet');
// @ts-expect-error Reassign
merge = (0, metrics_1.default)(merge, 'Onyx.merge');
// @ts-expect-error Reassign
mergeCollection = (0, metrics_1.default)(mergeCollection, 'Onyx.mergeCollection');
// @ts-expect-error Reassign
update = (0, metrics_1.default)(update, 'Onyx.update');
// @ts-expect-error Reassign
clear = (0, metrics_1.default)(clear, 'Onyx.clear');
/* eslint-enable rulesdir/prefer-actions-set-data */
}
exports.default = Onyx;