react-async-states
Version:
A low-level multi paradigm state management library
206 lines (203 loc) • 8.93 kB
JavaScript
import { assign, __DEV__ } from '../../shared/index.js';
import { requestContext, isSource, createContext, nextKey, createSource } from 'async-states';
import { isServer } from '../../provider/context.js';
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 = 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 (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 && 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 = createContext(config.context);
}
else if (nullableExecContext) {
executionContext = nullableExecContext;
}
else {
executionContext = 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 = 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 = 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 = 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 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 createSource(key, null, parsedConfiguration).inst;
}
function resolveFromFunctionConfig(parsedConfiguration) {
requireAKeyInTheServer();
let key = nextKey();
// anonymous states won't be stored in the context for easier GC
parsedConfiguration.storeInContext = false;
// todo: reuse instance from previous render
return 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");
}
export { parseConfig, setCurrentHookOverrides };
//# sourceMappingURL=HookResolveConfig.js.map