UNPKG

react-async-states

Version:

A low-level multi paradigm state management library

187 lines (184 loc) 7.61 kB
import { __DEV__, isArray } from '../../shared/index.js'; import { selectWholeState } from './HookReturnValue.js'; import { reconcileInstance, forceComponentUpdate, shouldRunSubscription, isRenderPhaseRun } from './HookSubscriptionUtils.js'; import { removePromiseFromSuspendersList } from './HookSubscription.js'; function resolveSubscriptionKey(subscription) { let key = subscription.config.subscriptionKey || subscription.at || undefined; return `${key}-${(subscription.instance.subsIndex || 0) + 1}`; } function commit(subscription, pendingAlternate) { // here, we commit the alternate Object.assign(subscription, pendingAlternate); subscription.initial = false; if (subscription.alternate === pendingAlternate) { subscription.alternate = null; } // on commit, the first thing to do is to detect whether a state change // occurred before commit let version = subscription.version; let currentInstance = subscription.instance; removePromiseFromSuspendersList(currentInstance.promise, subscription); reconcileInstance(currentInstance, subscription.config); if (version !== currentInstance.version) { subscription.update(forceComponentUpdate); return; } } function autoRunAndSubscribeEvents(subscription) { let currentConfig = subscription.config; let currentInstance = subscription.instance; let instanceActions = currentInstance.actions; // we capture this state here to test it against updates in a fast way let committedState = currentInstance.state; // perform the subscription to the instance here let onStateChangeCallback = (onStateChange); // when the subscription key is provided, take it. // otherwise, in dev take the component name let subscriptionKey = subscription.config.subscriptionKey ?? __DEV__ ? resolveSubscriptionKey(subscription) : undefined; let callback = onStateChangeCallback.bind(null, subscription, committedState); // we keep track of this callback so can know later which subscription // did a render phase run and update and only notify others in microtask subscription.cb = callback; let unsubscribeFromInstance = instanceActions.subscribe({ cb: callback, key: subscriptionKey, }); let cleanups = [unsubscribeFromInstance]; let subscribeEvents = currentConfig.events?.subscribe; if (subscribeEvents) { let unsubscribeFromEvents = invokeSubscribeEvents(currentInstance, subscribeEvents); if (unsubscribeFromEvents) { cleanups = cleanups.concat(unsubscribeFromEvents); } } let subscriptionSubscribeEvents = subscription.subscribeEvents; if (subscriptionSubscribeEvents) { let unsubscribeFromEvents = invokeSubscribeEvents(currentInstance, subscriptionSubscribeEvents); if (unsubscribeFromEvents) { cleanups = cleanups.concat(unsubscribeFromEvents); } } // now, we will run the subscription. In order to run, all these conditions // should be met: // 1. lazy = false in the configuration // 2. condition() is true // 3. dependencies did change // 4. concurrent isn't enabled (it will run on render) if (!currentConfig.concurrent && shouldRunSubscription(subscription, currentConfig)) { let autoRunArgs = (currentConfig.autoRunArgs || []); let thisRunAbort = currentInstance.actions.run.apply(null, autoRunArgs); // add this run abort to the cleanups to it is aborted automatically cleanups.push(thisRunAbort); } return function cleanup() { for (let fn of cleanups) { if (fn) { fn(); } } }; } function onStateChange(subscription, committedState, newState) { let isRendering = isRenderPhaseRun(); // this occurs on concurrent mode when suspending. // no need to do anything, if it was sync for some reason, the commit // will catch a version change and trigger a sync local re-render if (isRendering) { return; } let currentReturn = subscription.return; let currentConfig = subscription.config; let currentInstance = subscription.instance; // the very first thing to do, is to invoke change events if relevant let changeEvents = currentConfig.events?.change; if (changeEvents) { invokeChangeEvents(currentInstance, changeEvents); } let subscriptionChangeEvents = subscription.changeEvents; if (subscriptionChangeEvents) { invokeChangeEvents(currentInstance, subscriptionChangeEvents); } let actualVersion = currentInstance.version; // when we detect that this state is mismatching what was rendered // then we need to force the render and computation if (doesStateMismatchSubscriptionReturn(newState, currentReturn)) { subscription.update(forceComponentUpdate); return; } // this will happen if we consume the latest cached state if (committedState === newState) { return; } // at this point, we have a new state, so we need to perform checks let comparingFunction = currentConfig.areEqual || Object.is; let currentSelector = currentConfig.selector || selectWholeState; let { cache, lastSuccess } = currentInstance; let newSelectedValue = currentSelector(newState, lastSuccess, cache); if (!comparingFunction(currentReturn.state, newSelectedValue)) { subscription.update(forceComponentUpdate); } else { // we would keep the same previous state, but we will upgrade all // closure variables used in this callback subscription.version = actualVersion; } } // this will detect whether the returned value from the hook doesn't match // the new state's status. function doesStateMismatchSubscriptionReturn(newState, subscriptionReturn) { switch (newState.status) { case "initial": { return !subscriptionReturn.isInitial; } case "pending": { return !subscriptionReturn.isPending; } case "success": { return !subscriptionReturn.isSuccess; } case "error": { return !subscriptionReturn.isError; } default: { return false; } } } function invokeChangeEvents(instance, events) { let nextState = instance.state; const changeHandlers = isArray(events) ? events : [events]; const eventProps = { state: nextState, source: instance.actions, }; changeHandlers.forEach((event) => { if (typeof event === "object") { const { handler, status } = event; if (!status || nextState.status === status) { // @ts-expect-error: it is extremely difficult to satisfy typescript // here without a switch case and treat each status a part handler(eventProps); } } else { // @ts-expect-error: it is extremely difficult to satisfy typescript // here without a switch case and treat each status a part event(eventProps); } }); } function invokeSubscribeEvents(instance, events) { if (!events || !instance) { return null; } let eventProps = instance.actions; let handlers = isArray(events) ? events : [events]; return handlers.map((handler) => handler(eventProps)); } export { autoRunAndSubscribeEvents, commit, invokeChangeEvents, invokeSubscribeEvents }; //# sourceMappingURL=HookSubscriptionCommit.js.map