react-native-onyx
Version:
State management for React Native
299 lines (298 loc) • 11.8 kB
JavaScript
"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;