async-states
Version:
Core of async-states
325 lines (322 loc) • 11.7 kB
JavaScript
import { pending, success, initial } from '../enums.js';
import { notifySubscribers } from './StateSubscription.js';
import { isFunction, __DEV__, cloneProducerProps } from '../utils.js';
import devtools from '../devtools/Devtools.js';
import { shallowClone, now, freeze } from '../helpers/core.js';
import { invokeChangeCallbacks, invokeInstanceEvents } from './StateEvent.js';
import { hasCacheEnabled, saveCacheAfterSuccessfulUpdate } from './StateCache.js';
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;
}
export { disposeInstance, enqueueSetData, enqueueSetState, enqueueUpdate, ensureQueueIsScheduled, flushUpdateQueue, getQueueTail, isAlteringState, replaceInstanceState, scheduleDelayedPendingUpdate, setInstanceData, setInstanceState, startAlteringState, startEmitting, stopAlteringState, stopEmitting };
//# sourceMappingURL=StateUpdate.js.map