async-states
Version:
Core of async-states
1,368 lines (1,352 loc) • 55.5 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.AsyncStates = {}));
})(this, (function (exports) { 'use strict';
let __DEV__ = process.env.NODE_ENV !== "production";
let maybeWindow = typeof window !== "undefined" ? window : undefined;
let isServer = typeof maybeWindow === "undefined" || "Deno" in maybeWindow;
let emptyArray = [];
function defaultHash(args, payload) {
return JSON.stringify({ args, payload });
}
function isPromise(candidate) {
return !!candidate && isFunction(candidate.then);
}
function isFunction(fn) {
return typeof fn === "function";
}
function cloneProducerProps(props) {
return {
args: props.args,
payload: props.payload,
};
}
const defaultAnonymousPrefix = "async-state-";
const nextKey = (function autoKey() {
let key = 0;
return function incrementAndGet() {
key += 1;
return `${defaultAnonymousPrefix}${key}`;
};
})();
class LibraryDevtools {
// the list of connected devtools clients, can be the npm package,
// the devtools extension, or both for example
listeners = [];
// on connection from a client, we will give it the list of created
// LibraryContexts that hold all instances that are created.
// standalone instances and not stored in context will be gathered too
// in a separate map, and on dispose they will be removed
// this is relevant only on connection to give the full list of current
// instances, after connexion, there is no need because the event
// gives the instance itself and client should ensure instance is retained
standalone = new Set();
contexts = new Set();
constructor() {
this.connect = this.connect.bind(this);
this.emitRun = this.emitRun.bind(this);
this.emitSub = this.emitSub.bind(this);
this.emitUnsub = this.emitUnsub.bind(this);
this.emitUpdate = this.emitUpdate.bind(this);
this.disconnect = this.disconnect.bind(this);
this.startUpdate = this.startUpdate.bind(this);
this.emitDispose = this.emitDispose.bind(this);
this.emitCreation = this.emitCreation.bind(this);
this.emitInstance = this.emitInstance.bind(this);
this.captureContext = this.captureContext.bind(this);
this.releaseContext = this.releaseContext.bind(this);
this.ensureInstanceIsRetained = this.ensureInstanceIsRetained.bind(this);
}
connect(listener) {
this.listeners.push(listener);
// on any connection of any client, we give him the currently retained
// instances. All of them. The list include:
// - Any instance in any non-terminated LibraryContext
// - Any active 'standalone' instance
let instances = [...this.standalone];
for (let context of this.contexts) {
instances.push(...context.getAll());
}
listener.onConnect(instances);
}
disconnect(listener) {
this.listeners = this.listeners.filter((t) => t !== listener);
}
emitCreation(instance) {
if (instance.config.hideFromDevtools)
return;
this.listeners.forEach((listener) => listener.emitCreation(instance));
}
emitDispose(instance) {
if (instance.config.hideFromDevtools)
return;
this.listeners.forEach((listener) => listener.emitDispose(instance));
}
emitInstance(instance) {
if (instance.config.hideFromDevtools)
return;
this.listeners.forEach((listener) => listener.emitInstance(instance));
}
emitRun(instance, cache) {
if (instance.config.hideFromDevtools)
return;
this.listeners.forEach((listener) => listener.emitRun(instance, cache));
}
emitSub(instance, key) {
if (instance.config.hideFromDevtools)
return;
this.ensureInstanceIsRetained(instance);
this.listeners.forEach((listener) => listener.emitSub(instance, key));
}
emitUnsub(instance, key) {
if (instance.config.hideFromDevtools)
return;
// undefined is considered truthy for backward compatibility
// when an unsubscription occurs when not retained by a context, if
// there is no subscription remaining, remove standalone instances
if (instance.config.storeInContext === false &&
Object.keys(instance.subscriptions).length === 0) {
this.standalone.delete(instance);
}
this.listeners.forEach((listener) => listener.emitUnsub(instance, key));
}
startUpdate(instance) {
if (instance.config.hideFromDevtools)
return;
this.listeners.forEach((listener) => listener.startUpdate(instance));
}
emitUpdate(instance, replace) {
if (instance.config.hideFromDevtools)
return;
this.listeners.forEach((listener) => listener.emitUpdate(instance, replace));
}
captureContext(context) {
this.contexts.add(context);
}
onConnect() {
throw new Error("The library devtools agent doesn't have an onConnect.");
}
releaseContext(context) {
this.contexts.delete(context);
}
ensureInstanceIsRetained(instance) {
// undefined is considered truthy for backward compatibility
if (instance.config.storeInContext === false) {
this.standalone.add(instance);
}
}
}
let devtools$1;
if (__DEV__) {
devtools$1 = new LibraryDevtools();
}
var devtools = devtools$1;
let error = "error";
let pending = "pending";
let success = "success";
let initial = "initial";
let noop = () => { };
let now = () => Date.now();
let freeze = Object.freeze;
let isArray = Array.isArray;
function shallowClone(source1, source2) {
return Object.assign({}, source1, source2);
}
let uniqueId = 0;
function nextUniqueId() {
return ++uniqueId;
}
function invokeChangeCallbacks(state, callbacks) {
if (!callbacks) {
return;
}
let { onError, onSuccess } = callbacks;
if (onSuccess && state.status === success) {
onSuccess(state);
}
if (onError && state.status === error) {
onError(state);
}
}
function invokeSingleChangeEvent(state, event) {
if (isFunction(event)) {
event(state);
}
else if (typeof event === "object" && event.status === state.status) {
event.handler(state);
}
}
function invokeInstanceEvents(instance, type) {
let events = instance.events;
if (!events || !events[type]) {
return;
}
switch (type) {
case "change": {
Object.values(events[type]).forEach((registeredEvents) => {
if (isArray(registeredEvents)) {
registeredEvents.forEach((evt) => {
invokeSingleChangeEvent(instance.actions.getState(), evt);
});
}
else {
invokeSingleChangeEvent(instance.actions.getState(), registeredEvents);
}
});
return;
}
case "dispose": {
Object.values(events[type]).forEach((registeredEvents) => {
if (isArray(registeredEvents)) {
registeredEvents.forEach((evt) => evt());
}
else {
registeredEvents();
}
});
return;
}
case "cache-change": {
Object.values(events[type]).forEach((registeredEvents) => {
if (isArray(registeredEvents)) {
registeredEvents.forEach((evt) => evt(instance.cache));
}
else {
registeredEvents(instance.cache);
}
});
return;
}
}
}
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,
});
}
}
function subscribeToInstance(instance, options) {
let props = isFunction(options) ? { cb: options } : options;
if (!isFunction(props.cb)) {
return;
}
if (!instance.subsIndex) {
instance.subsIndex = 0;
}
if (!instance.subscriptions) {
instance.subscriptions = {};
}
instance.subsIndex += 1;
let subscriptionKey = props.key;
if (subscriptionKey === undefined) {
subscriptionKey = `$${instance.subsIndex}`;
}
function cleanup() {
delete instance.subscriptions[subscriptionKey];
if (__DEV__)
devtools.emitUnsub(instance, subscriptionKey);
if (instance.config.resetStateOnDispose) {
instance.actions.dispose();
}
}
instance.subscriptions[subscriptionKey] = { props, cleanup };
if (__DEV__)
devtools.emitSub(instance, subscriptionKey);
return cleanup;
}
function subscribeToInstanceEvent(instance, eventType, eventHandler) {
if (!instance.events) {
instance.events = {};
}
if (!instance.events[eventType]) {
instance.events[eventType] = {};
}
let events = instance.events[eventType];
if (!instance.eventsIndex) {
instance.eventsIndex = 0;
}
let index = ++instance.eventsIndex;
events[index] = eventHandler;
return function () {
delete events[index];
};
}
function notifySubscribers(instance) {
if (!instance.subscriptions) {
return;
}
Object.values(instance.subscriptions).forEach((subscription) => {
subscription.props.cb(instance.state);
});
}
let isCurrentlyEmitting = false;
let isCurrentlyAlteringState = false;
let isCurrentlyFlushingAQueue = false;
function startEmitting() {
let prevIsEmitting = isCurrentlyEmitting;
isCurrentlyEmitting = true;
return prevIsEmitting;
}
function stopEmitting(restoreToThisValue) {
isCurrentlyEmitting = restoreToThisValue;
}
function isAlteringState() {
return isCurrentlyAlteringState;
}
function startAlteringState() {
let prevIsAltering = isCurrentlyAlteringState;
isCurrentlyAlteringState = true;
return prevIsAltering;
}
function stopAlteringState(restoreToThisValue) {
isCurrentlyAlteringState = restoreToThisValue;
}
function getQueueTail(instance) {
if (!instance.queue) {
return null;
}
let current = instance.queue;
while (current.next !== null) {
current = current.next;
}
return current;
}
function addToQueueAndEnsureItsScheduled(instance, update) {
if (!instance.queue) {
instance.queue = update;
}
else {
let tail = getQueueTail(instance);
if (!tail) {
return;
}
tail.next = update;
}
ensureQueueIsScheduled(instance);
}
function enqueueUpdate(instance, newState, callbacks) {
let update = {
callbacks,
data: newState,
kind: 0,
next: null,
};
addToQueueAndEnsureItsScheduled(instance, update);
}
function enqueueSetState(instance, newValue, status = success, callbacks) {
let update = {
callbacks,
kind: 1,
data: { data: newValue, status },
next: null,
};
addToQueueAndEnsureItsScheduled(instance, update);
}
function enqueueSetData(instance, newValue) {
let update = {
kind: 2,
data: newValue,
next: null,
};
addToQueueAndEnsureItsScheduled(instance, update);
}
function ensureQueueIsScheduled(instance) {
if (!instance.queue) {
return;
}
let queue = instance.queue;
if (queue.id) {
return;
}
let delay = instance.config.keepPendingForMs || 0;
let elapsedTime = now() - instance.state.timestamp;
let remainingTime = delay - elapsedTime;
if (remainingTime > 0) {
queue.id = setTimeout(() => flushUpdateQueue(instance), remainingTime);
}
else {
flushUpdateQueue(instance);
}
}
function flushUpdateQueue(instance) {
if (!instance.queue) {
return;
}
let current = instance.queue;
instance.queue = null;
let prevIsFlushing = isCurrentlyFlushingAQueue;
isCurrentlyFlushingAQueue = true;
while (current !== null) {
// when we encounter a pending state that's not the last one
// we can safely skip it an process the next update
let canBailoutPendingStatus = current.kind !== 2 && // 2 is for setData
current.data.status === pending &&
current.next !== null;
// so we will only process the updates that we can't skip from the queue
if (!canBailoutPendingStatus) {
switch (current.kind) {
// there update came from replaceState(newWholeState)
case 0: {
let { data, callbacks } = current;
// when the queue isn't empty, there is a caveat that might happen
// when the path setting the whole state is in the queue.
// ** It may lead to inconsistency with the timestamp
// to avoid this, the state object is recreated here.
let newState = { ...data };
newState.timestamp = now();
freeze(newState);
replaceInstanceState(instance, newState, false, callbacks);
break;
}
// there update came from setState(value, status)
case 1: {
let { status, data } = current.data;
setInstanceState(instance, data, status, current.callbacks);
break;
}
// there update came from setData(value)
case 2: {
isCurrentSetStateSettingData = true;
setInstanceState(instance, current.data, success);
isCurrentSetStateSettingData = false;
break;
}
}
}
current = current.next;
}
isCurrentlyFlushingAQueue = prevIsFlushing;
// always notify after a state update
notifySubscribers(instance);
}
function scheduleDelayedPendingUpdate(instance, newState, notify) {
function callback() {
// callback always sets the state with a pending status
if (__DEV__)
devtools.startUpdate(instance);
let clonedState = shallowClone(newState);
clonedState.timestamp = now();
instance.state = freeze(clonedState); // <-- status is pending!
instance.pendingUpdate = null;
instance.version += 1;
invokeInstanceEvents(instance, "change");
if (__DEV__)
devtools.emitUpdate(instance, false);
if (notify) {
notifySubscribers(instance);
}
}
let timeoutId = setTimeout(callback, instance.config.skipPendingDelayMs);
instance.pendingUpdate = { callback, id: timeoutId };
}
function replaceInstanceState(instance, newState, notify = true, callbacks) {
let { config } = instance;
let isPending = newState.status === pending;
if (isPending && config.skipPendingStatus) {
return;
}
if (instance.queue) {
enqueueUpdate(instance, newState, callbacks);
return;
}
if (config.keepPendingForMs &&
instance.state.status === pending &&
!isCurrentlyFlushingAQueue) {
enqueueUpdate(instance, newState, callbacks);
return;
}
// pending update has always a pending status
// setting the state should always clear this pending update
// because it is stale, and we can safely skip it
if (instance.pendingUpdate) {
clearTimeout(instance.pendingUpdate.id);
instance.pendingUpdate = null;
}
if (isPending &&
config.skipPendingDelayMs &&
isFunction(setTimeout) &&
config.skipPendingDelayMs > 0) {
scheduleDelayedPendingUpdate(instance, newState, notify);
return;
}
if (__DEV__)
devtools.startUpdate(instance);
instance.version += 1;
instance.state = newState;
invokeChangeCallbacks(newState, callbacks);
invokeInstanceEvents(instance, "change");
if (__DEV__)
devtools.emitUpdate(instance, false);
if (instance.state.status === success) {
instance.lastSuccess = instance.state;
if (hasCacheEnabled(instance)) {
saveCacheAfterSuccessfulUpdate(instance);
}
}
if (notify && !isCurrentlyFlushingAQueue) {
notifySubscribers(instance);
}
}
let isCurrentSetStateSettingData = false;
function setInstanceData(instance, newValue) {
let previouslySettingData = isCurrentSetStateSettingData;
isCurrentSetStateSettingData = true;
setInstanceState(instance, newValue);
isCurrentSetStateSettingData = previouslySettingData;
}
function setInstanceState(instance, newValue, status = success, callbacks) {
if (instance.queue) {
if (isCurrentSetStateSettingData) {
let update = newValue;
enqueueSetData(instance, update);
isCurrentSetStateSettingData = false;
return;
}
let update = newValue;
enqueueSetState(instance, update, status, callbacks);
return;
}
let wasAlteringState = startAlteringState();
if (instance.state.status === pending ||
(isFunction(instance.currentAbort) && !isCurrentlyEmitting)) {
instance.actions.abort();
instance.currentAbort = undefined;
}
let effectiveValue = newValue;
if (isFunction(newValue)) {
// there two possible ways to update the state by updater:
// 1. the legacy setState(prevState => newData);
// 2. the new setData(prevData => newData);
// To avoid duplicating this whole function, a module level variable
// was introduced to distinguish between both of them
// this variable is set to true when the call occurs from setData
if (isCurrentSetStateSettingData) {
let update = newValue;
effectiveValue = update(instance.lastSuccess.data ?? null);
}
else {
effectiveValue = newValue(instance.state);
}
}
// setting state from this path passes without props as a direct call to
// setState; which means there are no "props", but we only care about
// args and payload, so we hack it here like this
let partialProducerProps = {
args: [effectiveValue],
payload: shallowClone(instance.payload),
};
const savedProps = cloneProducerProps(partialProducerProps);
let newState = {
status,
timestamp: now(),
props: savedProps,
data: effectiveValue,
};
// if the next state has a pending status, we need to populate the prev
// property, we take the previous state, if it was also pending, take
// its prev and set it as previous.
if (newState.status === pending) {
let previousState = instance.state;
if (previousState.status === pending) {
previousState = previousState.prev;
}
if (previousState) {
newState.prev = previousState;
}
}
replaceInstanceState(instance, newState, true, callbacks);
stopAlteringState(wasAlteringState);
}
function disposeInstance(instance) {
if (instance.subscriptions && Object.keys(instance.subscriptions).length) {
// this means that this state is retained by some subscriptions
return false;
}
let wasAltering = startAlteringState();
instance.actions.abort();
if (instance.queue) {
clearTimeout(instance.queue.id);
instance.queue = null;
}
let initialState = instance.config.initialValue;
if (isFunction(initialState)) {
initialState = initialState(instance.cache);
}
let initialSavedProps = {
args: [initialState],
payload: shallowClone(instance.payload),
};
const newState = {
status: initial,
timestamp: now(),
data: initialState,
props: initialSavedProps,
};
replaceInstanceState(instance, newState);
// resetting the state may cause problems in the sense that lastSuccess.data
// stays alive, but is now un-synced
instance.lastSuccess = newState;
if (__DEV__)
devtools.emitDispose(instance);
stopAlteringState(wasAltering);
invokeInstanceEvents(instance, "dispose");
return true;
}
function createProps(instance, indicators, payload, runProps) {
let getState = instance.actions.getState;
let args = (runProps?.args || emptyArray);
let controller = new AbortController();
let producerProps = {
emit,
args,
abort,
getState,
payload: payload,
signal: controller.signal,
onAbort(callback) {
if (isFunction(callback)) {
controller.signal.addEventListener("abort", () => {
callback(controller.signal.reason);
});
}
},
isAborted() {
return indicators.aborted;
},
get lastSuccess() {
return instance.lastSuccess;
},
};
return producerProps;
function emit(value, status) {
if (indicators.cleared) {
return;
}
if (!indicators.done) {
if (__DEV__) {
console.error("Called props.emit before the producer resolves. This is" +
" not supported in the library and will have no effect");
}
return;
}
let prevIsEmitting = startEmitting();
let newValue = value;
let newStatus = status;
instance.actions.setState(newValue, newStatus, runProps);
stopEmitting(prevIsEmitting);
}
function abort(reason) {
if (indicators.aborted || indicators.cleared) {
return;
}
if (!indicators.done) {
indicators.aborted = true;
let currentState = instance.state;
if (currentState.status === pending) {
currentState = currentState.prev;
}
// revert back to previous state when aborting only if we won't be updating
// the state right next.
let isCurrentlyAlteringState = isAlteringState();
if (!isCurrentlyAlteringState) {
replaceInstanceState(instance, currentState, true, runProps);
}
}
indicators.cleared = true; // before calling user land onAbort that may emit
controller.abort(reason);
instance.currentAbort = undefined;
}
}
function run(producer, props, indicators, onSettled, retryConfig, callbacks) {
let pendingPromise;
let executionValue;
try {
executionValue = producer(props);
// when producer runs, it can decide to bailout everything
// by calling props.abort(reason?) as early as possible
if (indicators.aborted) {
return;
}
}
catch (e) {
// same, you can props.abort(); throw "Ignored error";
if (indicators.aborted) {
return;
}
onFail(e);
return;
}
if (isPromise(executionValue)) {
pendingPromise = executionValue;
}
else {
// final value
onSuccess(executionValue);
return;
}
pendingPromise = pendingPromise.then(onSuccess, onFail);
pendingPromise.status = pending;
return pendingPromise;
function onSuccess(data) {
if (pendingPromise) {
pendingPromise.status = "fulfilled";
pendingPromise.value = data;
}
if (!indicators.aborted) {
indicators.done = true;
onSettled(data, success, cloneProducerProps(props), callbacks);
}
return data;
}
function onFail(error$1) {
if (pendingPromise) {
pendingPromise.status = "rejected";
pendingPromise.reason = error$1;
}
if (indicators.aborted) {
return;
}
if (retryConfig?.enabled &&
shouldRetry(indicators.index, retryConfig, error$1)) {
let backoff = getRetryBackoff(indicators.index, retryConfig, error$1);
indicators.index += 1;
let id = setTimeout(() => {
run(producer, props, indicators, onSettled, retryConfig, callbacks);
}, backoff);
props.onAbort(() => {
clearTimeout(id);
});
return;
}
indicators.done = true;
onSettled(error$1, error, cloneProducerProps(props), callbacks);
}
}
function shouldRetry(attempt, retryConfig, error) {
let { retry, maxAttempts } = retryConfig;
let canRetry = !!maxAttempts && attempt <= maxAttempts;
let shouldRetry = retry === undefined ? true : !!retry;
if (isFunction(retry)) {
shouldRetry = retry(attempt, error);
}
return canRetry && shouldRetry;
}
function getRetryBackoff(attempt, retryConfig, error) {
let { backoff } = retryConfig;
if (isFunction(backoff)) {
return backoff(attempt, error);
}
return backoff || 0;
}
function runcInstance(instance, props) {
let config = instance.config;
let effectDurationMs = Number(config.runEffectDurationMs) || 0;
let shouldRunImmediately = !config.runEffect || effectDurationMs === 0;
if (shouldRunImmediately) {
let clonedPayload = Object.assign({}, instance.payload);
return runInstanceImmediately(instance, clonedPayload, props);
}
return runInstanceWithEffects(instance, config.runEffect, effectDurationMs, props);
}
function scheduleDelayedRun(instance, durationMs, props) {
let abortCallback = null;
let timeoutId = setTimeout(function theDelayedRunExecution() {
instance.pendingTimeout = null;
let clonedPayload = Object.assign({}, instance.payload);
abortCallback = runInstanceImmediately(instance, clonedPayload, props);
}, durationMs);
instance.pendingTimeout = { at: now(), id: timeoutId };
return function abortCleanup(reason) {
clearTimeout(timeoutId);
instance.pendingTimeout = null;
if (isFunction(abortCallback)) {
abortCallback(reason);
}
};
}
function runInstanceWithEffects(instance, effect, durationMs, props) {
switch (effect) {
case "delay":
case "debounce": {
if (instance.pendingTimeout) {
let rightNow = now();
let deadline = instance.pendingTimeout.at + durationMs;
if (rightNow < deadline) {
clearTimeout(instance.pendingTimeout.id);
}
}
return scheduleDelayedRun(instance, durationMs, props);
}
case "throttle": {
if (instance.pendingTimeout) {
let rightNow = now();
let deadline = instance.pendingTimeout.at + durationMs;
if (rightNow <= deadline) {
return function noop() {
// do nothing when throttled
};
}
break;
}
else {
return scheduleDelayedRun(instance, durationMs, props);
}
}
}
let clonedPayload = Object.assign({}, instance.payload);
return runInstanceImmediately(instance, clonedPayload, props);
}
function cleanInstancePendingStateBeforeImmediateRun(instance) {
if (instance.pendingUpdate) {
clearTimeout(instance.pendingUpdate.id);
instance.pendingUpdate = null;
}
instance.actions.abort();
instance.currentAbort = undefined;
}
function replaceStateBecauseNoProducerProvided(instance, props) {
let args = (props?.args ?? emptyArray);
// keep these for readability
// we can't really type these
// this is supported for backward compatibility
let newStateData = args[0];
let newStateStatus = args[1];
instance.actions.setState(newStateData, newStateStatus, props);
return noop;
}
function replaceStateAndBailoutRunFromCachedState(instance, cachedState) {
let actualState = instance.state;
let nextState = cachedState.state;
// this means that the current state reference isn't the same
if (actualState !== nextState) {
// this sets the new state and notifies subscriptions
// true for notifying
// todo: update latest run
replaceInstanceState(instance, nextState, true);
}
}
function runInstanceImmediately(instance, payload, props) {
// when there is no producer attached to the instance, skip everything
// and replace state immediately. This will skip cache too.
instance.promise = null;
if (!instance.fn) {
return replaceStateBecauseNoProducerProvided(instance, props);
}
let wasAltering = startAlteringState();
// the pendingUpdate has always a pending status, it is delayed because
// of the config.skipPendingDelayMs configuration option.
let hasPendingUpdate = instance.pendingUpdate !== null;
let isCurrentlyPending = instance.state.status === pending;
if (isCurrentlyPending || hasPendingUpdate) {
cleanInstancePendingStateBeforeImmediateRun(instance);
}
// this should not be into the previous if, because we need all the time
// to invoke the currentAbort so that cleanups of the previous run are invoked
if (isFunction(instance.currentAbort)) {
instance.currentAbort();
}
if (hasCacheEnabled(instance)) {
let cacheConfig = instance.config.cacheConfig;
let runHash = computeRunHash(payload, props, cacheConfig?.hash);
let cachedState = getCachedState(instance, runHash);
// only use a cached entry if not expired
if (cachedState && !didCachedStateExpire(cachedState)) {
replaceStateAndBailoutRunFromCachedState(instance, cachedState);
if (__DEV__)
devtools.emitRun(instance, true);
return;
}
// this means that the cache entry was expired, we need to removed it
// from the cache
if (cachedState) {
removeCachedStateAndSpreadOnLanes(instance, runHash);
}
}
let indicators = { index: 1, done: false, cleared: false, aborted: false };
let producerProps = createProps(instance, indicators, payload, props);
instance.latestRun = {
args: producerProps.args,
payload: producerProps.payload,
};
instance.currentAbort = producerProps.abort;
let runResult = run(instance.fn, producerProps, indicators, onSettled, instance.config.retryConfig, props // callbacks
);
if (runResult) {
// Promise<TData>
if (__DEV__)
devtools.emitRun(instance, false);
instance.promise = runResult;
let currentState = instance.state;
if (currentState.status === pending) {
currentState = currentState.prev;
}
let savedProps = cloneProducerProps(producerProps);
let pendingState = {
data: null,
timestamp: now(),
props: savedProps,
prev: currentState,
status: pending,
};
replaceInstanceState(instance, pendingState, true, props);
stopAlteringState(wasAltering);
return producerProps.abort;
}
if (__DEV__)
devtools.emitRun(instance, false);
stopAlteringState(wasAltering);
return producerProps.abort;
function onSettled(data, status, savedProps, callbacks) {
let state = freeze({
status,
data,
props: savedProps,
timestamp: now(),
});
replaceInstanceState(instance, state, true, callbacks);
}
}
let contexts = new WeakMap();
let globalContextKey = freeze({});
let globalContext = createContext(globalContextKey);
function createNewContext(arg) {
let instances = new Map();
let createdContext = {
ctx: arg,
payload: {},
get(key) {
return instances.get(key);
},
remove(key) {
return instances.delete(key);
},
set(key, inst) {
instances.set(key, inst);
},
getAll() {
return [...instances.values()];
},
terminate() {
instances = new Map();
},
};
if (__DEV__)
devtools.captureContext(createdContext);
return createdContext;
}
function createContext(arg) {
// null means the static global context
if (arg === null) {
return globalContext;
}
if (typeof arg !== "object") {
throw new Error("createContext requires an object. Received " + String(typeof arg));
}
let existingContext = contexts.get(arg);
if (existingContext) {
return existingContext;
}
let newContext = createNewContext(arg);
contexts.set(arg, newContext);
return newContext;
}
function requestContext(arg) {
if (arg === null) {
return globalContext;
}
let context = contexts.get(arg);
if (!context) {
throw new Error("Context not found. Please make sure to call createContext before " +
"requestContext.");
}
return context;
}
function terminateContext(arg) {
if (!arg) {
return false;
}
let desiredContext = contexts.get(arg);
if (!desiredContext) {
return false;
}
// this will un-reference all instances from the context
desiredContext.terminate();
if (__DEV__)
devtools.releaseContext(desiredContext);
return contexts.delete(arg);
}
let HYDRATION_DATA_KEY = "__$$";
const attemptHydratedState = isServer
? attemptHydratedStateServer
: attemptHydratedStateDOM;
// unused parameters to keep the same exported signature
function attemptHydratedStateServer(_instance) {
return null;
}
function attemptHydratedStateDOM(instance) {
if (!maybeWindow) {
return null;
}
let { key, config, ctx } = instance;
let contextName = ctx?.name ?? null;
// why there are two possible keys ?
// When coming from the server, sources there can be created from two ways:
// globally via createSource, or locally via hooks.
// either ways, the hydrated source belongs to a Provider's Context, which has
// a name, so the hydrated data will come to the client with that key.
// So, this source either has a context with a name (React.useId()) when
// created initially inside a Provider, or else, we'll fallback to the
// global context hydration in case the source wasn't created from inside
// a Provider.
let hydrationKey = HYDRATION_DATA_KEY;
if (contextName !== null) {
hydrationKey = contextName;
}
let savedHydrationData = maybeWindow[hydrationKey];
if (!savedHydrationData) {
return null;
}
let instanceHydration = savedHydrationData[key];
if (!instanceHydration) {
return null;
}
delete savedHydrationData[key];
if (Object.keys(savedHydrationData).length === 0) {
delete maybeWindow[hydrationKey];
}
let [state] = instanceHydration;
let { status, props } = state;
if (typeof state.data === "undefined") {
state.data = undefined;
if (status === initial && props.args[0] === null && !config.initialValue) {
props.args[0] = undefined;
}
}
return instanceHydration;
}
function initializeInstance(instance) {
loadCache(instance);
let maybeHydratedState = attemptHydratedState(instance);
if (maybeHydratedState) {
let [state, latestRun, payload] = maybeHydratedState;
instance.state = state;
instance.payload = payload;
instance.latestRun = latestRun;
if (instance.state.status === success) {
instance.lastSuccess = instance.state;
}
else {
let initializer = instance.config.initialValue;
let initialData = isFunction(initializer)
? initializer(instance.cache)
: initializer;
let savedInitialProps = {
args: [initialData],
payload: shallowClone(instance.payload),
};
instance.lastSuccess = {
status: initial,
data: initialData,
timestamp: now(),
props: savedInitialProps,
};
if (state.status === pending) {
let normalPromise = new Promise((resolve, reject) => {
instance.res = { res: resolve, rej: reject };
});
let promise = normalPromise;
promise.status = pending;
instance.promise = promise;
state.prev = instance.lastSuccess;
}
}
}
else {
let initializer = instance.config.initialValue;
let initialData = isFunction(initializer)
? initializer(instance.cache)
: initializer;
let savedInitialProps = {
args: [initialData],
payload: shallowClone(instance.payload),
};
let initialState = {
status: initial,
data: initialData,
timestamp: now(),
props: savedInitialProps,
};
instance.state = initialState;
instance.lastSuccess = initialState;
}
}
// this is the main instance that will hold and manipulate the state
// it is referenced by its 'key' or name.
// when a state with the same name exists, it is returned instead of creating
// a new one.
class AsyncState {
ctx;
// used only in __DEV__ mode
journal = null;
// this contains all methods, such as getState, setState and so on
actions;
id;
key;
version = 0;
config;
payload = null;
cache = null;
parent = null;
lanes = null;
state;
queue = null;
latestRun = null;
lastSuccess;
promise = null;
res;
currentAbort = null;
fn = null;
pendingUpdate = null;
pendingTimeout = null;
subsIndex = null;
subscriptions = null;
eventsIndex = null;
events = null;
global = null;
constructor(key, producer, config) {
let instanceConfig = shallowClone(config);
// this means that the instance won't be stored in the LibraryContext
// object, will be mostly used with anonymous instances
if (instanceConfig.storeInContext !== false) {
// fallback to globalContext (null)
let context = config?.context || null;
let libraryContext = requestContext(context);
let existingInstance = libraryContext.get(key);
// when an instance with the same key exists, reuse it
if (existingInstance) {
existingInstance.actions.patchConfig(config);
existingInstance.actions.replaceProducer(producer || null);
return existingInstance;
}
// start recording journal events early before end of creation
if (__DEV__) {
this.journal = [];
}
this.ctx = libraryContext;
libraryContext.set(key, this);
}
else {
this.ctx = null;
}
this.key = key;
this.id = nextUniqueId();
this.fn = producer ?? null;
this.config = instanceConfig;
this.actions = new StateSource(this);
// this function will loadCache if applied and also attempt
// hydrated data if existing and also set the initial state
// it was moved from here for simplicity and keep the constructor readable
initializeInstance(this);
if (__DEV__)
devtools.emitCreation(this);
}
}
class StateSource {
key;
uniqueId;
inst;
constructor(instance) {
this.inst = instance;
this.key = instance.key;
this.uniqueId = instance.id;
}
getState = () => {
return this.inst.state;
};
replaceState = (newState, notify = true, callbacks) => {
replaceInstanceState(this.inst, newState, notify, callbacks);
};
setState = (newValue, status = success, callbacks) => {
setInstanceState(this.inst, newValue, status, callbacks);
};
setData = (newData) => {
setInstanceData(this.inst, newData);
};
on = (eventType,