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