├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ ├── npm-publish.yml │ └── test.js.yml ├── .gitignore ├── .gitlab-ci.yml ├── .nycrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── bootstrap.min.css └── main.css ├── ava.config.js ├── components ├── ChannelSelect.vue ├── ChannelUsers.vue ├── ChatInput.vue ├── Chats.vue ├── Composition1.vue ├── EmitbackSamples.vue ├── Messages.vue ├── Navbar.vue ├── ProgressBar.vue ├── RoomSelect.vue ├── Stats.vue └── Toaster.vue ├── jsconfig.json ├── layouts └── default.vue ├── lib ├── components │ ├── SocketStatus.css │ └── SocketStatus.js ├── module.js ├── plugin.js ├── standalone.js └── types.d.ts ├── nuxt.config.js ├── package-lock.json ├── package.json ├── pages ├── composition.vue ├── examples.vue ├── index.vue ├── ioApi.vue ├── ioStatus.vue ├── rooms.vue └── rooms │ ├── [room].vue │ └── [room] │ └── [channel].vue ├── server ├── apis.js ├── db.js ├── io.bad1.js ├── io.bad2.js ├── io.js ├── io.mjs ├── io.ts └── io │ ├── channel.js │ ├── chat.js │ ├── dynamic.js │ ├── examples.js │ ├── index.js │ ├── middlewares.js │ ├── newfile.ts │ ├── nsp.bad1.js │ ├── nsp.bad2.js │ ├── nsp.bad3.js │ ├── p2p.js │ ├── room.js │ └── rooms.js ├── static └── favicon.ico ├── test ├── Demos.spec.js ├── Module.spec.js ├── Plugin.spec.js ├── SocketStatus.spec.js └── utils │ ├── loaders.js │ ├── module.js │ └── plugin.js ├── tsconfig.json └── types.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | components 2 | # server 3 | # test 4 | # pages 5 | # io 6 | # utils -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "@nuxtjs" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: richardeschloss # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 18 18 | - run: npm ci 19 | - run: npm run test:cov 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v2 27 | with: 28 | node-version: 18 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | -------------------------------------------------------------------------------- /.github/workflows/test.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run test:cov 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # Mac OSX 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | 92 | io/plugin.compiled.js 93 | 94 | # Ignore docs here, separate repo 95 | docs 96 | 97 | .output -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # This file is a template, and might need editing before it works on your project. 2 | # Official framework image. Look for the different tagged releases at: 3 | # https://hub.docker.com/r/library/node/tags/ 4 | image: node:latest 5 | 6 | # Pick zero or more services to be used on all builds. 7 | # Only needed when using a docker container to run your tests in. 8 | # Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service 9 | #services: 10 | # - mysql:latest 11 | # - redis:latest 12 | # - postgres:latest 13 | 14 | # This folder is cached between builds 15 | # http://docs.gitlab.com/ce/ci/yaml/README.html#cache 16 | cache: 17 | paths: 18 | - node_modules/ 19 | 20 | test_unit: 21 | script: 22 | - npm install 23 | - npm run test:cov 24 | coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' 25 | artifacts: 26 | paths: 27 | - coverage -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "all": false, 3 | "reporter": ["html", "text"], 4 | "exclude": ["node_modules"], 5 | "include": ["lib/**/*.js"], 6 | "extension": [".js", ".vue"] 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Change Log 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## 3.0.13 - 2023-07-24 6 | ### Added 7 | - Native support for namespaces on Windows platforms which require a "file://" to be prefixed to the full path for us to be able to import those files. Powershell and Git-Bash seem to run much faster than WSL2 on Windows. 8 | 9 | ## 3.0.11 - 2022-12-01 10 | ### Changed 11 | - Updated install instructions 12 | - Clamped socket.io deps to 4.1.1 for now 13 | 14 | ## 3.0.10 - 2022-11-30 15 | ### Added 16 | - Support for Nuxt3 and socket.io@4.x 17 | - Experimental feature for using the redis adapter. 18 | 19 | ## 2.0.0 - 2021-11-13 20 | ### Changed (major) 21 | - VuexOpts types and Namespace configuration types changed. Entries with the `Record` have been deprecated in favor of string-only entries, which are easier to work with. 22 | - Package type is now "module". Entirely ESM. 23 | - Tested against node lts (16.x). 24 | - Code reorganization and cleanup across the project. 25 | 26 | ## 1.1.24 - 2021-10-01 27 | ### Fixed 28 | - Namespace regex patterns and documentation links (credit: PitPietro) 29 | 30 | ## 1.1.23 - 2021-08-29 31 | ### Fixed 32 | - Automatic server registration with .ts and .mjs extensions (credit: ohashi14715369) 33 | 34 | ## 1.1.22 - 2021-08-16 35 | ### Removed 36 | - Dependency on vue-template-compiler (which should have never been there) 37 | 38 | ## 1.1.21 - 2021-08-12 39 | ### Reverted 40 | - Reverted changes introduced in 1.1.20 41 | 42 | ## 1.1.20 - 2021-08-09 43 | ### Changed 44 | - Make NuxtSocketOpts and NuxtSocket non-partial (credit: CS_Birb) 45 | 46 | ## 1.1.19 - 2021-08-06 47 | ### Changed 48 | - Type definitions file because socket.io-client types changed (credit: matthiasbaldi) 49 | 50 | ## 1.1.18 - 2021-05-11 51 | ### Added 52 | - Wired up support for the socket.io server options 53 | 54 | ### Updated 55 | - Socket.io dependencies to v4 56 | 57 | ## [1.1.17] - 2021-03-31 58 | ### Added 59 | - Support for using $nuxtSocket in the composition api. 60 | 61 | ## [1.1.16] - 2021-03-31 62 | ### Added 63 | - Middleware registration feature (sub namespaces) 64 | - Feature to access the `io` instance in the sub namespaces. 65 | 66 | ## [1.1.15] - 2021-03-30 67 | ### Added 68 | - Middleware registration feature (root namespace) 69 | - Feature to access the `io` instance in the root namespace. 70 | 71 | ## [1.1.14] - 2021-01-22 72 | ### Updated 73 | - socket.io and socket.io-client deps to v3 74 | - Ran security audit fix 75 | 76 | ## [1.1.13] - 2020-11-27 77 | ### Added 78 | - Option to disable the console.info messages (@ArticSeths) 79 | 80 | ## [1.1.12] - 2020-11-04 81 | ### Added 82 | - Types for plugin and module (@wkillerud) 83 | 84 | ## [1.1.11] - 2020-08-28 85 | ### Changed 86 | - Ran security audit and updated dependencies. 87 | 88 | ## [1.1.10] - 2020-08-24 89 | ### Fixed 90 | - Badges to improve branding score on npms. (FaCuZ) 91 | 92 | ## [1.1.9] - 2020-08-20 93 | ### Fixed 94 | - Namespace path resolution on Windows for real this time. Merged in the change. (filipepinheiro) 95 | 96 | ## [1.1.8] - 2020-08-06 97 | ### Fixed 98 | - Namespace path resolution on Windows for real this time. Merged in the change. 99 | 100 | ## [1.1.7] - 2020-08-05 101 | ### Added 102 | - Support for .ts and .mjs namespace files 103 | 104 | ### Fixed 105 | - Namespace path resolution on Windows. 106 | 107 | ## [1.1.6] - 2020-07-27 108 | ### Added 109 | - Support for Nuxt 2.13+ runtime config feature. Experimental feature. 110 | 111 | ## [1.1.5] - 2020-07-14 112 | ### Changed 113 | - Changed internal workings of socket persistence. Socket instances are no longer being persisted in vuex, but instead internally by the plugin. This may break any code that relies on accessessing the sockets directly from vuex. 114 | 115 | ## [1.1.4] - 2020-06-27 116 | ### Changed 117 | - Reverted to 1.1.2. Module is back in ESM. I think going forward, if CJS is needed, a duplicate or built file with .cjs will be created. It seems like the future is going towards ESM. 118 | 119 | ## [1.1.3] - 2020-06-25 120 | ### Changed 121 | - module to CommonJS format. may be easier to re-use this way for now. 122 | - consola to console in the module. 123 | 124 | ## [1.1.2] - 2020-06-22 125 | ### Changed 126 | - consola to console in the plugin. Allows logging statements to be dropped with TerserWebpack plugin. 127 | 128 | ## [1.1.0] - 2020-06-07 129 | ### Added 130 | - Much cleaner documentation, hosted on their own site 131 | 132 | ### Changed 133 | - This github's README. I consider this to be a major change in docs, even though the code change was minor. Code was just linted. 134 | 135 | ## [1.0.25] - 2020-04-23 136 | ### Added 137 | - Automatic IO Server registration based on presence of IO server file and folder (added to module) 138 | 139 | ## [1.0.24] - 2020-04-05 140 | ### Added 141 | - Default handler for url missing. Now `window.location` will be used. 142 | 143 | ### Changed 144 | - Before when URL was empty, an error would be thrown. Now it will just be a warning. 145 | 146 | ## [1.0.23] - 2020-04-04 147 | ### Added 148 | - Instance options for vuex and namespace config (that will override nuxt.config entries) 149 | 150 | ## [1.0.22] - 2020-04-03 151 | ### Added 152 | - Registration of $nuxtSocket vuex module 153 | - Persistence of a given socket (using the "persist" option) 154 | - Registration of serverAPI methods and events, with loose peer-detection 155 | - Registration of clientAPI methods and events 156 | 157 | ### Changed 158 | - Default value of "teardown" option. Previous versions had it default to true. Now it defaults to the *opposite* of the "persist" value (persist defaults to false, therefore teardown to true, just like before) 159 | 160 | ## [1.0.21] - 2020-03-17 161 | ### Added 162 | - Pre-emit hook validation feature for emitbacks; if validation fails, emit event won't be sent. 163 | 164 | ### Fixed 165 | - propExists method in the plugin. Properly checks to see if the property is defined. 166 | 167 | ## [1.0.20] - 2020-03-17 168 | ### Added 169 | - Pre-emit hook validation feature; if validation fails, emit event won't be sent. 170 | 171 | ### Fixed 172 | - Before, emitter "args" would not be used if they were set to `false`. A more proper check has replaced the old one. 173 | 174 | ## [1.0.19] - 2020-03-15 175 | ### Added 176 | - Improved documentation on usage 177 | - Testing section in the docs; i.e., how to mock NuxtSocket, and how to inject. 178 | 179 | ## [1.0.18] - 2020-02-08 180 | ### Added 181 | - Added a safeguard against duplicate registration of Vuex listeners 182 | 183 | ## [1.0.17] - 2020-02-06 184 | ### Added 185 | - Added documentation on the auto-teardown feature 186 | 187 | ## [1.0.16] - 2020-02-06 188 | ### Added 189 | - Added option to disable console warnings in non-production mode. 190 | 191 | ### Changed 192 | - Took out the warning about the emitBack being an object. 193 | 194 | ## [1.0.15] - 2020-01-27 195 | ### Fixed 196 | - Added a fix for the case where an emitter response is undefined. I have a line the attempts to destructure resp to get `emitError`, but it can't do that if resp is undefined. With the fix it can. 197 | 198 | ## [1.0.14] - 2020-01-17 199 | ### Added 200 | - Added debug logging feature 201 | 202 | ### Changed 203 | - Allow objects defined in Vuex state to be emitted back. Before I disallowed it (only allowed primitives), but I now enabled it because some developers may want that. I still warn the user about emitting the entire object when it may be desired to just emit the properties that have changed. 204 | 205 | ### Fixed 206 | - Potential duplicate emitback registrations now prevented in non-test environment. Previous version only prevented it in the TEST env. 207 | 208 | ## [1.0.13] - 2020-01-15 209 | ### Added 210 | - Hosted demo, split across heroku (server) and netlify (client) 211 | - Contributing guidelines. 212 | 213 | ### Changed 214 | - Improved documentation, and now host them on the `gh-pages` branch. Docs now have a table of contents 215 | 216 | ### Fixed 217 | - Minor bug fix in the chat rooms example (`@keyup.enter` defined on the input message just needed to be updated) 218 | 219 | ## [AFK NOTICE] - 2020-01-06 to 2020-01-11 220 | Important NOTE: The maintainer of this project will be away from keyboard for this time. If issues arise in 1.0.11, please consider reverting to v1.0.10 or deferring your issues until I return. Thanks for your understanding! 221 | 222 | ## [1.0.12] - 2020-01-06 223 | ### Fixed 224 | - Moved plugin utils back into plugin. It seemed like the utils were not getting built by Nuxt. 225 | 226 | ## [1.0.11] - 2020-01-05 227 | ### Added 228 | - Error-handling feature for emitters. Handles both timeout and non-timeout kinds of errors. 229 | - Feature to pass arguments to the emitter function (arguments would take priority over the "msg" specified in emitters config in `nuxt.config`) 230 | 231 | ### Fixed 232 | - Fixed potential overwriting of emitter methods by properly setting `mapTo`. (overwriting could have accidentally been done by the call to `assignResp`) 233 | - Expanded test timeout to fix broken tests 234 | 235 | ## [1.0.10] - 2020-01-03 236 | ### Changed 237 | - Minor change: v1.0.9 accidentally packaged `plugin.compiled.js` which is only used for tests and not needed in the distro. 238 | 239 | ## [1.0.9] - 2020-01-03 240 | ### Added 241 | - SocketStatus feature. Disabled by default, opt-in to use it. SocketStatus component will also be included now. 242 | 243 | ### Fixed 244 | - Potential stack overflow error in the auto-teardown code; error was only noticeable when multiple nuxtSockets were instantiated on the same component. The error was fixed. 245 | 246 | ## [1.0.8] - 2019-12-29 247 | ### Fixed 248 | - Fixed `propByPath` method, which was incorrectly treating empty (falsy) strings as undefined. 249 | 250 | ## [1.0.7] - 2019-12-27 251 | 252 | ### Added 253 | - Support for namespace configuration 254 | - Chat rooms example in [`examples/rooms`](https://github.com/richardeschloss/nuxt-socket-io/tree/examples/rooms) branch. 255 | - Automated tests for new feature and example pages. 256 | 257 | ### Changed 258 | - Internally, refactored some of the plugin code to improve readability, input validation and consistency. 259 | - Organization of automated tests. Tests now run a bit faster and are easier to maintain. 260 | 261 | ## [1.0.6] - 2019-12-21 262 | 263 | ### Added 264 | - Check to see if specified emitbacks exist in Vuex store. If they don't, provide friendly error message 265 | 266 | ### Changed 267 | - Moved examples to components. Added tests. Eliminated need for the old and slow e2e tests 268 | - Updated plugin tests. 269 | 270 | ### Fixed 271 | 272 | - Potential duplicate emitback registrations now prevented 273 | - [Dev server] ioServer.listen resolves correctly now 274 | 275 | ## [1.0.5] - 2019-12-06 276 | 277 | ### Added 278 | 279 | - Check for missing vuex options; i.e., missing mutations or actions 280 | 281 | ## [1.0.4] - 2019-11-19 282 | 283 | ### Changed 284 | 285 | - Only export plugin opts in TEST mode 286 | - TEST mode can get/set plugin opts from outside the plugin, non-TEST mode can only get plugin opts inside the plugin 287 | 288 | ## [1.0.3] - 2019-11-18 289 | 290 | ### Added 291 | 292 | - Improved Test coverage. Now at 100% 293 | 294 | ## [1.0.2] - 2019-11-14 295 | 296 | ### Changed 297 | 298 | - Badges format 299 | 300 | ## [1.0.1] - 2019-11-14 301 | 302 | ### Added 303 | 304 | - Unit tests to help get better test coverage 305 | - This changelog 306 | 307 | ### Changed 308 | 309 | ### Fixed 310 | 311 | 312 | ## See Also: 313 | 314 | [Releases](https://github.com/richardeschloss/nuxt-socket-io/releases) 315 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Want to contribute? I think that's fantastic! This project is used all over the world and your help is appreciated! Here are some basic guidelines for contributing. 4 | 5 | ## Have a question or an issue? 6 | 7 | Since the nuxt-socket-io has reached a stable state, I'm currently disabling the "issues" feature, since it encourages more whining than actual problem solving. Instead, if you have an issue, you can describe it in specific detail in a pull request, where the pull request clearly explains the problem it's solving. The pull request requires accompanying tests. I will ignore and close PRs that are not fully tested. 8 | 9 | ## Want to merge in your awesome code? 10 | 11 | Great! Here are the steps to help make the process go smoothly: 12 | 13 | 1. First, *fork* this repo (click the button at the top right on Github). Always work out of your fork please. This way, if something goes wrong in your fork, you can always refer back to the main project. 14 | 2. Only push changes to your repo, and when you feel your changes are ready to be merged, open a pull request. 15 | 3. Because this project is tied into a CI/CD system that runs when branches are pushed to it, I ask that you please avoid asking changes to be merged directly into the master branch. Ideally, I ask that you merge request into `[main project repo]:development` branch, or better yet, into the same branch name `[main project repo]:your-feature <-- [your project repo]:your-feature`. This way, the CI pipeline should kick-off the moment you open the pull request, and won't worry the user base if the test fails (since the test wouldn't be failing on master / released code). 16 | 4. For simple documentation changes, such as changes to README.md, please submit the merge request to `gh-pages`. That branch will host the documentation on github pages. 17 | 5. Code changes should be tested and accompanied with automated tests that get coverage on all added cases. Tests use the ava test framework. If it is difficult to obtain 100% coverage, please ask for help and I will try to help. If still too difficult, we'll simply add a note about manually testing that was done. If others want to help too, I'll try my best to credit everyone who does! 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Richard Schloss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/nuxt-socket-io.svg)](https://www.npmjs.com/package/nuxt-socket-io) 2 | [![npm](https://img.shields.io/npm/dt/nuxt-socket-io.svg)](https://www.npmjs.com/package/nuxt-socket-io) 3 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/richardeschloss/nuxt-socket-io/test.js.yml?branch=master) 4 | [![](https://gitlab.com/richardeschloss/nuxt-socket-io/badges/master/coverage.svg)](https://gitlab.com/richardeschloss/nuxt-socket-io) 5 | [![NPM](https://img.shields.io/npm/l/nuxt-socket-io.svg)](https://github.com/richardeschloss/nuxt-socket-io/blob/development/LICENSE) 6 | 7 | [📖 **Release Notes**](./CHANGELOG.md) 8 | 9 | # nuxt-socket-io 10 | 11 | [Socket.io](https://socket.io/) client and server module for Nuxt 12 | 13 | ## Features 14 | - Configuration of multiple IO sockets 15 | - Configuration of per-socket namespaces (simplified format) 16 | - Automatic IO Server Registration 17 | - Socket IO Status 18 | - Automatic Error Handling 19 | - Debug logging, enabled with localStorage item 'debug' set to 'nuxt-socket-io' 20 | - Automatic Teardown, enabled by default 21 | - $nuxtSocket vuex module and socket persistence in vuex 22 | - Support for dynamic APIs using the KISS API format 23 | - Support for the IO config in the new Nuxt runtime config (for Nuxt versions >= 2.13) 24 | - Automatic middleware registration 25 | - ES module 26 | - Experimental support for ioRedis 27 | 28 | # Important updates 29 | 30 | * v3.x has been tested against Nuxt3 stable and socket.io@4.1.1. If you absolutely require socket.io@4.5.3 it's recommended to install it and follow the [workaround](https://github.com/richardeschloss/nuxt-socket-io/issues/278#issuecomment-1287133733). 31 | * v2.x may contain breaking changes in it's attempt to get Nuxt3 reaady. `npm i nuxt-socket@1` should help revert any breaking changes in your code. 32 | * VuexOpts types and Namespace configuration types changed. Entries with the `Record` have been deprecated in favor of string-only entries, which are easier to work with. 33 | * Package type is now "module". Entirely ESM. 34 | * Tested against node lts (16.x). 35 | * v1.1.17+ uses socket.io 4.x. You may find the migration [here](https://socket.io/docs/v4/migrating-from-3-x-to-4-0/) 36 | * v1.1.14+ uses socket.io 3.x. You may find the migration [here](https://socket.io/docs/v4/migrating-from-2-x-to-3-0/) 37 | * v1.1.13 uses socket.io 2.x. 38 | 39 | # Setup 40 | 41 | 1. Add `nuxt-socket-io` dependency to your project 42 | 43 | * Nuxt 3.x: 44 | ```bash 45 | npm i nuxt-socket-io 46 | ``` 47 | 48 | * Nuxt 2.x: 49 | ```bash 50 | npm i nuxt-socket-io@2 51 | ``` 52 | 53 | 2. Add `nuxt-socket-io` to the `modules` section of `nuxt.config.js` 54 | 55 | ```js 56 | { 57 | modules: [ 58 | 'nuxt-socket-io', 59 | ], 60 | io: { 61 | // module options 62 | sockets: [{ 63 | name: 'main', 64 | url: 'http://localhost:3000' 65 | }] 66 | } 67 | } 68 | ``` 69 | 70 | 3. Use it in your components: 71 | 72 | ```js 73 | { 74 | mounted() { 75 | this.socket = this.$nuxtSocket({ 76 | channel: '/index' 77 | }) 78 | /* Listen for events: */ 79 | this.socket 80 | .on('someEvent', (msg, cb) => { 81 | /* Handle event */ 82 | }) 83 | }, 84 | methods: { 85 | method1() { 86 | /* Emit events */ 87 | this.socket.emit('method1', { 88 | hello: 'world' 89 | }, (resp) => { 90 | /* Handle response, if any */ 91 | }) 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | ## Documentation 98 | 99 | But WAIT! There's so much more you can do!! Check out the documentation: 100 | > https://nuxt-socket-io.netlify.app/ 101 | 102 | There you will see: 103 | - More details about the features, configuration and usage 104 | 105 | ### Resources 106 | 107 | - Follow me and the series on [medium.com](https://medium.com/@richard.e.schloss) 108 | - Socket.io Client docs [here](https://socket.io/docs/v4/client-api/) 109 | - Socket.io Server docs [here](https://socket.io/docs/v4/server-api/) 110 | 111 | 112 | ## Development 113 | 114 | 1. Clone this repository 115 | 2. Install dependencies using `yarn install` or `npm install` 116 | 3. Start development server using `yarn dev` or `npm run dev` 117 | -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- 1 | .page-enter-active, 2 | .page-leave-active { 3 | transition: opacity 0.5s; 4 | } 5 | .page-enter, 6 | .page-leave-to { 7 | opacity: 0; 8 | } 9 | 10 | .demo-container { 11 | margin: 0 auto; 12 | min-height: 100vh; 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | text-align: center; 17 | } 18 | 19 | .title { 20 | font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 21 | 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; 22 | display: block; 23 | font-weight: 300; 24 | font-size: 24px; 25 | color: #35495e; 26 | letter-spacing: 1px; 27 | } 28 | -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | serial: true, 3 | files: [ 4 | 'test/Module.spec.js', 5 | 'test/Plugin.spec.js', 6 | 'test/SocketStatus.spec.js' 7 | // 'test/Demos.spec.js' // For demo code only. To be updated as needed 8 | ], 9 | nodeArguments: [ 10 | '--experimental-loader=./test/utils/loaders.js' 11 | ], 12 | tap: false, 13 | verbose: true 14 | } 15 | -------------------------------------------------------------------------------- /components/ChannelSelect.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 100 | 101 | 118 | -------------------------------------------------------------------------------- /components/ChannelUsers.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /components/ChatInput.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 41 | -------------------------------------------------------------------------------- /components/Chats.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 43 | 44 | 68 | -------------------------------------------------------------------------------- /components/Composition1.vue: -------------------------------------------------------------------------------- 1 | 10 | 60 | -------------------------------------------------------------------------------- /components/EmitbackSamples.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 85 | -------------------------------------------------------------------------------- /components/Messages.vue: -------------------------------------------------------------------------------- 1 | 100 | 101 | 131 | -------------------------------------------------------------------------------- /components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /components/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 88 | 89 | 95 | -------------------------------------------------------------------------------- /components/RoomSelect.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 45 | 46 | 54 | -------------------------------------------------------------------------------- /components/Stats.vue: -------------------------------------------------------------------------------- 1 | 49 | -------------------------------------------------------------------------------- /components/Toaster.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | 33 | 43 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "baseUrl": ".", 5 | "paths": { 6 | "~/*": ["./*"], 7 | "@/*": ["./*"], 8 | "~~/*": ["./*"], 9 | "@@/*": ["./*"] 10 | } 11 | }, 12 | "exclude": ["node_modules", ".nuxt", "dist"] 13 | } 14 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 28 | -------------------------------------------------------------------------------- /lib/components/SocketStatus.css: -------------------------------------------------------------------------------- 1 | .socket-status .label { 2 | width: 100%; 3 | text-align: left; 4 | } 5 | 6 | .socket-status .grid { 7 | display: grid; 8 | grid-template-columns: 1fr 1fr; 9 | } 10 | 11 | .socket-status .grid:hover { 12 | color: #212529; 13 | background-color: rgba(0, 0, 0, 0.075); 14 | } 15 | 16 | .socket-status .striped:nth-of-type(odd) { 17 | background-color: rgba(0, 0, 0, 0.05); 18 | } 19 | 20 | .socket-status .col-key { 21 | grid-column: 1; 22 | font-weight: bold; 23 | text-align: right; 24 | padding: 0.75rem; 25 | border-top: 1px solid #dee2e6; 26 | } 27 | 28 | .socket-status .col-val { 29 | grid-column: 2; 30 | text-align: left; 31 | padding: 0.75rem; 32 | border-top: 1px solid #dee2e6; 33 | } -------------------------------------------------------------------------------- /lib/components/SocketStatus.js: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import './SocketStatus.css' 3 | export default { 4 | render () { 5 | if (!this.status.connectUrl) { return } // h() } 6 | const label = h('label', { 7 | class: 'label' 8 | }, [ 9 | h('b', 'Status for: '), 10 | this.status.connectUrl 11 | ]) 12 | const entries = [] 13 | for (const entry of this.statusTbl) { 14 | const entryElm = h('div', { 15 | class: 'grid striped' 16 | }, [ 17 | h('span', { 18 | class: 'col-key' 19 | }, entry.item), 20 | h('span', { 21 | class: 'col-val' 22 | }, entry.info) 23 | ]) 24 | entries.push(entryElm) 25 | } 26 | 27 | return h('div', { 28 | class: 'socket-status' 29 | }, [ 30 | label, 31 | entries 32 | ]) 33 | }, 34 | props: { 35 | status: { 36 | type: Object, 37 | default () { 38 | return {} 39 | } 40 | } 41 | }, 42 | computed: { 43 | statusTbl () { 44 | const { status } = this 45 | let err 46 | const items = Object.entries(status).reduce((arr, [item, info]) => { 47 | if (item !== 'connectUrl' && info !== undefined && info !== '') { 48 | if (item.match(/Error|Failed/)) { 49 | err = true 50 | } 51 | arr.push({ 52 | item, 53 | info: typeof info === 'string' 54 | ? info 55 | : info.toString() 56 | }) 57 | } 58 | return arr 59 | }, []) 60 | 61 | if (!err) { 62 | items.unshift({ item: 'status', info: 'OK' }) 63 | } 64 | 65 | return items 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/module.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-callback-literal */ 2 | /* 3 | * Copyright 2022 Richard Schloss (https://github.com/richardeschloss/nuxt-socket-io) 4 | */ 5 | 6 | import http from 'http' 7 | import { existsSync } from 'fs' 8 | import { resolve as pResolve, parse as pParse, dirname } from 'path' 9 | import { fileURLToPath } from 'url' 10 | import { promisify } from 'util' 11 | import consola from 'consola' 12 | import { defineNuxtModule, addPlugin } from '@nuxt/kit' 13 | import Debug from 'debug' 14 | import { Server as SocketIO } from 'socket.io' 15 | import Glob from 'glob' 16 | 17 | // @ts-ignore 18 | const __dirname = dirname(fileURLToPath(import.meta.url)) 19 | 20 | const debug = Debug('nuxt-socket-io') 21 | const glob = promisify(Glob) 22 | 23 | const register = { 24 | /** 25 | * @param {import('socket.io').Server | import('socket.io').Namespace} io 26 | * @param {{ [s: string]: any; } | ArrayLike} middlewares 27 | */ 28 | middlewares (io, middlewares) { 29 | Object.values(middlewares).forEach(m => io.use(m)) 30 | }, 31 | /** 32 | * @param {import('socket.io').Server} io 33 | * @param {string} ioSvc 34 | */ 35 | async ioSvc (io, ioSvc) { 36 | const { default: Svc, middlewares = {}, setIO = () => {} } = await import( 37 | (process.platform === 'win32' ? 'file://' : '') + ioSvc 38 | ) 39 | register.middlewares(io, middlewares) 40 | setIO(io) 41 | 42 | if (Svc && typeof Svc === 'function') { 43 | io.on('connection', (socket) => { 44 | const svc = Svc(socket, io) 45 | register.socket(svc, socket, '/') 46 | }) 47 | } else { 48 | throw new Error( 49 | `io service at ${ioSvc} does not export a default "Svc()" function. Not registering` 50 | ) 51 | } 52 | }, 53 | /** 54 | * @param {import('socket.io').Server} io 55 | * @param {string} nspDir 56 | */ 57 | async nspSvc (io, nspDir) { 58 | const nspFiles = await glob(`${nspDir}/**/*.{js,ts,mjs}`) 59 | const nspDirResolved = pResolve(nspDir).replace(/\\/g, '/') 60 | const namespaces = nspFiles.map( 61 | f => f.split(nspDirResolved)[1].split(/\.(js|ts|mjs)$/)[0] 62 | ) 63 | namespaces.forEach(async (namespace, idx) => { 64 | const imports = await import((process.platform === 'win32' ? 'file://' : '') + nspFiles[idx]) 65 | const { 66 | default: Svc, 67 | middlewares = {}, 68 | setIO = () => {} 69 | } = imports 70 | 71 | register.middlewares(io.of(namespace), middlewares) 72 | setIO(io) 73 | 74 | if (Svc && typeof Svc === 'function') { 75 | io.of(`${namespace}`).on('connection', (socket) => { 76 | const svc = Svc(socket, io) 77 | register.socket(svc, socket, namespace) 78 | }) 79 | } else { 80 | debug( 81 | `io service at ${nspDirResolved}${namespace} does not export a default "Svc()" function. Not registering` 82 | ) 83 | } 84 | }) 85 | }, 86 | async redis (io, redisClient) { 87 | let useClient = redisClient 88 | const dfltClient = { url: 'redis://localhost:6379' } 89 | if (redisClient === true) { 90 | useClient = dfltClient 91 | } 92 | debug('starting redis adapter', useClient) 93 | const { createAdapter } = await import('@socket.io/redis-adapter') 94 | const { createClient } = await import('redis') 95 | const pubClient = createClient(useClient) 96 | const subClient = pubClient.duplicate() 97 | await Promise.all([pubClient.connect(), subClient.connect()]) 98 | io.adapter(createAdapter(pubClient, subClient)) 99 | }, 100 | listener (server = http.createServer(), port = 3000, host = 'localhost') { 101 | return new Promise((resolve, reject) => { 102 | server 103 | .listen(port, host) 104 | .on('error', reject) 105 | .on('listening', () => { 106 | consola.info(`socket.io server listening on ${host}:${port}`) 107 | resolve(server) 108 | }) 109 | }) 110 | }, 111 | server (options = {}, server = http.createServer()) { 112 | const { 113 | ioSvc = './server/io', 114 | nspDir = ioSvc, 115 | host = 'localhost', 116 | port = 3000, 117 | redisClient, 118 | ...ioServerOpts // Options that get passed down to SocketIO instance. 119 | } = options 120 | 121 | const { ext: ioSvcExt } = pParse(ioSvc) 122 | const { ext: nspDirExt } = pParse(ioSvc) 123 | const extList = ['.js', '.ts', '.mjs'] 124 | const ioSvcFull = ioSvcExt 125 | ? pResolve(ioSvc) 126 | : extList 127 | .map(ext => pResolve(ioSvc + ext)) 128 | .find(path => existsSync(path)) 129 | const nspDirFull = pResolve( 130 | extList.includes(nspDirExt) 131 | ? nspDir.substr(0, nspDir.length - nspDirExt.length) 132 | : nspDir 133 | ) 134 | 135 | const io = new SocketIO(server, ioServerOpts) 136 | if (redisClient) { 137 | register.redis(io, redisClient) 138 | } 139 | const svcs = { ioSvc: ioSvcFull, nspSvc: nspDirFull } 140 | const p = [] 141 | const errs = [] 142 | Object.entries(svcs).forEach(([svcName, svc]) => { 143 | if (existsSync(svc)) { 144 | p.push(register[svcName](io, svc, nspDirFull) 145 | .catch((err) => { 146 | debug(err) 147 | errs.push(err) 148 | })) 149 | } 150 | }) 151 | 152 | if (!server.listening) { 153 | p.push(register.listener(server, port, host)) 154 | } 155 | return Promise.all(p).then(() => ({ io, server, errs })) 156 | }, 157 | socket (svc, socket, namespace) { 158 | consola.info('socket.io client connected to ', namespace) 159 | Object.entries(svc).forEach(([evt, fn]) => { 160 | if (typeof fn === 'function') { 161 | socket.on(evt, async (msg, cb = () => {}) => { 162 | try { 163 | const resp = await fn(msg) 164 | // @ts-ignore 165 | cb(resp) 166 | } catch (err) { 167 | // @ts-ignore 168 | cb({ emitError: err.message, evt }) 169 | } 170 | }) 171 | } 172 | }) 173 | socket.on('disconnect', () => { 174 | consola.info('client disconnected from', namespace) 175 | }) 176 | } 177 | } 178 | 179 | function includeDeps (nuxt, deps) { 180 | /* c8 ignore start */ 181 | if (!nuxt.options.vite) { 182 | nuxt.options.vite = {} 183 | } 184 | 185 | if (!nuxt.options.vite.optimizeDeps) { 186 | nuxt.options.vite.optimizeDeps = {} 187 | } 188 | if (!nuxt.options.vite.optimizeDeps.include) { 189 | nuxt.options.vite.optimizeDeps.include = [] 190 | } 191 | nuxt.options.vite.optimizeDeps.include.push(...deps) 192 | /* c8 ignore stop */ 193 | } 194 | 195 | /** @param {import('./types').NuxtSocketIoOptions} moduleOptions */ 196 | export default defineNuxtModule({ 197 | setup (moduleOptions, nuxt) { 198 | const options = { ...nuxt.options.io, ...moduleOptions } 199 | nuxt.hook('components:dirs', (dirs) => { 200 | dirs.push({ 201 | path: pResolve(__dirname, 'components'), 202 | prefix: 'io' 203 | }) 204 | }) 205 | nuxt.options.runtimeConfig.public.nuxtSocketIO = { ...options } 206 | nuxt.hook('listen', (server) => { 207 | if (options.server !== false) { 208 | // PORT=4444 npm run dev # would run nuxt app on a different port. 209 | // socket.io server will run on process.env.PORT + 1 or 3001 by default. 210 | // Specifying io.server.port will override this behavior. 211 | // NOTE: nuxt.options.server is planned to be deprecated, so we'll pull from env vars instead. 212 | // Not sure why they're deprecating that, it's super useful! 213 | // const { host = 'localhost', port = 3000 } = nuxt.options?.server || {} 214 | const ioServerOpts = { 215 | teardown: true, 216 | serverInst: server, // serverInst can be overridden by options.server.serverInst below 217 | host: process.env.HOST || 'localhost', 218 | port: process.env.PORT !== undefined 219 | ? parseInt(process.env.PORT) // + 1 220 | : 3000, // 3001, 221 | ...options.server // <-- This is different from nuxt.options server. This is the server options to pass to socket.io server 222 | } 223 | if (ioServerOpts.teardown) { 224 | nuxt.hook('close', () => { 225 | ioServerOpts.serverInst.close() 226 | }) 227 | } 228 | 229 | register.server(ioServerOpts, ioServerOpts.serverInst).catch((err) => { 230 | debug('error registering socket.io server', err) 231 | }) 232 | } 233 | }) 234 | 235 | includeDeps(nuxt, [ 236 | 'deepmerge', 237 | 'socket.io-client', 238 | // '@socket.io/component-emitter', 239 | 'engine.io-client', 240 | 'debug', 241 | 'tiny-emitter/instance.js' 242 | ]) 243 | nuxt.options.build.transpile.push(__dirname) 244 | addPlugin({ 245 | src: pResolve(__dirname, 'plugin.js') 246 | }) 247 | } 248 | }) 249 | 250 | export { register } 251 | -------------------------------------------------------------------------------- /lib/plugin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* 3 | * Copyright 2022 Richard Schloss (https://github.com/richardeschloss/nuxt-socket-io) 4 | */ 5 | 6 | import io from 'socket.io-client' 7 | import Debug from 'debug' 8 | import emitter from 'tiny-emitter/instance.js' 9 | // @ts-ignore 10 | import { watch } from 'vue' 11 | import { defineNuxtPlugin, useState } from '#app' 12 | 13 | /* 14 | TODO: 15 | 1) will enable when '@nuxtjs/composition-api' reaches stable version: 16 | 2) will bump from devDep to dep when stable 17 | */ 18 | 19 | const debug = Debug('nuxt-socket-io') 20 | const isRefImpl = any => any && any.constructor.name === 'RefImpl' 21 | 22 | const delay = (ms, timerObj) => new Promise((resolve, reject) => { 23 | timerObj.timer = setTimeout(() => resolve(true), ms) 24 | timerObj.abort = () => { 25 | clearTimeout(timerObj.timer) 26 | reject(new Error('AbortError')) 27 | } 28 | }) 29 | 30 | const _sockets = {} 31 | 32 | export const mutations = { 33 | SET_API (state, { label, api }) { 34 | state.ioApis[label] = api 35 | }, 36 | 37 | SET_CLIENT_API (state, { label = 'clientAPI', ...api }) { 38 | state.clientApis[label] = api 39 | }, 40 | 41 | SET_EMIT_ERRORS (state, { label, emitEvt, err }) { 42 | if (state.emitErrors[label] === undefined) { 43 | state.emitErrors[label] = {} 44 | } 45 | 46 | if (state.emitErrors[label][emitEvt] === undefined) { 47 | state.emitErrors[label][emitEvt] = [] 48 | } 49 | 50 | state.emitErrors[label][emitEvt].push(err) 51 | }, 52 | 53 | SET_EMIT_TIMEOUT (state, { label, emitTimeout }) { 54 | if (!state.emitTimeouts[label]) { 55 | state.emitTimeouts[label] = {} 56 | } 57 | state.emitTimeouts[label] = emitTimeout 58 | } 59 | } 60 | 61 | export const ioState = () => useState('$io', () => ({})) 62 | 63 | export const useNuxtSocket = () => useState('$nuxtSocket', () => ({ 64 | clientApis: {}, 65 | ioApis: {}, 66 | emitErrors: {}, 67 | emitTimeouts: {} 68 | })) 69 | 70 | export async function emit ({ // TBD: test... 71 | label = '', 72 | socket = _sockets[label], 73 | evt, 74 | msg, 75 | emitTimeout = useNuxtSocket().value.emitTimeouts[label], 76 | noAck = false 77 | }) { 78 | const state = useNuxtSocket().value 79 | debug('$nuxtSocket.emit', label, evt) 80 | if (socket === undefined) { 81 | throw new Error( 82 | 'socket instance required. Please provide a valid socket label or socket instance' 83 | ) 84 | } 85 | 86 | register.emitP(socket) 87 | 88 | debug(`Emitting ${evt} with msg`, msg) 89 | const timerObj = {} 90 | const p = [socket.emitP(evt, msg)] 91 | if (noAck) { 92 | return 93 | } 94 | 95 | if (emitTimeout) { 96 | p.push( 97 | register.emitTimeout({ 98 | emitTimeout, 99 | timerObj 100 | }).catch((err) => { 101 | if (label !== undefined && label !== '') { 102 | mutations.SET_EMIT_ERRORS(state, { label, emitEvt: evt, err }) 103 | debug( 104 | `[nuxt-socket-io]: ${label} Emit error occurred and logged to vuex `, 105 | err 106 | ) 107 | } else { 108 | throw new Error(err.message) 109 | } 110 | }) 111 | ) 112 | } 113 | const resp = await Promise.race(p) 114 | debug('Emitter response rxd', { evt, resp }) 115 | if (timerObj.abort) { 116 | timerObj.abort() 117 | } 118 | const { emitError, ...errorDetails } = resp || {} 119 | if (emitError !== undefined) { 120 | const err = { 121 | message: emitError, 122 | emitEvt: evt, 123 | errorDetails, 124 | timestamp: Date.now() 125 | } 126 | debug('Emit error occurred', err) 127 | if (label !== undefined && label !== '') { 128 | debug( 129 | `[nuxt-socket-io]: ${label} Emit error ${err.message} occurred and logged to vuex `, 130 | err 131 | ) 132 | mutations.SET_EMIT_ERRORS(state, { label, emitEvt: evt, err }) 133 | } else { 134 | throw new Error(err.message) 135 | } 136 | } else { 137 | return resp 138 | } 139 | } 140 | 141 | let warn, infoMsgs 142 | 143 | function camelCase (str) { 144 | return str 145 | .replace(/[_\-\s](.)/g, function ($1) { 146 | return $1.toUpperCase() 147 | }) 148 | .replace(/[-_\s]/g, '') 149 | .replace(/^(.)/, function ($1) { 150 | return $1.toLowerCase() 151 | }) 152 | .replace(/[^\w\s]/gi, '') 153 | } 154 | 155 | function propExists (obj, path) { 156 | // eslint-disable-next-line array-callback-return 157 | const exists = path.split('.').reduce((out, prop) => { 158 | if (out !== undefined && out[prop] !== undefined) { 159 | return out[prop] 160 | } 161 | }, obj) 162 | 163 | return exists !== undefined 164 | } 165 | 166 | function parseEntry (entry, entryType) { 167 | let evt, mapTo, pre, emitEvt, msgLabel, post 168 | if (typeof entry === 'string') { 169 | let subItems = [] 170 | let body 171 | const items = entry.trim().split(/\s*\]\s*/) 172 | if (items.length > 1) { 173 | pre = items[0] 174 | subItems = items[1].split(/\s*\[\s*/) 175 | } else { 176 | subItems = items[0].split(/\s*\[\s*/) 177 | } 178 | 179 | ;[body, post] = subItems 180 | if (body.includes('-->')) { 181 | ;[evt, mapTo] = body.split(/\s*-->\s*/) 182 | } else if (body.includes('<--')) { 183 | ;[evt, mapTo] = body.split(/\s*<--\s*/) 184 | } else { 185 | evt = body 186 | } 187 | 188 | if (entryType === 'emitter') { 189 | ;[emitEvt, msgLabel] = evt.split(/\s*\+\s*/) 190 | } else if (mapTo === undefined) { 191 | mapTo = evt 192 | } 193 | } 194 | return { pre, post, evt, mapTo, emitEvt, msgLabel } 195 | } 196 | 197 | function assignMsg (ctx, prop) { 198 | let msg 199 | if (prop !== undefined) { 200 | if (ctx[prop] !== undefined) { 201 | if (typeof ctx[prop] === 'object') { 202 | msg = Array.isArray(ctx[prop]) ? [] : {} 203 | Object.assign(msg, ctx[prop]) 204 | } else { 205 | msg = ctx[prop] 206 | } 207 | } else { 208 | warn(`prop or data item "${prop}" not defined`) 209 | } 210 | debug(`assigned ${prop} to ${msg}`) 211 | } 212 | return msg 213 | } 214 | 215 | function assignResp (ctx, prop, resp) { 216 | if (prop !== undefined) { 217 | if (ctx[prop] !== undefined) { 218 | if (typeof ctx[prop] !== 'function') { 219 | // In vue3, it's possible to create 220 | // reactive refs on the fly with ref() 221 | // so check for that here. 222 | // (this would elimnate the need for v2's 223 | // this.$set because we just set the value prop 224 | // to trigger the UI changes) 225 | if (isRefImpl(ctx[prop])) { 226 | ctx[prop].value = resp 227 | } else { 228 | ctx[prop] = resp 229 | } 230 | debug(`assigned ${resp} to ${prop}`) 231 | } 232 | } else { 233 | warn(`${prop} not defined on instance`) 234 | } 235 | } 236 | } 237 | 238 | async function runHook (ctx, prop, data) { 239 | if (prop !== undefined) { 240 | if (ctx[prop]) { return await ctx[prop](data) } else { warn(`method ${prop} not defined`) } 241 | } 242 | } 243 | 244 | /** 245 | * Validate the provided sockets are an array 246 | * of at least 1 item 247 | * @param {Array<*>} sockets 248 | */ 249 | function validateSockets (sockets) { 250 | return (sockets && 251 | Array.isArray(sockets) && 252 | sockets.length > 0) 253 | } 254 | 255 | export const register = { 256 | clientApiEvents ({ ctx, socket, api }) { 257 | const { evts } = api 258 | Object.entries(evts).forEach(([emitEvt, schema]) => { 259 | const { data: dataT } = schema 260 | const fn = emitEvt + 'Emit' 261 | if (ctx[emitEvt] !== undefined) { 262 | if (dataT !== undefined) { 263 | Object.entries(dataT).forEach(([key, val]) => { 264 | ctx.$set(ctx[emitEvt], key, val) 265 | }) 266 | debug('Initialized data for', emitEvt, dataT) 267 | } 268 | } 269 | 270 | if (ctx[fn] !== undefined) { 271 | return 272 | } 273 | 274 | ctx[fn] = async (fnArgs) => { 275 | const { label: apiLabel, ack, ...args } = fnArgs || {} 276 | const label = apiLabel || api.label 277 | const msg = Object.keys(args).length > 0 ? args : { ...ctx[emitEvt] } 278 | msg.method = fn 279 | if (ack) { 280 | const ackd = await emit({ 281 | label, 282 | socket, 283 | evt: emitEvt, 284 | msg 285 | }) 286 | return ackd 287 | } else { 288 | emit({ 289 | label, 290 | socket, 291 | evt: emitEvt, 292 | msg, 293 | noAck: true 294 | }) 295 | } 296 | } 297 | debug('Registered clientAPI method', fn) 298 | }) 299 | }, 300 | clientApiMethods ({ ctx, socket, api }) { 301 | const { methods } = api 302 | const evts = Object.assign({}, methods, { getAPI: {} }) 303 | Object.entries(evts).forEach(([evt, schema]) => { 304 | if (socket.hasListeners(evt)) { 305 | warn(`evt ${evt} already has a listener registered`) 306 | } 307 | 308 | socket.on(evt, async (msg, cb) => { 309 | if (evt === 'getAPI') { 310 | if (cb) { cb(api) } 311 | } else if (ctx[evt] !== undefined) { 312 | msg.method = evt 313 | const resp = await ctx[evt](msg) 314 | if (cb) { cb(resp) } 315 | } else if (cb) { 316 | // eslint-disable-next-line node/no-callback-literal 317 | cb({ 318 | emitErr: 'notImplemented', 319 | msg: `Client has not yet implemented method (${evt})` 320 | }) 321 | } 322 | }) 323 | 324 | debug(`registered client api method ${evt}`) 325 | if (evt !== 'getAPI' && ctx[evt] === undefined) { 326 | warn( 327 | `client api method ${evt} has not been defined. ` + 328 | 'Either update the client api or define the method so it can be used by callers' 329 | ) 330 | } 331 | }) 332 | }, 333 | clientAPI ({ ctx, socket, clientAPI }) { 334 | const state = useNuxtSocket().value 335 | if (clientAPI.methods) { 336 | register.clientApiMethods({ ctx, socket, api: clientAPI }) 337 | } 338 | 339 | if (clientAPI.evts) { 340 | register.clientApiEvents({ ctx, socket, api: clientAPI }) 341 | } 342 | 343 | mutations.SET_CLIENT_API(state, clientAPI) 344 | debug('clientAPI registered', clientAPI) 345 | }, 346 | serverApiEvents ({ ctx, socket, api, label, ioDataProp, apiIgnoreEvts }) { 347 | const { evts } = api 348 | Object.entries(evts).forEach(([evt, entry]) => { 349 | const { methods = [], data: dataT } = entry 350 | if (apiIgnoreEvts.includes(evt)) { 351 | debug( 352 | `Event ${evt} is in ignore list ("apiIgnoreEvts"), not registering.` 353 | ) 354 | return 355 | } 356 | 357 | if (socket.hasListeners(evt)) { 358 | warn(`evt ${evt} already has a listener registered`) 359 | } 360 | 361 | if (methods.length === 0) { 362 | let initVal = dataT 363 | if (typeof initVal === 'object') { 364 | initVal = Array.isArray(dataT) ? [] : {} 365 | } 366 | ctx.$set(ctx[ioDataProp], evt, initVal) 367 | } else { 368 | methods.forEach((method) => { 369 | if (ctx[ioDataProp][method] === undefined) { 370 | ctx.$set(ctx[ioDataProp], method, {}) 371 | } 372 | 373 | ctx.$set( 374 | ctx[ioDataProp][method], 375 | evt, 376 | Array.isArray(dataT) ? [] : {} 377 | ) 378 | }) 379 | } 380 | 381 | socket.on(evt, (msg, cb) => { 382 | debug(`serverAPI event ${evt} rxd with msg`, msg) 383 | const { method, data } = msg 384 | if (method !== undefined) { 385 | if (ctx[ioDataProp][method] === undefined) { 386 | ctx.$set(ctx[ioDataProp], method, {}) 387 | } 388 | 389 | ctx.$set(ctx[ioDataProp][method], evt, data) 390 | } else { 391 | ctx.$set(ctx[ioDataProp], evt, data) 392 | } 393 | 394 | if (cb) { 395 | // eslint-disable-next-line node/no-callback-literal 396 | cb({ ack: 'ok' }) 397 | } 398 | }) 399 | debug(`Registered listener for ${evt} on ${label}`) 400 | }) 401 | }, 402 | serverApiMethods ({ ctx, socket, api, label, ioApiProp, ioDataProp }) { 403 | Object.entries(api.methods).forEach(([fn, schema]) => { 404 | const { msg: msgT, resp: respT } = schema 405 | if (ctx[ioDataProp][fn] === undefined) { 406 | ctx.$set(ctx[ioDataProp], fn, {}) 407 | if (msgT !== undefined) { 408 | ctx.$set(ctx[ioDataProp][fn], 'msg', { ...msgT }) 409 | } 410 | 411 | if (respT !== undefined) { 412 | ctx.$set( 413 | ctx[ioDataProp][fn], 414 | 'resp', 415 | Array.isArray(respT) ? [] : {} 416 | ) 417 | } 418 | } 419 | 420 | ctx[ioApiProp][fn] = async (args) => { 421 | const emitEvt = fn 422 | const msg = args !== undefined ? args : { ...ctx[ioDataProp][fn].msg } 423 | debug(`${ioApiProp}:${label}: Emitting ${emitEvt} with ${msg}`) 424 | const resp = await emit({ 425 | label, 426 | socket, 427 | evt: emitEvt, 428 | msg 429 | }) 430 | 431 | ctx[ioDataProp][fn].resp = resp 432 | return resp 433 | } 434 | }) 435 | }, 436 | async serverAPI ({ 437 | ctx, 438 | socket, 439 | label, 440 | apiIgnoreEvts, 441 | ioApiProp, 442 | ioDataProp, 443 | serverAPI, 444 | clientAPI = {} 445 | }) { 446 | const state = useNuxtSocket().value 447 | if (ctx[ioApiProp] === undefined) { 448 | console.error( 449 | `[nuxt-socket-io]: ${ioApiProp} needs to be defined in the current context for ` + 450 | 'serverAPI registration (vue requirement)' 451 | ) 452 | return 453 | } 454 | 455 | const apiLabel = serverAPI.label || label 456 | debug('register api for', apiLabel) 457 | const api = state.ioApis[apiLabel] || {} 458 | const fetchedApi = await emit({ 459 | label: apiLabel, 460 | socket, 461 | evt: serverAPI.evt || 'getAPI', 462 | msg: serverAPI.data || {} 463 | }) 464 | 465 | const isPeer = 466 | clientAPI.label === fetchedApi.label && 467 | parseFloat(clientAPI.version) === parseFloat(fetchedApi.version) 468 | if (isPeer) { 469 | Object.assign(api, clientAPI) 470 | mutations.SET_API(state, { label: apiLabel, api }) 471 | debug(`api for ${apiLabel} registered`, api) 472 | } else if (parseFloat(api.version) !== parseFloat(fetchedApi.version)) { 473 | Object.assign(api, fetchedApi) 474 | mutations.SET_API(state, { label: apiLabel, api }) 475 | debug(`api for ${apiLabel} registered`, api) 476 | } 477 | 478 | ctx.$set(ctx, ioApiProp, api) 479 | 480 | if (api.methods !== undefined) { 481 | register.serverApiMethods({ 482 | ctx, 483 | socket, 484 | api, 485 | label, 486 | ioApiProp, 487 | ioDataProp 488 | }) 489 | debug( 490 | `Attached methods for ${label} to ${ioApiProp}`, 491 | Object.keys(api.methods) 492 | ) 493 | } 494 | 495 | if (api.evts !== undefined) { 496 | register.serverApiEvents({ 497 | ctx, 498 | socket, 499 | api, 500 | label, 501 | ioDataProp, 502 | apiIgnoreEvts 503 | }) 504 | debug(`registered evts for ${label} to ${ioApiProp}`) 505 | } 506 | 507 | ctx.$set(ctx[ioApiProp], 'ready', true) 508 | debug('ioApi', ctx[ioApiProp]) 509 | }, 510 | emitErrors ({ ctx, err, emitEvt, emitErrorsProp }) { 511 | if (ctx[emitErrorsProp][emitEvt] === undefined) { 512 | ctx[emitErrorsProp][emitEvt] = [] 513 | } 514 | ctx[emitErrorsProp][emitEvt].push(err) 515 | }, 516 | /** 517 | * @param {*} info 518 | * @param {number} info.emitTimeout 519 | * @param {*} info.timerObj 520 | * @param {*} [info.ctx] 521 | * @param {string} [info.emitEvt] 522 | * @param {string} [info.emitErrorsProp] 523 | */ 524 | async emitTimeout ({ ctx, emitEvt, emitErrorsProp, emitTimeout, timerObj }) { 525 | const timedOut = await delay(emitTimeout, timerObj).catch(() => {}) 526 | if (!timedOut) { 527 | return 528 | } 529 | const err = { 530 | message: 'emitTimeout', 531 | emitEvt, 532 | emitTimeout, 533 | hint: [ 534 | `1) Is ${emitEvt} supported on the backend?`, 535 | `2) Is emitTimeout ${emitTimeout} ms too small?` 536 | ].join('\r\n'), 537 | timestamp: Date.now() 538 | } 539 | debug('emitEvt timed out', err) 540 | if (ctx !== undefined && typeof ctx[emitErrorsProp] === 'object') { 541 | register.emitErrors({ ctx, err, emitEvt, emitErrorsProp }) 542 | } else { 543 | throw err 544 | } 545 | }, 546 | emitBacks ({ ctx, socket, entries }) { 547 | entries.forEach((entry) => { 548 | const { pre, post, evt, mapTo } = parseEntry(entry, 'emitBack') 549 | if (propExists(ctx, mapTo)) { 550 | debug('registered local emitBack', { mapTo }) 551 | ctx.$watch(mapTo, async function (data, oldData) { 552 | debug('local data changed', evt, data) 553 | const preResult = await runHook(ctx, pre, { data, oldData }) 554 | if (preResult === false) { 555 | return 556 | } 557 | debug('Emitting back:', { evt, mapTo, data }) 558 | const p = socket.emitP(evt, { data }) 559 | if (post === undefined) { 560 | return 561 | } 562 | const resp = await p 563 | runHook(ctx, post, resp) 564 | return resp 565 | }) 566 | } else { 567 | warn(`Specified emitback ${mapTo} is not defined in component`) 568 | } 569 | }) 570 | }, 571 | emitters ({ ctx, socket, entries, emitTimeout, emitErrorsProp }) { 572 | entries.forEach((entry) => { 573 | const { pre, post, mapTo, emitEvt, msgLabel } = parseEntry( 574 | entry, 575 | 'emitter' 576 | ) 577 | ctx[emitEvt] = async function (msg = assignMsg(ctx, msgLabel)) { 578 | debug('Emit evt', { emitEvt, msg }) 579 | const preResult = await runHook(ctx, pre, msg) 580 | if (preResult === false) { 581 | return 582 | } 583 | const p = [socket.emitP(emitEvt, msg)] 584 | const timerObj = {} 585 | if (emitTimeout) { 586 | p.push(register 587 | .emitTimeout({ 588 | ctx, 589 | emitEvt, 590 | emitErrorsProp, 591 | emitTimeout, 592 | timerObj 593 | })) 594 | } 595 | const resp = await Promise.race(p) 596 | debug('Emitter response rxd', { emitEvt, resp }) 597 | if (timerObj.abort) { 598 | timerObj.abort() 599 | } 600 | const { emitError, ...errorDetails } = resp || {} 601 | if (emitError !== undefined) { 602 | const err = { 603 | message: emitError, 604 | emitEvt, 605 | errorDetails, 606 | timestamp: Date.now() 607 | } 608 | debug('Emit error occurred', err) 609 | if (typeof ctx[emitErrorsProp] === 'object') { 610 | register.emitErrors({ 611 | ctx, 612 | err, 613 | emitEvt, 614 | emitErrorsProp 615 | }) 616 | } else { 617 | throw err 618 | } 619 | } else { 620 | assignResp(ctx.$data || ctx, mapTo, resp) 621 | runHook(ctx, post, resp) 622 | return resp 623 | } 624 | } 625 | debug('Emitter created', { emitter: emitEvt }) 626 | }) 627 | }, 628 | listeners ({ ctx, socket, entries }) { 629 | entries.forEach((entry) => { 630 | const { pre, post, evt, mapTo } = parseEntry(entry) 631 | debug('Registered local listener', evt) 632 | socket.on(evt, async (resp) => { 633 | debug('Local listener received data', { evt, resp }) 634 | await runHook(ctx, pre) 635 | assignResp(ctx.$data || ctx, mapTo, resp) 636 | runHook(ctx, post, resp) 637 | }) 638 | }) 639 | }, 640 | namespace ({ ctx, namespaceCfg, socket, emitTimeout, emitErrorsProp }) { 641 | const { emitters = [], listeners = [], emitBacks = [] } = namespaceCfg 642 | const sets = { emitters, listeners, emitBacks } 643 | Object.entries(sets).forEach(([setName, entries]) => { 644 | if (Array.isArray(entries)) { 645 | register[setName]({ ctx, socket, entries, emitTimeout, emitErrorsProp }) 646 | } else { 647 | warn( 648 | `[nuxt-socket-io]: ${setName} needs to be an array in namespace config` 649 | ) 650 | } 651 | }) 652 | }, 653 | iox ({ stateOpts, socket, useSocket }) { 654 | debug('register.iox', stateOpts) 655 | const iox = ioState().value 656 | const entryRegex = /\s*<*-->*\s*/ 657 | const toRegex = /\s*[^<]-->\s*/ 658 | const fromRegex = /\s*<--[^>]\s*/ 659 | 660 | stateOpts.forEach((entry) => { 661 | let [evt, dest] = entry.split(entryRegex) 662 | let nsp 663 | if (dest === undefined) { 664 | dest = evt 665 | } 666 | 667 | const destParts = dest.split('/') 668 | if (destParts.length > 1) { 669 | nsp = destParts[0] 670 | if (iox[nsp] === undefined) { 671 | iox[nsp] = {} 672 | } 673 | dest = destParts[1] 674 | } 675 | 676 | function receiveEvt () { 677 | socket.on(evt, (msg) => { 678 | debug('iox evt received', evt, msg) 679 | if (nsp) { 680 | iox[nsp][dest] = msg 681 | debug('iox evt saved', evt, `${nsp}/${dest}`) 682 | } else { 683 | iox[dest] = msg 684 | debug('iox evt saved', evt, dest) 685 | } 686 | }) 687 | } 688 | 689 | function emitBack () { 690 | const watchPath = nsp ? `${nsp}/${dest}` : dest 691 | if (useSocket.registeredWatchers.includes(watchPath)) { 692 | return 693 | } 694 | 695 | watch(() => nsp 696 | ? iox[nsp][dest] 697 | : iox[dest], (n, o) => { 698 | socket.emit(evt, n) 699 | }) 700 | useSocket.registeredWatchers.push(watchPath) 701 | } 702 | 703 | if (toRegex.test(entry)) { 704 | receiveEvt() 705 | } else if (fromRegex.test(entry)) { 706 | emitBack() 707 | } else { 708 | receiveEvt() 709 | emitBack() 710 | } 711 | }) 712 | }, 713 | /** 714 | * @param {import('@nuxt/types').Context } ctx 715 | * @param {import('socket.io-client').Socket} socket 716 | * @param {string} connectUrl 717 | * @param {string} statusProp 718 | */ 719 | socketStatus (ctx, socket, connectUrl, statusProp) { 720 | const socketStatus = { connectUrl } 721 | const clientEvts = [ 722 | 'connect_error', 723 | 'connect_timeout', 724 | 'reconnect', 725 | 'reconnect_attempt', 726 | 'reconnect_error', 727 | 'reconnect_failed', 728 | 'ping', 729 | 'pong' 730 | ] 731 | clientEvts.forEach((evt) => { 732 | const prop = camelCase(evt) 733 | socketStatus[prop] = '' 734 | // @ts-ignore 735 | socket.io.on(evt, 736 | /** @param {*} resp */ 737 | (resp) => { 738 | Object.assign(ctx[statusProp], { [prop]: resp }) 739 | }) 740 | }) 741 | Object.assign(ctx, { [statusProp]: socketStatus }) 742 | }, 743 | teardown ({ ctx, socket, useSocket }) { 744 | // Setup listener for "closeSockets" in case 745 | // multiple instances of nuxtSocket exist in the same 746 | // component (only one destroy/unmount event takes place). 747 | // When we teardown, we want to remove the listeners of all 748 | // the socket.io-client instances 749 | ctx.$once('closeSockets', function () { 750 | debug('closing socket id=' + socket.id) 751 | socket.removeAllListeners() 752 | socket.close() 753 | }) 754 | 755 | if (!ctx.registeredTeardown) { 756 | // ctx.$destroy is defined in vue2 757 | // but will go away in vue3 (in favor of onUnmounted) 758 | // save user's destroy method and honor it after 759 | // we run nuxt-socket-io's teardown 760 | ctx.onComponentDestroy = ctx.$destroy || ctx.onUnmounted 761 | debug('teardown enabled for socket', { name: useSocket.name }) 762 | // Our $destroy method 763 | // Gets called automatically on the destroy lifecycle 764 | // in v2. In v3, we have call it with the 765 | // onUnmounted hook 766 | ctx.$destroy = function () { 767 | debug('component destroyed, closing socket(s)', { 768 | name: useSocket.name, 769 | url: useSocket.url 770 | }) 771 | useSocket.registeredVuexListeners = [] 772 | ctx.$emit('closeSockets') 773 | // Only run the user's destroy method 774 | // if it exists 775 | if (ctx.onComponentDestroy) { 776 | ctx.onComponentDestroy() 777 | } 778 | } 779 | 780 | // onUnmounted will only exist in v3 781 | if (ctx.onUnmounted) { 782 | ctx.onUnmounted = ctx.$destroy 783 | } 784 | ctx.registeredTeardown = true 785 | } 786 | 787 | socket.on('disconnect', () => { 788 | debug('server disconnected', { name: useSocket.name, url: useSocket.url }) 789 | socket.close() 790 | }) 791 | }, 792 | /** 793 | * @param {import('vue/types/vue').Vue } ctx 794 | */ 795 | stubs (ctx) { 796 | // Use a tiny event bus now. Can probably 797 | // be replaced by watch eventually. For now this works. 798 | if (!('$on' in ctx)) { // || !ctx.$emit || !ctx.$once) { 799 | ctx.$once = (...args) => emitter.once(...args) 800 | ctx.$on = (...args) => emitter.on(...args) 801 | ctx.$off = (...args) => emitter.off(...args) 802 | ctx.$$emit = (...args) => emitter.emit(...args) // TBD: replace ctx.$emit with ctx.$$emit calls (Vue already defines ctx.$emit). ctx.$$emit for internal use 803 | } 804 | 805 | if (!('$set' in ctx)) { 806 | ctx.$set = (obj, key, val) => { 807 | if (isRefImpl(obj[key])) { 808 | obj[key].value = val 809 | } else { 810 | obj[key] = val 811 | } 812 | } 813 | } 814 | 815 | if (!('$watch' in ctx)) { // TBD: seems to be defined in Nuxt3 ? 816 | ctx.$watch = (label, cb) => { 817 | // will enable when '@nuxtjs/composition-api' reaches stable version: 818 | // vueWatch(ctx.$data[label], cb) 819 | } 820 | } 821 | }, 822 | /** 823 | * Promisified emit 824 | */ 825 | emitP (socket) { 826 | socket.emitP = (evt, msg) => new Promise(resolve => 827 | socket.emit(evt, msg, resolve) 828 | ) 829 | }, 830 | /** 831 | * Promisified once. 832 | * Where's the promisified on? Answer: doesn't 833 | * really fit in to the websocket design. 834 | */ 835 | onceP (socket) { 836 | socket.onceP = evt => new Promise(resolve => 837 | socket.once(evt, resolve) 838 | ) 839 | } 840 | } 841 | 842 | /** 843 | * @param {import('./types.d').NuxtSocketOpts} ioOpts 844 | */ 845 | function nuxtSocket (ioOpts) { 846 | const { 847 | name, 848 | channel = '', 849 | statusProp = 'socketStatus', 850 | persist, 851 | teardown = !persist, 852 | emitTimeout, 853 | emitErrorsProp = 'emitErrors', 854 | ioApiProp = 'ioApi', 855 | ioDataProp = 'ioData', 856 | apiIgnoreEvts = [], 857 | serverAPI, 858 | clientAPI, 859 | vuex, 860 | namespaceCfg, 861 | ...connectOpts 862 | } = ioOpts 863 | const { $config } = this 864 | 865 | const { nuxtSocketIO: pluginOptions } = $config.public 866 | const state = useNuxtSocket().value 867 | 868 | const runtimeOptions = { ...pluginOptions } 869 | // If runtime config is also defined, 870 | // gracefully merge those options in here. 871 | // If naming conflicts extist between sockets, give 872 | // module options the priority 873 | if ($config.io) { 874 | Object.assign(runtimeOptions, $config.io) 875 | runtimeOptions.sockets = validateSockets(pluginOptions.sockets) 876 | ? pluginOptions.sockets 877 | : [] 878 | if (validateSockets($config.io.sockets)) { 879 | $config.io.sockets.forEach((socket) => { 880 | const fnd = runtimeOptions.sockets.find(({ name }) => name === socket.name) 881 | if (fnd === undefined) { 882 | runtimeOptions.sockets.push(socket) 883 | } 884 | }) 885 | } 886 | } 887 | 888 | const mergedOpts = { ...runtimeOptions, ...ioOpts } 889 | const { sockets, warnings = true, info = true } = mergedOpts 890 | 891 | warn = 892 | warnings && process.env.NODE_ENV !== 'production' ? console.warn : () => {} 893 | infoMsgs = 894 | info && process.env.NODE_ENV !== 'production' ? console.info : () => {} 895 | 896 | if (!validateSockets(sockets)) { 897 | throw new Error( 898 | "Please configure sockets if planning to use nuxt-socket-io: \r\n [{name: '', url: ''}]" 899 | ) 900 | } 901 | 902 | register.stubs(this) 903 | 904 | let useSocket = null 905 | 906 | if (!name) { 907 | useSocket = sockets.find(s => s.default === true) 908 | } else { 909 | useSocket = sockets.find(s => s.name === name) 910 | } 911 | 912 | if (!useSocket) { 913 | useSocket = sockets[0] 914 | } 915 | 916 | if (!useSocket.name) { 917 | useSocket.name = 'dflt' 918 | } 919 | 920 | if (!useSocket.url) { 921 | warn( 922 | `URL not defined for socket "${useSocket.name}". Defaulting to "window.location"` 923 | ) 924 | } 925 | 926 | if (!useSocket.registeredWatchers) { 927 | useSocket.registeredWatchers = [] 928 | } 929 | 930 | if (!useSocket.registeredVuexListeners) { 931 | useSocket.registeredVuexListeners = [] 932 | } 933 | 934 | let { url: connectUrl } = useSocket 935 | if (connectUrl) { 936 | connectUrl += channel 937 | } 938 | 939 | const { namespaces = {} } = useSocket 940 | 941 | let socket 942 | const label = 943 | persist && typeof persist === 'string' 944 | ? persist 945 | : `${useSocket.name}${channel}` 946 | 947 | function connectSocket () { 948 | if (connectUrl) { 949 | socket = io(connectUrl, connectOpts) 950 | infoMsgs('[nuxt-socket-io]: connect', useSocket.name, connectUrl, connectOpts) 951 | } else { 952 | socket = io(channel, connectOpts) 953 | infoMsgs( 954 | '[nuxt-socket-io]: connect', 955 | useSocket.name, 956 | window.location, 957 | channel, 958 | connectOpts 959 | ) 960 | } 961 | } 962 | 963 | if (persist) { 964 | if (_sockets[label]) { 965 | debug(`resuing persisted socket ${label}`) 966 | socket = _sockets[label] 967 | if (socket.disconnected) { 968 | debug('persisted socket disconnected, reconnecting...') 969 | connectSocket() 970 | } 971 | } else { 972 | debug(`socket ${label} does not exist, creating and connecting to it..`) 973 | connectSocket() 974 | _sockets[label] = socket 975 | } 976 | } else { 977 | connectSocket() 978 | } 979 | 980 | register.emitP(socket) 981 | register.onceP(socket) 982 | 983 | if (emitTimeout) { 984 | mutations.SET_EMIT_TIMEOUT(state, { label, emitTimeout }) 985 | } 986 | 987 | const mergedNspCfg = Object.assign({ ...namespaces[channel] }, namespaceCfg) 988 | if (mergedNspCfg.emitters || mergedNspCfg.listeners || mergedNspCfg.emitBacks) { 989 | register.namespace({ 990 | ctx: this, 991 | namespaceCfg: mergedNspCfg, 992 | socket, 993 | emitTimeout, 994 | emitErrorsProp 995 | }) 996 | debug('namespaces configured for socket', { 997 | name: useSocket.name, 998 | channel, 999 | namespaceCfg 1000 | }) 1001 | } 1002 | 1003 | if (serverAPI) { 1004 | register.serverAPI({ 1005 | label, 1006 | apiIgnoreEvts, 1007 | ioApiProp, 1008 | ioDataProp, 1009 | ctx: this, 1010 | socket, 1011 | serverAPI, 1012 | clientAPI 1013 | }) 1014 | } 1015 | 1016 | if (clientAPI) { 1017 | register.clientAPI({ 1018 | ctx: this, 1019 | socket, 1020 | clientAPI 1021 | }) 1022 | } 1023 | 1024 | const stateOpts = [...(useSocket.iox || []), ...(ioOpts.iox || [])] 1025 | if (stateOpts) { 1026 | register.iox({ stateOpts, socket, useSocket }) 1027 | } 1028 | 1029 | if ('socketStatus' in this && 1030 | typeof this.socketStatus === 'object' 1031 | ) { 1032 | register.socketStatus(this, socket, connectUrl || window.location.origin, statusProp) 1033 | debug('socketStatus registered for socket', { 1034 | name: useSocket.name, 1035 | url: connectUrl 1036 | }) 1037 | } 1038 | 1039 | if (teardown) { 1040 | register.teardown({ 1041 | ctx: this, 1042 | socket, 1043 | useSocket 1044 | }) 1045 | } 1046 | 1047 | return socket 1048 | } 1049 | 1050 | export default defineNuxtPlugin((nuxtApp) => { 1051 | nuxtApp.provide('nuxtSocket', nuxtSocket) 1052 | nuxtApp.provide('ioState', ioState) 1053 | }) 1054 | -------------------------------------------------------------------------------- /lib/standalone.js: -------------------------------------------------------------------------------- 1 | import '../.output/server/index.mjs' // <== "nuxi preview" 2 | import { server } from '../.output/server/chunks/nitro/server.mjs' 3 | import { register } from './module.js' 4 | 5 | register.server({ 6 | cors: { 7 | credentials: true, 8 | origin: [ 9 | 'http://localhost:3000' 10 | ] 11 | } 12 | }, server) 13 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nuxt/types'; 2 | import * as SocketIOClient from 'socket.io-client'; 3 | import Vue from 'vue'; 4 | 5 | /** 6 | * The format of each entry in mutations can be: 7 | * 8 | * 1. A single name string - the event name acts as the mutation 9 | * 2. A string with a double-dashed arrow - the left side of the arrow is the event name, the right side is the mutation 10 | */ 11 | type MutationNotation = string; 12 | 13 | /** 14 | * The format of each entry in actions can be: 15 | * 16 | * 1. A single name string - the event name acts as the action 17 | * 2. A string with a double-dashed arrow - the left side of the arrow is the event name, the right side is the action 18 | */ 19 | type ActionNotation = MutationNotation; 20 | 21 | /** 22 | * Similar to mutations and actions, but the placements of event names and mutations are reversed. 23 | * 24 | * The format of each entry in emitBacks can be: 25 | * 26 | * 1. A single name string - the event name acts as the mutation 27 | * 2. A string with a double-dashed arrow - the **right** side of the arrow is the event name, the **left** side is the mutation 28 | */ 29 | type EmitBackNotation = MutationNotation; 30 | 31 | 32 | /** 33 | * Options to let you sync incoming events to a Vuex store and emit events 34 | * on Vuex store changes. These options will override settings from your Nuxt 35 | * configuration. 36 | * https://nuxt-socket-io.netlify.app/configuration#vuex-options-per-socket 37 | */ 38 | interface NuxtSocketVueOptions { 39 | mutations?: Array; 40 | actions?: Array; 41 | emitBacks?: Array; 42 | } 43 | 44 | /** 45 | * The format of each entry in emitters can be: 46 | * 47 | * 1. A single name string - the method name on [this] component that emits an event of the same name 48 | * 2. A string with the following format: 49 | * 'preEmit hook] componentMethod + msg --> componentProp [postRx hook' 50 | * 51 | * Hooks are optional. calling this[componentMethod] will send the event [componentMethod] 52 | * with data this[msg]. It will save the response to this[componentProp] 53 | * If the preEmit hook returns false, emitting will be canceled. 54 | */ 55 | type EmitterNotation = string; 56 | 57 | /** 58 | * The format of each entry in listeners can be: 59 | * 60 | * 1. A single name string - the event name to listen to, whose data will be 61 | * saved in this[eventName] 62 | * 2. A string with the following format: 63 | * 'preHook] listenEvent --> componentProp [postRx hook' 64 | * 65 | * Hooks are optional. When listenEvent received, it will be saved to this[componentProp] 66 | */ 67 | type ListenerNotation = string; 68 | 69 | /** 70 | * Options to let you configure emitters, listeners and/or 71 | * emitBacks for a given namespace (a.k.a. "channel") 72 | * https://nuxt-socket-io.netlify.app/configuration#namespace-configuration 73 | */ 74 | interface NuxtSocketNspCfg { 75 | emitters?: Array; 76 | listeners?: Array; 77 | emitBacks?: Array; 78 | } 79 | 80 | /** 81 | * Kiss API format used by Nuxt Socket Dynamic API feature. 82 | * https://medium.com/swlh/nuxt-socket-io-the-magic-of-dynamic-api-registration-9af180383869 83 | */ 84 | interface NuxtSocketKissApi { 85 | label: string; 86 | version: number; 87 | evts?: Record; 88 | methods?: Record; 89 | } 90 | 91 | /** 92 | * Options to use for a socket.io server you want the module 93 | * to start 94 | * https://nuxt-socket-io.netlify.app/configuration#automatic-io-server-registration 95 | */ 96 | interface NuxtSocketIoServerOpts { 97 | /** 98 | * Path to IO service used for clients that connect to "/" 99 | * @default '[projectRoot]/server/io.js' 100 | */ 101 | ioSvc?: string; 102 | /** 103 | * Directory containing IO services for clients that connect 104 | * to the namespace matching the file name 105 | * Example: a file "namespace1.js" in this folder will listen 106 | * clients that connect to "/namespace1" 107 | * 108 | * @default '[projectRoot]/server/io' 109 | */ 110 | nspDir?: string; 111 | /** 112 | * Socket.io server host 113 | * @default 'localhost' 114 | */ 115 | host?: string; 116 | /** 117 | * Socket.io server port 118 | * @default 3000 119 | */ 120 | port?: number; 121 | /** 122 | * Auto close socket.io server (default: true) 123 | */ 124 | teardown?: boolean; 125 | } 126 | 127 | export interface NuxtSocketOpts extends Partial { 128 | /** Name of the socket. If omitted, the default socket will be used. */ 129 | name?: string; 130 | /** 131 | * The channel (a.k.a namespace) to connect to. 132 | * @default '' 133 | */ 134 | channel?: string; 135 | /** Specifies whether to enable or disable the "auto-teardown" feature 136 | * (see section below). 137 | * @default true 138 | */ 139 | teardown?: boolean; 140 | /** 141 | * Specifies whether to persist this socket so it can be reused 142 | * (see [vuexModule](https://nuxt-socket-io.netlify.app/vuexModule)). 143 | * @default false 144 | */ 145 | persist?: boolean | string; 146 | /** Specifies the property in [this] component that will be used 147 | * to contain the socket status (referring to an object). 148 | * @default 'socketStatus' 149 | */ 150 | statusProp?: string; 151 | /** Specifies the timeout in milliseconds for an emit event, 152 | * after which waiting for the emit response will be canceled. 153 | * @default undefined 154 | */ 155 | emitTimeout?: number; 156 | /** 157 | * Specifies the property in [this] component that will be used 158 | * to contain emit errors (see section below). 159 | * (referring to this.emitErrors, an object of arrays) 160 | * @default 'emitErrors' 161 | */ 162 | emitErrorsProp?: string; 163 | /** 164 | * Namespace config. Specifies emitters, listeners, and/or emitBacks. 165 | */ 166 | namespaceCfg?: NuxtSocketNspCfg; 167 | /** 168 | * @default 'ioApi' 169 | */ 170 | ioApiProp?: string; 171 | /** 172 | * @default 'ioData' 173 | */ 174 | ioDataProp?: string; 175 | /** 176 | * @default [] 177 | */ 178 | apiIgnoreEvts?: Array; 179 | serverAPI?: NuxtSocketKissApi; 180 | clientAPI?: NuxtSocketKissApi; 181 | vuex?: NuxtSocketVueOptions; 182 | } 183 | 184 | interface NuxtSocketConfig { 185 | /** 186 | * Recommended for all sockets, but required for any non-default socket. 187 | */ 188 | name?: string; 189 | /** 190 | * URL for the Socket.IO server. 191 | * @default window.location 192 | */ 193 | url?: string; 194 | /** 195 | * Determines which socket is used as the default when creating new sockets 196 | * with `nuxtSocket()` 197 | * @default true // for the first entry in the array 198 | */ 199 | default?: boolean; 200 | /** 201 | * Options to let you sync incoming events to a Vuex store and emit events 202 | * on Vuex store changes. These options will override settings from your Nuxt 203 | * configuration. 204 | */ 205 | vuex?: NuxtSocketVueOptions; 206 | /** 207 | * Socket.IO namespaces configuration. Supports an arrow syntax in each entry 208 | * to help describe the flow (with pre/post hook designation support too). 209 | */ 210 | namespaces?: Record; 211 | } 212 | 213 | interface NuxtSocketRuntimeConfig extends NuxtSocketConfig { 214 | /** 215 | * Name is required when using public/privateRuntimeConfig since the plugin 216 | * uses it to merge the configurations together. 217 | */ 218 | name: string; 219 | } 220 | 221 | interface NuxtSocketIoOptions { 222 | /** 223 | * Minimum one socket required. 224 | */ 225 | sockets: Array; 226 | 227 | /** 228 | * Options for starting a socket.io server 229 | * and automatically registering socket io service(s). 230 | * By default, registers services in 231 | * - [projectRoot]/server/io.js 232 | * - [projectRoot]/server/io/*.js 233 | */ 234 | server?: boolean | NuxtSocketIoServerOpts 235 | 236 | /** 237 | * Console warnings enabled/disabled 238 | * @default true 239 | */ 240 | warnings?: boolean; 241 | 242 | /** 243 | * Console info enabled/disabled 244 | * @default true 245 | */ 246 | info?: boolean; 247 | } 248 | 249 | interface NuxtSocketIoRuntimeOptions { 250 | /** 251 | * Minimum one socket required. 252 | */ 253 | sockets: Array; 254 | 255 | /** 256 | * Options for starting a socket.io server 257 | * and automatically registering socket io service(s). 258 | * By default, registers services in 259 | * - [projectRoot]/server/io.js 260 | * - [projectRoot]/server/io/*.js 261 | */ 262 | server?: boolean | NuxtSocketIoServerOpts 263 | 264 | /** 265 | * Console warnings enabled/disabled 266 | * @default true 267 | */ 268 | warnings?: boolean; 269 | 270 | /** 271 | * Console info enabled/disabled 272 | * @default true 273 | */ 274 | info?: boolean; 275 | } 276 | 277 | interface NuxtSocket extends SocketIOClient.Socket { 278 | emitP: (evt: String, msg?: any) => Promise; 279 | }; 280 | 281 | type Factory = (ioOpts: NuxtSocketOpts) => NuxtSocket; 282 | 283 | declare module 'vue/types/vue' { 284 | interface Vue { 285 | $nuxtSocket: Factory; 286 | } 287 | } 288 | 289 | declare module '@nuxt/types' { 290 | interface Configuration { 291 | /** 292 | * nuxt-socket-io configuration. 293 | * See https://nuxt-socket-io.netlify.app/configuration 294 | * for documentation. 295 | */ 296 | io?: NuxtSocketIoOptions; 297 | } 298 | 299 | interface NuxtOptionsRuntimeConfig { 300 | /** 301 | * nuxt-socket-io runtime configuration. 302 | * See https://nuxt-socket-io.netlify.app/configuration 303 | * for documentation. 304 | */ 305 | io?: NuxtSocketIoRuntimeOptions; 306 | } 307 | 308 | interface Context { 309 | $nuxtSocket: Factory; 310 | } 311 | } 312 | 313 | /* Nuxt 3 */ 314 | declare module '@nuxt/schema' { 315 | interface NuxtConfig { 316 | runtimeConfig?: { 317 | io?: NuxtSocketIoRuntimeOptions, 318 | public?: { 319 | io: NuxtSocketIoRuntimeOptions 320 | } 321 | }, 322 | io?: NuxtSocketIoOptions 323 | } 324 | } 325 | 326 | declare module '#app' { 327 | interface NuxtApp { 328 | $nuxtSocket: Factory; 329 | } 330 | } 331 | 332 | declare module '@vue/runtime-core' { 333 | interface ComponentCustomProperties { 334 | $nuxtSocket: Factory; 335 | } 336 | } 337 | /* --- */ 338 | 339 | export { NuxtSocket, NuxtSocketIoOptions, NuxtSocketIoRuntimeOptions } 340 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | 3 | export default defineNuxtConfig({ 4 | // vite: { 5 | // optimizeDeps: { 6 | // include: [ 7 | // '@socket.io/component-emitter', 8 | // // 'engine.io-client', 9 | // 'debug', 10 | // 'tiny-emitter/instance.js' 11 | // ] 12 | // } 13 | // }, 14 | server: { 15 | host: process.env.NODE_ENV === 'production' ? '0.0.0.0' : 'localhost', 16 | port: process.env.PORT !== undefined 17 | ? parseInt(process.env.PORT) 18 | : 3000 19 | }, 20 | telemetry: false, 21 | runtimeConfig: { 22 | io: { 23 | sockets: [ 24 | { 25 | name: 'privateSocket', 26 | url: 'url2' 27 | } 28 | ] 29 | }, 30 | public: { 31 | io: { 32 | sockets: [ 33 | { 34 | name: 'publicSocket', 35 | url: 'url1' 36 | } 37 | ] 38 | } 39 | } 40 | }, 41 | /* 42 | ** Global CSS 43 | */ 44 | css: [ 45 | '~/assets/bootstrap.min.css', 46 | '~/assets/main.css' 47 | ], 48 | /* 49 | ** Plugins to load before mounting the App 50 | */ 51 | plugins: [], 52 | /* 53 | ** Nuxt.js dev-modules 54 | */ 55 | buildModules: [ 56 | // '@nuxtjs/composition-api' 57 | ], 58 | /* 59 | ** Nuxt.js modules 60 | */ 61 | modules: [ 62 | // Doc: https://bootstrap-vue.js.org 63 | // 'bootstrap-vue/nuxt', 64 | '~/lib/module.js' 65 | ], 66 | io: { 67 | server: { 68 | // @ts-ignore 69 | // redisClient: true // uncomment to start redisClient 70 | // cors: { 71 | // credentials: true, 72 | // origin: [ 73 | // 'https://nuxt-socket-io.netlify.app', 74 | // 'http://localhost:3000' // TBD: added 75 | // ] 76 | // } 77 | }, 78 | sockets: [ 79 | { 80 | name: 'home', 81 | url: 82 | process.env.NODE_ENV === 'production' 83 | ? 'https://nuxt-socket-io.herokuapp.com' 84 | : 'http://localhost:3000', // Updated, //'http://localhost:3001', // Updated, 85 | // @ts-ignore 86 | iox: [ 87 | 'chatMessage --> chats/message', 88 | 'progress --> examples/progress', 89 | 'examples/sample <-- examples/sample', 90 | 'examples/someObj', // Bidirectional 91 | 'bidirectional' 92 | ], 93 | vuex: { 94 | mutations: ['progress --> examples/SET_PROGRESS'], 95 | actions: ['chatMessage --> io/FORMAT_MESSAGE'], 96 | emitBacks: [ 97 | 'examples/someObj', 98 | 'examples/sample', 99 | 'sample2 <-- examples/sample2', 100 | 'io/titleFromUser' // TBD: update tests 101 | ] 102 | }, 103 | namespaces: { 104 | '/index': { 105 | emitters: ['getMessage2 + testMsg --> message2Rxd'], 106 | listeners: ['chatMessage2', 'chatMessage3 --> message3Rxd'] 107 | }, 108 | '/examples': { 109 | emitBacks: ['sample3', 'sample4 <-- myObj.sample4'], 110 | emitters: [ 111 | 'reset] getProgress + refreshInfo --> progress [handleDone' 112 | ], 113 | listeners: ['progress'] 114 | } 115 | } 116 | }, 117 | { 118 | name: 'chatSvc', // TBD: redundant? 119 | url: 120 | process.env.NODE_ENV === 'production' 121 | ? 'https://nuxt-socket-io.herokuapp.com' 122 | : 'http://localhost:3001' 123 | }, 124 | { name: 'goodSocket', url: 'http://localhost:3001' }, 125 | { name: 'badSocket', url: 'http://localhost:3002' }, 126 | { name: 'work', url: 'http://somedomain1:3000' }, 127 | { name: 'car', url: 'http://somedomain2:3000' }, 128 | { name: 'tv', url: 'http://somedomain3:3000' }, 129 | { 130 | name: 'test', 131 | url: 'http://localhost:4000', 132 | vuex: { 133 | mutations: ['progress --> examples/SET_PROGRESS'], 134 | actions: ['chatMessage --> FORMAT_MESSAGE'], 135 | emitBacks: ['examples/sample', 'sample2 <-- examples/sample2'] 136 | } 137 | } 138 | ] 139 | } 140 | }) 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-socket-io", 3 | "version": "3.0.13", 4 | "description": "Socket.io client and server module for Nuxt. Just plug it in and GO", 5 | "author": "Richard Schloss", 6 | "type": "module", 7 | "main": "lib/module.js", 8 | "types": "lib/types.d.ts", 9 | "license": "MIT", 10 | "contributors": [ 11 | { 12 | "name": "Richard Schloss" 13 | } 14 | ], 15 | "keywords": [ 16 | "nuxt", 17 | "socket.io", 18 | "socket.io-client", 19 | "vue", 20 | "vuejs", 21 | "easy" 22 | ], 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "repository": "https://github.com/richardeschloss/nuxt-socket-io", 27 | "scripts": { 28 | "dev": "nuxi dev", 29 | "build": "nuxi build && echo 'export { server }' >> .output/server/chunks/nitro/server.mjs", 30 | "start": "nuxi preview", 31 | "cleanup": "nuxi cleanup", 32 | "standalone": "node --experimental-loader=./test/utils/loaders.js lib/standalone.js", 33 | "generate:local": "nuxi generate", 34 | "generate:gh-pages": "cross-env nuxt generate", 35 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", 36 | "test": "ava --colors --watch --timeout=10m", 37 | "test:cov": "c8 ava --timeout=10m" 38 | }, 39 | "files": [ 40 | "lib", 41 | "utils" 42 | ], 43 | "imports": { 44 | "#root/*": "./*", 45 | "#app": "./test/utils/plugin.js" 46 | }, 47 | "dependencies": { 48 | "@nuxt/types": "^2.15.5", 49 | "glob": "^7.1.7", 50 | "socket.io": "4.1.1", 51 | "socket.io-client": "4.1.1", 52 | "tiny-emitter": "^2.1.0" 53 | }, 54 | "devDependencies": { 55 | "@nuxtjs/eslint-config": "^6.0.1", 56 | "ava": "^3.15.0", 57 | "browser-env": "^3.3.0", 58 | "c8": "^7.10.0", 59 | "eslint": "^7.32.0", 60 | "jsdom": "^16.7.0", 61 | "jsdom-global": "^3.0.2", 62 | "les-utils": "^2.0.4", 63 | "nuxt": "^3.0.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pages/composition.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /pages/examples.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /pages/ioApi.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 136 | -------------------------------------------------------------------------------- /pages/ioStatus.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 41 | 42 | 51 | -------------------------------------------------------------------------------- /pages/rooms.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 35 | 36 | 50 | -------------------------------------------------------------------------------- /pages/rooms/[room].vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 76 | 77 | 85 | -------------------------------------------------------------------------------- /pages/rooms/[room]/[channel].vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 118 | 119 | 132 | -------------------------------------------------------------------------------- /server/apis.js: -------------------------------------------------------------------------------- 1 | /* Schemas */ 2 | export const ChatMsg = { 3 | timestamp: Date.now(), 4 | from: '', 5 | to: '', 6 | inputMsg: '' 7 | } 8 | 9 | export const User = { 10 | name: '' 11 | } 12 | export const Users = [User] 13 | 14 | export const Channel = { 15 | name: '', 16 | chats: [ChatMsg], 17 | users: [User] 18 | } 19 | 20 | export const Channels = [Channel] 21 | 22 | export const Room = { 23 | name: '', 24 | channels: [], 25 | users: Users 26 | } 27 | 28 | export const Rooms = [Room] 29 | 30 | export const Schemas = { 31 | ChatMsg, 32 | Channel, 33 | Channels, 34 | Room, 35 | Rooms 36 | } 37 | 38 | -------------------------------------------------------------------------------- /server/db.js: -------------------------------------------------------------------------------- 1 | const rooms = [ 2 | { 3 | name: 'vueJS', 4 | channels: [ 5 | { 6 | name: 'general', 7 | chats: [], 8 | users: [] 9 | }, 10 | { 11 | name: 'funStuff', 12 | chats: [], 13 | users: [] 14 | } 15 | ] 16 | }, 17 | { 18 | name: 'vuex', 19 | channels: [ 20 | { 21 | name: 'general', 22 | chats: [], 23 | users: [] 24 | } 25 | ] 26 | }, 27 | { 28 | name: 'nuxtJS', 29 | channels: [ 30 | { 31 | name: 'general', 32 | chats: [], 33 | users: [] 34 | }, 35 | { 36 | name: 'help', 37 | chats: [], 38 | users: [] 39 | }, 40 | { 41 | name: 'jobs', 42 | chats: [], 43 | users: [] 44 | } 45 | ] 46 | } 47 | ] 48 | 49 | export default { 50 | rooms 51 | } 52 | -------------------------------------------------------------------------------- /server/io.bad1.js: -------------------------------------------------------------------------------- 1 | export function NotDefault() {} 2 | -------------------------------------------------------------------------------- /server/io.bad2.js: -------------------------------------------------------------------------------- 1 | export default { 2 | svc: 'not a function' 3 | } 4 | -------------------------------------------------------------------------------- /server/io.js: -------------------------------------------------------------------------------- 1 | export default function (socket, io) { 2 | return { 3 | getNamespaces () { 4 | return Object.keys(io.nsps) 5 | }, 6 | echo (msg) { 7 | return msg 8 | }, 9 | echoUndef () { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/io.mjs: -------------------------------------------------------------------------------- 1 | export default function Svc() { 2 | return Object.freeze({ 3 | mjsTest() { 4 | return 'hi from io.mjs' 5 | } 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /server/io.ts: -------------------------------------------------------------------------------- 1 | export default function Svc() { 2 | return Object.freeze({ 3 | tsTest() { 4 | return 'hi from io.ts' 5 | } 6 | }) 7 | } -------------------------------------------------------------------------------- /server/io/channel.js: -------------------------------------------------------------------------------- 1 | import { ChatMsg } from '../apis.js' 2 | import RoomSvc from './room.js' 3 | 4 | const API = { 5 | version: 1.0, 6 | evts: { 7 | chat: { 8 | data: ChatMsg 9 | }, 10 | users: { 11 | data: [''] 12 | }, 13 | userJoined: { 14 | data: '' 15 | }, 16 | userLeft: { 17 | data: '' 18 | } 19 | }, 20 | methods: { 21 | join: { 22 | msg: { 23 | room: '', 24 | channel: '', 25 | user: '' 26 | }, 27 | resp: { 28 | room: '', 29 | channel: '', 30 | chats: [ChatMsg] 31 | } 32 | }, 33 | leave: { 34 | msg: { 35 | room: '', 36 | channel: '', 37 | user: '' 38 | } 39 | }, 40 | sendMsg: { 41 | msg: { 42 | room: '', 43 | channel: '', 44 | inputMsg: '', 45 | user: '' 46 | } 47 | } 48 | } 49 | } 50 | 51 | const roomSvc = RoomSvc() 52 | const chatLimit = 100 53 | 54 | export default function (socket, io) { 55 | const channelSvc = Object.freeze({ 56 | getAPI () { 57 | return API 58 | }, 59 | getChannel (room, channel) { 60 | const fndRoom = roomSvc.getRoom({ room }) 61 | if (fndRoom.channels === undefined) { 62 | throw new Error(`Channels not found in ${room}`) 63 | } 64 | const fndChannel = fndRoom.channels.find(({ name }) => name === channel) 65 | if (fndChannel === undefined) { 66 | throw new Error(`Channel ${channel} not found in room ${room}`) 67 | } 68 | return fndChannel 69 | }, 70 | join ({ room, channel, user }) { 71 | const fndChannel = channelSvc.getChannel(room, channel) 72 | 73 | return new Promise((resolve, reject) => { 74 | if (!fndChannel.users.includes(user)) { 75 | fndChannel.users.push(user) 76 | } 77 | 78 | const { users, chats } = fndChannel 79 | const namespace = `rooms/${room}/${channel}` 80 | // socket.io v3: socket.join now synchronous 81 | socket.join(namespace) 82 | socket.to(namespace).emit('userJoined', { data: user }) 83 | socket.to(namespace).emit('users', { data: users }) 84 | socket.emit('users', { data: users }) 85 | socket.once('disconnect', () => { 86 | channelSvc.leave({ room, channel, user }) 87 | }) 88 | }) 89 | }, 90 | leave ({ room, channel, user }) { 91 | const fndChannel = channelSvc.getChannel(room, channel) 92 | return new Promise((resolve, reject) => { 93 | if (fndChannel.users.includes(user)) { 94 | const userIdx = fndChannel.users.findIndex(u => u === user) 95 | fndChannel.users.splice(userIdx, 1) 96 | } 97 | 98 | const { users } = fndChannel 99 | const namespace = `rooms/${room}/${channel}` 100 | // socket.io v3: socket.leave now synchronous 101 | socket.leave(namespace) 102 | socket.to(namespace).emit('userLeft', { data: user }) 103 | socket.to(namespace).emit('users', { data: users }) 104 | socket.emit('users', { data: users }) 105 | }) 106 | }, 107 | sendMsg ({ inputMsg, room, channel, user }) { 108 | if (!inputMsg || inputMsg === '') { 109 | throw new Error('no input msg rxd') 110 | } 111 | const fndChannel = channelSvc.getChannel(room, channel) 112 | const chatMsg = { 113 | user, 114 | inputMsg, 115 | timestamp: Date.now() 116 | } 117 | if (fndChannel.chats.length > chatLimit) { 118 | fndChannel.chats = fndChannel.chats.splice(Math.floor(chatLimit / 2)) 119 | } 120 | 121 | fndChannel.chats.push(chatMsg) 122 | const namespace = `rooms/${room}/${channel}` 123 | socket.to(namespace).emit('chat', { data: chatMsg }) 124 | socket.emit('chat', { data: chatMsg }) 125 | return Promise.resolve() 126 | } 127 | }) 128 | 129 | return channelSvc 130 | } 131 | -------------------------------------------------------------------------------- /server/io/chat.js: -------------------------------------------------------------------------------- 1 | export default function(socket, io) { 2 | return { 3 | echo(msg) { 4 | msg.data += ' from chat' 5 | return msg 6 | }, 7 | me: { 8 | info: 'I am not a function. Do not register' 9 | }, 10 | badRequest({ data }) { 11 | socket.emit('dataAck', data) 12 | throw new Error('double check format') 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/io/dynamic.js: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | const debug = Debug('nuxt-socket-io:dynamic') 3 | 4 | /* Schemas */ 5 | const Item = { 6 | id: '', 7 | name: '', 8 | desc: '' 9 | } 10 | 11 | /* API */ 12 | const api = { 13 | version: 1.02, 14 | evts: { 15 | ignoreMe: {}, 16 | someList: { 17 | data: [''] 18 | }, 19 | someList2: { 20 | methods: ['getList'], 21 | data: [''] 22 | }, 23 | itemRxd: { 24 | methods: ['getItems', 'toBeAdded'], 25 | data: { 26 | progress: 0, 27 | item: {} 28 | } 29 | }, 30 | msgRxd: { 31 | data: { 32 | date: new Date(), 33 | msg: '' 34 | } 35 | } 36 | }, 37 | methods: { 38 | getItems: { 39 | resp: [Item] 40 | }, 41 | getItem: { 42 | msg: { 43 | id: '' 44 | }, 45 | resp: Item 46 | }, 47 | getList: { 48 | resp: [{ user: '' }] 49 | }, 50 | noResp: {} 51 | } 52 | } 53 | 54 | /* SVC */ 55 | export default function (socket) { 56 | return Object.freeze({ 57 | getAPI (data) { 58 | socket.emit('getAPI', {}, (clientApi) => { 59 | socket.emit( 60 | 'receiveMsg', 61 | { 62 | date: new Date(), 63 | from: 'server1', 64 | to: 'client1', 65 | text: 'Hi client from server!' 66 | }, 67 | (resp) => { 68 | debug('receiveMsg response', resp) 69 | } 70 | ) 71 | socket.on('warnings', (msg) => { 72 | debug('warnings from client', msg) 73 | }) 74 | }) 75 | return Promise.resolve(api) 76 | }, 77 | 78 | /* Methods */ 79 | getItems () { 80 | const items = Array(4) 81 | let idx = 0 82 | return new Promise((resolve) => { 83 | const timer = setInterval(() => { 84 | items[idx] = { 85 | id: `item${idx}`, 86 | name: `Some Item ${idx}`, 87 | desc: `Some description ${idx}` 88 | } 89 | const item = items[idx] 90 | socket.emit('itemRxd', { 91 | method: 'getItems', 92 | data: { 93 | progress: ++idx / items.length, 94 | item 95 | } 96 | }) 97 | socket.emit('itemRxd', { 98 | method: 'toBeDone', 99 | data: {} 100 | }) 101 | if (idx >= items.length) { 102 | clearInterval(timer) 103 | resolve(items) 104 | } 105 | }, 500) 106 | }) 107 | }, 108 | getItem ({ id }) { 109 | const data = { 110 | date: new Date(), 111 | msg: id 112 | } 113 | socket.emit('msgRxd', { data }, (resp) => { 114 | debug('ack received', resp) 115 | }) 116 | const ItemOut = Object.assign( 117 | { ...Item }, 118 | { 119 | id, 120 | name: 'Some Item', 121 | desc: 'Some description' 122 | } 123 | ) 124 | return ItemOut 125 | }, 126 | getList () { 127 | socket.emit('someList', ['user1']) 128 | socket.emit('someList2', ['user1', 'user2']) 129 | // return [{ user: 'user1' }] 130 | }, 131 | noResp () { 132 | return {} 133 | }, 134 | badRequest () { 135 | throw new Error('badRequest...Input does not match schema') 136 | } 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /server/io/examples.js: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | 3 | export const middlewares = { 4 | m1 (_, next) { 5 | consola.log('m1 in examples namespace') 6 | next() 7 | } 8 | } 9 | 10 | export default function Svc (socket, io) { 11 | return Object.freeze({ 12 | getProgress ({ period }) { 13 | return new Promise((resolve) => { 14 | let progress = 0 15 | const timer = setInterval(() => { 16 | progress += 10 17 | socket.emit('progress', progress) 18 | if (progress === 100) { 19 | clearInterval(timer) 20 | resolve(progress) 21 | } 22 | }, period) 23 | }) 24 | }, 25 | echoBack (msg = {}) { 26 | const { evt = 'echoBack', data = evt } = msg 27 | socket.emit(evt, data) 28 | return { evt, data } 29 | }, 30 | echoHello (data) { 31 | return { evt: 'echoHello', data: 'hello' } 32 | }, 33 | echoError () { 34 | throw new Error('ExampleError') 35 | }, 36 | 'examples/sample' (sample) { 37 | console.log('examples/sample rxd', sample) 38 | socket.emit('sampleDataRxd', { 39 | data: { 40 | msg: 'Sample data rxd on state change', 41 | sample 42 | } 43 | }) 44 | }, 45 | echo ({ evt, msg }) { 46 | socket.emit(evt, msg) 47 | }, 48 | 'examples/someObj' (data) { 49 | consola.log('someObj received!', data) 50 | socket.emit('examples/someObjRxd', data) 51 | return { msg: 'ok' } 52 | }, 53 | sample2 ({ data: sample }) { 54 | socket.emit('sample2DataRxd', { 55 | data: { 56 | msg: 'Sample2 data rxd on state change', 57 | sample 58 | } 59 | }) 60 | }, 61 | sample2b ({ data: sample }) { 62 | socket.emit('sample2bDataRxd', { 63 | data: { 64 | msg: 'Sample2b data rxd on state change', 65 | sample 66 | } 67 | }) 68 | }, 69 | sample3 (msg) { 70 | console.log('sample3', msg) 71 | const { data: sample } = msg || {} 72 | return { 73 | msg: 'rxd sample ' + (sample || 'undef') 74 | } 75 | }, 76 | sample4 ({ data: sample }) { 77 | console.log('sample4 rxd', sample) 78 | return { 79 | msg: 'rxd sample ' + sample 80 | } 81 | }, 82 | sample5 ({ data: sample }) { 83 | return { 84 | msg: 'rxd sample ' + sample 85 | } 86 | }, 87 | receiveArray (msg) { 88 | return { 89 | resp: 'Received array', 90 | length: msg.length 91 | } 92 | }, 93 | receiveArray2 (msg) { 94 | return { 95 | resp: 'Received array2', 96 | msg 97 | } 98 | }, 99 | receiveString (msg) { 100 | return { 101 | resp: 'Received string', 102 | length: msg.length 103 | } 104 | }, 105 | receiveString2 (msg) { 106 | return { 107 | resp: 'Received string again', 108 | length: msg.length 109 | } 110 | }, 111 | receiveUndef (msg) {} 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /server/io/index.js: -------------------------------------------------------------------------------- 1 | export default function Svc (socket, io) { 2 | return Object.freeze({ 3 | getItems (ids) { 4 | return ids 5 | }, 6 | getMessage (data) { 7 | return new Promise((resolve) => { 8 | const msgs = [ 9 | 'Hi, this is a chat message from IO server!', 10 | 'Hi, this is another chat message from IO server!' 11 | ] 12 | let msgIdx = 0 13 | const timer = setInterval(() => { 14 | socket.emit('chatMessage', msgs[msgIdx]) 15 | if (++msgIdx >= msgs.length) { 16 | clearInterval(timer) 17 | resolve('It worked! Received msg: ' + JSON.stringify(data)) 18 | } 19 | }, 500) 20 | }) 21 | }, 22 | getMessage2 (data) { 23 | return new Promise((resolve) => { 24 | const msgs = [ 25 | 'Hi, this is a chat message from IO server!', 26 | 'Hi, this is another chat message from IO server!' 27 | ] 28 | let msgIdx = 0 29 | socket.emit('chatMessage4', { data: 'Hi again' }) 30 | socket.emit('chatMessage5', { data: 'Hi again from 5' }) 31 | const timer = setInterval(() => { 32 | socket.emit('chatMessage2', msgs[msgIdx]) 33 | socket.emit('chatMessage3', 'sending chat message3...') 34 | if (++msgIdx >= msgs.length) { 35 | clearInterval(timer) 36 | resolve('It worked! Received msg: ' + JSON.stringify(data)) 37 | } 38 | }, 500) 39 | }) 40 | }, 41 | bidirectional () { 42 | console.log('bidirectional rxd!') 43 | }, 44 | echo (msg) { 45 | return msg 46 | }, 47 | echoBack ({ evt, data }) { 48 | socket.emit(evt, data) 49 | return { evt, data } 50 | }, 51 | echoHello () { 52 | return { evt: 'echoHello', data: 'hello' } 53 | }, 54 | echoError () { 55 | throw new Error('SomeError') 56 | }, 57 | echoUndefMsg (msg) { 58 | return msg 59 | }, 60 | titleFromUser (msg) { 61 | return { 62 | data: `received msg ${msg.name}!` 63 | } 64 | } 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /server/io/middlewares.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable no-console */ 3 | export const middlewares = { 4 | m1 (socket, next) { 5 | console.log('m1 hit') 6 | // Must call next: 7 | next() 8 | }, 9 | m2 (socket, next) { 10 | console.log('m2 hit') 11 | // Must call next: 12 | next() 13 | } 14 | } 15 | 16 | export const setIO = (io) => { 17 | // console.log('setIO') 18 | } 19 | 20 | export default function (socket, io) { 21 | return { 22 | getNamespaces () { 23 | return Object.keys(io.nsps) 24 | }, 25 | echo (msg) { 26 | console.log('echo rxd', msg) 27 | return msg 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/io/newfile.ts: -------------------------------------------------------------------------------- 1 | export default function(socket, io) { 2 | return Object.freeze({ 3 | hi() { 4 | return 'world' 5 | } 6 | }) 7 | } -------------------------------------------------------------------------------- /server/io/nsp.bad1.js: -------------------------------------------------------------------------------- 1 | export const Svc = {} 2 | -------------------------------------------------------------------------------- /server/io/nsp.bad2.js: -------------------------------------------------------------------------------- 1 | export function Svc() {} 2 | -------------------------------------------------------------------------------- /server/io/nsp.bad3.js: -------------------------------------------------------------------------------- 1 | function noExports () { 2 | return 'no exports here' 3 | } 4 | -------------------------------------------------------------------------------- /server/io/p2p.js: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | const debug = Debug('nuxt-socket-io:p2p') 3 | 4 | /* API */ 5 | const api = { 6 | label: 'ioApi_page', 7 | version: 1.31 8 | } 9 | 10 | /* SVC */ 11 | export default function Svc (socket) { 12 | return Object.freeze({ 13 | getAPI (msg) { 14 | debug('getAPI', msg) 15 | return Promise.resolve(api) 16 | }, 17 | 18 | /* Methods */ 19 | sendEvts (msg) { 20 | debug('sendEvts', msg) 21 | return new Promise((resolve) => { 22 | let doneCnt = 0 23 | const expCnt = 3 24 | const testMsg = { 25 | date: new Date(), 26 | from: 'server1', 27 | to: 'client1', 28 | text: 'Hi client from server!' 29 | } 30 | 31 | function handleResp (resp) { 32 | debug('received resp', resp) 33 | doneCnt++ 34 | if (doneCnt === expCnt) { 35 | resolve() 36 | } 37 | } 38 | socket.emit('getAPI') 39 | socket.emit('getAPI', {}, handleResp) 40 | socket.emit('receiveMsg', testMsg) 41 | socket.emit('receiveMsg', testMsg, handleResp) 42 | socket.emit('undefMethod', testMsg) 43 | socket.emit('undefMethod', testMsg, handleResp) 44 | }) 45 | }, 46 | 47 | warnings (msg) { 48 | debug('received warnings', msg) 49 | return msg 50 | }, 51 | 52 | receiveMsg (msg) { 53 | debug('[peer] receiveMsg', msg) 54 | return { 55 | status: 'ok' 56 | } 57 | } 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /server/io/room.js: -------------------------------------------------------------------------------- 1 | import Data from '../db.js' 2 | 3 | const API = { 4 | version: 1.0, 5 | evts: { 6 | users: { 7 | data: [''] 8 | }, 9 | userJoined: { 10 | data: '' 11 | }, 12 | userLeft: { 13 | data: '' 14 | } 15 | }, 16 | methods: { 17 | join: { 18 | msg: { 19 | room: '', 20 | user: '' 21 | }, 22 | resp: { 23 | room: '', 24 | channels: [''] 25 | } 26 | }, 27 | leave: { 28 | msg: { 29 | room: '', 30 | user: '' 31 | } 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * 38 | * @param {import('socket.io').Socket} [socket] 39 | * @param {import('socket.io').Server} [io] 40 | */ 41 | export default function Svc (socket, io) { 42 | const roomSvc = Object.freeze({ 43 | getAPI () { 44 | return API 45 | }, 46 | getRoom ({ room }) { 47 | if (room === undefined) { 48 | throw new Error('Room name not specified') 49 | } 50 | const fndRoom = Data.rooms.find(({ name }) => name === room) 51 | if (fndRoom === undefined) { 52 | throw new Error(`Room ${room} not found`) 53 | } 54 | return fndRoom 55 | }, 56 | join ({ room, user }) { 57 | const fndRoom = roomSvc.getRoom({ room }) 58 | if (!fndRoom.users) { 59 | fndRoom.users = [] 60 | } 61 | if (!fndRoom.users.includes(user)) { 62 | fndRoom.users.push(user) 63 | } 64 | 65 | const namespace = `rooms/${room}` 66 | socket.once('disconnect', () => { 67 | roomSvc.leave({ room, user }) 68 | }) 69 | 70 | // socket.io v3: socket.join now synchronous 71 | socket.join(namespace) 72 | socket.to(namespace).emit('userJoined', { data: user }) 73 | socket.to(namespace).emit('users', { data: fndRoom.users }) 74 | socket.emit('users', { data: fndRoom.users }) 75 | return { 76 | room, 77 | channels: fndRoom.channels.map(({ name }) => name) 78 | } 79 | }, 80 | leave ({ room, user }) { 81 | const fndRoom = roomSvc.getRoom({ room }) 82 | if (!fndRoom) { 83 | throw new Error(`room ${room} not found`) 84 | } 85 | 86 | if (fndRoom.users && fndRoom.users.includes(user)) { 87 | const userIdx = fndRoom.users.findIndex(u => u === user) 88 | fndRoom.users.splice(userIdx, 1) 89 | } 90 | 91 | const namespace = `rooms/${room}` 92 | // socket.io v3: socket.leave now synchronous 93 | socket.leave(namespace) 94 | socket.to(namespace).emit('userLeft', { data: user }) 95 | socket.to(namespace).emit('users', { data: fndRoom.users }) 96 | socket.emit('users', { data: fndRoom.users }) 97 | } 98 | }) 99 | return roomSvc 100 | } 101 | -------------------------------------------------------------------------------- /server/io/rooms.js: -------------------------------------------------------------------------------- 1 | import Data from '../db.js' 2 | 3 | const API = { 4 | version: 1.0, 5 | methods: { 6 | getRooms: { 7 | resp: [''] 8 | } 9 | } 10 | } 11 | 12 | export default function Svc (socket, io) { 13 | return Object.freeze({ 14 | getAPI () { 15 | return API 16 | }, 17 | getRooms (msg) { 18 | return Data.rooms.map(({ name }) => name) 19 | } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardeschloss/nuxt-socket-io/dea2bfdaf5bbda1ccec7446669a2e69c4b5e5d8c/static/favicon.ico -------------------------------------------------------------------------------- /test/Demos.spec.js: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register.js' 2 | import Vue from 'vue' 3 | import Vuex, { Store } from 'vuex' 4 | import ava from 'ava' 5 | import config from '../nuxt.config.js' 6 | import { register } from '../lib/module.js' 7 | import Plugin from '../lib/plugin.js' 8 | import * as store from '../store/index.js' 9 | import * as examplesStore from '../store/examples.js' 10 | import Messages from '../components/Messages.vue' 11 | 12 | const { serial: test, before } = ava 13 | 14 | Vue.use(Vuex) 15 | 16 | const mocks = ctx => ({ 17 | $store: new Store({ 18 | state: store.state(), 19 | mutations: store.mutations, 20 | actions: store.actions, 21 | modules: { 22 | examples: { 23 | namespaced: true, 24 | state: examplesStore.state() 25 | } 26 | } 27 | }), 28 | $config: { 29 | nuxtSocketIO: {}, 30 | io: config.io 31 | }, 32 | inject: (label, fn) => { 33 | ctx['$' + label] = fn 34 | } 35 | }) 36 | 37 | before('Start IO Server', async (t) => { 38 | await register.server({ port: 3000 }) 39 | }) 40 | 41 | test('Messages', async (t) => { 42 | const Comp = Vue.extend(Messages) 43 | const comp = new Comp() 44 | Object.assign(comp, mocks(comp)) 45 | Plugin(null, comp.inject) 46 | comp.$mount() 47 | await comp.getMessage() 48 | t.is(comp.messageRxd, 'It worked! Received msg: {"id":"abc123"}') 49 | t.true(comp.chatMessages.length > 0) 50 | const p = comp.socket.onceP('chatMessage') 51 | comp.getMessage() 52 | const r = await p 53 | t.is(r, 'Hi, this is a chat message from IO server!') 54 | 55 | t.truthy(comp.getMessage2) 56 | }) 57 | -------------------------------------------------------------------------------- /test/Module.spec.js: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import path from 'path' 3 | import consola from 'consola' 4 | import ava from 'ava' 5 | import ioClient from 'socket.io-client' 6 | import { delay } from 'les-utils/utils/promise.js' 7 | import Module, { register } from '../lib/module.js' 8 | import { getModuleOptions, initNuxt, useNuxt } from './utils/module.js' 9 | 10 | const { serial: test } = ava 11 | const srcDir = path.resolve('.') 12 | const { io } = getModuleOptions('io/module', 'io') 13 | 14 | const { listener: listen } = register 15 | 16 | async function send ({ 17 | host = 'localhost', 18 | port = 3000, 19 | nsp = '', 20 | evt = 'echo', 21 | msg = {}, 22 | listeners = [], 23 | notify = () => {}, 24 | emitTimeout = 1500 25 | }) { 26 | consola.log('connect', nsp) 27 | const socket = ioClient(`http://${host}:${port}${nsp}`) 28 | listeners.forEach((listenEvt) => { 29 | socket.on(listenEvt, (data) => { 30 | notify(listenEvt, data) 31 | }) 32 | }) 33 | socket.emit(evt, msg) 34 | await delay(emitTimeout) 35 | } 36 | 37 | function sendReceive ({ 38 | host = 'localhost', 39 | port = 3000, 40 | nsp = '', 41 | evt = 'echo', 42 | msg = {}, 43 | listeners = [], 44 | notify = () => {}, 45 | emitTimeout = 1500 46 | }) { 47 | return new Promise((resolve, reject) => { 48 | consola.log('connect', nsp) 49 | const socket = ioClient(`http://${host}:${port}${nsp}`) 50 | listeners.forEach((listenEvt) => { 51 | socket.on(listenEvt, (data) => { 52 | // @ts-ignore 53 | notify(listenEvt, data) 54 | }) 55 | }) 56 | socket.emit(evt, msg, 57 | /** 58 | * @param {any} resp 59 | */ 60 | (resp) => { 61 | socket.close() 62 | resolve(resp) 63 | }) 64 | setTimeout(() => { 65 | socket.close() 66 | reject(new Error('emitTimeout')) 67 | }, emitTimeout) 68 | }) 69 | } 70 | 71 | const waitForListening = server => new Promise( 72 | resolve => server.on('listening', resolve) 73 | ) 74 | 75 | const waitForClose = server => new Promise( 76 | resolve => server.listening ? server.on('close', resolve) : resolve() 77 | ) 78 | 79 | test('Register.server: server created if undef', async (t) => { 80 | const { io, server } = await register.server({}) 81 | t.truthy(server) 82 | t.true(server.listening) 83 | io.close() 84 | server.close() 85 | await waitForClose(server) 86 | }) 87 | 88 | test('Register.server: options undef', async (t) => { 89 | const { io, server } = await register.server() 90 | t.truthy(server) 91 | t.true(server.listening) 92 | io.close() 93 | server.close() 94 | await waitForClose(server) 95 | }) 96 | 97 | test('Register.server: server already listening', async (t) => { 98 | const serverIn = await listen() 99 | const { io, server } = await register.server({}, serverIn) 100 | t.true(server.listening) 101 | io.close() 102 | server.close() 103 | await waitForClose(server) 104 | }) 105 | 106 | test('Register.ioSvc (ioSvc does not exist)', async (t) => { 107 | const ioSvc = '/tmp/ioNotHere' 108 | const { io, server } = await register.server({ ioSvc }) 109 | const msg = { data: 'hello' } 110 | await sendReceive({ msg }).catch((err) => { 111 | t.is(err.message, 'emitTimeout') 112 | }) 113 | io.close() 114 | server.close() 115 | await waitForClose(server) 116 | }) 117 | 118 | test('Register.ioSvc (ioSvc exists, default export undefined)', async (t) => { 119 | const serverIn = http.createServer() 120 | const ioSvc = './server/io.bad1.js' 121 | const rootSvc = path.resolve(ioSvc) 122 | const { io, server, errs } = await register.server({ ioSvc }, serverIn) 123 | t.is( 124 | errs[0].message, 125 | `io service at ${rootSvc} does not export a default "Svc()" function. Not registering` 126 | ) 127 | io.close() 128 | server.close() 129 | await waitForClose(server) 130 | }) 131 | 132 | test('Register.ioSvc (ioSvc exists, default export not a function)', async (t) => { 133 | const serverIn = http.createServer() 134 | const ioSvc = './server/io.bad2.js' 135 | const rootSvc = path.resolve(ioSvc) 136 | const { io, server, errs } = await register.server({ ioSvc }, serverIn) 137 | t.is( 138 | errs[0].message, 139 | `io service at ${rootSvc} does not export a default "Svc()" function. Not registering` 140 | ) 141 | io.close() 142 | server.close() 143 | await waitForClose(server) 144 | }) 145 | 146 | test('Register.ioSvc (ioSvc exists, ok)', async (t) => { 147 | const serverIn = http.createServer() 148 | const ioSvc = './server/io' 149 | const { io, server } = await register.server({ ioSvc }, serverIn) 150 | const msg = { data: 'hello' } 151 | const resp = await sendReceive({ msg }) 152 | t.is(resp.data, msg.data) 153 | io.close() 154 | server.close() 155 | await waitForClose(server) 156 | }) 157 | 158 | test('Register.ioSvc (ioSvc exists, .ts extension)', async (t) => { 159 | const serverIn = http.createServer() 160 | const ioSvc = './server/io.ts' 161 | const { io, server } = await register.server({ ioSvc }, serverIn) 162 | const resp = await sendReceive({ evt: 'tsTest' }) 163 | t.is(resp, 'hi from io.ts') 164 | io.close() 165 | server.close() 166 | await waitForClose(server) 167 | }) 168 | 169 | test('Register.ioSvc (ioSvc exists, .mjs extension)', async (t) => { 170 | const serverIn = http.createServer() 171 | const ioSvc = './server/io.mjs' 172 | const { io, server } = await register.server({ ioSvc }, serverIn) 173 | const resp = await sendReceive({ evt: 'mjsTest' }) 174 | t.is(resp, 'hi from io.mjs') 175 | io.close() 176 | server.close() 177 | await waitForClose(server) 178 | }) 179 | 180 | test('Register.ioSvc (ioSvc exists, middlewares defined)', async (t) => { 181 | const serverIn = http.createServer() 182 | const ioSvc = './server/io/middlewares' 183 | const { io, server } = await register.server({ ioSvc }, serverIn) 184 | const msg = { data: 'hello' } 185 | const resp = await sendReceive({ msg }) 186 | t.is(resp.data, msg.data) 187 | io.close() 188 | server.close() 189 | await waitForClose(server) 190 | }) 191 | 192 | test('Register.ioSvc (ioSvc exists, ok, callback undef)', async (t) => { 193 | const serverIn = http.createServer() 194 | const ioSvc = './server/io' 195 | const { io, server } = await register.server({ ioSvc }, serverIn) 196 | const msg = { data: 'hello' } 197 | const resp = await send({ msg }) 198 | t.falsy(resp) 199 | io.close() 200 | server.close() 201 | await waitForClose(server) 202 | }) 203 | 204 | test('Register.ioSvc (ioSvc exists, ok, strip off ".js" ext)', async (t) => { 205 | const serverIn = http.createServer() 206 | const ioSvc = './server/io.js' 207 | const { io, server } = await register.server({ ioSvc }, serverIn) 208 | const msg = { data: 'hello' } 209 | const resp = await sendReceive({ msg }) 210 | t.is(resp.data, msg.data) 211 | io.close() 212 | server.close() 213 | await waitForClose(server) 214 | }) 215 | 216 | test('Register.nspSvc (nspDir does not exist)', async (t) => { 217 | const serverIn = http.createServer() 218 | const nspDir = '/tmp/io/notAvail' 219 | const { io, server } = await register.server({ ioSvc: '/tmp/ignore', nspDir }, serverIn) 220 | const msg = { data: 'hello' } 221 | await sendReceive({ nsp: '/chat', msg }).catch((err) => { 222 | t.is(err.message, 'emitTimeout') 223 | }) 224 | io.close() 225 | server.close() 226 | await waitForClose(server) 227 | }) 228 | 229 | test('Register.nspSvc (nspDir exists, some nsp malformed)', async (t) => { 230 | const serverIn = http.createServer() 231 | const nspDir = './server/io' 232 | const { io, server } = await register.server({ ioSvc: '/tmp/ignore', nspDir }, serverIn) 233 | const msg = { data: 'hello' } 234 | const resp = await sendReceive({ nsp: '/chat', msg }) 235 | t.is(resp.data, msg.data + ' from chat') 236 | const evt = 'badRequest' 237 | const resp2 = await sendReceive({ 238 | nsp: '/chat', 239 | evt, 240 | msg, 241 | listeners: ['dataAck'], 242 | notify (evt, data) { 243 | t.is(evt, 'dataAck') 244 | t.is(data, msg.data) 245 | } 246 | }) 247 | t.is(resp2.emitError, 'double check format') 248 | t.is(resp2.evt, evt) 249 | io.close() 250 | server.close() 251 | await waitForClose(server) 252 | }) 253 | 254 | test('Module: various options', async (t) => { 255 | const dirs = [] 256 | initNuxt() 257 | await Module({ 258 | server: false, 259 | sockets: [{ name: 'home', url: 'https://localhost:3000' }] 260 | }, useNuxt()) 261 | const nuxt1 = useNuxt() 262 | t.truthy(nuxt1.hooks['components:dirs']) 263 | t.truthy(nuxt1.hooks.listen) 264 | nuxt1.hooks['components:dirs'](dirs) 265 | nuxt1.hooks.listen(http.createServer()) 266 | t.is(dirs[0].path, path.resolve('./lib/components')) 267 | t.is(dirs[0].prefix, 'io') 268 | const [pluginInfo] = nuxt1.options.plugins 269 | t.is(path.resolve(pluginInfo.src), path.resolve(srcDir, 'lib/plugin.js')) 270 | const { nuxtSocketIO } = nuxt1.options.runtimeConfig.public 271 | t.is(nuxtSocketIO.sockets[0].name, 'home') 272 | t.is(nuxtSocketIO.sockets[0].url, 'https://localhost:3000') 273 | 274 | const serverInst = http.createServer() 275 | const p = waitForListening(serverInst) 276 | const p2 = waitForClose(serverInst) 277 | serverInst.listen() 278 | initNuxt() 279 | await Module({ 280 | ...io, 281 | server: { serverInst } 282 | }, useNuxt()) 283 | await p 284 | const nuxt2 = useNuxt() 285 | nuxt2.hooks.listen(http.createServer()) 286 | 287 | t.truthy(nuxt2.hooks.close) 288 | nuxt2.hooks.close() 289 | await p2 290 | }) 291 | 292 | test('Module: edge cases', async (t) => { 293 | initNuxt() 294 | // @ts-ignore 295 | process.env.PORT = 5000 296 | await Module({}, useNuxt()) 297 | useNuxt().hooks.listen(http.createServer()) 298 | await delay(100) 299 | 300 | /* console shows listening at 5001 */ 301 | await Module({}, useNuxt()) 302 | useNuxt().hooks.listen(http.createServer()) 303 | await delay(100) 304 | useNuxt().hooks.close() 305 | /* attempt to register server twice... error handler catches it (in coverage report) */ 306 | t.pass() 307 | }) 308 | -------------------------------------------------------------------------------- /test/Plugin.spec.js: -------------------------------------------------------------------------------- 1 | import ava from 'ava' 2 | import { delay } from 'les-utils/utils/promise.js' 3 | import { register } from '../lib/module.js' 4 | import { useNuxtSocket, emit } from '../lib/plugin.js' // Plugin will import defineNuxtPlugin from '#app' which during test is our mocked function 5 | import { pluginCtx } from './utils/plugin.js' 6 | 7 | const { serial: test, before, after } = ava 8 | let ioServerObj 9 | 10 | const ChatMsg = { 11 | date: new Date(), 12 | from: '', 13 | to: '', 14 | text: '' 15 | } 16 | 17 | const clientAPI = { 18 | label: 'ioApi_page', 19 | version: 1.31, 20 | evts: { 21 | undefApiData: {}, 22 | undefEvt: {}, 23 | alreadyDefd: {}, 24 | warnings: { 25 | data: { 26 | lostSignal: false, 27 | battery: 0 28 | } 29 | } 30 | }, 31 | methods: { 32 | undefMethod: {}, 33 | receiveMsg: { 34 | msg: ChatMsg, 35 | resp: { 36 | status: '' 37 | } 38 | } 39 | } 40 | } 41 | 42 | const ctx = pluginCtx() 43 | ctx.$config.public = { nuxtSocketIO: {} } 44 | 45 | /** 46 | * @param {import('socket.io-client').Socket} s 47 | */ 48 | function waitForSocket (s) { 49 | return new Promise((resolve) => { 50 | if (s.connected) { 51 | resolve(s) 52 | return 53 | } 54 | s.on('connect', () => resolve(s)) 55 | }) 56 | } 57 | 58 | /** 59 | * @param {import('socket.io-client').Socket} s 60 | * @param {function} trigger 61 | * @param {Array} evts 62 | */ 63 | function triggerEvents (s, trigger, evts) { 64 | const p = evts.map(evt => 65 | new Promise((resolve) => { 66 | s.on(evt, resolve) 67 | }) 68 | ) 69 | trigger() 70 | return Promise.all(p) 71 | } 72 | 73 | function RefImpl (arg) { 74 | this.value = arg 75 | } 76 | 77 | before(async (t) => { 78 | ioServerObj = await register.server({ port: 3000 }) 79 | global.window = { 80 | // @ts-ignore 81 | location: { 82 | host: 'localhost:4000', 83 | hostname: 'localhost', 84 | href: 'http://localhost:4000/', 85 | port: '4000', 86 | origin: 'http://localhost:4000' 87 | } 88 | } 89 | }) 90 | 91 | after(() => { 92 | ioServerObj.io.close() 93 | ioServerObj.server.close() 94 | }) 95 | 96 | test('nuxtSocket, ioState injected', (t) => { 97 | t.truthy(ctx.$nuxtSocket) 98 | t.truthy(ctx.$ioState) 99 | }) 100 | 101 | test('ioState (state for the new "iox")', (t) => { 102 | const state = ctx.$ioState().value 103 | state.xyz = 123 104 | const next = ctx.$ioState().value 105 | t.is(next.xyz, 123) 106 | }) 107 | 108 | test('useNuxtSocket (nuxtSocket internal state)', (t) => { 109 | const state = useNuxtSocket().value 110 | t.truthy(state.emitTimeouts) 111 | }) 112 | 113 | test('Socket plugin (runtime IO $config defined, sockets undef)', (t) => { 114 | ctx.$config.io = {} 115 | try { 116 | ctx.$nuxtSocket({ name: 'runtime' }) 117 | } catch (err) { 118 | t.is(err.message, "Please configure sockets if planning to use nuxt-socket-io: \r\n [{name: '', url: ''}]") 119 | } 120 | }) 121 | 122 | test('Socket plugin (runtime IO $config defined, duplicate sockets)', (t) => { 123 | ctx.$config.io = { 124 | sockets: [ 125 | { 126 | name: 'runtime', 127 | url: 'http://localhost:3000' 128 | }, 129 | { 130 | name: 'runtime', 131 | url: 'http://someurl' 132 | } 133 | ] 134 | } 135 | ctx.socketStatus = {} 136 | const socket = ctx.$nuxtSocket({ name: 'runtime', info: false }) 137 | t.truthy(socket) 138 | t.is(ctx.socketStatus.connectUrl, 'http://localhost:3000') 139 | }) 140 | 141 | test('Socket plugin (runtime IO $config defined, merges safely with modOptions)', (t) => { 142 | ctx.$config.io = { 143 | sockets: [ 144 | { 145 | name: 'runtime', 146 | url: 'http://localhost:3000' 147 | } 148 | ] 149 | } 150 | ctx.$config.public.nuxtSocketIO = { 151 | sockets: [ 152 | { 153 | name: 'main', 154 | url: 'http://localhost:3001' 155 | } 156 | ] 157 | } 158 | ctx.socketStatus = {} 159 | ctx.$nuxtSocket({ default: true, info: false }) 160 | t.is(ctx.socketStatus.connectUrl, 'http://localhost:3001') 161 | ctx.socketStatus = {} 162 | ctx.$nuxtSocket({ name: 'runtime', info: false }) 163 | t.is(ctx.socketStatus.connectUrl, 'http://localhost:3000') 164 | }) 165 | 166 | test('Socket.url not defined', (t) => { 167 | ctx.$config.public.nuxtSocketIO = { 168 | sockets: [ 169 | { 170 | name: 'main' 171 | } 172 | ] 173 | } 174 | ctx.socketStatus = {} 175 | ctx.$nuxtSocket({ info: false }) 176 | t.is(ctx.socketStatus.connectUrl, window.location.origin) 177 | }) 178 | 179 | test('Socket Persistence (persist = true)', async (t) => { 180 | ctx.$config = { 181 | public: { 182 | nuxtSocketIO: { 183 | sockets: [ 184 | { 185 | name: 'main', 186 | url: 'http://localhost:3000' 187 | } 188 | ] 189 | } 190 | } 191 | } 192 | const s1 = ctx.$nuxtSocket({ persist: true, teardown: false }) 193 | await waitForSocket(s1) 194 | const s2 = ctx.$nuxtSocket({ persist: true, teardown: false }) 195 | await waitForSocket(s2) 196 | t.is(s1.id, s2.id) 197 | s1.close() 198 | s2.close() 199 | }) 200 | 201 | test('Socket Persistence (persist = label)', async (t) => { 202 | ctx.$config = { 203 | public: { 204 | nuxtSocketIO: { 205 | sockets: [ 206 | { 207 | name: 'main', 208 | url: 'http://localhost:3000' 209 | } 210 | ] 211 | } 212 | } 213 | } 214 | const s1 = ctx.$nuxtSocket({ persist: 'mySocket', teardown: false }) 215 | await waitForSocket(s1) 216 | const s2 = ctx.$nuxtSocket({ persist: 'mySocket', teardown: false }) 217 | await waitForSocket(s2) 218 | t.is(s1.id, s2.id) 219 | s1.close() 220 | s2.close() 221 | }) 222 | 223 | test('Socket Persistence (persist = true, persisted socket disconnected)', async (t) => { 224 | ctx.$config = { 225 | public: { 226 | nuxtSocketIO: { 227 | sockets: [ 228 | { 229 | name: 'main', 230 | url: 'http://localhost:3000' 231 | } 232 | ] 233 | } 234 | } 235 | } 236 | const s1 = ctx.$nuxtSocket({ persist: true, teardown: false }) 237 | await waitForSocket(s1) 238 | s1.close() 239 | const s2 = ctx.$nuxtSocket({ persist: true, teardown: false }) 240 | await waitForSocket(s2) 241 | t.not(s1.id, s2.id) 242 | }) 243 | 244 | test('Namespace config (registration)', async (t) => { 245 | ctx.$config = { 246 | public: { 247 | nuxtSocketIO: { 248 | sockets: [ 249 | { 250 | name: 'main', 251 | url: 'http://localhost:3000', 252 | namespacesx: { 253 | '/': { 254 | emitters: ['echo2 --> respx'], 255 | listeners: ['xyz'] 256 | } 257 | } 258 | } 259 | ] 260 | } 261 | } 262 | } 263 | ctx.resp = '' 264 | 265 | ctx.$nuxtSocket({ 266 | channel: '/', 267 | namespaceCfg: { 268 | emitters: { echo: 'resp' } 269 | } 270 | }) 271 | t.falsy(ctx.echo) 272 | 273 | const s = ctx.$nuxtSocket({ 274 | channel: '/', 275 | namespaceCfg: { 276 | emitters: [ 277 | 'echo --> resp' 278 | ] 279 | } 280 | }) 281 | await waitForSocket(s) 282 | await ctx.echo('Hi') 283 | t.is(ctx.resp, 'Hi') 284 | s.close() 285 | }) 286 | 287 | test('Namespace config (emitters)', async (t) => { 288 | let preEmit, handleAck 289 | ctx.$config = { 290 | public: { 291 | nuxtSocketIO: { 292 | sockets: [ 293 | { 294 | name: 'main', 295 | url: 'http://localhost:3000' 296 | } 297 | ] 298 | } 299 | } 300 | } 301 | Object.assign(ctx, { 302 | chatMessage2: '', 303 | chatMessage4: '', 304 | message5Rxd: '', 305 | echoBack: {}, // Expect to be overwritten by nspCfg. 306 | resp: '', 307 | testMsg: 'A test msg', 308 | userInfo: { 309 | name: 'John Smith' 310 | }, 311 | ids: [123, 444], 312 | items: [], 313 | titleResp: '', 314 | hello: new RefImpl(false), 315 | preEmit () { 316 | preEmit = true 317 | }, 318 | preEmitFail () { 319 | return false 320 | }, 321 | handleAck () { 322 | handleAck = true 323 | } 324 | }) 325 | const s = ctx.$nuxtSocket({ 326 | channel: '/index', 327 | emitTimeout: 5000, 328 | namespaceCfg: { 329 | emitters: [ 330 | 'echoBack --> echoBack', 331 | 'preEmit] titleFromUser + userInfo --> titleResp [handleAck', 332 | 'preEmitFail] echo', 333 | 'echoError', 334 | 'echoHello --> hello', 335 | 'getItems + ids --> items', 336 | 'echoUndefMsg + undefMsg', 337 | 111 // invalid type...nothing should happen 338 | ] 339 | } 340 | }) 341 | t.is(typeof ctx.echoBack, 'function') 342 | const resp = await ctx.echoBack({ data: 'Hi' }) 343 | t.is(resp.data, 'Hi') 344 | await ctx.titleFromUser() 345 | t.true(preEmit) 346 | t.true(handleAck) 347 | t.is(ctx.titleResp.data, 'received msg John Smith!') 348 | const r2 = await ctx.echo('Hi') 349 | t.falsy(r2) 350 | 351 | await ctx.echoError() 352 | .catch((err) => { 353 | t.is(err.message, 'SomeError') 354 | }) 355 | ctx.emitErrors = {} 356 | await ctx.echoError() 357 | t.is(ctx.emitErrors.echoError[0].message, 'SomeError') 358 | 359 | const s2 = ctx.$nuxtSocket({ 360 | channel: '/index', 361 | emitTimeout: 100, 362 | namespaceCfg: { 363 | emitters: ['noHandler'] 364 | } 365 | }) 366 | await ctx.noHandler() 367 | t.is(ctx.emitErrors.noHandler[0].message, 'emitTimeout') 368 | delete ctx.emitErrors 369 | await ctx.noHandler().catch((err) => { 370 | t.is(err.message, 'emitTimeout') 371 | }) 372 | await ctx.echoHello() 373 | t.is(ctx.hello.value.data, 'hello') 374 | 375 | await ctx.getItems() 376 | t.is(ctx.items.length, 2) 377 | 378 | const resp3 = await ctx.echoUndefMsg() 379 | t.falsy(resp3) 380 | 381 | s.close() 382 | s2.close() 383 | }) 384 | 385 | test('Namespace config (listeners)', async (t) => { 386 | ctx.$config = { 387 | public: { 388 | nuxtSocketIO: { 389 | sockets: [ 390 | { 391 | name: 'main', 392 | url: 'http://localhost:3000' 393 | } 394 | ] 395 | } 396 | } 397 | } 398 | let preEmit, handleAck 399 | Object.assign(ctx, { 400 | chatMessage2: '', 401 | chatMessage4: '', 402 | message5Rxd: '', 403 | testMsg: 'A test msg', 404 | preEmit () { 405 | preEmit = true 406 | }, 407 | handleAck () { 408 | handleAck = true 409 | } 410 | }) 411 | 412 | const s = ctx.$nuxtSocket({ 413 | channel: '/index', 414 | namespaceCfg: { 415 | emitters: [ 416 | 'getMessage2 + testMsg --> message2Rxd' 417 | ], 418 | listeners: [ 419 | 'preEmit] chatMessage2 [handleAck', 420 | 'undef1] chatMessage3 --> message3Rxd [undef2', 421 | 'chatMessage4', 422 | 'chatMessage5 --> message5Rxd' 423 | ] 424 | } 425 | }) 426 | 427 | await waitForSocket(s) 428 | t.truthy(ctx.getMessage2) 429 | await triggerEvents(s, ctx.getMessage2, ['chatMessage2', 'chatMessage3']) 430 | t.falsy(ctx.message2Rxd) 431 | t.true(preEmit) 432 | t.true(handleAck) 433 | t.is(ctx.chatMessage2, 'Hi, this is a chat message from IO server!') 434 | t.falsy(ctx.chatMessage3) 435 | t.is(ctx.chatMessage4.data, 'Hi again') 436 | t.is(ctx.message5Rxd.data, 'Hi again from 5') 437 | s.close() 438 | }) 439 | 440 | test('Namespace config (emitBacks)', async (t) => { 441 | let preEmit, postEmit 442 | ctx.$config = { 443 | public: { 444 | nuxtSocketIO: { 445 | sockets: [ 446 | { 447 | name: 'main', 448 | url: 'http://localhost:3000' 449 | } 450 | ] 451 | } 452 | } 453 | } 454 | Object.assign(ctx, { 455 | hello: false, 456 | hello2: false, 457 | sample3: 100, 458 | myObj: { 459 | sample4: 50 460 | }, 461 | sample5: 421, 462 | preEmit () { 463 | preEmit = true 464 | }, 465 | preEmitValid ({ data }) { 466 | return data === 'yes' 467 | }, 468 | postEmitHook () { 469 | postEmit = true 470 | }, 471 | handleDone ({ msg }) { 472 | t.is(msg, 'rxd sample ' + newData.sample3) 473 | } 474 | }) 475 | const newData = { 476 | sample3: ctx.sample3 + 1, 477 | 'myObj.sample4': ctx.myObj.sample4 + 1, 478 | 'myObj.sample5': ctx.myObj.sample5 + 1, 479 | sample5: 111, 480 | hello: 'no', 481 | hello2: 'yes' 482 | } 483 | const emitEvts = Object.keys(newData) 484 | ctx.$watch = (label, cb) => { 485 | t.true(emitEvts.includes(label)) 486 | cb(newData[label]) 487 | if (label === 'sample5') { 488 | t.true(preEmit) 489 | } 490 | } 491 | 492 | const s = ctx.$nuxtSocket({ 493 | channel: '/examples', 494 | namespaceCfg: { 495 | emitBacks: [ 496 | 'sample3 [handleDone', 497 | 'noMethod] sample4 <-- myObj.sample4 [handleX', 498 | 'myObj.sample5', 499 | 'preEmit] sample5', 500 | 'preEmitValid] hello [postEmitHook', 501 | 'preEmitValid] echoHello <-- hello2 [postEmitHook' 502 | ] 503 | } 504 | }) 505 | await delay(1000) 506 | t.true(postEmit) 507 | s.close() 508 | }) 509 | 510 | test('Teardown', (t) => { 511 | const ctx = pluginCtx() 512 | ctx.$config.public.nuxtSocketIO = {} 513 | let componentDestroyCnt = 0 514 | ctx.$config = { 515 | public: { 516 | nuxtSocketIO: { 517 | sockets: [ 518 | { 519 | name: 'main', 520 | url: 'http://localhost:3000' 521 | } 522 | ] 523 | } 524 | } 525 | } 526 | Object.assign(ctx, { 527 | $destroy () { 528 | componentDestroyCnt++ 529 | } 530 | }) 531 | 532 | const s = ctx.$nuxtSocket({ teardown: true }) 533 | ctx.$emit = ctx.$$emit 534 | const s2 = ctx.$nuxtSocket({ teardown: true }) 535 | s.on('someEvt', () => {}) 536 | s2.on('someEvt', () => {}) 537 | t.true(s.hasListeners('someEvt')) 538 | t.true(s2.hasListeners('someEvt')) 539 | ctx.$destroy() 540 | t.is(componentDestroyCnt, 1) 541 | t.false(s.hasListeners('someEvt')) 542 | t.false(s2.hasListeners('someEvt')) 543 | 544 | const ctx3 = { ...ctx } 545 | Object.assign(ctx3, { 546 | registeredTeardown: false, 547 | onUnmounted: ctx.$destroy 548 | }) 549 | 550 | const s3 = ctx3.$nuxtSocket({ teardown: true }) 551 | ctx3.onUnmounted() 552 | t.is(componentDestroyCnt, 2) 553 | }) 554 | 555 | test('Stubs (composition api support)', async (t) => { 556 | const ctx = pluginCtx() 557 | ctx.$config.public.nuxtSocketIO = { sockets: [{ url: 'http://localhost:3000' }] } 558 | 559 | async function validateEventHub () { 560 | const props = ['$on', '$off', '$once', '$$emit'] 561 | props.forEach(p => t.truthy(ctx[p])) 562 | 563 | let rxCnt = 0 564 | let rx2Cnt = 0 565 | ctx.$on('msg', (arg) => { 566 | rxCnt++ 567 | t.is(arg, 'hello') 568 | }) 569 | ctx.$once('msg2', (arg) => { 570 | rx2Cnt++ 571 | t.is(arg, 'hello 2') 572 | }) 573 | ctx.$$emit('msg', 'hello') 574 | ctx.$off('msg') 575 | ctx.$$emit('msg', 'hello again') 576 | ctx.$$emit('msg2', 'hello 2') 577 | await delay(100) 578 | t.is(rxCnt, 1) 579 | t.is(rx2Cnt, 1) 580 | } 581 | 582 | function validateSet () { 583 | const obj = { 584 | val1: new RefImpl(10), 585 | val2: 10 586 | } 587 | ctx.$set(obj, 'val1', 22) 588 | ctx.$set(obj, 'val2', 22) 589 | t.is(obj.val1.value, 22) 590 | t.is(obj.val2, 22) 591 | } 592 | 593 | function validateWatch () { 594 | ctx.$watch('someLabel', () => {}) 595 | t.pass() 596 | } 597 | ctx.$nuxtSocket({}) 598 | const p = [validateEventHub(), validateSet(), validateWatch()] 599 | await Promise.all(p) 600 | }) 601 | 602 | test('Dynamic API Feature (Server)', async (t) => { 603 | const ctx = pluginCtx() 604 | ctx.$config = { 605 | public: { 606 | nuxtSocketIO: { 607 | sockets: [ 608 | { 609 | name: 'main', 610 | url: 'http://localhost:3000' 611 | } 612 | ] 613 | } 614 | } 615 | } 616 | const apiIgnoreEvts = ['ignoreMe'] 617 | ctx.$nuxtSocket({ 618 | channel: '/dynamic', 619 | serverAPI: {}, 620 | apiIgnoreEvts 621 | }) 622 | await delay(500) 623 | t.falsy(ctx.ioApi) 624 | Object.assign(ctx, { 625 | ioApi: {}, 626 | ioData: {} 627 | }) 628 | 629 | const s = ctx.$nuxtSocket({ 630 | channel: '/dynamic', 631 | serverAPI: {}, 632 | apiIgnoreEvts 633 | }) 634 | // eslint-disable-next-line no-console 635 | console.log('creating a duplicate listener to see if plugin handles it') 636 | s.on('itemRxd', () => {}) 637 | await delay(500) 638 | t.true(ctx.ioApi.ready) 639 | const state = useNuxtSocket().value 640 | t.truthy(state.ioApis['main/dynamic']) 641 | const items = await ctx.ioApi.getItems() 642 | const item1 = await ctx.ioApi.getItem({ id: 'abc123' }) 643 | Object.assign(ctx.ioData.getItem.msg, { id: 'something' }) 644 | const item2 = await ctx.ioApi.getItem() 645 | const noResp = await ctx.ioApi.noResp() 646 | t.true(items.length > 0) 647 | t.is(item1.id, 'abc123') 648 | t.is(item2.id, 'something') 649 | Object.keys(ctx.ioApi.evts).forEach((evt) => { 650 | if (!apiIgnoreEvts.includes(evt)) { 651 | t.true(s.hasListeners(evt)) 652 | } else { 653 | t.false(s.hasListeners(evt)) 654 | } 655 | }) 656 | t.true(Object.keys(noResp).length === 0) 657 | 658 | Object.assign(ctx, { 659 | ioApi: {}, 660 | ioData: {} 661 | }) 662 | 663 | const s2 = ctx.$nuxtSocket({ 664 | channel: '/p2p', 665 | serverAPI: {}, 666 | clientAPI 667 | }) 668 | await delay(500) 669 | t.truthy(state.ioApis['main/p2p']) 670 | const props = ['evts', 'methods'] 671 | props.forEach((prop) => { 672 | const clientProps = Object.keys(clientAPI[prop]) 673 | const serverProps = Object.keys(ctx.ioApi[prop]) 674 | clientProps.forEach((cProp) => { 675 | t.true(serverProps.includes(cProp)) 676 | }) 677 | }) 678 | t.true(ctx.ioApi.ready) 679 | }) 680 | 681 | test('Dynamic API Feature (Client)', async (t) => { 682 | const ctx = pluginCtx() 683 | ctx.$config = { 684 | public: { 685 | nuxtSocketIO: { 686 | sockets: [ 687 | { 688 | name: 'main', 689 | url: 'http://localhost:3000' 690 | } 691 | ] 692 | } 693 | } 694 | } 695 | Object.assign(ctx, { 696 | ioApi: {}, 697 | ioData: {}, 698 | alreadyDefdEmit () { 699 | 700 | } 701 | }) 702 | const s = ctx.$nuxtSocket({ 703 | channel: '/p2p', 704 | clientAPI: {} 705 | }) 706 | 707 | const state = useNuxtSocket().value 708 | t.falsy(state.clientApis['main/p2p']) 709 | s.close() 710 | const callCnt = { receiveMsg: 0 } 711 | Object.assign(ctx, { 712 | undefApiData: {}, 713 | warnings: {}, 714 | receiveMsg (msg) { 715 | callCnt.receiveMsg++ 716 | return Promise.resolve({ 717 | status: 'ok' 718 | }) 719 | } 720 | }) 721 | const s2 = ctx.$nuxtSocket({ 722 | channel: '/p2p', 723 | persist: true, 724 | serverAPI: {}, 725 | clientAPI 726 | }) 727 | t.truthy(state.clientApis) 728 | 729 | // @ts-ignore 730 | await emit({ 731 | evt: 'sendEvts' 732 | }).catch((err) => { 733 | t.is(err.message, 'socket instance required. Please provide a valid socket label or socket instance') 734 | }) 735 | 736 | await emit({ 737 | evt: 'sendEvts', 738 | msg: {}, 739 | socket: s2 740 | }) 741 | t.is(callCnt.receiveMsg, 2) 742 | Object.keys(clientAPI.evts.warnings.data).forEach((prop) => { 743 | t.true(ctx.warnings[prop] !== undefined) 744 | }) 745 | ctx.warnings.battery = 11 746 | 747 | const resp = await ctx.warningsEmit() 748 | const resp2 = await ctx.warningsEmit({ ack: true }) 749 | const resp3 = await ctx.warningsEmit({ ack: true, battery: 22 }) 750 | t.falsy(resp) 751 | t.truthy(resp2) 752 | t.is(resp2.battery, ctx.warnings.battery) 753 | t.is(resp3.battery, 22) 754 | 755 | ctx.$nuxtSocket({ 756 | warnings: true, // show the warnings 757 | channel: '/p2p', 758 | persist: true, 759 | serverAPI: {}, 760 | clientAPI 761 | }) 762 | 763 | ctx.$nuxtSocket({ 764 | warnings: false, // hide the warnings 765 | channel: '/p2p', 766 | persist: true, 767 | serverAPI: {}, 768 | clientAPI 769 | }) 770 | }) 771 | 772 | test('Promisified emit and once', async (t) => { 773 | const ctx = pluginCtx() 774 | ctx.$config.public.nuxtSocketIO = { sockets: [{ url: 'http://localhost:3000' }] } 775 | const s = ctx.$nuxtSocket({ channel: '/index', teardown: false, reconnection: true }) 776 | t.truthy(s.emitP) 777 | t.truthy(s.onceP) 778 | const p = s.onceP('chatMessage') 779 | t.true(s.hasListeners('chatMessage')) 780 | const r = await s.emitP('getMessage', { id: 'abc123' }) 781 | const r2 = await p 782 | t.false(s.hasListeners('chatMessage')) 783 | t.is(r, 'It worked! Received msg: {"id":"abc123"}') 784 | t.is(r2, 'Hi, this is a chat message from IO server!') 785 | }) 786 | 787 | test('global emit()', async (t) => { 788 | const ctx = pluginCtx() 789 | ctx.$config = { 790 | public: { 791 | nuxtSocketIO: { 792 | sockets: [ 793 | { 794 | name: 'home', 795 | url: 'http://localhost:3000' 796 | } 797 | ] 798 | } 799 | } 800 | } 801 | const state = useNuxtSocket().value 802 | const s = ctx.$nuxtSocket({ 803 | channel: '/p2p', 804 | clientAPI: {} 805 | }) 806 | await emit({ 807 | emitTimeout: 1, 808 | evt: 'deadEnd', 809 | msg: {}, 810 | socket: s 811 | }).catch((err) => { 812 | t.is(err.message, 'emitTimeout') 813 | }) 814 | 815 | await emit({ 816 | label: 'catch', 817 | emitTimeout: 1, 818 | evt: 'deadEnd', 819 | msg: {}, 820 | socket: s 821 | }) 822 | t.is(state.emitErrors.catch.deadEnd[0].message, 'emitTimeout') 823 | 824 | const s2 = ctx.$nuxtSocket({ 825 | channel: '/dynamic', 826 | persist: true, 827 | emitTimeout: 3000 828 | }) 829 | await emit({ 830 | evt: 'badRequest', 831 | msg: {}, 832 | socket: s2 833 | }) 834 | .catch((err) => { 835 | t.is(err.message, 'badRequest...Input does not match schema') 836 | }) 837 | 838 | await emit({ 839 | label: 'deadEnd', 840 | evt: 'badRequest', 841 | msg: {}, 842 | socket: s2 843 | }) 844 | 845 | t.is(state.emitErrors.deadEnd.badRequest[0].message, 'badRequest...Input does not match schema') 846 | }) 847 | 848 | test('iox', async (t) => { 849 | const ctx = pluginCtx() 850 | ctx.$config = { 851 | public: { 852 | nuxtSocketIO: { 853 | sockets: [ 854 | { 855 | name: 'home', 856 | url: 'http://localhost:3000', 857 | iox: [ 858 | 'chatMessage --> chats/message', 859 | 'chatMessage4 --> msg4', 860 | 'progress --> examples/progress', 861 | 'examples/sample <-- examples/sample', 862 | 'examples/someObj', // Bidirectional 863 | 'bidirectional' 864 | ] 865 | } 866 | ] 867 | } 868 | } 869 | } 870 | const s = ctx.$nuxtSocket({ 871 | channel: '/index' 872 | }) 873 | await s.emitP('getMessage', { id: 'abc123' }) 874 | await s.emitP('getMessage2') 875 | const state = ctx.$ioState().value 876 | t.is(state.chats.message, 'Hi, this is another chat message from IO server!') 877 | t.is(state.msg4.data, 'Hi again') 878 | state.bidirectional = 'set' // See console 879 | 880 | ctx.$config.public.nuxtSocketIO.sockets[0].registeredWatchers = [] 881 | const s2 = ctx.$nuxtSocket({ 882 | channel: '/examples' 883 | }) 884 | await s2.emitP('getProgress', { refreshPeriod: 500 }) 885 | t.is(state.examples.progress, 100) 886 | 887 | s2.on('sampleDataRxd', (msg) => { 888 | t.is(msg.data.sample, 200) 889 | }) 890 | state.examples.sample = 100 891 | state.examples.sample = 200 892 | 893 | await delay(100) 894 | 895 | state.examples.someObj = {} 896 | await s2.emitP('echo', { evt: 'examples/someObj', msg: { a: 111 } }) 897 | t.is(state.examples.someObj.a, 111) 898 | const msgsRxd = [] 899 | s2.on('examples/someObjRxd', (msg) => { 900 | msgsRxd.push(msg) 901 | }) 902 | 903 | state.examples.someObj = { a: 222 } 904 | 905 | await delay(100) 906 | t.is(msgsRxd.at(-1).a, 222) 907 | 908 | ctx.$nuxtSocket({ // Attempt to register duplicate watchers 909 | channel: '/examples' 910 | }) 911 | // coverage report will show it was hit. 912 | }) 913 | -------------------------------------------------------------------------------- /test/SocketStatus.spec.js: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register.js' 2 | import { h, createApp } from 'vue' 3 | import ava from 'ava' 4 | import BrowserEnv from 'browser-env' 5 | import SocketStatus from '#root/lib/components/SocketStatus.js' 6 | BrowserEnv() 7 | 8 | const { serial: test } = ava 9 | 10 | test('IO Status', (t) => { 11 | const app = document.createElement('div') 12 | app.id = 'app' 13 | document.body.appendChild(app) 14 | 15 | const badStatus = { 16 | connectUrl: 'http://localhost:3001/index', 17 | connectError: 'Connect Error', 18 | connectTimeout: '', 19 | reconnect: '', 20 | reconnectAttempt: '5', 21 | reconnectError: new Error('Error xhr poll error'), 22 | reconnectFailed: '', 23 | ping: '', 24 | pong: '' 25 | } 26 | const vueApp = createApp({ 27 | render () { 28 | return h('div', [ 29 | h(SocketStatus, { status: badStatus, ref: 'c1' }), 30 | h(SocketStatus, { 31 | status: { 32 | connectUrl: 'http://localhost:3001/index' 33 | }, 34 | ref: 'c2' 35 | }), 36 | h(SocketStatus, { ref: 'c3' }) 37 | ]) 38 | } 39 | }).mount('#app') 40 | const comp = vueApp.$refs.c1 41 | const comp2 = vueApp.$refs.c2 42 | const comp3 = vueApp.$refs.c3 43 | 44 | const expTbl = [ 45 | { item: 'connectError', info: 'Connect Error' }, 46 | { item: 'reconnectAttempt', info: '5' }, 47 | { item: 'reconnectError', info: 'Error: Error xhr poll error' } 48 | ] 49 | 50 | expTbl.forEach(({ item, info }, idx) => { 51 | t.is(comp.statusTbl[idx].item, item) 52 | t.is(comp.statusTbl[idx].info, info) 53 | }) 54 | t.is(comp2.statusTbl[0].item, 'status') 55 | t.is(comp2.statusTbl[0].info, 'OK') 56 | t.is(comp2.statusTbl[0].item, 'status') 57 | t.is(comp2.statusTbl[0].info, 'OK') 58 | 59 | t.truthy(comp.$el.outerHTML.includes('socket-status')) 60 | t.truthy(comp2.$el.outerHTML.includes('socket-status')) 61 | t.falsy(comp3.$el.innerHTML) 62 | }) 63 | -------------------------------------------------------------------------------- /test/utils/loaders.js: -------------------------------------------------------------------------------- 1 | import { URL, pathToFileURL } from 'url' 2 | // import * as compiler from 'vue-template-compiler' 3 | import { parse, compileTemplate } from '@vue/compiler-sfc' // /dist/compiler-sfc.js' // '@vue/component-compiler-utils' 4 | 5 | const baseURL = pathToFileURL(`${process.cwd()}/`).href 6 | const regex = /(\.ts|\.css|\.vue)$/ 7 | 8 | function transformVue (source, url) { 9 | const filename = '/' + url.split(baseURL)[1] 10 | const parsed = parse(source, { 11 | // source, 12 | // @ts-ignore 13 | // compiler, 14 | filename 15 | // sourceMap: true 16 | }) 17 | 18 | const compiledTemplate = compileTemplate({ 19 | id: filename, 20 | filename, 21 | source: parsed.descriptor.template.content 22 | // @ts-ignore 23 | // compiler 24 | }) 25 | 26 | return compiledTemplate.code + 27 | (parsed.script 28 | ? parsed.script.content 29 | .replace('export default {\n', 'export default {\n render,\n') 30 | : 'export default {\n render,\n _compiled: true\n }') 31 | } 32 | 33 | /** 34 | * @param {string} specifier 35 | * @param {{ parentURL?: string; url: any; }} context 36 | * @param {(arg0: any, arg1: any, arg2: any) => any} defaultResolve 37 | */ 38 | export function resolve (specifier, context, defaultResolve) { 39 | const { parentURL = baseURL } = context 40 | if (regex.test(specifier)) { 41 | return { 42 | shortCircuit: true, 43 | url: new URL(specifier, parentURL).href 44 | } 45 | } 46 | 47 | // Let Node.js handle all other specifiers. 48 | return defaultResolve(specifier, context, defaultResolve) 49 | } 50 | 51 | export async function load (url, context, defaultLoad) { 52 | if (url.endsWith('vue.runtime.esm.js')) { 53 | const { source } = await defaultLoad(url, { format: 'module' }) 54 | return { 55 | shortCircuit: true, 56 | format: 'module', 57 | source: source.toString() 58 | } 59 | } else if (url.endsWith('.vue')) { 60 | const { source } = await defaultLoad(url, { format: 'module' }) 61 | return { 62 | shortCircuit: true, 63 | format: 'module', 64 | source: transformVue(source.toString(), url) 65 | } 66 | } else if (url.endsWith('.ts')) { 67 | const { source } = await defaultLoad(url, { format: 'module' }) 68 | return { 69 | shortCircuit: true, 70 | format: 'module', 71 | source: source.toString() 72 | } 73 | } else if (url.endsWith('.css')) { 74 | return { 75 | shortCircuit: true, 76 | format: 'module', 77 | source: 'export default {}' 78 | } 79 | } 80 | 81 | // Let Node.js handle all other URLs. 82 | return defaultLoad(url, context, defaultLoad) 83 | } 84 | -------------------------------------------------------------------------------- /test/utils/module.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { nuxtCtx, useNuxt } from '@nuxt/kit' 3 | import config from '#root/nuxt.config.js' 4 | 5 | const srcDir = path.resolve('.') 6 | 7 | export { useNuxt } 8 | 9 | export function getModuleOptions (moduleName, optsContainer) { 10 | const opts = {} 11 | const containers = ['buildModules', 'modules', optsContainer] 12 | containers.some((container) => { 13 | if (container === optsContainer) { 14 | Object.assign(opts, { [optsContainer]: config[container] }) 15 | return true 16 | } 17 | const arr = config[container] 18 | const mod = arr.find( 19 | /** 20 | * @param {string | any[]} item 21 | */ 22 | (item) => { 23 | if (typeof item === 'string') { 24 | return item === moduleName 25 | } else if (item.length) { 26 | return item[0] === moduleName 27 | } 28 | return false 29 | }) 30 | if (mod) { 31 | if (mod.length) { 32 | Object.assign(opts, mod[1]) 33 | } 34 | return true 35 | } 36 | return false 37 | }) 38 | return opts 39 | } 40 | 41 | export function initNuxt () { 42 | nuxtCtx.unset() 43 | const nuxt = { 44 | __nuxt2_shims_key__: true, 45 | version: '3.x', 46 | hooks: { 47 | addHooks: () => {} 48 | }, 49 | hook (evt, cb) { 50 | nuxtCtx.use().hooks[evt] = cb 51 | }, 52 | options: { 53 | css: [], 54 | srcDir, 55 | plugins: [], 56 | modules: [], 57 | serverMiddleware: [], 58 | build: { 59 | transpile: [], 60 | templates: [] 61 | }, 62 | runtimeConfig: { 63 | public: {} 64 | } 65 | } 66 | } 67 | // @ts-ignore 68 | nuxtCtx.set(nuxt) 69 | } 70 | -------------------------------------------------------------------------------- /test/utils/plugin.js: -------------------------------------------------------------------------------- 1 | import { reactive, toRef, isReactive } from 'vue' 2 | const ctx = { 3 | payload: {}, 4 | provide (label, fn) { 5 | ctx['$' + label] = fn 6 | }, 7 | $config: { 8 | public: {} 9 | } 10 | } 11 | 12 | // lib/plugin.js will call this... 13 | export function defineNuxtPlugin (cb) { 14 | cb(ctx) 15 | return { ...ctx } 16 | } 17 | 18 | // This returns a clean copy of the ctx 19 | export function pluginCtx () { 20 | return { ...ctx } 21 | } 22 | 23 | export function useState (key, init) { 24 | const nuxtApp = pluginCtx() 25 | if (!nuxtApp.payload.useState) { 26 | nuxtApp.payload.useState = {} 27 | } 28 | if (!isReactive(nuxtApp.payload.useState)) { 29 | nuxtApp.payload.useState = reactive(nuxtApp.payload.useState) 30 | } 31 | 32 | const state = toRef(nuxtApp.payload.useState, key) 33 | if (state.value === undefined && init) { 34 | state.value = init() 35 | } 36 | return state 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } --------------------------------------------------------------------------------