UNPKG

nervue

Version:

Vue composition and options API-compatible lightweight store library

454 lines (445 loc) 13.7 kB
'use strict'; 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;