mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
1084 lines
30 KiB
JavaScript
1084 lines
30 KiB
JavaScript
/* eslint-disable no-console */
|
|
/*
|
|
* Copyright 2021 Richard Schloss (https://github.com/richardeschloss/nuxt-socket-io)
|
|
*/
|
|
|
|
import io from 'socket.io-client'
|
|
import Debug from 'debug'
|
|
import emitter from 'tiny-emitter/instance'
|
|
/*
|
|
TODO:
|
|
1) will enable when '@nuxtjs/composition-api' reaches stable version:
|
|
2) will bump from devDep to dep when stable
|
|
*/
|
|
// import { watch as vueWatch } from '@nuxtjs/composition-api'
|
|
|
|
const debug = Debug('nuxt-socket-io')
|
|
|
|
function PluginOptions() {
|
|
let _pluginOptions
|
|
if (process.env.TEST === undefined) {
|
|
_pluginOptions = {"sockets":[{"name":"dev","url":"http://localhost:3333"},{"name":"prod"}]}
|
|
}
|
|
|
|
return Object.freeze({
|
|
get: () => _pluginOptions,
|
|
set: (opts) => (_pluginOptions = opts)
|
|
})
|
|
}
|
|
|
|
const isRefImpl = (any) => any && any.constructor.name === 'RefImpl'
|
|
|
|
const _pOptions = PluginOptions()
|
|
|
|
const _sockets = {}
|
|
|
|
let warn, infoMsgs
|
|
|
|
function camelCase(str) {
|
|
return str
|
|
.replace(/[_\-\s](.)/g, function($1) {
|
|
return $1.toUpperCase()
|
|
})
|
|
.replace(/[-_\s]/g, '')
|
|
.replace(/^(.)/, function($1) {
|
|
return $1.toLowerCase()
|
|
})
|
|
.replace(/[^\w\s]/gi, '')
|
|
}
|
|
|
|
function propExists(obj, path) {
|
|
const exists = path.split('.').reduce((out, prop) => {
|
|
if (out !== undefined && out[prop] !== undefined) {
|
|
return out[prop]
|
|
}
|
|
}, obj)
|
|
|
|
return exists !== undefined
|
|
}
|
|
|
|
function parseEntry(entry, entryType) {
|
|
let evt, mapTo, pre, body, post, emitEvt, msgLabel
|
|
if (typeof entry === 'string') {
|
|
let subItems = []
|
|
const items = entry.trim().split(/\s*\]\s*/)
|
|
if (items.length > 1) {
|
|
pre = items[0]
|
|
subItems = items[1].split(/\s*\[\s*/)
|
|
} else {
|
|
subItems = items[0].split(/\s*\[\s*/)
|
|
}
|
|
;[body, post] = subItems
|
|
if (body.includes('-->')) {
|
|
;[evt, mapTo] = body.split(/\s*-->\s*/)
|
|
} else if (body.includes('<--')) {
|
|
;[evt, mapTo] = body.split(/\s*<--\s*/)
|
|
} else {
|
|
evt = body
|
|
}
|
|
|
|
if (entryType === 'emitter') {
|
|
;[emitEvt, msgLabel] = evt.split(/\s*\+\s*/)
|
|
} else if (mapTo === undefined) {
|
|
mapTo = evt
|
|
}
|
|
} else if (entryType === 'emitBack') {
|
|
;[[mapTo, evt]] = Object.entries(entry)
|
|
} else {
|
|
;[[evt, mapTo]] = Object.entries(entry)
|
|
}
|
|
return { pre, post, evt, mapTo, emitEvt, msgLabel }
|
|
}
|
|
|
|
function assignMsg(ctx, prop) {
|
|
let msg
|
|
if (prop !== undefined) {
|
|
if (ctx[prop] !== undefined) {
|
|
if (typeof ctx[prop] === 'object') {
|
|
msg = ctx[prop].constructor.name === 'Array' ? [] : {}
|
|
Object.assign(msg, ctx[prop])
|
|
} else {
|
|
msg = ctx[prop]
|
|
}
|
|
} else {
|
|
warn(`prop or data item "${prop}" not defined`)
|
|
}
|
|
debug(`assigned ${prop} to ${msg}`)
|
|
}
|
|
return msg
|
|
}
|
|
|
|
function assignResp(ctx, prop, resp) {
|
|
if (prop !== undefined) {
|
|
if (ctx[prop] !== undefined) {
|
|
if (typeof ctx[prop] !== 'function') {
|
|
// In vue3, it's possible to create
|
|
// reactive refs on the fly with ref()
|
|
// so check for that here.
|
|
// (this would elimnate the need for v2's
|
|
// this.$set because we just set the value prop
|
|
// to trigger the UI changes)
|
|
if (isRefImpl(ctx[prop])) {
|
|
ctx[prop].value = resp
|
|
} else {
|
|
ctx[prop] = resp
|
|
}
|
|
debug(`assigned ${resp} to ${prop}`)
|
|
}
|
|
} else {
|
|
warn(`${prop} not defined on instance`)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function runHook(ctx, prop, data) {
|
|
if (prop !== undefined) {
|
|
if (ctx[prop]) return await ctx[prop](data)
|
|
else warn(`method ${prop} not defined`)
|
|
}
|
|
}
|
|
|
|
function propByPath(obj, path) {
|
|
return path.split(/[/.]/).reduce((out, prop) => {
|
|
if (out !== undefined && out[prop] !== undefined) {
|
|
return out[prop]
|
|
}
|
|
}, obj)
|
|
}
|
|
|
|
function validateSockets(sockets) {
|
|
return (sockets
|
|
&& Array.isArray(sockets)
|
|
&& sockets.length > 0)
|
|
}
|
|
|
|
const register = {
|
|
clientApiEvents({ ctx, store, socket, api }) {
|
|
const { evts } = api
|
|
Object.entries(evts).forEach(([emitEvt, schema]) => {
|
|
const { data: dataT } = schema
|
|
const fn = emitEvt + 'Emit'
|
|
if (ctx[emitEvt] !== undefined) {
|
|
if (dataT !== undefined) {
|
|
Object.entries(dataT).forEach(([key, val]) => {
|
|
ctx.$set(ctx[emitEvt], key, val)
|
|
})
|
|
debug('Initialized data for', emitEvt, dataT)
|
|
}
|
|
}
|
|
|
|
if (ctx[fn] !== undefined) return
|
|
|
|
ctx[fn] = (fnArgs) => {
|
|
const { label: apiLabel, ack, ...args } = fnArgs || {}
|
|
return new Promise(async (resolve, reject) => {
|
|
const label = apiLabel || api.label
|
|
const msg = Object.keys(args).length > 0 ? args : { ...ctx[emitEvt] }
|
|
msg.method = fn
|
|
if (ack) {
|
|
const ackd = await store.dispatch('$nuxtSocket/emit', {
|
|
label,
|
|
socket,
|
|
evt: emitEvt,
|
|
msg
|
|
})
|
|
resolve(ackd)
|
|
} else {
|
|
store.dispatch('$nuxtSocket/emit', {
|
|
label,
|
|
socket,
|
|
evt: emitEvt,
|
|
msg,
|
|
noAck: true
|
|
})
|
|
resolve()
|
|
}
|
|
})
|
|
}
|
|
debug('Registered clientAPI method', fn)
|
|
})
|
|
},
|
|
clientApiMethods({ ctx, socket, api }) {
|
|
const { methods } = api
|
|
const evts = Object.assign({}, methods, { getAPI: {} })
|
|
Object.entries(evts).forEach(([evt, schema]) => {
|
|
if (socket.hasListeners(evt)) {
|
|
warn(`evt ${evt} already has a listener registered`)
|
|
}
|
|
|
|
socket.on(evt, async (msg, cb) => {
|
|
if (evt === 'getAPI') {
|
|
if (cb) cb(api)
|
|
} else if (ctx[evt] !== undefined) {
|
|
msg.method = evt
|
|
const resp = await ctx[evt](msg)
|
|
if (cb) cb(resp)
|
|
} else if (cb) {
|
|
cb({
|
|
emitErr: 'notImplemented',
|
|
msg: `Client has not yet implemented method (${evt})`
|
|
})
|
|
}
|
|
})
|
|
|
|
debug(`registered client api method ${evt}`)
|
|
if (evt !== 'getAPI' && ctx[evt] === undefined) {
|
|
warn(
|
|
`client api method ${evt} has not been defined. ` +
|
|
`Either update the client api or define the method so it can be used by callers`
|
|
)
|
|
}
|
|
})
|
|
},
|
|
clientAPI({ ctx, store, socket, clientAPI }) {
|
|
if (clientAPI.methods) {
|
|
register.clientApiMethods({ ctx, socket, api: clientAPI })
|
|
}
|
|
|
|
if (clientAPI.evts) {
|
|
register.clientApiEvents({ ctx, store, socket, api: clientAPI })
|
|
}
|
|
|
|
store.commit('$nuxtSocket/SET_CLIENT_API', clientAPI)
|
|
debug('clientAPI registered', clientAPI)
|
|
},
|
|
serverApiEvents({ ctx, socket, api, label, ioDataProp, apiIgnoreEvts }) {
|
|
const { evts } = api
|
|
Object.entries(evts).forEach(([evt, entry]) => {
|
|
const { methods = [], data: dataT } = entry
|
|
if (apiIgnoreEvts.includes(evt)) {
|
|
debug(
|
|
`Event ${evt} is in ignore list ("apiIgnoreEvts"), not registering.`
|
|
)
|
|
return
|
|
}
|
|
|
|
if (socket.hasListeners(evt)) {
|
|
warn(`evt ${evt} already has a listener registered`)
|
|
}
|
|
|
|
if (methods.length === 0) {
|
|
let initVal = dataT
|
|
if (typeof initVal === 'object') {
|
|
initVal = dataT.constructor.name === 'Array' ? [] : {}
|
|
}
|
|
ctx.$set(ctx[ioDataProp], evt, initVal)
|
|
} else {
|
|
methods.forEach((method) => {
|
|
if (ctx[ioDataProp][method] === undefined) {
|
|
ctx.$set(ctx[ioDataProp], method, {})
|
|
}
|
|
|
|
ctx.$set(
|
|
ctx[ioDataProp][method],
|
|
evt,
|
|
dataT.constructor.name === 'Array' ? [] : {}
|
|
)
|
|
})
|
|
}
|
|
|
|
socket.on(evt, (msg, cb) => {
|
|
debug(`serverAPI event ${evt} rxd with msg`, msg)
|
|
const { method, data } = msg
|
|
if (method !== undefined) {
|
|
if (ctx[ioDataProp][method] === undefined) {
|
|
ctx.$set(ctx[ioDataProp], method, {})
|
|
}
|
|
|
|
ctx.$set(ctx[ioDataProp][method], evt, data)
|
|
} else {
|
|
ctx.$set(ctx[ioDataProp], evt, data)
|
|
}
|
|
|
|
if (cb) {
|
|
cb({ ack: 'ok' })
|
|
}
|
|
})
|
|
debug(`Registered listener for ${evt} on ${label}`)
|
|
})
|
|
},
|
|
serverApiMethods({ ctx, socket, store, api, label, ioApiProp, ioDataProp }) {
|
|
Object.entries(api.methods).forEach(([fn, schema]) => {
|
|
const { msg: msgT, resp: respT } = schema
|
|
if (ctx[ioDataProp][fn] === undefined) {
|
|
ctx.$set(ctx[ioDataProp], fn, {})
|
|
if (msgT !== undefined) {
|
|
ctx.$set(ctx[ioDataProp][fn], 'msg', { ...msgT })
|
|
}
|
|
|
|
if (respT !== undefined) {
|
|
ctx.$set(
|
|
ctx[ioDataProp][fn],
|
|
'resp',
|
|
respT.constructor.name === 'Array' ? [] : {}
|
|
)
|
|
}
|
|
}
|
|
|
|
ctx[ioApiProp][fn] = (args) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
const emitEvt = fn
|
|
const msg = args !== undefined ? args : { ...ctx[ioDataProp][fn].msg }
|
|
debug(`${ioApiProp}:${label}: Emitting ${emitEvt} with ${msg}`)
|
|
const resp = await store.dispatch('$nuxtSocket/emit', {
|
|
label,
|
|
socket,
|
|
evt: emitEvt,
|
|
msg
|
|
})
|
|
|
|
ctx[ioDataProp][fn].resp = resp
|
|
resolve(resp)
|
|
})
|
|
}
|
|
})
|
|
},
|
|
async serverAPI({
|
|
ctx,
|
|
socket,
|
|
store,
|
|
label,
|
|
apiIgnoreEvts,
|
|
ioApiProp,
|
|
ioDataProp,
|
|
serverAPI,
|
|
clientAPI = {}
|
|
}) {
|
|
if (ctx[ioApiProp] === undefined) {
|
|
console.error(
|
|
`[nuxt-socket-io]: ${ioApiProp} needs to be defined in the current context for ` +
|
|
`serverAPI registration (vue requirement)`
|
|
)
|
|
return
|
|
}
|
|
|
|
const apiLabel = serverAPI.label || label
|
|
debug('register api for', apiLabel)
|
|
const api = store.state.$nuxtSocket.ioApis[apiLabel] || {}
|
|
const fetchedApi = await store.dispatch('$nuxtSocket/emit', {
|
|
label: apiLabel,
|
|
socket,
|
|
evt: serverAPI.evt || 'getAPI',
|
|
msg: serverAPI.data || {}
|
|
})
|
|
|
|
const isPeer =
|
|
clientAPI.label === fetchedApi.label &&
|
|
parseFloat(clientAPI.version) === parseFloat(fetchedApi.version)
|
|
if (isPeer) {
|
|
Object.assign(api, clientAPI)
|
|
store.commit('$nuxtSocket/SET_API', { label: apiLabel, api })
|
|
debug(`api for ${apiLabel} registered`, api)
|
|
} else if (parseFloat(api.version) !== parseFloat(fetchedApi.version)) {
|
|
Object.assign(api, fetchedApi)
|
|
store.commit('$nuxtSocket/SET_API', { label: apiLabel, api })
|
|
debug(`api for ${apiLabel} registered`, api)
|
|
}
|
|
|
|
ctx.$set(ctx, ioApiProp, api)
|
|
|
|
if (api.methods !== undefined) {
|
|
register.serverApiMethods({
|
|
ctx,
|
|
socket,
|
|
store,
|
|
api,
|
|
label,
|
|
ioApiProp,
|
|
ioDataProp
|
|
})
|
|
debug(
|
|
`Attached methods for ${label} to ${ioApiProp}`,
|
|
Object.keys(api.methods)
|
|
)
|
|
}
|
|
|
|
if (api.evts !== undefined) {
|
|
register.serverApiEvents({
|
|
ctx,
|
|
socket,
|
|
api,
|
|
label,
|
|
ioDataProp,
|
|
apiIgnoreEvts
|
|
})
|
|
debug(`registered evts for ${label} to ${ioApiProp}`)
|
|
}
|
|
|
|
ctx.$set(ctx[ioApiProp], 'ready', true)
|
|
debug('ioApi', ctx[ioApiProp])
|
|
},
|
|
emitErrors({ ctx, err, emitEvt, emitErrorsProp }) {
|
|
if (ctx[emitErrorsProp][emitEvt] === undefined) {
|
|
ctx[emitErrorsProp][emitEvt] = []
|
|
}
|
|
ctx[emitErrorsProp][emitEvt].push(err)
|
|
},
|
|
emitTimeout({ ctx, emitEvt, emitErrorsProp, emitTimeout, timerObj }) {
|
|
return new Promise((resolve, reject) => {
|
|
timerObj.timer = setTimeout(() => {
|
|
const err = {
|
|
message: 'emitTimeout',
|
|
emitEvt,
|
|
emitTimeout,
|
|
hint: [
|
|
`1) Is ${emitEvt} supported on the backend?`,
|
|
`2) Is emitTimeout ${emitTimeout} ms too small?`
|
|
].join('\r\n'),
|
|
timestamp: Date.now()
|
|
}
|
|
debug('emitEvt timed out', err)
|
|
if (typeof ctx[emitErrorsProp] === 'object') {
|
|
register.emitErrors({ ctx, err, emitEvt, emitErrorsProp })
|
|
resolve()
|
|
} else {
|
|
reject(err)
|
|
}
|
|
}, emitTimeout)
|
|
})
|
|
},
|
|
emitBacks({ ctx, socket, entries }) {
|
|
entries.forEach((entry) => {
|
|
const { pre, post, evt, mapTo } = parseEntry(entry, 'emitBack')
|
|
if (propExists(ctx, mapTo)) {
|
|
debug('registered local emitBack', { mapTo })
|
|
ctx.$watch(mapTo, async function(data, oldData) {
|
|
debug('local data changed', evt, data)
|
|
const preResult = await runHook(ctx, pre, { data, oldData })
|
|
if (preResult === false) {
|
|
return Promise.resolve()
|
|
}
|
|
debug('Emitting back:', { evt, mapTo, data })
|
|
return new Promise((resolve) => {
|
|
socket.emit(evt, { data }, (resp) => {
|
|
runHook(ctx, post, resp)
|
|
resolve(resp)
|
|
})
|
|
if (post === undefined) resolve()
|
|
})
|
|
})
|
|
} else {
|
|
warn(`Specified emitback ${mapTo} is not defined in component`)
|
|
}
|
|
})
|
|
},
|
|
emitBacksVuex({ ctx, store, useSocket, socket, entries }) {
|
|
entries.forEach((entry) => {
|
|
const { pre, post, evt, mapTo } = parseEntry(entry, 'emitBack')
|
|
|
|
if (useSocket.registeredWatchers.includes(mapTo)) {
|
|
return
|
|
}
|
|
|
|
store.watch(
|
|
(state) => {
|
|
const watchProp = propByPath(state, mapTo)
|
|
if (watchProp === undefined) {
|
|
throw new Error(
|
|
[
|
|
`[nuxt-socket-io]: Trying to register emitback ${mapTo} failed`,
|
|
`because it is not defined in Vuex.`,
|
|
'Is state set up correctly in your stores folder?'
|
|
].join('\n')
|
|
)
|
|
}
|
|
useSocket.registeredWatchers.push(mapTo)
|
|
debug('emitBack registered', { mapTo })
|
|
return watchProp
|
|
},
|
|
async (data, oldData) => {
|
|
debug('vuex emitBack data changed', { emitBack: evt, data, oldData })
|
|
const preResult = await runHook(ctx, pre, { data, oldData })
|
|
if (preResult === false) {
|
|
return Promise.resolve()
|
|
}
|
|
debug('Emitting back:', { evt, mapTo, data })
|
|
socket.emit(evt, { data }, (resp) => {
|
|
runHook(ctx, post, resp)
|
|
})
|
|
}
|
|
)
|
|
})
|
|
},
|
|
emitters({ ctx, socket, entries, emitTimeout, emitErrorsProp }) {
|
|
entries.forEach((entry) => {
|
|
const { pre, post, mapTo, emitEvt, msgLabel } = parseEntry(
|
|
entry,
|
|
'emitter'
|
|
)
|
|
ctx[emitEvt] = async function(args) {
|
|
const msg = args !== undefined ? args : assignMsg(ctx, msgLabel)
|
|
debug('Emit evt', { emitEvt, msg })
|
|
const preResult = await runHook(ctx, pre, msg)
|
|
if (preResult === false) {
|
|
return Promise.resolve()
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const timerObj = {}
|
|
socket.emit(emitEvt, msg, (resp) => {
|
|
debug('Emitter response rxd', { emitEvt, resp })
|
|
clearTimeout(timerObj.timer)
|
|
const { emitError, ...errorDetails } = resp || {}
|
|
if (emitError !== undefined) {
|
|
const err = {
|
|
message: emitError,
|
|
emitEvt,
|
|
errorDetails,
|
|
timestamp: Date.now()
|
|
}
|
|
debug('Emit error occurred', err)
|
|
if (typeof ctx[emitErrorsProp] === 'object') {
|
|
register.emitErrors({
|
|
ctx,
|
|
err,
|
|
emitEvt,
|
|
emitErrorsProp
|
|
})
|
|
resolve()
|
|
} else {
|
|
reject(err)
|
|
}
|
|
} else {
|
|
assignResp(ctx.$data || ctx, mapTo, resp)
|
|
runHook(ctx, post, resp)
|
|
resolve(resp)
|
|
}
|
|
})
|
|
if (emitTimeout) {
|
|
register
|
|
.emitTimeout({
|
|
ctx,
|
|
emitEvt,
|
|
emitErrorsProp,
|
|
emitTimeout,
|
|
timerObj
|
|
})
|
|
.then(resolve)
|
|
.catch(reject)
|
|
debug('Emit timeout registered for evt', { emitEvt, emitTimeout })
|
|
}
|
|
})
|
|
}
|
|
debug('Emitter created', { emitter: emitEvt })
|
|
})
|
|
},
|
|
listeners({ ctx, socket, entries }) {
|
|
entries.forEach((entry) => {
|
|
const { pre, post, evt, mapTo } = parseEntry(entry)
|
|
debug('Registered local listener', evt)
|
|
socket.on(evt, async (resp) => {
|
|
debug('Local listener received data', { evt, resp })
|
|
await runHook(ctx, pre)
|
|
assignResp(ctx.$data || ctx, mapTo, resp)
|
|
runHook(ctx, post, resp)
|
|
})
|
|
})
|
|
},
|
|
listenersVuex({ ctx, socket, entries, storeFn, useSocket }) {
|
|
entries.forEach((entry) => {
|
|
const { pre, post, evt, mapTo } = parseEntry(entry)
|
|
async function vuexListenerEvt(resp) {
|
|
debug('Vuex listener received data', { evt, resp })
|
|
await runHook(ctx, pre)
|
|
storeFn(mapTo, resp)
|
|
runHook(ctx, post, resp)
|
|
}
|
|
|
|
if (useSocket.registeredVuexListeners.includes(evt)) return
|
|
|
|
socket.on(evt, vuexListenerEvt)
|
|
debug('Registered vuex listener', evt)
|
|
useSocket.registeredVuexListeners.push(evt)
|
|
})
|
|
},
|
|
namespace({ ctx, namespaceCfg, socket, emitTimeout, emitErrorsProp }) {
|
|
const { emitters = [], listeners = [], emitBacks = [] } = namespaceCfg
|
|
const sets = { emitters, listeners, emitBacks }
|
|
Object.entries(sets).forEach(([setName, entries]) => {
|
|
if (entries.constructor.name === 'Array') {
|
|
register[setName]({ ctx, socket, entries, emitTimeout, emitErrorsProp })
|
|
} else {
|
|
warn(
|
|
`[nuxt-socket-io]: ${setName} needs to be an array in namespace config`
|
|
)
|
|
}
|
|
})
|
|
},
|
|
vuexModule({ store }) {
|
|
store.registerModule(
|
|
'$nuxtSocket',
|
|
{
|
|
namespaced: true,
|
|
state: {
|
|
clientApis: {},
|
|
ioApis: {},
|
|
emitErrors: {},
|
|
emitTimeouts: {}
|
|
},
|
|
mutations: {
|
|
SET_API(state, { label, api }) {
|
|
state.ioApis[label] = api
|
|
},
|
|
|
|
SET_CLIENT_API(state, { label = 'clientAPI', ...api }) {
|
|
state.clientApis[label] = api
|
|
},
|
|
|
|
SET_EMIT_ERRORS(state, { label, emitEvt, err }) {
|
|
if (state.emitErrors[label] === undefined) {
|
|
state.emitErrors[label] = {}
|
|
}
|
|
|
|
if (state.emitErrors[label][emitEvt] === undefined) {
|
|
state.emitErrors[label][emitEvt] = []
|
|
}
|
|
|
|
state.emitErrors[label][emitEvt].push(err)
|
|
},
|
|
|
|
SET_EMIT_TIMEOUT(state, { label, emitTimeout }) {
|
|
state.emitTimeouts[label] = emitTimeout
|
|
}
|
|
},
|
|
actions: {
|
|
emit(
|
|
{ state, commit },
|
|
{ label, socket, evt, msg, emitTimeout, noAck }
|
|
) {
|
|
debug('$nuxtSocket vuex action "emit" dispatched', label, evt)
|
|
return new Promise((resolve, reject) => {
|
|
const _socket = socket || _sockets[label]
|
|
const _emitTimeout =
|
|
emitTimeout !== undefined
|
|
? emitTimeout
|
|
: state.emitTimeouts[label]
|
|
|
|
if (_socket === undefined) {
|
|
reject(
|
|
new Error(
|
|
'socket instance required. Please provide a valid socket label or socket instance'
|
|
)
|
|
)
|
|
}
|
|
debug(`Emitting ${evt} with msg`, msg)
|
|
let timer
|
|
_socket.emit(evt, msg, (resp) => {
|
|
debug('Emitter response rxd', { evt, resp })
|
|
clearTimeout(timer)
|
|
const { emitError, ...errorDetails } = resp || {}
|
|
if (emitError !== undefined) {
|
|
const err = {
|
|
message: emitError,
|
|
emitEvt: evt,
|
|
errorDetails,
|
|
timestamp: Date.now()
|
|
}
|
|
debug('Emit error occurred', err)
|
|
if (label !== undefined && label !== '') {
|
|
debug(
|
|
`[nuxt-socket-io]: ${label} Emit error ${err.message} occurred and logged to vuex `,
|
|
err
|
|
)
|
|
commit('SET_EMIT_ERRORS', { label, emitEvt: evt, err })
|
|
resolve()
|
|
} else {
|
|
reject(new Error(JSON.stringify(err, null, '\t')))
|
|
}
|
|
} else {
|
|
resolve(resp)
|
|
}
|
|
})
|
|
|
|
if (noAck) {
|
|
resolve()
|
|
}
|
|
|
|
if (_emitTimeout) {
|
|
debug(`registering emitTimeout ${_emitTimeout} ms for ${evt}`)
|
|
timer = setTimeout(() => {
|
|
const err = {
|
|
message: 'emitTimeout',
|
|
emitEvt: evt,
|
|
emitTimeout,
|
|
hint: [
|
|
`1) Is ${evt} supported on the backend?`,
|
|
`2) Is emitTimeout ${_emitTimeout} ms too small?`
|
|
].join('\r\n'),
|
|
timestamp: Date.now()
|
|
}
|
|
if (label !== undefined && label !== '') {
|
|
commit('SET_EMIT_ERRORS', { label, emitEvt: evt, err })
|
|
debug(
|
|
`[nuxt-socket-io]: ${label} Emit error occurred and logged to vuex `,
|
|
err
|
|
)
|
|
resolve()
|
|
} else {
|
|
reject(new Error(JSON.stringify(err, null, '\t')))
|
|
}
|
|
}, _emitTimeout)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
},
|
|
{ preserveState: false }
|
|
)
|
|
},
|
|
vuexOpts({ ctx, vuexOpts, useSocket, socket, store }) {
|
|
const { mutations = [], actions = [], emitBacks = [] } = vuexOpts
|
|
const sets = { mutations, actions, emitBacks }
|
|
const storeFns = {
|
|
mutations: 'commit',
|
|
actions: 'dispatch'
|
|
}
|
|
Object.entries(sets).forEach(([setName, entries]) => {
|
|
if (entries.constructor.name === 'Array') {
|
|
const fnName = storeFns[setName]
|
|
if (fnName) {
|
|
register.listenersVuex({
|
|
ctx,
|
|
socket,
|
|
entries,
|
|
storeFn: store[fnName],
|
|
useSocket
|
|
})
|
|
} else {
|
|
register.emitBacksVuex({ ctx, store, useSocket, socket, entries })
|
|
}
|
|
} else {
|
|
warn(`[nuxt-socket-io]: vuexOption ${setName} needs to be an array`)
|
|
}
|
|
})
|
|
},
|
|
socketStatus({ ctx, socket, connectUrl, statusProp }) {
|
|
const socketStatus = { connectUrl }
|
|
// See also:
|
|
// https://socket.io/docs/v3/migrating-from-2-x-to-3-0/index.html#The-Socket-instance-will-no-longer-forward-the-events-emitted-by-its-Manager
|
|
const clientEvts = [
|
|
'connect_error',
|
|
'connect_timeout',
|
|
'reconnect',
|
|
'reconnect_attempt',
|
|
'reconnecting', // socket.io-client v2.x only
|
|
'reconnect_error',
|
|
'reconnect_failed',
|
|
'ping',
|
|
'pong'
|
|
]
|
|
clientEvts.forEach((evt) => {
|
|
const prop = camelCase(evt)
|
|
socketStatus[prop] = ''
|
|
function handleEvt(resp) {
|
|
Object.assign(ctx[statusProp], { [prop]: resp })
|
|
}
|
|
socket.on(evt, handleEvt)
|
|
socket.io.on(evt, handleEvt)
|
|
})
|
|
Object.assign(ctx, { [statusProp]: socketStatus })
|
|
},
|
|
teardown({ ctx, socket, useSocket }) {
|
|
// Setup listener for "closeSockets" in case
|
|
// multiple instances of nuxtSocket exist in the same
|
|
// component (only one destroy/unmount event takes place).
|
|
// When we teardown, we want to remove the listeners of all
|
|
// the socket.io-client instances
|
|
ctx.$once('closeSockets', function() {
|
|
debug('closing socket id=' + socket.id)
|
|
socket.removeAllListeners()
|
|
socket.close()
|
|
})
|
|
|
|
if (!ctx.registeredTeardown) {
|
|
// ctx.$destroy is defined in vue2
|
|
// but will go away in vue3 (in favor of onUnmounted)
|
|
// save user's destroy method and honor it after
|
|
// we run nuxt-socket-io's teardown
|
|
ctx.onComponentDestroy = ctx.$destroy
|
|
debug('teardown enabled for socket', { name: useSocket.name })
|
|
// Our $destroy method
|
|
// Gets called automatically on the destroy lifecycle
|
|
// in v2. In v3, we have call it with the
|
|
// onUnmounted hook
|
|
ctx.$destroy = function() {
|
|
debug('component destroyed, closing socket(s)', {
|
|
name: useSocket.name,
|
|
url: useSocket.url
|
|
})
|
|
useSocket.registeredVuexListeners = []
|
|
ctx.$emit('closeSockets')
|
|
// Only run the user's destroy method
|
|
// if it exists
|
|
if (ctx.onComponentDestroy) {
|
|
ctx.onComponentDestroy()
|
|
}
|
|
}
|
|
|
|
// onUnmounted will only exist in v3
|
|
if (ctx.onUnmounted) {
|
|
ctx.onUnmounted(ctx.$destroy)
|
|
}
|
|
ctx.registeredTeardown = true
|
|
}
|
|
|
|
socket.on('disconnect', () => {
|
|
debug('server disconnected', { name: useSocket.name, url: useSocket.url })
|
|
socket.close()
|
|
})
|
|
},
|
|
stubs(ctx) {
|
|
// Use a tiny event bus now. Can probably
|
|
// be replaced by watch eventually. For now this works.
|
|
if (!ctx.$on || !ctx.$emit || !ctx.$once) {
|
|
ctx.$once = (...args) => emitter.once(...args)
|
|
ctx.$on = (...args) => emitter.on(...args)
|
|
ctx.$off = (...args) => emitter.off(...args)
|
|
ctx.$emit = (...args) => emitter.emit(...args)
|
|
}
|
|
|
|
if (!ctx.$set) {
|
|
ctx.$set = (obj, key, val) => {
|
|
if (isRefImpl(obj[key])) {
|
|
obj[key].value = val
|
|
} else {
|
|
obj[key] = val
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!ctx.$watch) {
|
|
ctx.$watch = (label, cb) => {
|
|
// will enable when '@nuxtjs/composition-api' reaches stable version:
|
|
// vueWatch(ctx.$data[label], cb)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function nuxtSocket(ioOpts) {
|
|
const {
|
|
name,
|
|
channel = '',
|
|
statusProp = 'socketStatus',
|
|
persist,
|
|
teardown = !persist,
|
|
emitTimeout,
|
|
emitErrorsProp = 'emitErrors',
|
|
ioApiProp = 'ioApi',
|
|
ioDataProp = 'ioData',
|
|
apiIgnoreEvts = [],
|
|
serverAPI,
|
|
clientAPI,
|
|
vuex,
|
|
namespaceCfg,
|
|
...connectOpts
|
|
} = ioOpts
|
|
const pluginOptions = _pOptions.get()
|
|
const { $config, $store } = this
|
|
const store = this.$store || this.store
|
|
|
|
const runtimeOptions = { ...pluginOptions }
|
|
if ($config && $config.io) {
|
|
Object.assign(runtimeOptions, $config.io)
|
|
runtimeOptions.sockets = validateSockets(pluginOptions.sockets)
|
|
? pluginOptions.sockets
|
|
: []
|
|
if (validateSockets($config.io.sockets)) {
|
|
$config.io.sockets.forEach((socket) => {
|
|
const fnd = runtimeOptions.sockets.find(({ name }) => name === socket.name)
|
|
if (fnd === undefined) {
|
|
runtimeOptions.sockets.push(socket)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const mergedOpts = Object.assign({}, runtimeOptions, ioOpts)
|
|
const { sockets, warnings = true, info = true } = mergedOpts
|
|
|
|
warn =
|
|
warnings && process.env.NODE_ENV !== 'production' ? console.warn : () => {}
|
|
|
|
infoMsgs =
|
|
info && process.env.NODE_ENV !== 'production' ? console.info : () => {}
|
|
|
|
if (!validateSockets(sockets)) {
|
|
throw new Error(
|
|
"Please configure sockets if planning to use nuxt-socket-io: \r\n [{name: '', url: ''}]"
|
|
)
|
|
}
|
|
|
|
register.stubs(this)
|
|
|
|
let useSocket = null
|
|
|
|
if (!name) {
|
|
useSocket = sockets.find((s) => s.default === true)
|
|
} else {
|
|
useSocket = sockets.find((s) => s.name === name)
|
|
}
|
|
|
|
if (!useSocket) {
|
|
useSocket = sockets[0]
|
|
}
|
|
|
|
if (!useSocket.name) {
|
|
useSocket.name = 'dflt'
|
|
}
|
|
|
|
if (!useSocket.url) {
|
|
warn(
|
|
`URL not defined for socket "${useSocket.name}". Defaulting to "window.location"`
|
|
)
|
|
}
|
|
|
|
if (!useSocket.registeredWatchers) {
|
|
useSocket.registeredWatchers = []
|
|
}
|
|
|
|
if (!useSocket.registeredVuexListeners) {
|
|
useSocket.registeredVuexListeners = []
|
|
}
|
|
|
|
let { url: connectUrl } = useSocket
|
|
if (connectUrl) {
|
|
connectUrl += channel
|
|
}
|
|
|
|
const vuexOpts = vuex || useSocket.vuex
|
|
const { namespaces = {} } = useSocket
|
|
|
|
let socket
|
|
const label =
|
|
persist && typeof persist === 'string'
|
|
? persist
|
|
: `${useSocket.name}${channel}`
|
|
|
|
if (!store.state.$nuxtSocket) {
|
|
debug('vuex store $nuxtSocket does not exist....registering it')
|
|
register.vuexModule({ store })
|
|
}
|
|
|
|
if (emitTimeout) {
|
|
store.commit('$nuxtSocket/SET_EMIT_TIMEOUT', { label, emitTimeout })
|
|
}
|
|
|
|
function connectSocket() {
|
|
if (connectUrl) {
|
|
socket = io(connectUrl, connectOpts)
|
|
infoMsgs('[nuxt-socket-io]: connect', useSocket.name, connectUrl, connectOpts)
|
|
} else {
|
|
socket = io(channel, connectOpts)
|
|
infoMsgs(
|
|
'[nuxt-socket-io]: connect',
|
|
useSocket.name,
|
|
window.location,
|
|
channel,
|
|
connectOpts
|
|
)
|
|
}
|
|
}
|
|
|
|
if (persist) {
|
|
if (_sockets[label]) {
|
|
debug(`resuing persisted socket ${label}`)
|
|
socket = _sockets[label]
|
|
if (socket.disconnected) {
|
|
debug('persisted socket disconnected, reconnecting...')
|
|
connectSocket()
|
|
}
|
|
} else {
|
|
debug(`socket ${label} does not exist, creating and connecting to it..`)
|
|
connectSocket()
|
|
_sockets[label] = socket
|
|
}
|
|
} else {
|
|
connectSocket()
|
|
}
|
|
|
|
const _namespaceCfg = namespaceCfg || namespaces[channel]
|
|
if (_namespaceCfg) {
|
|
register.namespace({
|
|
ctx: this,
|
|
namespace: channel,
|
|
namespaceCfg: _namespaceCfg,
|
|
socket,
|
|
useSocket,
|
|
emitTimeout,
|
|
emitErrorsProp
|
|
})
|
|
debug('namespaces configured for socket', {
|
|
name: useSocket.name,
|
|
channel,
|
|
namespaceCfg
|
|
})
|
|
}
|
|
|
|
if (serverAPI) {
|
|
register.serverAPI({
|
|
store,
|
|
label,
|
|
apiIgnoreEvts,
|
|
ioApiProp,
|
|
ioDataProp,
|
|
ctx: this,
|
|
socket,
|
|
emitTimeout,
|
|
emitErrorsProp,
|
|
serverAPI,
|
|
clientAPI
|
|
})
|
|
}
|
|
|
|
if (clientAPI) {
|
|
register.clientAPI({
|
|
ctx: this,
|
|
store,
|
|
socket,
|
|
clientAPI
|
|
})
|
|
}
|
|
|
|
if (vuexOpts) {
|
|
register.vuexOpts({
|
|
ctx: this,
|
|
vuexOpts,
|
|
useSocket,
|
|
socket,
|
|
store
|
|
})
|
|
debug('vuexOpts configured for socket', { name: useSocket.name, vuexOpts })
|
|
}
|
|
|
|
if (
|
|
this.socketStatus !== undefined &&
|
|
typeof this.socketStatus === 'object'
|
|
) {
|
|
register.socketStatus({ ctx: this, socket, connectUrl, statusProp })
|
|
debug('socketStatus registered for socket', {
|
|
name: useSocket.name,
|
|
url: connectUrl
|
|
})
|
|
}
|
|
|
|
if (teardown) {
|
|
register.teardown({
|
|
ctx: this,
|
|
socket,
|
|
useSocket
|
|
})
|
|
}
|
|
_pOptions.set({ sockets })
|
|
return socket
|
|
}
|
|
|
|
export default function(context, inject) {
|
|
inject('nuxtSocket', nuxtSocket)
|
|
}
|
|
|
|
export let pOptions
|
|
if (process.env.TEST) {
|
|
pOptions = {}
|
|
Object.assign(pOptions, _pOptions)
|
|
}
|