async-states
Version:
Core of async-states
190 lines (187 loc) • 7.16 kB
JavaScript
import { isFunction, emptyArray, defaultHash, isPromise } from '../utils.js';
import { now } from '../helpers/core.js';
import { invokeInstanceEvents } from './StateEvent.js';
function hasCacheEnabled(instance) {
return !!instance.config.cacheConfig?.enabled;
}
function getTopLevelParent(base) {
let current = base;
while (current.parent) {
current = current.parent;
}
return current;
}
function computeRunHash(payload, props, hashFn) {
let args = props?.args || emptyArray;
let hashFunction = hashFn ?? defaultHash;
return hashFunction(args, payload);
}
function getCachedState(instance, hash) {
let topLevelParent = getTopLevelParent(instance);
return topLevelParent.cache?.[hash];
}
function removeCachedStateAndSpreadOnLanes(instance, hash) {
let topLevelParent = getTopLevelParent(instance);
if (!topLevelParent.cache) {
return;
}
delete topLevelParent.cache[hash];
persistAndSpreadCache(topLevelParent);
}
function persistAndSpreadCache(topLevelParent) {
if (topLevelParent.cache &&
isFunction(topLevelParent.config.cacheConfig?.persist)) {
topLevelParent.config.cacheConfig.persist(topLevelParent.cache);
}
spreadCacheChangeOnLanes(topLevelParent);
}
function didCachedStateExpire(cachedState) {
const { addedAt, deadline } = cachedState;
return addedAt + deadline < now();
}
function spreadCacheChangeOnLanes(topLevelParent) {
invokeInstanceEvents(topLevelParent, "cache-change");
if (!topLevelParent.lanes) {
return;
}
Object.values(topLevelParent.lanes).forEach((lane) => {
lane.cache = topLevelParent.cache;
spreadCacheChangeOnLanes(lane);
});
}
// from remix
function hasHeadersSet(headers) {
return headers && isFunction(headers.get);
}
function saveCacheAfterSuccessfulUpdate(instance) {
let topLevelParent = getTopLevelParent(instance);
let { config: { cacheConfig }, } = topLevelParent;
let state = instance.state;
let { props } = state;
if (!topLevelParent.cache) {
topLevelParent.cache = {};
}
let hashFunction = cacheConfig?.hash || defaultHash;
let runHash = hashFunction(props?.args, props?.payload);
if (topLevelParent.cache[runHash]?.state !== state) {
let deadline = getStateDeadline(state, cacheConfig?.timeout);
let cachedState = (topLevelParent.cache[runHash] = {
deadline,
state: state,
addedAt: now(),
});
// avoid infinity deadline timeouts
if (cacheConfig?.auto && Number.isFinite(deadline)) {
// after this deadline is elapsed, we would removed the cached entry
// from the cache: only if it has the same reference.
// because invalidateCache or replaceCache may have been called in
// between.
let id = setTimeout(() => {
let topLevelParentCache = topLevelParent.cache;
if (!topLevelParentCache) {
return;
}
let currentCacheAtHash = topLevelParentCache[runHash];
if (!currentCacheAtHash) {
return;
}
if (id !== currentCacheAtHash.id) {
// this means that this cached state's id changed, probably due
// to some unknown error, such as persisting the cache, running
// and forcing loading it again.
return;
}
delete topLevelParentCache[runHash];
// todo: dispatch cache change event
// only refresh the cached state if:
// - we have subscriptions
// - it is the latest state
if (instance.subscriptions &&
Object.keys(instance.subscriptions).length) {
// re-run only when the current state is the same as the cached one
// or else, when the user will want it, it won't find it in the cache
// and thus it will request it again.
if (instance.state === state) {
// being the latest state, means that it we are not pending, nor
// error, which means there has been no runs in between. so we will
// use replay.
// an alternative would be taking args and payload from the state
// and invoking the run again. if the replay is proved to cause
// issues, we will use the other alternative
instance.actions.replay();
}
}
}, deadline);
cachedState.id = id;
}
if (cacheConfig && isFunction(cacheConfig.persist)) {
cacheConfig.persist(topLevelParent.cache);
}
spreadCacheChangeOnLanes(topLevelParent);
}
}
function getStateDeadline(state, timeout) {
// fast path for numbers
if (timeout && !isFunction(timeout)) {
return timeout;
}
let { data } = state;
let deadline = Infinity;
if (!timeout && data && hasHeadersSet(data.headers)) {
let maybeMaxAge = readCacheControlMaxAgeHeader(data.headers);
if (maybeMaxAge && maybeMaxAge > 0) {
deadline = maybeMaxAge;
}
}
if (isFunction(timeout)) {
deadline = timeout(state);
}
return deadline;
}
// https://stackoverflow.com/a/60154883/7104283
function readCacheControlMaxAgeHeader(headers) {
let cacheControl = headers.get("cache-control");
if (cacheControl) {
let matches = cacheControl.match(/max-age=(\d+)/);
return matches ? parseInt(matches[1], 10) : undefined;
}
}
function loadCache(instance) {
if (!hasCacheEnabled(instance) ||
!isFunction(instance.config.cacheConfig?.load)) {
return;
}
// inherit cache from the parent if exists!
if (instance.parent) {
let topLevelParent = getTopLevelParent(instance);
instance.cache = topLevelParent.cache;
return;
}
let loadedCache = instance.config.cacheConfig.load();
if (!loadedCache) {
return;
}
if (isPromise(loadedCache)) {
waitForAsyncCache(instance, loadedCache);
}
else {
resolveCache(instance, loadedCache);
}
}
function waitForAsyncCache(instance, promise) {
promise.then((asyncCache) => {
resolveCache(instance, asyncCache);
});
}
function resolveCache(instance, resolvedCache) {
instance.cache = resolvedCache;
const cacheConfig = instance.config.cacheConfig;
if (isFunction(cacheConfig.onCacheLoad)) {
cacheConfig.onCacheLoad({
cache: instance.cache,
source: instance.actions,
});
}
}
export { computeRunHash, didCachedStateExpire, getCachedState, getTopLevelParent, hasCacheEnabled, hasHeadersSet, loadCache, persistAndSpreadCache, removeCachedStateAndSpreadOnLanes, saveCacheAfterSuccessfulUpdate, spreadCacheChangeOnLanes };
//# sourceMappingURL=StateCache.js.map