react-async-states
Version:
A low-level multi paradigm state management library
1,122 lines (1,106 loc) • 55.8 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('async-states')) :
typeof define === 'function' && define.amd ? define(['exports', 'react', 'async-states'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReactAsyncStates = {}, global.React, global.AsyncStates));
})(this, (function (exports, React, asyncStates) { 'use strict';
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
const __DEV__ = process.env.NODE_ENV !== "production";
function isFunction(fn) {
return typeof fn === "function";
}
const emptyArray = [];
let assign = Object.assign;
let freeze = Object.freeze;
let isArray = Array.isArray;
function computeCallerName(level = 3) {
const stack = new Error().stack?.toString();
if (!stack) {
return undefined;
}
const regex = new RegExp(/at.(\w+).*$/, "gm");
let levelsCount = 0;
let match = regex.exec(stack);
while (levelsCount < level && match) {
match = regex.exec(stack);
levelsCount += 1;
}
return match?.[1];
}
function useCallerName(level) {
if (!__DEV__) {
return undefined;
}
// Here, we are using useMemo like a useRef.
// It is safe to read/write to it here during render, because it is a single
// time computed information that will never change.
// Why not using useMemo(() => computeCallerName(level), [level])
// is because in this case we would have to deal with the call level from
// inside React, which is unstable and isn't linked to the project.
// To avoid that entirely, we force the computation of the caller here
// in this hook so it won't leak further into React internals.
// Using useMemo because it is lightweight.
// Avoiding useRef so we won't have any warnings issues from a future
// StrictMode or compiler thing.
let ref = React__namespace.useMemo(() => ({}), [level]);
if (!ref.current) {
ref.current = computeCallerName(level);
}
return ref.current;
}
let Context = React__namespace.createContext(null);
let maybeWindow = typeof window !== "undefined" ? window : undefined;
let isServer = typeof maybeWindow === "undefined" || "Deno" in maybeWindow;
let currentOverrides;
function setCurrentHookOverrides(overrides) {
currentOverrides = overrides;
}
function cloneSourceInTheServer(globalSource, context, useGlobalSourceState) {
let instance = globalSource.inst;
let { config, fn, key } = instance;
let newConfig = assign({}, config, { context });
let source = asyncStates.createSource(key, fn, newConfig);
let newInstance = source.inst;
if (useGlobalSourceState === true) {
let globalInstance = globalSource.inst;
// we will clone all relevant things: state, cache, lastRun
newInstance.state = globalInstance.state;
newInstance.cache = globalInstance.cache;
newInstance.latestRun = globalInstance.latestRun;
newInstance.lastSuccess = globalInstance.lastSuccess;
}
newInstance.global = true;
return source;
}
// the goal of this function is to retrieve the following objects:
// - a configuration object to use { key, producer, source, lazy ... }
// - the state instance
function parseConfig(currentLibContext, options, overrides) {
requireAnExecContextInServer(currentLibContext, options);
let executionContext;
let instance;
let parsedConfiguration;
switch (typeof options) {
// the user provided an object configuration or a Source
// In the case of a source, we will detect if it was a global source
// (the heuristic to detect that is to check if it belongs to another
// context object), if that's the case, a new source will be created
// in the current context and used
case "object": {
if (asyncStates.isSource(options)) {
if (isServer) {
// requireAnExecContextInServer would throw if nullish
let ctx = currentLibContext.ctx;
instance = cloneSourceInTheServer(options, ctx).inst;
}
else {
instance = options.inst;
}
parsedConfiguration = assign({}, overrides, currentOverrides);
parsedConfiguration.source = options;
break;
}
let config = options;
if (config.source && asyncStates.isSource(config.source)) {
let baseSource = config.source;
if (isServer) {
// requireAnExecContextInServer would throw if nullish
let ctx = currentLibContext.ctx;
let useServerState = config.useServerState;
baseSource = cloneSourceInTheServer(baseSource, ctx, useServerState);
}
let realSource = baseSource.getLane(config.lane);
instance = realSource.inst;
parsedConfiguration = assign({}, config, overrides, currentOverrides);
parsedConfiguration.source = realSource;
break;
}
let nullableExecContext = currentLibContext;
if (config.context) {
executionContext = asyncStates.createContext(config.context);
}
else if (nullableExecContext) {
executionContext = nullableExecContext;
}
else {
executionContext = asyncStates.requestContext(null);
}
parsedConfiguration = assign({}, options, overrides, currentOverrides);
// parsedConfig is created by the library, so okay to mutate it internally
parsedConfiguration.context = executionContext.ctx;
if (!executionContext) {
throw new Error("Exec context not defined, this is a bug");
}
instance = resolveFromObjectConfig(executionContext, parsedConfiguration);
break;
}
// this is a string provided to useAsync, that we will lookup the instance
// by the given key in the context, if not found it will be created
// with the given config and it will stay there
case "string": {
parsedConfiguration = assign({}, overrides, currentOverrides);
parsedConfiguration.key = options;
let nullableExecContext = currentLibContext;
if (nullableExecContext) {
executionContext = nullableExecContext;
}
else {
executionContext = asyncStates.requestContext(null);
}
// parsedConfig is created by the library, so okay to mutate it internally
parsedConfiguration.context = executionContext.ctx;
instance = resolveFromStringConfig(executionContext, parsedConfiguration);
break;
}
// this is a function provided to useAsync, means the state instance
// will be removed when the component unmounts and it won't be stored in the
// context
case "function": {
parsedConfiguration = assign({}, overrides, currentOverrides);
parsedConfiguration.producer = options;
parsedConfiguration.context = currentLibContext?.ctx ?? null;
instance = resolveFromFunctionConfig(parsedConfiguration);
break;
}
// at this point, config is a plain object
default: {
parsedConfiguration = assign({}, overrides, currentOverrides);
let nullableExecContext = currentLibContext;
if (nullableExecContext) {
executionContext = nullableExecContext;
}
else {
executionContext = asyncStates.requestContext(null);
}
// the parsed config is created by the library, so okay to mutate it.
parsedConfiguration.context = executionContext.ctx;
instance = resolveFromObjectConfig(executionContext, parsedConfiguration);
}
}
return {
instance,
config: parsedConfiguration,
};
}
// object type has these specific rules:
// - it is not a source
// - the user provided a configuration object (not through overrides)
// - cases when it contains { source } should be supported before calling this
function resolveFromObjectConfig(executionContext, parsedConfiguration) {
let { key, producer } = parsedConfiguration;
if (!key) {
requireAKeyInTheServer();
key = asyncStates.nextKey();
// anonymous states won't be stored in the context for easier GC
parsedConfiguration.storeInContext = false;
}
let existingInstance = executionContext.get(key);
if (existingInstance) {
return existingInstance.actions.getLane(parsedConfiguration.lane).inst;
}
return asyncStates.createSource(key, producer, parsedConfiguration).getLane(parsedConfiguration.lane).inst;
}
// the user provided a string to useAsync(key, deps)
function resolveFromStringConfig(executionContext, parsedConfiguration) {
// key should never be undefined in this path
let key = parsedConfiguration.key;
let existingInstance = executionContext.get(key);
if (existingInstance) {
return existingInstance;
}
return asyncStates.createSource(key, null, parsedConfiguration).inst;
}
function resolveFromFunctionConfig(parsedConfiguration) {
requireAKeyInTheServer();
let key = asyncStates.nextKey();
// anonymous states won't be stored in the context for easier GC
parsedConfiguration.storeInContext = false;
// todo: reuse instance from previous render
return asyncStates.createSource(key, parsedConfiguration.producer, parsedConfiguration)
.inst;
}
// this function throws in the server when there is no context provided
function requireAnExecContextInServer(parentExecContext, mixedConfig) {
// opt-out for these cases:
// - not in server
// - we are in a Library Context provider tree (and not using a source)
// - the provided config is not an object (then, we will attach to parent provider)
if (!isServer || typeof mixedConfig !== "object") {
return;
}
if (parentExecContext) {
return;
}
let baseConfig = mixedConfig;
// at this point, we have an object (not a source)
if (!baseConfig.context) {
if (__DEV__) {
console.error("A context object is mandatory when working in the server " +
"to avoid leaks between requests. \nAdd the following up in the tree:\n" +
"import { Provider } from 'react-async-states';\n" +
"<Provider>{yourChildrenTree}</Provider>;\n");
}
throw new Error("A Provider is mandatory in the server");
}
}
function requireAKeyInTheServer() {
if (!isServer) {
return;
}
throw new Error("A key is required in the server");
}
let idPrefix = "$$as-";
function Provider({ exclude, children, context: contextArg, serverInsertedHtmlHook, }) {
// automatically reuse parent context when there is and no 'context' object
// is provided
let reactId = React__namespace.useId();
let parentLibraryProvider = React__namespace.useContext(Context);
let libraryContextObject = React__namespace.useMemo(() => {
if (!contextArg && contextArg !== null) {
if (parentLibraryProvider) {
return parentLibraryProvider;
}
let libraryContext = asyncStates.createContext({});
libraryContext.name = `__$$${reactId}`;
return libraryContext;
}
let libraryContext = asyncStates.createContext(contextArg);
if (contextArg !== null) {
libraryContext.name = `__$$${reactId}`;
}
else if (isServer) {
throw new Error("Global context cannot be used server");
}
return libraryContext;
}, [reactId, contextArg]);
if (isFunction(serverInsertedHtmlHook)) {
serverInsertedHtmlHook(() => (React__namespace.createElement(HydrateRemainingContextInstances, { exclude: exclude, context: libraryContextObject })));
}
React__namespace.useEffect(() => ensureOnDemandHydrationExistsInClient(libraryContextObject), [libraryContextObject]);
// memoized children will unlock the React context children optimization:
// if the children reference is the same as the previous render, it will
// bail out and skip the children render and only propagates the context
// change.
return (React__namespace.createElement(Context.Provider, { value: libraryContextObject }, children));
}
function ensureOnDemandHydrationExistsInClient(libraryContextObject) {
let ctxName = libraryContextObject.name;
let hydrationDataPropName = !ctxName ? "__$$" : ctxName;
let rehydrationFunctionName = !ctxName ? "__$$_H" : `${ctxName}_H`;
window[rehydrationFunctionName] = function rehydrateContext() {
let hydrationData = window[hydrationDataPropName];
if (!hydrationData || !libraryContextObject) {
return;
}
Object.entries(hydrationData).forEach(([key, instanceHydration]) => {
let instance = libraryContextObject.get(key);
if (!instance || !instanceHydration) {
return;
}
let [state, latestRun, payload] = instanceHydration;
instance.version += 1;
instance.state = state;
let promise = instance.promise;
// next, we may have already hydrated the "pending" state, in this case
// we put a never resolving promise and it probably did suspend a tree
// in this case, we will resolve/reject it imperatively because
// we keep track of this value.
// setting state won't resolve because if this is the first ever component
// render and mount, it won't run any effects and thus no subscribers.
// so, the only way is to inform react that the suspending promise did
// fulfill, via its resolve and reject functions.
if (state.status === "success") {
instance.lastSuccess = state;
if (promise) {
promise.value = state.data;
promise.status = "fulfilled";
}
instance.res?.res(state.data);
}
else if (state.status === "error" && promise) {
promise.status = "rejected";
promise.reason = state.data;
instance.res?.rej(state.data);
}
instance.payload = payload;
instance.latestRun = latestRun;
let subscriptions = instance.subscriptions;
if (subscriptions) {
Object.values(subscriptions).forEach((sub) => sub.props.cb(state));
}
delete hydrationData[key];
});
};
}
function HydrateRemainingContextInstances({ context, exclude, }) {
let sources = context
.getAll()
.filter((instance) => {
// this means we already hydrated this instance in this context with this
// exact same version
if (context.payload[instance.key] === instance.version) {
return false;
}
if (isFunction(exclude)) {
return !exclude(instance.key, instance.state);
}
return true;
})
.map((t) => t.actions);
return (React__namespace.createElement(HydrationServer, { useReactId: false, context: context, target: sources }));
}
function HydrationComponent({ target }) {
let context = React__namespace.useContext(Context);
if (isServer) {
return React__namespace.createElement(HydrationServer, { context: context, target: target });
}
return React__namespace.createElement(HydrationClient, { target: target });
}
function HydrationServer({ context, target, useReactId, }) {
let reactId = React__namespace.useId();
let hydrationData = buildWindowAssignment(target, context);
if (!hydrationData) {
return null;
}
let id = useReactId === false ? undefined : `${idPrefix}${reactId}`;
return (React__namespace.createElement("script", { id: id, dangerouslySetInnerHTML: { __html: hydrationData } }));
}
function HydrationClient(_props) {
let reactId = React__namespace.useId();
let id = `${idPrefix}${reactId}`;
let existingHtml = React__namespace.useMemo(() => ({ current: { html: null, init: false } }), []);
// We are using the "init" property explicitly to be more precise:
// If we didn't compute, let's do it, or else, just pass.
// In the or else path, it may be difficult to distinguish between falsy
// values and we would end up using two values anyways. So, we better use
// an object to be more explicit and readable.
// For example, we could do:
// if (existingHTML.current === null) ...
// But there is no guarantee that the innerHTML computation will always yield
// non null values. To avoid all of that, let's stick to basic javascript.
if (!existingHtml.current.init) {
let container = document.getElementById(id);
let containerInnerHTML = container?.innerHTML ?? null;
existingHtml.current = { init: true, html: containerInnerHTML };
}
let __html = existingHtml.current.html;
if (__html) {
return React__namespace.createElement("script", { id: id, dangerouslySetInnerHTML: { __html } });
}
return null;
}
function buildWindowAssignment(sources, context) {
if (!sources.length) {
return null;
}
let globalHydrationData = null;
let contextHydrationData = null;
for (let source of sources) {
let key = source.key;
let instance = context.get(key);
if (!instance) {
if (__DEV__) {
__DEV__warnInDevAboutHydratingSourceNotInContext(key);
}
throw new Error("Cannot leak server global source");
}
let { state, latestRun, payload, version } = instance;
if (context.payload[key] === version) {
continue;
}
else {
context.payload[key] = version;
}
// instance.global is true only and only if this instance was cloned
// from a server instance:
// ie: You have a global source object in the server, that you clone per
// request. When we perform this clone, we mark these sources as global:
// It was cloned from a globally accessible source.
// We do all of this because when hydrating, there are two types of states:
// Those we were global (not related to this render, but more of was
// created far away and a subscription is performed from this render),
// and those we are bound to the current Context in this particular render.
// When hydrating we distinguish between them so we won't leak source state
// and we properly assign the state to its instance.
// The script will later use __$$ for global context, and <context.name>
// for more granular contexts.
// When using the server, using a context is mandatory, but if your app
// is all global sources, then contextHydrationData will be basically empty
// and your global sources in the client will get hydrated correctly.
if (instance.global) {
if (!globalHydrationData) {
globalHydrationData = {};
}
globalHydrationData[key] = [state, latestRun, payload];
}
else {
if (!contextHydrationData) {
contextHydrationData = {};
}
contextHydrationData[key] = [state, latestRun, payload];
}
}
if (!globalHydrationData && !contextHydrationData) {
return null;
}
let hydrationData = ["var win=window;"];
if (globalHydrationData) {
let globalHydrationDataAsString = JSON.stringify(globalHydrationData);
hydrationData.push(buildHydrationScriptContent("__$$", globalHydrationDataAsString));
}
if (contextHydrationData) {
let contextName = context.name;
if (!contextName) {
throw new Error("Hydrating context without name, this is a bug");
}
let contextHydrationDataAsString = JSON.stringify(contextHydrationData);
hydrationData.push(buildHydrationScriptContent(contextName, contextHydrationDataAsString));
}
return hydrationData.join("");
}
function buildHydrationScriptContent(propName, data) {
return `win["${propName}"]=Object.assign(win["${propName}"]||{},${data});win["${propName}_H"]&&win["${propName}_H"]();`;
}
function __DEV__warnInDevAboutHydratingSourceNotInContext(key) {
if (__DEV__) {
console.error(`[async-states] source '${key}' doesn't exist` +
" in the context, this means that you tried to hydrate it " +
" before using it via hooks. Only hydrate a source after using it" +
" to avoid leaks and passing unused things to the client.");
}
}
function createLegacyReturn(subscription, config) {
return createBaseReturn(subscription, config);
}
function selectState(instance, selector) {
let { state: currentState, lastSuccess, cache } = instance;
if (selector) {
return selector(currentState, lastSuccess, cache);
}
else {
return currentState;
}
}
function createBaseReturn(subscription, config) {
let instance = subscription.instance;
let currentState = instance.state;
let lastSuccess = instance.lastSuccess;
let selectedState = selectState(instance, config.selector);
let previousState = currentState.status === "pending" ? currentState.prev : currentState;
let status = currentState.status;
let source = subscription.instance.actions;
return freeze({
source,
state: selectedState,
dataProps: lastSuccess.props,
isError: status === "error",
isInitial: status === "initial",
isPending: status === "pending",
isSuccess: status === "success",
data: lastSuccess.data ?? null,
error: previousState.status === "error" ? previousState.data : null,
onChange: subscription.onChange,
onSubscribe: subscription.onSubscribe,
read: subscription.read.bind(null, config),
Hydrate: () => React__namespace.createElement(HydrationComponent, { target: [source] }),
});
}
function selectWholeState(state) {
return state;
}
let isCurrentlyRunningOnRender = false;
function startRenderPhaseRun() {
let prev = isCurrentlyRunningOnRender;
isCurrentlyRunningOnRender = true;
return prev;
}
function endRenderPhaseRun(nextValue) {
isCurrentlyRunningOnRender = nextValue;
}
function isRenderPhaseRun() {
return isCurrentlyRunningOnRender;
}
function shouldRunSubscription(subscription, subscriptionConfig) {
let { instance, alternate } = subscription;
let config = alternate?.config || subscriptionConfig;
let { lazy, condition, autoRunArgs } = config;
return shouldSubscriptionRun(instance.state, instance.actions.getPayload(), lazy, condition, (autoRunArgs || emptyArray));
}
function shouldSubscriptionRun(state, payload, lazy, condition, args) {
if (lazy === false) {
if (condition === undefined || condition === true) {
return true;
}
else if (isFunction(condition)) {
return condition(state, args, payload);
}
}
return false;
}
function reconcileInstance(instance, currentConfig) {
let instanceActions = instance.actions;
// 📝 We can call this part the instance reconciliation
// patch the given config and the new producer if provided and different
// we might be able to iterate over properties and re-assign only the ones
// that changed and are supported.
let configToPatch = removeHookConfigToPatchToSource(currentConfig);
instanceActions.patchConfig(configToPatch);
if (currentConfig.payload) {
instanceActions.mergePayload(currentConfig.payload);
}
let currentProducer = instance.fn;
let pendingProducer = currentConfig.producer;
if (pendingProducer !== undefined && pendingProducer !== currentProducer) {
instanceActions.replaceProducer(pendingProducer);
}
}
function removeHookConfigToPatchToSource(currentConfig) {
// the patched config may contain the following properties
// - source
// - payload
// - events
// and other properties that can be retrieved from hooks usage and others
// so we are tearing them apart before merging
let { lazy, events, source, payload, concurrent, autoRunArgs, subscriptionKey, ...output } = currentConfig;
return output;
}
function forceComponentUpdate(prev) {
return prev + 1;
}
// dev mode helpers
let currentlyRenderingComponentName = null;
function __DEV__setHookCallerName(name) {
if (name && !currentlyRenderingComponentName) {
currentlyRenderingComponentName = name;
}
}
function __DEV__getCurrentlyRenderingComponentName() {
return currentlyRenderingComponentName;
}
function __DEV__unsetHookCallerName() {
currentlyRenderingComponentName = null;
}
let __DEV__betterSourceConfigProperties;
if (__DEV__) {
__DEV__betterSourceConfigProperties = {
runEffect: true,
retryConfig: true,
cacheConfig: true,
initialValue: true,
hideFromDevtools: true,
skipPendingStatus: true,
skipPendingDelayMs: true,
resetStateOnDispose: true,
runEffectDurationMs: true,
};
}
function __DEV__warnInDevAboutIncompatibleConfig(subscription) {
let { config } = subscription;
let { source, key, producer } = config;
if (source) {
if (key) {
console.error(`[Warning][async-states] Subscription in component ${subscription.at} ` +
`has a 'source' and 'key' as the same time, 'key' has no effect.`);
return;
}
if (producer) {
console.error(`[Warning][async-states] Subscription in component ${subscription.at} ` +
`has a 'source' (${source.key}) and 'producer' as the same time,` +
` the source's producer will be replaced.`);
return;
}
let configuredProps = Object.keys(config).filter((t) => config[t] !== undefined && __DEV__betterSourceConfigProperties[t]);
if (configuredProps.length) {
console.error(`[Warning][async-states] Subscription in component ${subscription.at} ` +
`has a 'source' and the following properties '` +
configuredProps.join(", ") +
"' at the same time. All these props will be flushed into the " +
"source config, better move them to the source creation.");
return;
}
}
}
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__namespace.useState(0);
return React__namespace.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));
});
}
}
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));
}
function beginRender(subscription, newConfig, deps) {
let instance = subscription.instance;
// this means that the dependencies did not change and the same config
// remains from the previous render, or this is the first render.
if (newConfig === subscription.config) {
// At this point, there is no need to create the alternate.
// which will be equivalent to a render bailout. But we'll need to check
// on the versions in case something bad happened.
if (subscription.version === instance.version) {
// null to bail out the render
completeRender(subscription);
return null;
}
}
let alternate = {
deps,
instance,
config: newConfig,
return: subscription.return,
update: subscription.update,
version: subscription.version,
};
subscription.alternate = alternate;
// at this point, we have a defined alternate. Let's perform a render
// first thing to do, is to verify the optimistic lock
if (alternate.version !== instance.version) {
// this means that the instance received an update in between, so we need
// to change the returned value
alternate.version = instance.version;
alternate.return = createLegacyReturn(subscription, newConfig);
// no need to check anything else since this is a fresh value
completeRender(subscription);
return alternate;
}
// next, we will check the selector function
let pendingSelector = newConfig.selector || selectWholeState;
if (pendingSelector !== subscription.config.selector) {
let { cache, state, lastSuccess } = instance;
let comparingFunction = newConfig.areEqual || Object.is;
let newSelectedValue = pendingSelector(state, lastSuccess, cache);
// this means that the selected value did change
if (!comparingFunction(subscription.return.state, newSelectedValue)) {
// todo: this will recalculate the selected state, make it not
alternate.return = createLegacyReturn(subscription, newConfig);
}
}
completeRender(subscription);
return alternate;
}
function completeRender(subscription) {
if (__DEV__) {
__DEV__unsetHookCallerName();
}
let { alternate } = subscription;
let usedSubscription = alternate || subscription;
let usedReturn = usedSubscription.return;
let usedConfig = usedSubscription.config;
if (usedConfig.concurrent) {
// Reading via "read" may result in running the instance's producer.
// So, it is important to reconcile before running.
// Reconciliation is done inside the "read" function and only
// when we should run.
usedReturn.read(true, !!usedConfig.throwError);
// in case of a render phase run/update, we would need to correct
// the returned value
if (subscription.instance.version !== usedSubscription.version) {
usedSubscription.return = createLegacyReturn(subscription, usedConfig);
}
}
}
// this is the main hook, useAsyncState previously
function useAsync_internal(options, deps, overrides) {
// only parse the configuration when deps change
// this process will yield the instance to subscribe to, along with
// the combined config (options)
let currentContext = React__namespace.useContext(Context);
let depsToUse = deps.concat(currentContext);
let { instance, config } = React__namespace.useMemo(() => parseConfig(currentContext, options, overrides), depsToUse);
// here, we will create a subscription from this component
// to this state instance. refer to HookSubscription type.
let subscription = useRetainInstance(instance, config, depsToUse);
if (__DEV__) {
__DEV__warnInDevAboutIncompatibleConfig(subscription);
}
// the alternate is similar to React alternate object that's
// created for every render and every fiber. It represents the
// work in progress essential information that will be flushed
// at the commit phase. It is important not to touch anything during
// render and port everything to the commit phase.
// a "null" alternate means that the render was bailed out.
let alternate = beginRender(subscription, config, depsToUse);
// this first effect will flush the alternate's properties inside
// the subscription, such as the current return, the parsed config...
// it is important to perform this work every time the alternate changes.
// if your state changes often, this effect may be executed everytime.
React__namespace.useLayoutEffect(() => commit(subscription, alternate), [alternate]);
// this second effect which is governed by the user's dependencies will:
// - subscribe to the state instance for changes
// - invoke onSubscribe events
// - run the state instance
React__namespace.useEffect(() => autoRunAndSubscribeEvents(subscription), depsToUse);
// the alternate may be null when we render the first time or when we bail out
// the render afterward.
// the returned priority is obviously for the alternate
let returnedSubscription = alternate ?? subscription;
return returnedSubscription.return;
}
// missing point:
// initial return may have data as null (in a typing point of view)
// this point will be challenging to be addressed, it should be typed deep down
// to the Source itself and the StateInterface. which may be impossible
// think about that later. a data: null may be okay for now.
let concurrentOverrides = {
concurrent: true,
throwError: true,
};
function useData_internal(options, deps, overrides) {
try {
setCurrentHookOverrides(concurrentOverrides);
// this will mimic useAsync and get its result
let result = useAsync_internal(options, deps, overrides);
// the result here is guaranteed to be either initial or success
return result;
}
finally {
setCurrentHookOverrides(null);
}
}
//endregion
function useData(config, deps = emptyArray) {
if (__DEV__) {
__DEV__setHookCallerName(useCallerName(3));
}
return useData_internal(config, deps);
}
//endregion
function useAsync_export(config, deps = emptyArray) {
if (__DEV__) {
__DEV__setHookCaller