UNPKG

react-async-states

Version:

A low-level multi paradigm state management library

161 lines (158 loc) 7.16 kB
import * as React from 'react'; import { createLegacyReturn } from './HookReturnValue.js'; import { isFunction } from '../../shared/index.js'; import { __DEV__getCurrentlyRenderingComponentName, shouldRunSubscription, reconcileInstance, startRenderPhaseRun, endRenderPhaseRun } from './HookSubscriptionUtils.js'; function useRetainInstance(instance, config, deps) { // ⚠️⚠️⚠️ // the subscription will be constructed fully in the first time (per instance) // then we will update its properties through the alternate after rendering // so basically, we won't care about any dependency array except the instance // itself. Because all the other information will be held by the alternate. // so, sorry typescript and all readers 🙂 let [, forceUpdate] = React.useState(0); return React.useMemo(() => createSubscription(instance, forceUpdate, config, deps), [instance]); } function createSubscription(instance, update, initialConfig, deps) { // these properties are to store the single onChange or onSubscribe // events (a single variable, but may be an array) // and every time you call onChange it overrides this value // sure, it receives the previous events as argument if function let changeEvents = null; let subscribeEvents = null; let incompleteSub = { deps, update, instance, cb: null, initial: true, config: initialConfig, version: instance.version, onChange, onSubscribe, alternate: null, get changeEvents() { return changeEvents; }, get subscribeEvents() { return subscribeEvents; }, // used in dev mode at: __DEV__getCurrentlyRenderingComponentName(), }; let subscription = incompleteSub; subscription.read = (readSubscriptionInConcurrentMode).bind(null, subscription); subscription.return = createLegacyReturn(subscription, initialConfig); return subscription; function onChange(newEvents) { if (isFunction(newEvents)) { let events = newEvents; let maybeEvents = events(changeEvents); if (maybeEvents) { changeEvents = maybeEvents; } } else if (newEvents) { changeEvents = newEvents; } } function onSubscribe(newEvents) { if (isFunction(newEvents)) { let events = newEvents; let maybeEvents = events(subscribeEvents); if (maybeEvents) { subscribeEvents = maybeEvents; } } else if (newEvents) { subscribeEvents = newEvents; } } } let suspendingPromises = new WeakMap(); function removePromiseFromSuspendersList(promise, subscription) { if (promise && promise.status !== "pending") { let suspender = suspendingPromises.get(promise); if (suspender === subscription) { suspendingPromises.delete(promise); } } } function readSubscriptionInConcurrentMode(subscription, config, suspend, throwError) { let alternate = subscription.alternate; let newConfig = alternate?.config || config; // this means we want to suspend and the subscription is fresh (created in // render and hasn't been committed yet, or dependencies changed) if (suspend && (subscription.initial || subscription.config !== newConfig)) { // this means that's the initial subscription to this state instance // either in this path or when deps change, we will need to run again // if the condition is truthy // subscription.config !== newConfig means that deps changed let instance = subscription.instance; let promise = instance.promise; let wasSuspending = !!promise && suspendingPromises.has(promise); let shouldRun = shouldRunSubscription(subscription, newConfig); if (shouldRun && !wasSuspending) { // The configuration may change the producer or an important option // So, it is important to reconcile before running. // In the normal flow, this reconciliation happens at the commit phase // but if we are to run during render, we should do it now. reconcileInstance(instance, newConfig); let runArgs = (newConfig.autoRunArgs || []); // we mark it as a render phase update so that subscriptions (already // mounted components) won't schedule a render right now, and thus // avoid scheduling a render on a component while rendering another let wasUpdatingOnRender = startRenderPhaseRun(); instance.actions.run.apply(null, runArgs); endRenderPhaseRun(wasUpdatingOnRender); promise = instance.promise; // this means that the run was sync, in this case, we would need // to notify the previous subscribers to force an update // If the run was aborted from the producer (props.abort()), currentAbort // would be null, and in this case we are there was no run or updates // so we will notify only when currentAbort is not null and the promise // is falsy (resolved sync) (or probably read from cache) if (!promise && instance.currentAbort !== null) { notifyOtherInstanceSubscriptionsInMicrotask(subscription); } } if (promise) { // we add this promise to the suspenders list in either ways: // - if pending, we will suspend, and we should only resolve from this path // - if we aren't pending and did resolve, this path means that the deps // changed, so we are having a commit where we will remove the promise, // but we should only allow resolve from this one path, because // if the promise resolves outside react, before this path commits // and another path commits while not being "suspensey", it will // cause infinite updates suspendingPromises.set(promise, subscription); if (promise.status === "pending") { throw promise; } } } let currentReturn = alternate ? alternate.return : subscription.return; if (throwError && currentReturn.isError) { throw currentReturn.error; } return currentReturn.state; } function notifyOtherInstanceSubscriptionsInMicrotask(subscription) { let { instance, cb } = subscription; let { subscriptions } = instance; let callbacks = []; if (subscriptions) { for (let sub of Object.values(subscriptions)) { let subCallback = sub.props.cb; if (subCallback !== cb) { callbacks.push(subCallback); } } } if (callbacks.length) { queueMicrotask(() => { callbacks.forEach((callback) => callback(instance.state)); }); } } export { createSubscription, removePromiseFromSuspendersList, useRetainInstance }; //# sourceMappingURL=HookSubscription.js.map