UNPKG

react-native-onyx

Version:

State management for React Native

545 lines (544 loc) 26.8 kB
"use strict"; 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;