UNPKG

async-states

Version:

Core of async-states

190 lines (187 loc) 7.16 kB
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