UNPKG

react-native-onyx

Version:

State management for React Native

400 lines (399 loc) 15.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 }); exports.TASK = void 0; const fast_equals_1 = require("fast-equals"); const bindAll_1 = __importDefault(require("lodash/bindAll")); const utils_1 = __importDefault(require("./utils")); const Str = __importStar(require("./Str")); // Task constants const TASK = { GET: 'get', GET_ALL_KEYS: 'getAllKeys', CLEAR: 'clear', }; exports.TASK = TASK; /** * In memory cache providing data by reference * Encapsulates Onyx cache related functionality */ class OnyxCache { constructor() { /** Maximum size of the keys store din cache */ this.maxRecentKeysSize = 0; /** List of keys that are safe to remove when we reach max storage */ this.evictionAllowList = []; /** Map of keys and connection arrays whose keys will never be automatically evicted */ this.evictionBlocklist = {}; /** List of keys that have been directly subscribed to or recently modified from least to most recent */ this.recentlyAccessedKeys = new Set(); /** Set of collection keys for fast lookup */ this.collectionKeys = new Set(); this.storageKeys = new Set(); this.nullishStorageKeys = new Set(); this.recentKeys = new Set(); this.storageMap = {}; this.collectionData = {}; this.pendingPromises = new Map(); // bind all public methods to prevent problems with `this` (0, bindAll_1.default)(this, 'getAllKeys', 'get', 'hasCacheForKey', 'addKey', 'addNullishStorageKey', 'hasNullishStorageKey', 'clearNullishStorageKeys', 'set', 'drop', 'merge', 'hasPendingTask', 'getTaskPromise', 'captureTask', 'addToAccessedKeys', 'removeLeastRecentlyUsedKeys', 'setRecentKeysLimit', 'setAllKeys', 'setEvictionAllowList', 'getEvictionBlocklist', 'isEvictableKey', 'removeLastAccessedKey', 'addLastAccessedKey', 'addEvictableKeysToRecentlyAccessedList', 'getKeyForEviction', 'setCollectionKeys', 'isCollectionKey', 'getCollectionKey', 'getCollectionData'); } /** Get all the storage keys */ getAllKeys() { return this.storageKeys; } /** * Allows to set all the keys at once. * This is useful when we are getting * all the keys from the storage provider * and we want to keep the cache in sync. * * Previously, we had to call `addKey` in a loop * to achieve the same result. * * @param keys - an array of keys */ setAllKeys(keys) { this.storageKeys = new Set(keys); } /** Saves a key in the storage keys list * Serves to keep the result of `getAllKeys` up to date */ addKey(key) { this.storageKeys.add(key); } /** Used to set keys that are null/undefined in storage without adding null to the storage map */ addNullishStorageKey(key) { this.nullishStorageKeys.add(key); } /** Used to set keys that are null/undefined in storage without adding null to the storage map */ hasNullishStorageKey(key) { return this.nullishStorageKeys.has(key); } /** Used to clear keys that are null/undefined in cache */ clearNullishStorageKeys() { this.nullishStorageKeys = new Set(); } /** Check whether cache has data for the given key */ hasCacheForKey(key) { return this.storageMap[key] !== undefined || this.hasNullishStorageKey(key); } /** * Get a cached value from storage * @param [shouldReindexCache] – This is an LRU cache, and by default accessing a value will make it become last in line to be evicted. This flag can be used to skip that and just access the value directly without side-effects. */ get(key, shouldReindexCache = true) { if (shouldReindexCache) { this.addToAccessedKeys(key); } return this.storageMap[key]; } /** * Set's a key value in cache * Adds the key to the storage keys list as well */ set(key, value) { this.addKey(key); this.addToAccessedKeys(key); // When a key is explicitly set in cache, we can remove it from the list of nullish keys, // since it will either be set to a non nullish value or removed from the cache completely. this.nullishStorageKeys.delete(key); const collectionKey = this.getCollectionKey(key); if (value === null || value === undefined) { delete this.storageMap[key]; // Remove from collection data cache if it's a collection member if (collectionKey && this.collectionData[collectionKey]) { delete this.collectionData[collectionKey][key]; } return undefined; } this.storageMap[key] = value; // Update collection data cache if this is a collection member if (collectionKey) { if (!this.collectionData[collectionKey]) { this.collectionData[collectionKey] = {}; } this.collectionData[collectionKey][key] = value; } return value; } /** Forget the cached value for the given key */ drop(key) { delete this.storageMap[key]; // Remove from collection data cache if this is a collection member const collectionKey = this.getCollectionKey(key); if (collectionKey && this.collectionData[collectionKey]) { delete this.collectionData[collectionKey][key]; } // If this is a collection key, clear its data if (this.isCollectionKey(key)) { delete this.collectionData[key]; } this.storageKeys.delete(key); this.recentKeys.delete(key); } /** * Deep merge data to cache, any non existing keys will be created * @param data - a map of (cache) key - values */ merge(data) { if (typeof data !== 'object' || Array.isArray(data)) { throw new Error('data passed to cache.merge() must be an Object of onyx key/value pairs'); } this.storageMap = Object.assign({}, utils_1.default.fastMerge(this.storageMap, data, { shouldRemoveNestedNulls: true, objectRemovalMode: 'replace', }).result); Object.entries(data).forEach(([key, value]) => { this.addKey(key); this.addToAccessedKeys(key); const collectionKey = this.getCollectionKey(key); if (value === null || value === undefined) { this.addNullishStorageKey(key); // Remove from collection data cache if it's a collection member if (collectionKey && this.collectionData[collectionKey]) { delete this.collectionData[collectionKey][key]; } } else { this.nullishStorageKeys.delete(key); // Update collection data cache if this is a collection member if (collectionKey) { if (!this.collectionData[collectionKey]) { this.collectionData[collectionKey] = {}; } this.collectionData[collectionKey][key] = this.storageMap[key]; } } }); } /** * Check whether the given task is already running * @param taskName - unique name given for the task */ hasPendingTask(taskName) { return this.pendingPromises.get(taskName) !== undefined; } /** * Use this method to prevent concurrent calls for the same thing * Instead of calling the same task again use the existing promise * provided from this function * @param taskName - unique name given for the task */ getTaskPromise(taskName) { return this.pendingPromises.get(taskName); } /** * Capture a promise for a given task so other caller can * hook up to the promise if it's still pending * @param taskName - unique name for the task */ captureTask(taskName, promise) { const returnPromise = promise.finally(() => { this.pendingPromises.delete(taskName); }); this.pendingPromises.set(taskName, returnPromise); return returnPromise; } /** Adds a key to the top of the recently accessed keys */ addToAccessedKeys(key) { this.recentKeys.delete(key); this.recentKeys.add(key); } /** Remove keys that don't fall into the range of recently used keys */ removeLeastRecentlyUsedKeys() { const numKeysToRemove = this.recentKeys.size - this.maxRecentKeysSize; if (numKeysToRemove <= 0) { return; } const iterator = this.recentKeys.values(); const keysToRemove = []; const recentKeysArray = Array.from(this.recentKeys); const mostRecentKey = recentKeysArray[recentKeysArray.length - 1]; let iterResult = iterator.next(); while (!iterResult.done) { const key = iterResult.value; // Don't consider the most recently accessed key for eviction // This ensures we don't immediately evict a key we just added if (key !== undefined && key !== mostRecentKey && this.isEvictableKey(key)) { keysToRemove.push(key); } iterResult = iterator.next(); } for (const key of keysToRemove) { delete this.storageMap[key]; // Remove from collection data cache if this is a collection member const collectionKey = this.getCollectionKey(key); if (collectionKey && this.collectionData[collectionKey]) { delete this.collectionData[collectionKey][key]; } this.recentKeys.delete(key); } } /** Set the recent keys list size */ setRecentKeysLimit(limit) { this.maxRecentKeysSize = limit; } /** Check if the value has changed */ hasValueChanged(key, value) { const currentValue = this.get(key, false); return !(0, fast_equals_1.deepEqual)(currentValue, value); } /** * Sets the list of keys that are considered safe for eviction * @param keys - Array of OnyxKeys that are safe to evict */ setEvictionAllowList(keys) { this.evictionAllowList = keys; } /** * Get the eviction block list that prevents keys from being evicted */ getEvictionBlocklist() { return this.evictionBlocklist; } /** * Checks to see if this key has been flagged as safe for removal. * @param testKey - Key to check */ isEvictableKey(testKey) { return this.evictionAllowList.some((key) => this.isKeyMatch(key, testKey)); } /** * Check if a given key matches a pattern key * @param configKey - Pattern that may contain a wildcard * @param key - Key to test against the pattern */ isKeyMatch(configKey, key) { const isCollectionKey = configKey.endsWith('_'); return isCollectionKey ? Str.startsWith(key, configKey) : configKey === key; } /** * Remove a key from the recently accessed key list. */ removeLastAccessedKey(key) { this.recentlyAccessedKeys.delete(key); } /** * Add a key to the list of recently accessed keys. The least * recently accessed key should be at the head and the most * recently accessed key at the tail. */ addLastAccessedKey(key, isCollectionKey) { // Only specific keys belong in this list since we cannot remove an entire collection. if (isCollectionKey || !this.isEvictableKey(key)) { return; } this.removeLastAccessedKey(key); this.recentlyAccessedKeys.add(key); } /** * Take all the keys that are safe to evict and add them to * the recently accessed list when initializing the app. This * enables keys that have not recently been accessed to be * removed. * @param isCollectionKeyFn - Function to determine if a key is a collection key * @param getAllKeysFn - Function to get all keys, defaults to Storage.getAllKeys */ addEvictableKeysToRecentlyAccessedList(isCollectionKeyFn, getAllKeysFn) { return getAllKeysFn().then((keys) => { this.evictionAllowList.forEach((evictableKey) => { keys.forEach((key) => { if (!this.isKeyMatch(evictableKey, key)) { return; } this.addLastAccessedKey(key, isCollectionKeyFn(key)); }); }); }); } /** * Finds a key that can be safely evicted */ getKeyForEviction() { for (const key of this.recentlyAccessedKeys) { if (!this.evictionBlocklist[key]) { return key; } } return undefined; } /** * Set the collection keys for optimized storage */ setCollectionKeys(collectionKeys) { this.collectionKeys = collectionKeys; // Initialize collection data for existing collection keys collectionKeys.forEach((collectionKey) => { if (this.collectionData[collectionKey]) { return; } this.collectionData[collectionKey] = {}; }); } /** * Check if a key is a collection key */ isCollectionKey(key) { return this.collectionKeys.has(key); } /** * Get the collection key for a given member key */ getCollectionKey(key) { for (const collectionKey of this.collectionKeys) { if (key.startsWith(collectionKey) && key.length > collectionKey.length) { return collectionKey; } } return null; } /** * Get all data for a collection key */ getCollectionData(collectionKey) { const cachedCollection = this.collectionData[collectionKey]; if (!cachedCollection || Object.keys(cachedCollection).length === 0) { return undefined; } // Return a shallow copy to ensure React detects changes when items are added/removed return Object.assign({}, cachedCollection); } } const instance = new OnyxCache(); exports.default = instance;