UNPKG

nuxt-socket-io

Version:

Socket.io client and server module for Nuxt. Just plug it in and GO

251 lines (234 loc) 7.92 kB
/* eslint-disable node/no-callback-literal */ /* * Copyright 2022 Richard Schloss (https://github.com/richardeschloss/nuxt-socket-io) */ import http from 'http' import { existsSync } from 'fs' import { resolve as pResolve, parse as pParse, dirname } from 'path' import { fileURLToPath } from 'url' import { promisify } from 'util' import consola from 'consola' import { defineNuxtModule, addPlugin } from '@nuxt/kit' import Debug from 'debug' import { Server as SocketIO } from 'socket.io' import Glob from 'glob' // @ts-ignore const __dirname = dirname(fileURLToPath(import.meta.url)) const debug = Debug('nuxt-socket-io') const glob = promisify(Glob) const register = { /** * @param {import('socket.io').Server | import('socket.io').Namespace} io * @param {{ [s: string]: any; } | ArrayLike<any>} middlewares */ middlewares (io, middlewares) { Object.values(middlewares).forEach(m => io.use(m)) }, /** * @param {import('socket.io').Server} io * @param {string} ioSvc */ async ioSvc (io, ioSvc) { const { default: Svc, middlewares = {}, setIO = () => {} } = await import( (process.platform === 'win32' ? 'file://' : '') + ioSvc ) register.middlewares(io, middlewares) setIO(io) if (Svc && typeof Svc === 'function') { io.on('connection', (socket) => { const svc = Svc(socket, io) register.socket(svc, socket, '/') }) } else { throw new Error( `io service at ${ioSvc} does not export a default "Svc()" function. Not registering` ) } }, /** * @param {import('socket.io').Server} io * @param {string} nspDir */ async nspSvc (io, nspDir) { const nspFiles = await glob(`${nspDir}/**/*.{js,ts,mjs}`) const nspDirResolved = pResolve(nspDir).replace(/\\/g, '/') const namespaces = nspFiles.map( f => f.split(nspDirResolved)[1].split(/\.(js|ts|mjs)$/)[0] ) namespaces.forEach(async (namespace, idx) => { const imports = await import((process.platform === 'win32' ? 'file://' : '') + nspFiles[idx]) const { default: Svc, middlewares = {}, setIO = () => {} } = imports register.middlewares(io.of(namespace), middlewares) setIO(io) if (Svc && typeof Svc === 'function') { io.of(`${namespace}`).on('connection', (socket) => { const svc = Svc(socket, io) register.socket(svc, socket, namespace) }) } else { debug( `io service at ${nspDirResolved}${namespace} does not export a default "Svc()" function. Not registering` ) } }) }, async redis (io, redisClient) { let useClient = redisClient const dfltClient = { url: 'redis://localhost:6379' } if (redisClient === true) { useClient = dfltClient } debug('starting redis adapter', useClient) const { createAdapter } = await import('@socket.io/redis-adapter') const { createClient } = await import('redis') const pubClient = createClient(useClient) const subClient = pubClient.duplicate() await Promise.all([pubClient.connect(), subClient.connect()]) io.adapter(createAdapter(pubClient, subClient)) }, listener (server = http.createServer(), port = 3000, host = 'localhost') { return new Promise((resolve, reject) => { server .listen(port, host) .on('error', reject) .on('listening', () => { consola.info(`socket.io server listening on ${host}:${port}`) resolve(server) }) }) }, server (options = {}, server = http.createServer()) { const { ioSvc = './server/io', nspDir = ioSvc, host = 'localhost', port = 3000, redisClient, ...ioServerOpts // Options that get passed down to SocketIO instance. } = options const { ext: ioSvcExt } = pParse(ioSvc) const { ext: nspDirExt } = pParse(ioSvc) const extList = ['.js', '.ts', '.mjs'] const ioSvcFull = ioSvcExt ? pResolve(ioSvc) : extList .map(ext => pResolve(ioSvc + ext)) .find(path => existsSync(path)) const nspDirFull = pResolve( extList.includes(nspDirExt) ? nspDir.substr(0, nspDir.length - nspDirExt.length) : nspDir ) const io = new SocketIO(server, ioServerOpts) if (redisClient) { register.redis(io, redisClient) } const svcs = { ioSvc: ioSvcFull, nspSvc: nspDirFull } const p = [] const errs = [] Object.entries(svcs).forEach(([svcName, svc]) => { if (existsSync(svc)) { p.push(register[svcName](io, svc, nspDirFull) .catch((err) => { debug(err) errs.push(err) })) } }) if (!server.listening) { p.push(register.listener(server, port, host)) } return Promise.all(p).then(() => ({ io, server, errs })) }, socket (svc, socket, namespace) { consola.info('socket.io client connected to ', namespace) Object.entries(svc).forEach(([evt, fn]) => { if (typeof fn === 'function') { socket.on(evt, async (msg, cb = () => {}) => { try { const resp = await fn(msg) // @ts-ignore cb(resp) } catch (err) { // @ts-ignore cb({ emitError: err.message, evt }) } }) } }) socket.on('disconnect', () => { consola.info('client disconnected from', namespace) }) } } function includeDeps (nuxt, deps) { /* c8 ignore start */ if (!nuxt.options.vite) { nuxt.options.vite = {} } if (!nuxt.options.vite.optimizeDeps) { nuxt.options.vite.optimizeDeps = {} } if (!nuxt.options.vite.optimizeDeps.include) { nuxt.options.vite.optimizeDeps.include = [] } nuxt.options.vite.optimizeDeps.include.push(...deps) /* c8 ignore stop */ } /** @param {import('./types').NuxtSocketIoOptions} moduleOptions */ export default defineNuxtModule({ setup (moduleOptions, nuxt) { const options = { ...nuxt.options.io, ...moduleOptions } nuxt.hook('components:dirs', (dirs) => { dirs.push({ path: pResolve(__dirname, 'components'), prefix: 'io' }) }) nuxt.options.runtimeConfig.public.nuxtSocketIO = { ...options } nuxt.hook('listen', (server) => { if (options.server !== false) { // PORT=4444 npm run dev # would run nuxt app on a different port. // socket.io server will run on process.env.PORT + 1 or 3001 by default. // Specifying io.server.port will override this behavior. // NOTE: nuxt.options.server is planned to be deprecated, so we'll pull from env vars instead. // Not sure why they're deprecating that, it's super useful! // const { host = 'localhost', port = 3000 } = nuxt.options?.server || {} const ioServerOpts = { teardown: true, serverInst: server, // serverInst can be overridden by options.server.serverInst below host: process.env.HOST || 'localhost', port: process.env.PORT !== undefined ? parseInt(process.env.PORT) // + 1 : 3000, // 3001, ...options.server // <-- This is different from nuxt.options server. This is the server options to pass to socket.io server } if (ioServerOpts.teardown) { nuxt.hook('close', () => { ioServerOpts.serverInst.close() }) } register.server(ioServerOpts, ioServerOpts.serverInst).catch((err) => { debug('error registering socket.io server', err) }) } }) includeDeps(nuxt, [ 'deepmerge', 'socket.io-client', // '@socket.io/component-emitter', 'engine.io-client', 'debug', 'tiny-emitter/instance.js' ]) nuxt.options.build.transpile.push(__dirname) addPlugin({ src: pResolve(__dirname, 'plugin.js') }) } }) export { register }