@genshi/core
Version:
A simple, composable and effective JavaScript state management library
567 lines (566 loc) • 16.7 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
var __accessCheck = (obj, member, msg) => {
if (!member.has(obj))
throw TypeError("Cannot " + msg);
};
var __privateGet = (obj, member, getter) => {
__accessCheck(obj, member, "read from private field");
return getter ? getter.call(obj) : member.get(obj);
};
var __privateAdd = (obj, member, value) => {
if (member.has(obj))
throw TypeError("Cannot add the same private member more than once");
member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
};
var __privateSet = (obj, member, value, setter) => {
__accessCheck(obj, member, "write to private field");
setter ? setter.call(obj, value) : member.set(obj, value);
return value;
};
var __privateWrapper = (obj, member, setter, getter) => ({
set _(value) {
__privateSet(obj, member, value, setter);
},
get _() {
return __privateGet(obj, member, getter);
}
});
var __privateMethod = (obj, member, method) => {
__accessCheck(obj, member, "access private method");
return method;
};
var _id, _displayName, _type, _storeId, _handler, _source, _id2, _name, _config, _counter, _state, _previousStates, _subscribers, _dispatchers, _exists, exists_fn, _history, _actionMiddlewares, _effectMiddlewares;
var Dispatcher = /* @__PURE__ */ ((Dispatcher2) => {
Dispatcher2["ACTION"] = "action";
Dispatcher2["EFFECT"] = "effect";
return Dispatcher2;
})(Dispatcher || {});
class BaseDispatcher {
constructor({
storeId,
displayName,
type,
handler
}) {
__privateAdd(this, _id, void 0);
__privateAdd(this, _displayName, void 0);
__privateAdd(this, _type, void 0);
__privateAdd(this, _storeId, void 0);
__privateAdd(this, _handler, void 0);
__privateAdd(this, _source, null);
__publicField(this, "payload", null);
__publicField(this, "parent", null);
__privateSet(this, _id, `store-0001`);
__privateSet(this, _displayName, displayName);
__privateSet(this, _type, type);
__privateSet(this, _storeId, storeId);
__privateSet(this, _handler, this.finalizeHandler(handler));
}
/**
* Simple wrapper for the handler function to remove the parent dispatcher reference.
*/
finalizeHandler(handler) {
const finalHandler = (...args) => {
this.parent = null;
return handler(...args);
};
return finalHandler;
}
get id() {
return __privateGet(this, _id);
}
set id(_value) {
throw new Error("Cannot set 'id' for dispatcher after creation");
}
get displayName() {
return __privateGet(this, _displayName);
}
set displayName(_value) {
throw new Error("Cannot set 'displayName' for dispatcher after creation");
}
get type() {
return __privateGet(this, _type);
}
set type(_value) {
throw new Error("Cannot set 'type' for dispatcher after creation");
}
get storeId() {
return __privateGet(this, _storeId);
}
set storeId(_value) {
throw new Error("Cannot set 'storeId' for dispatcher after creation");
}
get handler() {
return __privateGet(this, _handler);
}
set handler(_value) {
throw new Error("Cannot set 'handler' for dispatcher after creation");
}
source(source) {
if (source || source === null) {
__privateSet(this, _source, source);
}
return {
...__privateGet(this, _source)
};
}
}
_id = new WeakMap();
_displayName = new WeakMap();
_type = new WeakMap();
_storeId = new WeakMap();
_handler = new WeakMap();
_source = new WeakMap();
class Action extends BaseDispatcher {
constructor(storeId, name, handler) {
super({
storeId,
displayName: name,
type: Dispatcher.ACTION,
handler
});
}
}
class Effect extends BaseDispatcher {
constructor(storeId, name, handler) {
super({
storeId,
displayName: name,
type: Dispatcher.EFFECT,
handler
});
}
}
const _ConfigManager = class _ConfigManager {
constructor() {
__privateAdd(this, _id2, void 0);
__privateAdd(this, _name, "");
__privateAdd(this, _config, {});
__privateSet(this, _id2, `store-${(__privateWrapper(_ConfigManager, _counter)._++).toString().padStart(4, "0")}`);
}
setConfig(config) {
if (config.name) {
__privateSet(this, _name, config.name);
}
__privateSet(this, _config, config);
}
get id() {
return __privateGet(this, _id2);
}
set id(_value) {
throw new Error("Cannot set 'id' for store after creation");
}
get name() {
return __privateGet(this, _name);
}
set name(_value) {
throw new Error("Cannot set 'name' for store after creation");
}
get tag() {
return __privateGet(this, _name) || __privateGet(this, _id2);
}
set tag(_value) {
throw new Error("Cannot set 'tag' for store after creation");
}
get config() {
return __privateGet(this, _config);
}
set config(_value) {
throw new Error("Cannot set 'config' for store after creation");
}
};
_id2 = new WeakMap();
_name = new WeakMap();
_config = new WeakMap();
_counter = new WeakMap();
__privateAdd(_ConfigManager, _counter, 0);
let ConfigManager = _ConfigManager;
class StateManager extends ConfigManager {
constructor(state) {
super();
/**
* Only mutate this directly within `setState` method. Otherwise
*/
__privateAdd(this, _state, void 0);
__privateAdd(this, _previousStates, void 0);
__privateAdd(this, _subscribers, []);
__privateSet(this, _state, state);
__privateSet(this, _previousStates, [state]);
}
/**
* We have this property separately to make it easier to access the
* state in the classes that inherit `StateManager`.
*/
get state() {
return this.getState();
}
/**
* Never allow setting state directly via this property. It should only be
* mutated via the `setState` method to ensure any optimisations or features
* we might add while updating state will be centrally managed.
*/
set state(_value) {
throw new Error(
"You cannot set the state directly. Use the `setState` method instead."
);
}
/**
* We have this method separately to make it easier to access the
* previous state in the classes that inherit from `StateManager`.
*
* It is by design that the property only returns the immediately previous state.
* If you need to access more than one previous state, use the `getPreviousStates` method.
*/
get previousState() {
return __privateGet(this, _previousStates)[0];
}
/**
* Never allow setting previous state directly. It is populated internally as
* a side effect of setting the state.
*/
set previousState(_value) {
throw new Error(
"You cannot set the previous state directly. It is managed internally."
);
}
/**
* Set the new state. We do not provide any validation here as it is
* the responsibility of the consumer.
*/
setState(state) {
__privateGet(this, _previousStates).unshift(Object.seal(__privateGet(this, _state)));
__privateSet(this, _state, state);
__privateGet(this, _subscribers).forEach((callback) => callback(state));
}
/**
* The `getState` method is used to get the current state.
*/
getState() {
return __privateGet(this, _state);
}
/**
* The `getPreviousStates` returns all the previous states in the lifetime of the store.
*/
getPreviousStates() {
return __privateGet(this, _previousStates).slice();
}
/**
* Allows you to subscribe to the state changes. It returns an object with a `remove` method
* that you can call to unsubscribe.
*/
subscribe(callback) {
const index = __privateGet(this, _subscribers).push(callback) - 1;
return {
remove: () => {
__privateGet(this, _subscribers).splice(index, 1);
}
};
}
}
_state = new WeakMap();
_previousStates = new WeakMap();
_subscribers = new WeakMap();
class HandlerManager extends StateManager {
constructor() {
super(...arguments);
/**
* The `#exists` method is used to check if a dispatcher with the same name
* already exists in the store.
*/
__privateAdd(this, _exists);
__privateAdd(this, _dispatchers, /* @__PURE__ */ new Set());
}
/**
* The `getHandler` method is used to get the handler for the dispatcher.
* It checks if the dispatcher is registered with the store and if it is
* allowed to be fired from the store.
*
* @todo (samrith-s) Evaluate moving this to where the dispatcher is actually fired
* rather than when the handler is fetched.
*/
getHandler(dispatcher) {
const type = dispatcher.type;
const name = dispatcher.displayName;
if (!__privateGet(this, _dispatchers).has(`${type}-${name}`)) {
throw new TypeError(
`Dispatcher ${type} ${name} is not registered with the store '${this.tag}'`
);
}
return dispatcher.handler;
}
/**
* Register a dispatcher with the store. This method is used to register
* both actions and effects with the store.
*
* It performs an intrinsic non-blocking check to see if the dispatcher is already registered.
*/
registerDispatcher(dispatcher) {
__privateMethod(this, _exists, exists_fn).call(this, dispatcher);
__privateGet(this, _dispatchers).add(`${dispatcher.type}-${dispatcher.displayName}`);
return dispatcher;
}
}
_dispatchers = new WeakMap();
_exists = new WeakSet();
exists_fn = function(dispatcher) {
const type = dispatcher.type;
const name = dispatcher.displayName;
if (__privateGet(this, _dispatchers).has(`${type}-${name}`)) {
console.warn(
`The ${type} dispatcher with name '${name}' already exists in store '${this.tag}'. Setting it again will overwrite it.`
);
}
};
class HistoryManager extends HandlerManager {
constructor() {
super(...arguments);
__privateAdd(this, _history, /* @__PURE__ */ new Map());
}
/**
* The `trace` method is used to create a trace of the dispatch. It is the
* primary method used to start a trace of the dispatch.
*/
trace(history) {
const id = `trace-${__privateGet(this, _history).size.toString().padStart(4, "0")}`;
__privateGet(this, _history).set(id, {
id,
global: false,
timestamp: /* @__PURE__ */ new Date(),
...history
});
return id;
}
/**
* The `traceEnd` method is used to end a trace of the dispatch. The only way to
* end a trace is by calling this method with a valid trace id.
*/
traceEnd(id) {
const trace = __privateGet(this, _history).get(id);
if (!trace) {
console.warn(`A trace with id '${id}' does not exist.`);
return;
}
const updatedTrace = {
...trace,
previousState: this.previousState,
currentState: this.state
};
__privateGet(this, _history).set(id, updatedTrace);
}
/**
* The `history` method is used to get the history of the store. It returns a new instance
* of the history array, so that the original history array is not mutated.
*/
history() {
return [...__privateGet(this, _history).values()].reverse();
}
/**
* Convenience method to print the history in a table format. Useful for
* debugging.
*/
printHistory() {
console.table(
this.history().map((history) => {
var _a, _b;
return {
type: history.type,
dispatcher: history.name,
global: history.global,
payload: history.payload,
previous_state: history.previousState,
current_state: history.currentState,
...history.source ? {
source_type: (_a = history.source) == null ? void 0 : _a.type,
source_name: (_b = history.source) == null ? void 0 : _b.name
} : {}
};
})
);
}
}
_history = new WeakMap();
class MiddlewareManager extends HistoryManager {
constructor() {
super(...arguments);
__privateAdd(this, _actionMiddlewares, []);
__privateAdd(this, _effectMiddlewares, []);
}
/**
* A simple method to collect the middlewares from the store configuration,
* and make them separately available for the respective apply methods.
*/
collectMiddlewares(config) {
var _a, _b;
__privateSet(this, _actionMiddlewares, ((_a = config == null ? void 0 : config.middlewares) == null ? void 0 : _a.action) ?? []);
__privateSet(this, _effectMiddlewares, ((_b = config == null ? void 0 : config.middlewares) == null ? void 0 : _b.effect) ?? []);
}
/**
* Applies all the action middlewares, in the order they were specified,
* before the actual action handler.
*/
applyActionMiddleware({
handler,
payload
}) {
if (__privateGet(this, _actionMiddlewares).length === 0) {
return handler({
state: this.state,
payload
});
}
return __privateGet(this, _actionMiddlewares).reduce(
(acc, middleware) => middleware({
state: acc,
payload,
handler
}),
this.state
);
}
/**
* Applies all the effect middlewares, in the order they were specified,
* before the actual effect handler.
*/
applyEffectMiddleware({
handler,
payload,
dispatch
}) {
if (__privateGet(this, _effectMiddlewares).length === 0) {
return handler({
state: this.state,
payload,
dispatch
});
} else {
__privateGet(this, _effectMiddlewares).forEach(
(middleware) => middleware({
state: this.state,
payload,
dispatch,
handler
})
);
}
}
}
_actionMiddlewares = new WeakMap();
_effectMiddlewares = new WeakMap();
class DispatchManager extends MiddlewareManager {
constructor() {
super(...arguments);
/**
* The `dispatch` method is used to dispatch an action or an effect.
*/
__publicField(this, "dispatch", (dispatcher, ...args) => {
var _a, _b;
const payload = args[0];
const type = dispatcher.type;
const name = dispatcher.displayName;
const storeId = dispatcher.storeId;
const isGlobal = !dispatcher.parent;
if (this.id !== storeId) {
const prefix = type === Dispatcher.ACTION ? "Action" : "Effect";
throw new RangeError(
`${prefix} '${name}' cannot be fired from store '${this.tag}'.`
);
}
const handler = this.getHandler(dispatcher);
const trace = this.trace({
name,
type,
payload,
...!isGlobal ? {
global: false,
source: {
name: (_a = dispatcher.parent) == null ? void 0 : _a.displayName,
type: (_b = dispatcher.parent) == null ? void 0 : _b.type
}
} : {
global: true
}
});
switch (type) {
case Dispatcher.ACTION: {
this.setState(
this.applyActionMiddleware({
handler,
payload
})
);
this.traceEnd(trace);
break;
}
case Dispatcher.EFFECT: {
this.traceEnd(trace);
this.applyEffectMiddleware({
handler,
payload,
dispatch: (...argv) => {
const d = argv[0];
d.parent = dispatcher;
this.dispatch(...argv);
}
});
break;
}
}
});
}
}
class Store extends DispatchManager {
constructor(state, config) {
super(state);
this.setConfig(config || {});
this.collectMiddlewares(this.config);
}
/**
* Register an action with the store. In Genshi, an Action is used to
* update the state.
*
* ```ts
* const store = new Store({ count: 0 });
*
* const increment = store.action('increment', ({ state }) => ({
* ...state,
* count: state.count + 1
* }));
*
* store.dispatch(increment);
* ```
*/
action(name, handler) {
return this.registerDispatcher(
new Action(this.id, name, handler)
);
}
/**
* Register an effect with the store. In Genshi, an Effects is used to
* perform side effects like API calls, logging, etc.
*
* ```ts
* const store = new Store({ count: 0 });
*
* const tick = store.effect('tick', ({ state }) => {
* setInterval(() => {
* console.log(state.count);
* }, 1000)
* });
*
* store.dispatch(tick);
* ```
*/
effect(name, handler) {
return this.registerDispatcher(
new Effect(this.id, name, handler)
);
}
}
export {
Store
};