overmind
Version:
Frictionless state management
648 lines (646 loc) • 25.5 kB
JavaScript
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.js');
const require_internalTypes = require('./internalTypes.js');
const require_derived = require('./derived.js');
const require_rehydrate = require('./rehydrate.js');
const require_Devtools = require('./Devtools.js');
const require_proxyfyEffects = require('./proxyfyEffects.js');
const require_utils = require('./utils.js');
let proxy_state_tree = require("proxy-state-tree");
proxy_state_tree = require_rolldown_runtime.__toESM(proxy_state_tree);
let betsy = require("betsy");
let is_plain_obj = require("is-plain-obj");
is_plain_obj = require_rolldown_runtime.__toESM(is_plain_obj);
//#region src/Overmind.ts
const hotReloadingCache = {};
var Overmind = class {
constructor(configuration, options = {}, mode = { mode: require_utils.MODE_DEFAULT }) {
this.actionReferences = {};
this.nextExecutionId = 0;
this.rehydrateMutationsForHotReloading = [];
this.isStrict = false;
this.currentExecution = null;
this.reaction = (stateCallback, updateCallback, options$1 = {}) => {
let disposer;
if (options$1.nested) {
const value = stateCallback(this.state);
if (!value || !value[proxy_state_tree.IS_PROXY]) throw new Error("You have to return an object or array from the Overmind state when using a \"nested\" reaction");
const path = value[proxy_state_tree.PATH];
disposer = this.addFlushListener((mutations) => {
mutations.forEach((mutation) => {
if (mutation.path.startsWith(path)) updateCallback(path ? path.split(this.delimiter).reduce((aggr, key) => aggr[key], this.state) : this.state);
});
});
} else {
const tree = this.proxyStateTreeInstance.getTrackStateTree();
let returnValue;
const updateReaction = () => {
disposer?.();
tree.trackScope(() => returnValue = stateCallback(tree.state));
disposer = tree.subscribe(() => {
updateReaction();
updateCallback(returnValue);
});
};
updateReaction();
}
if (options$1.immediate) updateCallback(stateCallback(this.state));
return () => disposer?.();
};
this.addMutationListener = (cb) => {
return this.proxyStateTreeInstance.onMutation(cb);
};
this.addFlushListener = (cb) => {
return this.proxyStateTreeInstance.onFlush(cb);
};
const name = options.name || "OvermindApp";
const devEnv = options.devEnv || "development";
const isNode = typeof process !== "undefined" && process.title && process.title.includes("node");
this.delimiter = options.delimiter || ".";
this.isStrict = Boolean(options.strict);
if (require_utils.ENVIRONMENT === devEnv && mode.mode === require_utils.MODE_DEFAULT && options.hotReloading !== false && !isNode) if (hotReloadingCache[name]) return hotReloadingCache[name].reconfigure(configuration);
else hotReloadingCache[name] = this;
const eventHub = mode.mode === require_utils.MODE_SSR ? new require_utils.MockedEventEmitter() : new betsy.EventEmitter();
const proxyStateTreeInstance = this.createProxyStateTree(configuration, eventHub, mode.mode === require_utils.MODE_TEST || require_utils.ENVIRONMENT === devEnv, mode.mode === require_utils.MODE_SSR);
this.originalConfiguration = configuration;
this.state = proxyStateTreeInstance.state;
this.effects = configuration.effects || {};
this.proxyStateTreeInstance = proxyStateTreeInstance;
this.eventHub = eventHub;
this.mode = mode;
this.actions = this.getActions(configuration.actions);
if (mode.mode === require_utils.MODE_SSR) return;
if (require_utils.ENVIRONMENT === devEnv && mode.mode === require_utils.MODE_DEFAULT && typeof window !== "undefined") {
let warning = "OVERMIND: You are running in DEVELOPMENT mode.";
if (options.logProxies !== true) {
const originalConsoleLog = console.log;
console.log = (...args) => originalConsoleLog.apply(console, args.map((arg) => arg && arg[proxy_state_tree.IS_PROXY] ? arg[proxy_state_tree.VALUE] : arg));
warning += "\n\n - To improve debugging experience \"console.log\" will NOT log proxies from Overmind, but the actual value. Please see docs to turn off this behaviour";
}
if (options.devtools || typeof location !== "undefined" && location.hostname === "localhost" && options.devtools !== false) {
const host = options.devtools === true ? "localhost:3031" : options.devtools;
const name$1 = options.name ? options.name : typeof document === "undefined" ? "NoName" : document.title || "NoName";
this.initializeDevtools(host, name$1, eventHub, configuration.actions, options.devtoolsLogLevel);
} else if (options.devtools !== false) warning += "\n\n - You are not running on localhost. You will have to manually define the devtools option to connect";
if (!require_utils.IS_TEST) console.warn(warning);
}
if (require_utils.ENVIRONMENT === "production" && mode.mode === require_utils.MODE_DEFAULT) {
eventHub.on(require_internalTypes.EventType.OPERATOR_ASYNC, (execution) => {
if (!execution.parentExecution || !execution.parentExecution.isRunning) proxyStateTreeInstance.getMutationTree().flush(true);
});
eventHub.on(require_internalTypes.EventType.ACTION_END, (execution) => {
if (!execution.parentExecution || !execution.parentExecution.isRunning) proxyStateTreeInstance.getMutationTree().flush();
});
let nextTick;
const flushTree = () => {
proxyStateTreeInstance.getMutationTree().flush(true);
};
this.proxyStateTreeInstance.onMutation(() => {
nextTick && clearTimeout(nextTick);
nextTick = setTimeout(flushTree, 0);
});
} else if (mode.mode === require_utils.MODE_DEFAULT || mode.mode === require_utils.MODE_TEST) {
if (require_utils.ENVIRONMENT === "test" || this.devtools && options.hotReloading !== false) eventHub.on(require_internalTypes.EventType.MUTATIONS, (execution) => {
this.rehydrateMutationsForHotReloading = this.rehydrateMutationsForHotReloading.concat(execution.mutations);
});
eventHub.on(require_internalTypes.EventType.OPERATOR_ASYNC, (execution) => {
if (!execution.parentExecution || !execution.parentExecution.isRunning) {
const flushData = execution.flush(true);
if (this.devtools && flushData.mutations.length) this.devtools.send({
type: "flush",
data: {
...execution,
...flushData
}
});
}
});
eventHub.on(require_internalTypes.EventType.ACTION_END, (execution) => {
if (!execution.parentExecution || !execution.parentExecution.isRunning) {
const flushData = execution.flush();
if (this.devtools && flushData.mutations.length) this.devtools.send({
type: "flush",
data: {
...execution,
...flushData
}
});
}
});
}
if (mode.mode === require_utils.MODE_DEFAULT) {
const onInitialize = this.createAction("onInitialize", require_utils.createOnInitialize());
this.initialized = Promise.resolve(onInitialize(this));
} else this.initialized = Promise.resolve(null);
}
getCurrentExecution() {
return this.currentExecution;
}
createProxyStateTree(configuration, eventHub, devmode, ssr) {
const proxyStateTreeInstance = new proxy_state_tree.ProxyStateTree(this.getState(configuration), {
devmode: devmode && !ssr,
ssr,
delimiter: this.delimiter,
transformPath: (path) => {
const execution = this.getCurrentExecution();
if (!execution?.namespacePath?.length) return path;
if (!path) return path;
const firstSegment = path.split(this.delimiter)[0];
if ((this.state && typeof this.state === "object" ? Object.keys(this.state) : []).includes(firstSegment)) return path;
return execution.namespacePath.join(this.delimiter) + this.delimiter + path;
},
onSetFunction: (tree, path, target, prop, func) => {
if (func[require_derived.IS_DERIVED_CONSTRUCTOR]) return new require_derived.Derived(func);
return func;
},
onGetFunction: (tree, path, target, prop) => {
const func = target[prop];
if (func[require_derived.IS_DERIVED]) return func(eventHub, tree, proxyStateTreeInstance, path.split(this.delimiter));
if (func[require_derived.IS_DERIVED_CONSTRUCTOR]) {
const derived = new require_derived.Derived(func);
target[prop] = derived;
return derived(eventHub, tree, proxyStateTreeInstance, path.split(this.delimiter));
}
return func;
},
onGetter: devmode ? (path, value) => {
this.eventHub.emitAsync(require_internalTypes.EventType.GETTER, {
path,
value
});
} : void 0,
getDevtools: () => {
if (devmode && !ssr && this.devtools) return this.devtools;
}
});
return proxyStateTreeInstance;
}
createExecution(name, action, parentExecution) {
const namespacePath = name.split(".");
namespacePath.pop();
if (require_utils.ENVIRONMENT === "production") return {
[require_utils.EXECUTION]: true,
parentExecution,
namespacePath,
actionName: name,
getMutationTree: () => {
return this.proxyStateTreeInstance.getMutationTree();
},
getTrackStateTree: () => {
return this.proxyStateTreeInstance.getTrackStateTree();
},
emit: this.eventHub.emit.bind(this.eventHub)
};
const mutationTrees = [];
return {
[require_utils.EXECUTION]: true,
namespacePath,
actionId: name,
executionId: this.nextExecutionId++,
actionName: name,
operatorId: 0,
isRunning: true,
parentExecution,
path: [],
emit: this.eventHub.emit.bind(this.eventHub),
send: this.devtools ? this.devtools.send.bind(this.devtools) : () => {},
trackEffects: this.trackEffects.bind(this, this.effects),
getNextOperatorId: (() => {
let currentOperatorId = 0;
return () => ++currentOperatorId;
})(),
flush: parentExecution ? parentExecution.flush : (isAsync) => {
return this.proxyStateTreeInstance.flush(mutationTrees, isAsync);
},
getMutationTree: parentExecution ? parentExecution.getMutationTree : () => {
const mutationTree = this.proxyStateTreeInstance.getMutationTree();
mutationTrees.push(mutationTree);
return mutationTree;
},
getTrackStateTree: () => {
return this.proxyStateTreeInstance.getTrackStateTree();
},
onFlush: (cb) => {
return this.proxyStateTreeInstance.onFlush(cb);
},
scopeValue: (value, tree) => {
return this.scopeValue(value, tree);
}
};
}
createScopedProxy(target, namespacePath) {
if (!namespacePath.length) return target;
return new Proxy(target, {
get: (obj, prop) => {
if (typeof prop === "symbol" || String(prop).startsWith("__")) return obj[prop];
const namespaceObj = namespacePath.reduce((aggr, key) => aggr?.[key], obj);
if (require_utils.isStateMachine(namespaceObj)) {
if (prop in namespaceObj) {
const value$1 = namespaceObj[prop];
if (prop in obj && require_utils.isStateMachine(obj[prop])) return obj[prop];
if (typeof value$1 === "function") return value$1.bind(namespaceObj);
return value$1;
}
return obj[prop];
}
const value = namespaceObj && prop in namespaceObj ? namespaceObj[prop] : obj[prop];
if (namespaceObj && prop in namespaceObj && typeof value === "function") return value.bind(namespaceObj);
return value;
},
has: (obj, prop) => {
const namespaceObj = namespacePath.reduce((aggr, key) => aggr?.[key], obj);
return namespaceObj && prop in namespaceObj || prop in obj;
},
ownKeys: (obj) => {
const namespaceObj = namespacePath.reduce((aggr, key) => aggr?.[key], obj);
if (require_utils.isStateMachine(namespaceObj)) return Object.keys(namespaceObj);
const namespaceKeys = namespaceObj ? Object.keys(namespaceObj) : [];
const rootKeys = Object.keys(obj);
return Array.from(new Set([...namespaceKeys, ...rootKeys]));
},
getOwnPropertyDescriptor: (obj, prop) => {
const namespaceObj = namespacePath.reduce((aggr, key) => aggr?.[key], obj);
if (namespaceObj && prop in namespaceObj) return Object.getOwnPropertyDescriptor(namespaceObj, prop) || {
configurable: true,
enumerable: true
};
return Object.getOwnPropertyDescriptor(obj, prop) || {
configurable: true,
enumerable: true
};
}
});
}
createContext(execution, tree) {
const namespacePath = execution.namespacePath || [];
const actionsProxy = require_utils.createActionsProxy(this.actions, (action) => {
return (value) => action(value, execution.isRunning ? execution : null);
});
return {
state: tree.state,
actions: namespacePath.length ? this.createScopedProxy(actionsProxy, namespacePath) : actionsProxy,
execution,
proxyStateTree: this.proxyStateTreeInstance,
effects: namespacePath.length ? this.createScopedProxy(this.trackEffects(this.effects, execution), namespacePath) : this.trackEffects(this.effects, execution),
addNamespace: this.addNamespace.bind(this),
reaction: this.reaction.bind(this),
addMutationListener: this.addMutationListener.bind(this),
addFlushListener: this.addFlushListener.bind(this)
};
}
addNamespace(configuration, path, existingState) {
const state = existingState || this.state;
const namespaceKey = path.pop();
if (configuration.state) {
const stateTarget = path.reduce((aggr, key) => aggr[key], state);
stateTarget[namespaceKey] = require_utils.processState(configuration.state);
}
if (configuration.actions) {
const actionsTarget = path.reduce((aggr, key) => aggr[key], this.actions);
actionsTarget[namespaceKey] = this.getActions(configuration.actions);
}
if (configuration.effects) {
const effectsTarget = path.reduce((aggr, key) => aggr[key], this.effects);
effectsTarget[namespaceKey] = configuration.effects;
}
}
scopeValue(value, tree) {
if (!value) return value;
if (value[proxy_state_tree.IS_PROXY]) return this.proxyStateTreeInstance.rescope(value, tree);
else if ((0, is_plain_obj.default)(value)) return Object.assign({}, ...Object.keys(value).map((key) => ({ [key]: this.proxyStateTreeInstance.rescope(value[key], tree) })));
else return value;
}
addExecutionMutation(mutation) {
this.mutations.push(mutation);
}
createAction(name, originalAction) {
this.actionReferences[name] = originalAction;
const actionFunc = (value, boundExecution) => {
const action = this.actionReferences[name];
boundExecution = boundExecution && boundExecution[require_utils.EXECUTION] ? boundExecution : void 0;
if (require_utils.ENVIRONMENT === "production" || action[require_utils.IS_OPERATOR] || this.mode.mode === require_utils.MODE_SSR) {
const execution = this.createExecution(name, action, boundExecution);
const previousExecution = this.currentExecution;
this.currentExecution = execution;
try {
this.eventHub.emit(require_internalTypes.EventType.ACTION_START, {
...execution,
value
});
if (action[require_utils.IS_OPERATOR]) return new Promise((resolve, reject) => {
action(null, {
...this.createContext(execution, this.proxyStateTreeInstance),
value
}, (err, finalContext) => {
execution.isRunning = false;
finalContext && this.eventHub.emit(require_internalTypes.EventType.ACTION_END, {
...finalContext.execution,
operatorId: finalContext.execution.operatorId - 1
});
if (err) reject(err);
else resolve(finalContext.value);
});
}).finally(() => {
this.currentExecution = previousExecution;
});
else {
const mutationTree = execution.getMutationTree();
if (this.isStrict) mutationTree.blockMutations();
const returnValue = action(this.createContext(execution, mutationTree), value);
this.eventHub.emit(require_internalTypes.EventType.ACTION_END, execution);
return returnValue;
}
} finally {
if (!action[require_utils.IS_OPERATOR]) this.currentExecution = previousExecution;
}
} else {
const execution = {
...this.createExecution(name, action, boundExecution),
operatorId: 0,
type: "action"
};
const previousExecution = this.currentExecution;
this.currentExecution = execution;
this.eventHub.emit(require_internalTypes.EventType.ACTION_START, {
...execution,
value
});
this.eventHub.emit(require_internalTypes.EventType.OPERATOR_START, execution);
const mutationTree = execution.getMutationTree();
if (this.isStrict) mutationTree.blockMutations();
mutationTree.onMutation((mutation) => {
this.eventHub.emit(require_internalTypes.EventType.MUTATIONS, {
...execution,
mutations: [mutation]
});
});
const scopedValue = this.scopeValue(value, mutationTree);
const context = this.createContext(execution, mutationTree);
let result;
let isAsync = false;
try {
let pendingFlush;
mutationTree.onMutation((mutation) => {
if (pendingFlush) clearTimeout(pendingFlush);
if (this.mode.mode === require_utils.MODE_TEST) this.addExecutionMutation(mutation);
else if (this.mode.mode === require_utils.MODE_DEFAULT) pendingFlush = setTimeout(() => {
pendingFlush = null;
const flushData = execution.flush(true);
if (this.devtools && flushData.mutations.length) this.devtools.send({
type: "flush",
data: {
...execution,
...flushData,
mutations: flushData.mutations
}
});
});
});
result = action(context, scopedValue);
isAsync = require_utils.isPromise(result);
if (isAsync) {
this.eventHub.emit(require_internalTypes.EventType.OPERATOR_ASYNC, execution);
result = result.then((promiseResult) => {
execution.isRunning = false;
if (!boundExecution) mutationTree.dispose();
this.eventHub.emit(require_internalTypes.EventType.OPERATOR_END, {
...execution,
isAsync: true,
result: void 0
});
this.eventHub.emit(require_internalTypes.EventType.ACTION_END, execution);
return promiseResult;
}).catch((error) => {
execution.isRunning = false;
if (!boundExecution) mutationTree.dispose();
this.eventHub.emit(require_internalTypes.EventType.OPERATOR_END, {
...execution,
isAsync: true,
result: void 0,
error: error.message
});
this.eventHub.emit(require_internalTypes.EventType.ACTION_END, execution);
throw error;
}).finally(() => {
this.currentExecution = previousExecution;
});
} else {
execution.isRunning = false;
if (!boundExecution) mutationTree.dispose();
this.eventHub.emit(require_internalTypes.EventType.OPERATOR_END, {
...execution,
isAsync: false,
result: void 0
});
this.eventHub.emit(require_internalTypes.EventType.ACTION_END, execution);
}
return result;
} catch (err) {
this.eventHub.emit(require_internalTypes.EventType.OPERATOR_END, {
...execution,
isAsync: false,
result: void 0,
error: err.message
});
this.eventHub.emit(require_internalTypes.EventType.ACTION_END, execution);
throw err;
} finally {
if (!isAsync) this.currentExecution = previousExecution;
}
}
};
return actionFunc;
}
trackEffects(effects = {}, execution) {
if (require_utils.ENVIRONMENT === "production") return effects;
return require_proxyfyEffects.proxifyEffects(this.effects, (effect) => {
let result;
try {
if (this.mode.mode === require_utils.MODE_TEST) result = this.mode.options.effectsCallback(effect);
else {
this.eventHub.emit(require_internalTypes.EventType.EFFECT, {
...execution,
...effect,
args: effect.args,
isPending: true,
error: false
});
result = effect.func.apply(this, effect.args);
}
} catch (error) {
this.eventHub.emit(require_internalTypes.EventType.EFFECT, {
...execution,
...effect,
args: effect.args,
isPending: false,
error: error.message
});
throw error;
}
if (require_utils.isPromise(result)) {
this.eventHub.emit(require_internalTypes.EventType.EFFECT, {
...execution,
...effect,
args: effect.args,
isPending: true,
error: false
});
return result.then((promisedResult) => {
this.eventHub.emit(require_internalTypes.EventType.EFFECT, {
...execution,
...effect,
args: effect.args,
result: promisedResult,
isPending: false,
error: false
});
return promisedResult;
}).catch((error) => {
this.eventHub.emit(require_internalTypes.EventType.EFFECT, {
...execution,
...effect,
args: effect.args,
isPending: false,
error: error && error.message
});
throw error;
});
}
this.eventHub.emit(require_internalTypes.EventType.EFFECT, {
...execution,
...effect,
args: effect.args,
result,
isPending: false,
error: false
});
return result;
});
}
initializeDevtools(host, name, eventHub, actions, logLevel = "error") {
if (require_utils.ENVIRONMENT === "production") return;
const devtools = new require_Devtools.Devtools(name, logLevel);
devtools.connect(host, (message) => {
switch (message.type) {
case "refresh":
location.reload();
break;
case "executeAction": {
const action = message.data.name.split(".").reduce((aggr, key) => aggr[key], this.actions);
message.data.payload ? action(JSON.parse(message.data.payload)) : action();
break;
}
case "mutation": {
const tree = this.proxyStateTreeInstance.getMutationTree();
const path = message.data.path.slice();
const value = JSON.parse(`{ "value": ${message.data.value} }`).value;
const key = path.pop();
const state = path.reduce((aggr, key$1) => aggr[key$1], tree.state);
state[key] = value;
tree.flush(true);
tree.dispose();
this.devtools.send({
type: "state",
data: {
path: message.data.path,
value
}
});
break;
}
}
});
for (const type in require_internalTypes.EventType) eventHub.on(require_internalTypes.EventType[type], ((eventType) => (data) => {
devtools.send({
type: require_internalTypes.EventType[type],
data
});
if (eventType === require_internalTypes.EventType.MUTATIONS) data.mutations.forEach((mutation) => {
const value = mutation.path.split(this.delimiter).reduce((aggr, key) => aggr[key], this.proxyStateTreeInstance.state);
if ((0, is_plain_obj.default)(value)) Object.keys(value).forEach((key) => value[key]);
else if (Array.isArray(value)) value.forEach((item) => {
if ((0, is_plain_obj.default)(item)) Object.keys(item).forEach((key) => item[key]);
});
});
if (eventType === require_internalTypes.EventType.DERIVED_DIRTY) data.derivedPath.reduce((aggr, key) => aggr[key], this.proxyStateTreeInstance.state);
})(require_internalTypes.EventType[type]));
devtools.send({
type: "init",
data: {
state: this.proxyStateTreeInstance.state,
actions: require_utils.getActionPaths(actions),
delimiter: this.delimiter,
features: {
transitions: true,
charts: require_utils.detectStateCharts(this.state)
}
}
});
this.devtools = devtools;
}
getState(configuration) {
let state = {};
if (configuration.state) state = require_utils.processState(configuration.state);
return state;
}
getActions(actions = {}, path = []) {
return Object.keys(actions).reduce((aggr, name) => {
if (typeof actions[name] === "function") {
const action = this.createAction(path.concat(name).join("."), actions[name]);
action.displayName = path.concat(name).join(".");
return Object.assign(aggr, { [name]: action });
}
return Object.assign(aggr, { [name]: this.getActions(actions[name], path.concat(name)) });
}, {});
}
updateActions(actions = {}, path = []) {
Object.keys(actions).forEach((name) => {
if (typeof actions[name] === "function") {
const actionName = path.concat(name).join(".");
if (this.actionReferences[actionName]) this.actionReferences[actionName] = actions[name];
else {
const target = path.reduce((aggr, key) => {
if (!aggr[key]) aggr[key] = {};
return aggr[key];
}, this.actions);
target[name] = this.createAction(actionName, actions[name]);
target[name].displayName = path.concat(name).join(".");
}
} else this.updateActions(actions[name], path.concat(name));
}, {});
}
getTrackStateTree() {
return this.proxyStateTreeInstance.getTrackStateTree();
}
getMutationTree() {
return this.proxyStateTreeInstance.getMutationTree();
}
reconfigure(configuration) {
const changeMutations = require_utils.getChangeMutations(this.originalConfiguration.state, configuration.state || {});
this.updateActions(configuration.actions);
this.effects = configuration.effects || {};
const mutationTree = this.proxyStateTreeInstance.getMutationTree();
require_rehydrate.rehydrate(mutationTree.state, changeMutations);
this.rehydrateMutationsForHotReloading.forEach((mutation) => {
try {
require_rehydrate.rehydrate(mutationTree.state, [mutation]);
} catch (error) {}
});
mutationTree.flush();
mutationTree.dispose();
if (this.devtools) this.devtools.send({
type: "re_init",
data: {
state: this.state,
actions: require_utils.getActionPaths(configuration.actions),
features: {
transitions: true,
charts: require_utils.detectStateCharts(this.state)
}
}
});
return this;
}
};
//#endregion
exports.Overmind = Overmind;
//# sourceMappingURL=Overmind.js.map