UNPKG

react-native-onyx

Version:

State management for React Native

299 lines (298 loc) 11.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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __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 = []; this.storageKeys = new Set(); this.nullishStorageKeys = new Set(); this.recentKeys = new Set(); this.storageMap = {}; 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'); } /** 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); if (value === null || value === undefined) { delete this.storageMap[key]; return undefined; } this.storageMap[key] = value; return value; } /** Forget the cached value for the given key */ drop(key) { delete this.storageMap[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)); Object.entries(data).forEach(([key, value]) => { this.addKey(key); this.addToAccessedKeys(key); if (value === null || value === undefined) { this.addNullishStorageKey(key); } else { this.nullishStorageKeys.delete(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]; this.recentKeys.delete(key); } } /** Set the recent keys list size */ setRecentKeysLimit(limit) { this.maxRecentKeysSize = limit; } /** Check if the value has changed */ hasValueChanged(key, value) { return !(0, fast_equals_1.deepEqual)(this.storageMap[key], 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 = this.recentlyAccessedKeys.filter((recentlyAccessedKey) => recentlyAccessedKey !== 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.push(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() { return this.recentlyAccessedKeys.find((key) => !this.evictionBlocklist[key]); } } const instance = new OnyxCache(); exports.default = instance;