cheetah-framework
Version:
Cheetah Framework JS used in all our applications
508 lines (419 loc) • 14.3 kB
JavaScript
/* global Echo echoActive */
import StoreFactory from '@cheetah/store/StoreFactory'
const modules = {}
const resourceToModuleMap = {}
const registeredModules = {}
const persistedModules = {}
class CheetahStore {
constructor (module) {
this.options = getStoreOptions(module)
this.name = this.options.moduleName // Standard vuex module
this.subscribeUnregister = null
this.subscribeActionsUnregister = null
this.channel = null
if (this.isCheetahStore()) {
this.name = this.options.model.resourceName
this.module = this.options.preventFetch ? null : StoreFactory.make(this.options)
resourceToModuleMap[this.name] = this
} else {
this.module = module.default
}
if (!this.name) {
console.error('Missing mandatory moduleName for following module')
console.log(module)
throw new Error('Missing mandatory moduleName')
}
modules[this.name] = this
}
isStandardStore () {
return !this.isCheetahStore()
}
isCheetahStore () {
return !!this.options.model
}
/**
* Return true if a vuex module of that name exists.
* Include CheetahStore and custom vuex store.
* @param {string} moduleName
* @returns {boolean}
*/
static moduleExists (moduleName) {
return typeof modules[moduleName] !== 'undefined'
}
/**
* Return true if a CheetahStore with a laravel resource of that name exists.
* @param {string} resourceName
* @returns {boolean}
*/
static exists (resourceName) {
return typeof resourceToModuleMap[resourceName] !== 'undefined'
}
/**
* Return true if the vuex module is registered.
* $store.registerModule(resourceName, module) has been called
* @param resourceName
* @returns {boolean}
*/
static vuexModuleLoaded (resourceName) {
return typeof cheetahApp.$store._modules.root._children[resourceName] !== 'undefined'
}
/**
* Return true if a vuex store (CheetahStore only) listens a
* certain eventName of a certain channel. The channel has
* the name of the resourceName.
* @param resourceName
* @param eventName [created, updated, deleted, bulkCreated, bulkUpdated, bulkDeleted]
* @returns {boolean}
*/
static isListening (resourceName, eventName) {
if (!echoActive) {
return false
}
return modules?.[resourceName]?.channel?.subscription?.subscribed === true && this.canListen(resourceName, eventName)
}
/**
* Reload all items of a vuex store (CheetahStore only), but only if
* it's fetch already.
* @param {string} resourceName
*/
static reload (resourceName) {
if (CheetahStore.isFetched(resourceName)) {
CheetahStore.dispatch(
`${resourceName}/fetch`,
{ force: true }
)
}
}
/**
* Call this function after you delete an item
* @param {string} resourceName
* @param {object} payload - payload must contain id
*/
static delete (resourceName, payload) {
// if already waiting a pusher event of that type
if (CheetahStore.isListening(resourceName, 'deleted')) {
return
}
if (CheetahStore.vuexModuleLoaded(resourceName)) {
CheetahStore.commit(`${resourceName}/DELETE`, payload)
}
Bus.$emit(`deleted-${resourceName}`, payload)
}
/**
* Call this function after you bulk delete items
* @param resourceName
* @param ids - array of deleted ids [1,2,3,...]
*/
static bulkDelete (resourceName, ids) {
// if already waiting a pusher event of that type
if (CheetahStore.isListening(resourceName, 'bulkDeleted')) {
return
}
// if vuex store of that resourceName is loaded
if (CheetahStore.vuexModuleLoaded(resourceName)) {
CheetahStore.reload(resourceName)
}
Bus.$emit(`bulkDeleted-${resourceName}`, ids)
}
/**
* Call this function after you update an item
* @param {string} resourceName
* @param {object} payload - payload must contain id
*/
static update (resourceName, payload) {
// if already waiting a pusher event of that type
if (CheetahStore.isListening(resourceName, 'updated')) {
return
}
// if vuex store of that resourceName is loaded
if (CheetahStore.vuexModuleLoaded(resourceName)) {
CheetahStore.commit(`${resourceName}/UPDATE`, payload)
}
Bus.$emit(`updated-${resourceName}`, payload)
}
/**
* Call this function after you bulk update items
* @param resourceName
* @param payload - array of updated instances [{ id: 1, last_update: '...' }, ...]
*/
static bulkUpdate (resourceName, payload) {
// if already waiting a pusher event of that type
if (CheetahStore.isListening(resourceName, 'bulkUpdated')) {
return
}
// if vuex store of that resourceName is loaded
if (CheetahStore.vuexModuleLoaded(resourceName)) {
CheetahStore.reload(resourceName)
}
Bus.$emit(`bulkUpdated-${resourceName}`, payload)
}
/**
* Call this function after you store an item
* @param {string} resourceName
* @param {object} payload - payload must contain id
*/
static store (resourceName, payload) {
// if already waiting a pusher event of that type
if (CheetahStore.isListening(resourceName, 'created')) {
return
}
// if vuex store of that resourceName is loaded
if (CheetahStore.vuexModuleLoaded(resourceName)) {
CheetahStore.commit(`${resourceName}/STORE`, payload)
}
Bus.$emit(`created-${resourceName}`, payload)
}
static isFetched (resourceName) {
return _.get(cheetahApp.$store._modules.root._children, resourceName + '.state.loaded') === true
}
/**
* This method is automatically called to register vuex module and,
* in case it is a CheetahStore, bind Echo events if required (liveUpdate = true).
* @param moduleName
* @param {VueComponent} vueComponent - Vue instance requesting this module
* @param {boolean} persisted
* @returns {boolean} return true if module is successfully loaded
*/
static load (moduleName, vueComponent, persisted = false) {
if (!CheetahStore.moduleExists(moduleName)) {
console.error('Unknown vuex module ' + moduleName)
return false
}
if (!CheetahStore.vuexModuleLoaded(moduleName)) {
modules[moduleName].register()
}
if (modules[moduleName].options.persisted || persisted) {
persistedModules[moduleName] = true
}
if (!persistedModules[moduleName]) {
// increments registered modules
if (typeof registeredModules[moduleName] === 'undefined') {
registeredModules[moduleName] = 0
}
registeredModules[moduleName]++
vueComponent.$once('hook:beforeDestroy', _ => {
if (--registeredModules[moduleName] === 0) {
setTimeout(
() => {
if (persistedModules[moduleName]) {
return
}
if (registeredModules[moduleName] === 0) {
modules[moduleName].unregister()
}
},
modules[moduleName].options.unregisterDelay
)
}
})
}
(_.get(modules[moduleName], 'options.dependencies') || []).forEach(dependency => {
CheetahStore.load(dependency, vueComponent, modules[moduleName].options.persisted)
})
return true
}
register () {
if (!this.options.preventFetch || this.isStandardStore()) {
cheetahApp.$store.registerModule(this.name, this.module)
}
if (this.options.subscribe) {
this.subscribeUnregister = cheetahApp.$store.subscribe(this.options.subscribe)
}
if (this.options.subscribeAction) {
this.subscribeActionsUnregister = cheetahApp.$store.subscribeAction(this.options.subscribeAction)
}
if (this.isCheetahStore() && echoActive && this.options.liveUpdate) {
this.bindEvents()
}
this.options.register.call(this)
}
unregister () {
this.options.unregister.call(this)
if (this.isCheetahStore() && echoActive && this.options.liveUpdate) {
this.unbindEvents()
}
if (_.isFunction(this.subscribeUnregister)) {
this.subscribeUnregister()
this.subscribeUnregister = null
}
if (_.isFunction(this.subscribeActionsUnregister)) {
this.subscribeActionsUnregister()
this.subscribeActionsUnregister = null
}
if (this.isCheetahStore()) {
CheetahStore.commit(`${this.options.model.resourceName}/FLUSH`)
}
if (!this.options.preventFetch || this.isStandardStore()) {
cheetahApp.$store.unregisterModule(this.name)
}
}
canListen (eventName) {
if (this.options.liveUpdate === true) {
return true
}
if (_.isArray(this.options.liveUpdate)) {
return _.includes(this.options.liveUpdate, eventName)
}
return false
}
static canListen (resourceName, eventName) {
return !!resourceToModuleMap?.[resourceName]?.canListen(eventName)
}
static commit (mutatorFullPath, payload) {
if (typeof cheetahApp.$store._mutations[mutatorFullPath] === 'undefined') {
return
}
cheetahApp.$store.commit(mutatorFullPath, payload)
}
static dispatch (actionFullPath, payload) {
if (typeof cheetahApp.$store._actions[actionFullPath] === 'undefined') {
return Promise.reject(new CheetahStoreError(`Store action "${actionFullPath}" does not exists.`))
}
return cheetahApp.$store.dispatch(actionFullPath, payload)
}
bindEvents () {
if (this.isStandardStore()) {
return
}
if (this.channel === null) {
this.channel = Echo.private('cheetah.admin.' + this.options.model.resourceName)
if (this.canListen('created')) {
this.channel.listen('.created', createListener.bind(this))
}
if (this.canListen('bulkCreated')) {
this.channel.listen('.bulkCreated', bulkCreateListener.bind(this))
}
if (this.canListen('updated')) {
this.channel.listen('.updated', updateListener.bind(this))
}
if (this.canListen('bulkUpdated')) {
this.channel.listen('.bulkUpdated', bulkUpdateListener.bind(this))
}
if (this.canListen('deleted')) {
this.channel.listen('.deleted', deleteListener.bind(this))
}
if (this.canListen('bulkDeleted')) {
this.channel.listen('.bulkDeleted', bulkDeleteListener.bind(this))
}
if (this.options.broadcast) {
this.options.broadcast.call(this, this.channel)
}
} else {
this.channel.subscribe()
}
}
unbindEvents () {
if (this.isStandardStore()) {
return
}
this.channel.subscription.unsubscribe()
}
}
function getStoreOptions (module) {
return _.defaults(module.default ? _.get(module, 'default.options', {}) : module.options, {
model: null,
moduleName: null,
liveUpdate: true,
broadcast: null,
subscribe: null, // vuex plugin on mutations
subscribeAction: null, // vuex plugin on actions
unregisterDelay: 500,
persisted: false,
register: () => {},
unregister: () => {},
dependencies: [],
state: {},
getters: {},
actions: {},
mutations: {},
preventFetch: false
})
}
function createListener (payload) {
const model = this.options.model
const resourceName = model.resourceName
// Force store to fetch if we only received the id
if (isIdOnlyPayload(payload, model)) {
CheetahStore.dispatch(`${resourceName}/reloadItem`, payload[model.idKey]).then(response => {
Bus.$emit(`created-${resourceName}`, response.data)
}).catch(error => {
if (!(error instanceof CheetahStoreError)) {
throw error
}
// if module preventFetch is true
model.get(payload[model.idKey], null, 'list').then(response => {
Bus.$emit(`created-${resourceName}`, response.data)
})
})
} else {
CheetahStore.commit(`${resourceName}/STORE`, payload)
Bus.$emit(`created-${resourceName}`, payload)
}
}
function bulkCreateListener (payload) {
const resourceName = this.options.model.resourceName
// if vuex store of that resourceName is loaded
if (CheetahStore.vuexModuleLoaded(resourceName)) {
CheetahStore.reload(resourceName)
}
Bus.$emit(`bulkCreated-${resourceName}`, payload)
}
function updateListener (payload) {
const model = this.options.model
const resourceName = model.resourceName
// Force store to fetch if we only received the id
if (isIdOnlyPayload(payload, model)) {
CheetahStore.dispatch(`${resourceName}/reloadItem`, payload[model.idKey]).then(response => {
Bus.$emit(`updated-${resourceName}`, response.data)
}).catch(error => {
if (!(error instanceof CheetahStoreError)) {
throw error
}
// if module preventFetch is true
model.get(payload[model.idKey], null, 'list').then(response => {
Bus.$emit(`updated-${resourceName}`, response.data)
})
})
} else {
CheetahStore.commit(`${resourceName}/UPDATE`, payload)
Bus.$emit(`updated-${resourceName}`, payload)
}
}
function bulkUpdateListener (payload) {
const resourceName = this.options.model.resourceName
// if vuex store of that resourceName is loaded
if (CheetahStore.vuexModuleLoaded(resourceName)) {
CheetahStore.reload(resourceName)
}
Bus.$emit(`bulkUpdated-${resourceName}`, payload)
}
function deleteListener (payload) {
const resourceName = this.options.model.resourceName
if (CheetahStore.vuexModuleLoaded(resourceName)) {
CheetahStore.commit(`${resourceName}/DELETE`, payload)
}
Bus.$emit(`deleted-${resourceName}`, payload)
}
function bulkDeleteListener (payload) {
const resourceName = this.options.model.resourceName
// if vuex store of that resourceName is loaded
if (CheetahStore.vuexModuleLoaded(resourceName)) {
CheetahStore.reload(resourceName)
}
Bus.$emit(`bulkDeleted-${resourceName}`, payload)
}
class CheetahStoreError extends Error {}
/**
* Return true if the backend only sent the id of the instance.
*
* When the payload size of instance is to big to
* go through Pusher we just received the id.
*/
function isIdOnlyPayload (payload, model) {
return _.difference(
_.keys(payload),
[model.idKey, 'updated_at']
).length === 0
}
export default CheetahStore
export { isIdOnlyPayload, CheetahStoreError }