nervue
Version:
Vue composition and options API-compatible lightweight store library
454 lines (445 loc) • 13.7 kB
JavaScript
;
var vueDemi = require('vue-demi');
function logWarning(...args) {
console.warn(`[nervue]:`, ...args);
}
function typeOf(arg) {
return Object.prototype.toString.call(arg).slice(8, -1).toLowerCase();
}
/***
* @param target - state of store
* @param patch - object to merge
*/
function merge(target, patch) {
if (typeOf(target) === 'map') {
patch.forEach((it, key) => target.set(key, it));
}
for (const key in patch) {
if (!patch.hasOwnProperty(key)) {
continue;
}
if (typeOf(patch[key]) === 'object') {
target[key] = merge(target[key], patch[key]);
}
else {
target[key] = patch[key];
}
}
return target;
}
const nervueSymbol = Symbol.for('nervue');
class Nervue {
installed = false;
_s = {};
_p = [];
_e = vueDemi.effectScope();
_a = {};
static sets = [];
set(useStore) {
if (this.installed) {
this._s[useStore.$id] = useStore;
}
else {
Nervue.sets.push(useStore);
}
}
unset(id) {
if (this._s[id]) {
delete this._s[id];
}
}
use(plugin) {
this._p.push(plugin);
}
install() {
if (this.installed) {
return;
}
if (Nervue.sets.length) {
Nervue.sets.forEach(s => this._s[s.$id] = s);
Nervue.sets = [];
}
this.installed = true;
}
}
let root = null;
function useNervue() {
if (!root) {
root = new Nervue();
}
return root;
}
function genVue3Install() {
const nervue = useNervue();
const { install } = nervue;
return function (app) {
if (!app) {
return;
}
install.call(nervue);
nervue._a = app;
app.config.globalProperties.$nervue = useNervue();
app.provide(nervueSymbol, useNervue());
};
}
function genVue2Install() {
const nervue = useNervue();
const { install } = nervue;
return function (Vue) {
if (nervue.installed) {
return;
}
install.call(nervue);
// Vue.prototype.$nervue = useNervue()
Vue.mixin({
beforeCreate() {
const options = this.$options;
if (options.nervue) {
this.$nervue = options.nervue;
nervue._a = this;
}
}
});
};
}
function createNervue() {
if (vueDemi.isVue3) {
useNervue().constructor.prototype.install = genVue3Install();
}
else {
useNervue().constructor.prototype.install = genVue2Install();
}
return useNervue();
}
function useStore(id) {
const nervue = useNervue();
if (!nervue.installed) {
return logWarning('[nervue]: You should to create Nervue instance');
}
if (!nervue?._s[id]) {
logWarning(`"${id}" store doesn't exist in the root object`);
}
else {
return nervue._s[id]();
}
}
function setupStore(options) {
const { id, state, actions, guards, computed: $computed } = options;
const { assign } = Object;
/**
* @param {string} storeId - store id
* @param {object} state - state to wrap
* @param {object} guards - guards to protect the state
* @returns {proxy} wrapped state
*/
function wrapState(storeId, state, guards) {
function get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
}
function set(target, prop, value, receiver) {
let result = { next: false, value };
if (guards[prop]) {
for (const fn of guards[prop]) {
const ret = fn(result.value);
result.next = ret.next;
value = ret.value || value;
result.value = value;
}
if (result.next) {
Reflect.set(target, prop, result.value, receiver);
}
}
else {
Reflect.set(target, prop, value, receiver);
}
return true;
}
return new Proxy(state, { get, set });
}
const subscriptionsBefore = {};
const subscriptionsAfter = {};
const subscriptionsOnError = {};
/**
* @param {object} options - options for subscribing
* @returns {Unsubscribe} - unsubscribe function
*/
function $subscribe(options) {
const { name, detached, before, after, onError } = options;
if (before && !subscriptionsBefore[name]) {
subscriptionsBefore[name] = [];
}
if (after && !subscriptionsAfter[name]) {
subscriptionsAfter[name] = [];
}
if (onError && !subscriptionsOnError[name]) {
subscriptionsOnError[name] = [];
}
let bInd, aInd, oInd;
before && (bInd = subscriptionsBefore[name].push(before) - 1);
after && (aInd = subscriptionsAfter[name].push(after) - 1);
onError && (oInd = subscriptionsOnError[name].push(onError) - 1);
function unsubscribe() {
return new Promise((resolve) => {
subscriptionsBefore[name]?.splice(bInd, 1);
subscriptionsAfter[name]?.splice(aInd, 1);
subscriptionsOnError[name]?.splice(oInd, 1);
resolve(true);
});
}
if (!detached && vueDemi.getCurrentInstance()) {
vueDemi.onUnmounted(unsubscribe);
}
return unsubscribe;
}
/***
* @param {array} subscribers - array of subscribers
* @param {array} args - arguments for subscriber callback function
*/
function triggerSubs(subscribers, ...args) {
subscribers.slice().forEach(fn => fn(...args));
}
/***
* @param name {string} - name of action
* @returns {object} object of existing subscribers
*/
function getSubscribers(name) {
return {
beforeList: subscriptionsBefore[name],
afterList: subscriptionsAfter[name],
onErrorList: subscriptionsOnError[name]
};
}
/***
* @param {object} store - current store instance
* @param {string} name - name of action
* @param {function} action - action to wrap
* @returns {function} a wrapped action to handle subscriptions
*/
function wrapAction(store, name, action) {
return function () {
const { beforeList, afterList, onErrorList } = getSubscribers(name);
const args = Array.from(arguments);
if (beforeList) {
triggerSubs(beforeList, ...args);
}
let result;
try {
result = action.call(store, ...args);
}
catch (error) {
if (onErrorList) {
triggerSubs(onErrorList, error);
}
throw error;
}
if (result instanceof Promise) {
return result
.then(res => {
if (afterList) {
triggerSubs(afterList, res);
}
return res;
})
.catch(error => {
if (onErrorList) {
triggerSubs(onErrorList, error);
}
return Promise.reject(error);
});
}
if (afterList) {
triggerSubs(afterList, result);
}
return result;
};
}
function $patch(mutator) {
if (typeof mutator === 'function') {
mutator(this.$state);
}
else if (typeof mutator === 'object') {
merge(this.$state, mutator);
}
}
const initialState = state?.() || {};
const guardedState = guards ? wrapState(id, initialState, guards) : null;
const stateRef = vueDemi.ref(guardedState || initialState);
/**
* defining store properties
*/
const _storeProperties = {};
_storeProperties.$id = id;
_storeProperties.$patch = $patch;
_storeProperties.$subscribe = $subscribe;
Object.defineProperty(_storeProperties, '$state', {
get: () => vueDemi.toRaw(stateRef.value),
set: (val) => {
$patch(val);
}
});
Object.defineProperty(_storeProperties, '_guards', {
writable: false,
configurable: true,
value: guards || {}
});
Object.defineProperty(_storeProperties, '_computed', {
writable: false,
configurable: true,
value: Object.keys($computed || {})
});
const store = vueDemi.reactive(assign(_storeProperties, vueDemi.toRefs(stateRef.value), actions, Object.keys($computed || {}).reduce((mods, key) => {
// @ts-ignore
mods[key] = vueDemi.markRaw(vueDemi.computed(() => $computed[key].call(store, store)));
return mods;
}, {})));
if (actions) {
Object.keys(actions).forEach(name => {
const action = store[name];
store[name] = wrapAction(store, name, action);
});
}
return store;
}
/**
* @param {StoreOptions} options - store definition options object
* @returns {StoreDefinition} useStore function
*/
function defineStore(options) {
const { assign } = Object;
const nervue = useNervue();
/**
* create the store and wrapping
* into reactive for unwrapping the refs
*/
const store = nervue._e.run(() => {
const scope = vueDemi.effectScope();
/**
* effects scope for the created store
*/
return scope.run(() => setupStore(options));
});
/**
* wrapping the actions to handle subscribers
*/
const useStore = () => store;
const plugins = {};
/**
* install plugins
*/
nervue?._p.forEach(pl => assign(plugins, (pl({ store, options }) || {})));
assign(store, plugins);
useStore.$id = store.$id;
/**
* set useStore to the root object
*/
nervue.set(useStore);
return useStore;
}
/**
* @param useStore - store composition
* @param mapOrKeys - action keys map or array
*/
function mapActions(useStore, mapOrKeys) {
const store = useStore();
const map = {};
if (mapOrKeys) {
/**
* if the map is just a simple array with
* keys of the actions of store
*/
if (Array.isArray(mapOrKeys)) {
mapOrKeys.forEach(key => {
map[key] = store[key];
});
/**
* or it just simple keys map
* with custom namings
*/
}
else {
Object.keys(mapOrKeys).forEach(key => {
map[key] = store[mapOrKeys[key]];
});
}
}
else {
for (const key of Object.keys(store)) {
if (typeof store[key] === 'function') {
map[key] = store[key];
}
}
}
return map;
}
/**
* @param useStore - store composition
* @param mapOrKeys - map or array of state or computed keys
*/
function mapState(useStore, mapOrKeys) {
const map = {};
if (mapOrKeys) {
/**
* if the map is just a simple array with
* keys of the state of store
*/
if (Array.isArray(mapOrKeys)) {
mapOrKeys.forEach((key) => {
map[key] = function () {
return useStore()[key];
};
});
}
else {
/**
* if map of keys is the functions map
* or simple keys map
*/
Object.keys(mapOrKeys).forEach((key) => {
map[key] = function () {
const store = useStore();
if (typeof mapOrKeys[key] === 'function') {
return mapOrKeys[key].call(this, store);
}
return store[mapOrKeys[key]];
};
});
}
}
else {
const store = useStore();
/**
* if map of keys doesn't exist
* should return map of all state properties
* without any action functions from the store
*/
Object.keys(store).forEach((key) => {
if (typeof store[key] !== 'function') {
map[key] = function () {
return store[key];
};
}
});
}
return map;
}
function createComponent(useStore) {
return vueDemi.defineComponent({
name: (`use-${useStore.$id}-store`).toLowerCase(),
setup(_, { attrs, slots }) {
return () => vueDemi.h('div', {
class: 'v-nervue',
...attrs
}, {
default: () => slots.default?.({ ...useStore() })
});
}
});
}
exports.createComponent = createComponent;
exports.createNervue = createNervue;
exports.defineStore = defineStore;
exports.mapActions = mapActions;
exports.mapState = mapState;
exports.nervueSymbol = nervueSymbol;
exports.useNervue = useNervue;
exports.useStore = useStore;