overmind
Version:
Frictionless state management
668 lines • 31 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Overmind = void 0;
const tslib_1 = require("tslib");
const betsy_1 = require("betsy");
const is_plain_obj_1 = tslib_1.__importDefault(require("is-plain-obj"));
const proxyStateTree = tslib_1.__importStar(require("proxy-state-tree"));
const derived_1 = require("./derived");
const Devtools_1 = require("./Devtools");
const internalTypes = tslib_1.__importStar(require("./internalTypes"));
const proxyfyEffects_1 = require("./proxyfyEffects");
const rehydrate_1 = require("./rehydrate");
const utils = tslib_1.__importStar(require("./utils"));
const hotReloadingCache = {};
class Overmind {
constructor(configuration, options = {}, mode = {
mode: utils.MODE_DEFAULT,
}) {
this.actionReferences = {};
this.nextExecutionId = 0;
this.reydrateMutationsForHotReloading = [];
this.isStrict = false;
this.reaction = (stateCallback, updateCallback, options = {}) => {
let disposer;
if (options.nested) {
const value = stateCallback(this.state);
if (!value || !value[proxyStateTree.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[proxyStateTree.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 = () => {
tree.trackScope(() => (returnValue = stateCallback(tree.state)), () => {
updateReaction();
updateCallback(returnValue);
});
};
updateReaction();
disposer = () => {
tree.dispose();
};
}
if (options.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 (utils.ENVIRONMENT === devEnv &&
mode.mode === utils.MODE_DEFAULT &&
options.hotReloading !== false &&
!isNode) {
if (hotReloadingCache[name]) {
return hotReloadingCache[name].reconfigure(configuration);
}
else {
hotReloadingCache[name] = this;
}
}
/*
Set up an eventHub to trigger information from derived, computed and reactions
*/
const eventHub = mode.mode === utils.MODE_SSR
? new utils.MockedEventEmitter()
: new betsy_1.EventEmitter();
/*
Create the proxy state tree instance with the state and a wrapper to expose
the eventHub
*/
const proxyStateTreeInstance = this.createProxyStateTree(configuration, eventHub, mode.mode === utils.MODE_TEST || utils.ENVIRONMENT === devEnv, mode.mode === utils.MODE_SSR);
this.originalConfiguration = configuration;
this.state = proxyStateTreeInstance.state;
this.effects = configuration.effects || {};
this.proxyStateTreeInstance = proxyStateTreeInstance;
this.eventHub = eventHub;
this.mode = mode;
/*
Expose the created actions
*/
this.actions = this.getActions(configuration.actions);
if (mode.mode === utils.MODE_SSR) {
return;
}
if (utils.ENVIRONMENT === devEnv &&
mode.mode === 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[proxyStateTree.IS_PROXY]
? arg[proxyStateTree.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 = options.name
? options.name
: typeof document === 'undefined'
? 'NoName'
: document.title || 'NoName';
this.initializeDevtools(host, name, eventHub, proxyStateTreeInstance.sourceState, 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 (!utils.IS_TEST) {
console.warn(warning);
}
}
if (utils.ENVIRONMENT === 'production' &&
mode.mode === utils.MODE_DEFAULT) {
eventHub.on(internalTypes.EventType.OPERATOR_ASYNC, (execution) => {
if (!execution.parentExecution ||
!execution.parentExecution.isRunning) {
proxyStateTreeInstance.getMutationTree().flush(true);
}
});
eventHub.on(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 === utils.MODE_DEFAULT ||
mode.mode === utils.MODE_TEST) {
if (utils.ENVIRONMENT === 'test' ||
(this.devtools && options.hotReloading !== false)) {
eventHub.on(internalTypes.EventType.MUTATIONS, (execution) => {
this.reydrateMutationsForHotReloading = this.reydrateMutationsForHotReloading.concat(execution.mutations);
});
}
eventHub.on(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: Object.assign(Object.assign({}, execution), flushData),
});
}
}
});
eventHub.on(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: Object.assign(Object.assign({}, execution), flushData),
});
}
}
});
}
if (mode.mode === utils.MODE_DEFAULT) {
const onInitialize = this.createAction('onInitialize', utils.createOnInitialize());
this.initialized = Promise.resolve(onInitialize(this));
}
else {
this.initialized = Promise.resolve(null);
}
}
createProxyStateTree(configuration, eventHub, devmode, ssr) {
const proxyStateTreeInstance = new proxyStateTree.ProxyStateTree(this.getState(configuration), {
devmode: devmode && !ssr,
ssr,
delimiter: this.delimiter,
onSetFunction: (tree, path, target, prop, func) => {
if (func[derived_1.IS_DERIVED_CONSTRUCTOR]) {
return new derived_1.Derived(func);
}
return func;
},
onGetFunction: (tree, path, target, prop) => {
const func = target[prop];
if (func[derived_1.IS_DERIVED]) {
return func(eventHub, tree, proxyStateTreeInstance, path.split(this.delimiter));
}
if (func[derived_1.IS_DERIVED_CONSTRUCTOR]) {
const derived = new derived_1.Derived(func);
target[prop] = derived;
return derived(eventHub, tree, proxyStateTreeInstance, path.split(this.delimiter));
}
return func;
},
onGetter: devmode
? (path, value) => {
this.eventHub.emitAsync(internalTypes.EventType.GETTER, {
path,
value,
});
}
: undefined,
});
return proxyStateTreeInstance;
}
createExecution(name, action, parentExecution) {
const namespacePath = name.split('.');
namespacePath.pop();
if (utils.ENVIRONMENT === 'production') {
return {
[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 = [];
const execution = {
[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);
},
};
return execution;
}
createContext(execution, tree) {
return {
state: tree.state,
actions: utils.createActionsProxy(this.actions, (action) => {
return (value) => action(value, execution.isRunning ? execution : null);
}),
execution,
proxyStateTree: this.proxyStateTreeInstance,
effects: 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] = 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[proxyStateTree.IS_PROXY]) {
return this.proxyStateTreeInstance.rescope(value, tree);
}
else if ((0, is_plain_obj_1.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];
// Developer might unintentionally pass more arguments, so have to ensure
// that it is an actual execution
boundExecution =
boundExecution && boundExecution[utils.EXECUTION]
? boundExecution
: undefined;
if (utils.ENVIRONMENT === 'production' ||
action[utils.IS_OPERATOR] ||
this.mode.mode === utils.MODE_SSR) {
const execution = this.createExecution(name, action, boundExecution);
this.eventHub.emit(internalTypes.EventType.ACTION_START, Object.assign(Object.assign({}, execution), { value }));
if (action[utils.IS_OPERATOR]) {
return new Promise((resolve, reject) => {
action(null, Object.assign(Object.assign({}, this.createContext(execution, this.proxyStateTreeInstance)), { value }), (err, finalContext) => {
execution.isRunning = false;
finalContext &&
this.eventHub.emit(internalTypes.EventType.ACTION_END, Object.assign(Object.assign({}, finalContext.execution), { operatorId: finalContext.execution.operatorId - 1 }));
if (err)
reject(err);
else {
resolve(finalContext.value);
}
});
});
}
else {
const mutationTree = execution.getMutationTree();
if (this.isStrict) {
mutationTree.blockMutations();
}
const returnValue = action(this.createContext(execution, mutationTree), value);
this.eventHub.emit(internalTypes.EventType.ACTION_END, execution);
return returnValue;
}
}
else {
const execution = Object.assign(Object.assign({}, this.createExecution(name, action, boundExecution)), { operatorId: 0, type: 'action' });
this.eventHub.emit(internalTypes.EventType.ACTION_START, Object.assign(Object.assign({}, execution), { value }));
this.eventHub.emit(internalTypes.EventType.OPERATOR_START, execution);
const mutationTree = execution.getMutationTree();
if (this.isStrict) {
mutationTree.blockMutations();
}
mutationTree.onMutation((mutation) => {
this.eventHub.emit(internalTypes.EventType.MUTATIONS, Object.assign(Object.assign({}, execution), { mutations: [mutation] }));
});
const scopedValue = this.scopeValue(value, mutationTree);
const context = this.createContext(execution, mutationTree);
try {
let pendingFlush;
mutationTree.onMutation((mutation) => {
if (pendingFlush) {
clearTimeout(pendingFlush);
}
if (this.mode.mode === utils.MODE_TEST) {
this.addExecutionMutation(mutation);
}
else if (this.mode.mode === utils.MODE_DEFAULT) {
pendingFlush = setTimeout(() => {
pendingFlush = null;
const flushData = execution.flush(true);
if (this.devtools && flushData.mutations.length) {
this.devtools.send({
type: 'flush',
data: Object.assign(Object.assign(Object.assign({}, execution), flushData), { mutations: flushData.mutations }),
});
}
});
}
});
let result = action(context, scopedValue);
if (utils.isPromise(result)) {
this.eventHub.emit(internalTypes.EventType.OPERATOR_ASYNC, execution);
result = result
.then((promiseResult) => {
execution.isRunning = false;
if (!boundExecution) {
mutationTree.dispose();
}
this.eventHub.emit(internalTypes.EventType.OPERATOR_END, Object.assign(Object.assign({}, execution), { isAsync: true, result: undefined }));
this.eventHub.emit(internalTypes.EventType.ACTION_END, execution);
return promiseResult;
})
.catch((error) => {
execution.isRunning = false;
if (!boundExecution) {
mutationTree.dispose();
}
this.eventHub.emit(internalTypes.EventType.OPERATOR_END, Object.assign(Object.assign({}, execution), { isAsync: true, result: undefined, error: error.message }));
this.eventHub.emit(internalTypes.EventType.ACTION_END, execution);
throw error;
});
}
else {
execution.isRunning = false;
if (!boundExecution) {
mutationTree.dispose();
}
this.eventHub.emit(internalTypes.EventType.OPERATOR_END, Object.assign(Object.assign({}, execution), { isAsync: false, result: undefined }));
this.eventHub.emit(internalTypes.EventType.ACTION_END, execution);
}
return result;
}
catch (err) {
this.eventHub.emit(internalTypes.EventType.OPERATOR_END, Object.assign(Object.assign({}, execution), { isAsync: false, result: undefined, error: err.message }));
this.eventHub.emit(internalTypes.EventType.ACTION_END, execution);
throw err;
}
}
};
return actionFunc;
}
trackEffects(effects = {}, execution) {
if (utils.ENVIRONMENT === 'production') {
return effects;
}
return (0, proxyfyEffects_1.proxifyEffects)(this.effects, (effect) => {
let result;
try {
if (this.mode.mode === utils.MODE_TEST) {
const mode = this.mode;
result = mode.options.effectsCallback(effect);
}
else {
this.eventHub.emit(internalTypes.EventType.EFFECT, Object.assign(Object.assign(Object.assign({}, execution), effect), { args: effect.args, isPending: true, error: false }));
result = effect.func.apply(this, effect.args);
}
}
catch (error) {
this.eventHub.emit(internalTypes.EventType.EFFECT, Object.assign(Object.assign(Object.assign({}, execution), effect), { args: effect.args, isPending: false, error: error.message }));
throw error;
}
if (utils.isPromise(result)) {
this.eventHub.emit(internalTypes.EventType.EFFECT, Object.assign(Object.assign(Object.assign({}, execution), effect), { args: effect.args, isPending: true, error: false }));
return result
.then((promisedResult) => {
this.eventHub.emit(internalTypes.EventType.EFFECT, Object.assign(Object.assign(Object.assign({}, execution), effect), { args: effect.args, result: promisedResult, isPending: false, error: false }));
return promisedResult;
})
.catch((error) => {
this.eventHub.emit(internalTypes.EventType.EFFECT, Object.assign(Object.assign(Object.assign({}, execution), effect), { args: effect.args, isPending: false, error: error && error.message }));
throw error;
});
}
this.eventHub.emit(internalTypes.EventType.EFFECT, Object.assign(Object.assign(Object.assign({}, execution), effect), { args: effect.args, result: result, isPending: false, error: false }));
return result;
});
}
initializeDevtools(host, name, eventHub, initialState, actions, logLevel = 'error') {
if (utils.ENVIRONMENT === 'production')
return;
const devtools = new Devtools_1.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) => aggr[key], 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 internalTypes.EventType) {
eventHub.on(internalTypes.EventType[type], ((eventType) => (data) => {
devtools.send({
type: internalTypes.EventType[type],
data,
});
if (eventType === internalTypes.EventType.MUTATIONS) {
// We want to trigger property access when setting objects and arrays, as any derived set would
// then trigger and update the devtools
data.mutations.forEach((mutation) => {
const value = mutation.path
.split(this.delimiter)
.reduce((aggr, key) => aggr[key], this.proxyStateTreeInstance.state);
if ((0, is_plain_obj_1.default)(value)) {
Object.keys(value).forEach((key) => value[key]);
}
else if (Array.isArray(value)) {
value.forEach((item) => {
if ((0, is_plain_obj_1.default)(item)) {
Object.keys(item).forEach((key) => item[key]);
}
});
}
});
}
// Access the derived which will trigger calculation and devtools
if (eventType === internalTypes.EventType.DERIVED_DIRTY) {
data.derivedPath.reduce((aggr, key) => aggr[key], this.proxyStateTreeInstance.state);
}
})(internalTypes.EventType[type]));
}
devtools.send({
type: 'init',
data: {
state: this.proxyStateTreeInstance.state,
actions: utils.getActionPaths(actions),
delimiter: this.delimiter,
},
});
this.devtools = devtools;
}
getState(configuration) {
let state = {};
if (configuration.state) {
state = 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)),
});
}, {});
}
/*
Related to hot reloading we update the existing action references and add any new
actions.
*/
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 = utils.getChangeMutations(this.originalConfiguration.state, configuration.state || {});
this.updateActions(configuration.actions);
this.effects = configuration.effects || {};
const mutationTree = this.proxyStateTreeInstance.getMutationTree();
// We change the state to match the new structure
(0, rehydrate_1.rehydrate)(mutationTree.state, changeMutations);
// We run any mutations ran during the session, it might fail though
// as the state structure might have changed, but no worries we just
// ignore that
this.reydrateMutationsForHotReloading.forEach((mutation) => {
try {
(0, rehydrate_1.rehydrate)(mutationTree.state, [mutation]);
}
catch (error) {
// No worries, structure changed and we do not want to mutate anyways
}
});
mutationTree.flush();
mutationTree.dispose();
if (this.devtools) {
this.devtools.send({
type: 're_init',
data: {
state: this.state,
actions: utils.getActionPaths(configuration.actions),
},
});
}
return this;
}
}
exports.Overmind = Overmind;
//# sourceMappingURL=Overmind.js.map