UNPKG

async-states

Version:

Core of async-states

325 lines (322 loc) 11.7 kB
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