react-async-states
Version:
A low-level multi paradigm state management library
187 lines (184 loc) • 7.61 kB
JavaScript
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