├── test.js ├── .gitignore ├── .editorconfig ├── .github └── workflows │ └── test.yml ├── example.js ├── license.md ├── package.json ├── readme.md └── index.js /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const todo = require('.') 4 | 5 | // todo 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | 4 | .nvm-version 5 | node_modules 6 | npm-debug.log 7 | pnpm-debug.log 8 | 9 | package-lock.json 10 | shrinkwrap.yaml 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | # Use tabs in JavaScript and JSON. 11 | [**.{js, json}] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | # Use spaces in YAML. 16 | [**.{yml,yaml}] 17 | indent_style = spaces 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: ['8', '10', '12', '14', '16', '18'] 17 | 18 | steps: 19 | - name: checkout 20 | uses: actions/checkout@v2 21 | - name: setup Node v${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm install 26 | 27 | - run: npm test 28 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const WebSocket = require('ws') 4 | const createRoundRobin = require('@derhuerst/round-robin-scheduler') 5 | const createPool = require('.') 6 | 7 | const pool = createPool(WebSocket, createRoundRobin) 8 | 9 | pool.on('message', (msg, ws) => { 10 | console.log(ws.url, 'says:', msg.data) 11 | }) 12 | pool.on('error', (err) => { 13 | console.error(err) 14 | }) 15 | pool.once('open', () => { 16 | setInterval(() => { 17 | pool.send('hello there') 18 | }, 3000) 19 | }) 20 | 21 | const urls = [ 22 | 'ws://echo.websocket.org/#1', 23 | 'ws://echo.websocket.org/#2', 24 | 'ws://echo.websocket.org/#3' 25 | ] 26 | for (let url of urls) pool.add(url) 27 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, Jannis R 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websocket-pool", 3 | "description": "A pool of WebSocket connections. Supports reconnecting.", 4 | "version": "1.3.2", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "keywords": [ 10 | "websocket", 11 | "ws", 12 | "pool", 13 | "load balancer", 14 | "load balancing", 15 | "retry", 16 | "reconnect" 17 | ], 18 | "author": "Jannis R ", 19 | "homepage": "https://github.com/derhuerst/websocket-pool", 20 | "repository": "derhuerst/websocket-pool", 21 | "bugs": "https://github.com/derhuerst/websocket-pool/issues", 22 | "license": "ISC", 23 | "engines": { 24 | "node": ">=6" 25 | }, 26 | "dependencies": { 27 | "debug": "^3.1.0", 28 | "retry": "^0.13.1" 29 | }, 30 | "devDependencies": { 31 | "@derhuerst/round-robin-scheduler": "^1.0.3", 32 | "ws": "^5.2.2" 33 | }, 34 | "scripts": { 35 | "test": "env NODE_ENV=dev node test.js", 36 | "prepublishOnly": "npm test" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # websocket-pool 2 | 3 | **A pool of [WebSocket](https://en.wikipedia.org/wiki/WebSocket) connections. Supports reconnecting.** Allows load-balancing messages over multiple WebSocket servers, using any [scheduling scheme](https://en.wikipedia.org/wiki/Scheduling_(computing)#Scheduling_disciplines) (e.g. [round robin](https://en.wikipedia.org/wiki/Round-robin_scheduling)). 4 | 5 | [![npm version](https://img.shields.io/npm/v/websocket-pool.svg)](https://www.npmjs.com/package/websocket-pool) 6 | [![build status](https://api.travis-ci.org/derhuerst/websocket-pool.svg?branch=master)](https://travis-ci.org/derhuerst/websocket-pool) 7 | ![ISC-licensed](https://img.shields.io/github/license/derhuerst/websocket-pool.svg) 8 | [![support me via GitHub Sponsors](https://img.shields.io/badge/support%20me-donate-fa7664.svg)](https://github.com/sponsors/derhuerst) 9 | [![chat with me on Twitter](https://img.shields.io/badge/chat%20with%20me-on%20Twitter-1da1f2.svg)](https://twitter.com/derhuerst) 10 | 11 | 12 | ## Installation 13 | 14 | ```shell 15 | npm install websocket-pool 16 | ``` 17 | 18 | 19 | ## Usage 20 | 21 | ```js 22 | const createPool = require('websocket-pool') 23 | const WebSocket = require('ws') 24 | const createRoundRobin = require('@derhuerst/round-robin-scheduler') 25 | 26 | const pool = createPool(WebSocket, createRoundRobin) 27 | 28 | // incoming message, just like websocket.on('message') 29 | pool.on('message', (msg) => { 30 | console.log('<-', msg.data) 31 | pool.close() 32 | }) 33 | 34 | // >= 1 connection in the pool is open 35 | pool.once('open', () => { 36 | pool.send('hello there') 37 | }) 38 | 39 | // the pool failed to reconnect after retrying 40 | pool.on('error', (err) => { 41 | console.error(err) 42 | }) 43 | 44 | const urls = [ 45 | 'ws://echo.websocket.org/#1', 46 | 'ws://echo.websocket.org/#2', 47 | 'ws://echo.websocket.org/#3' 48 | ] 49 | for (let url of urls) pool.add(url) 50 | ``` 51 | 52 | `websocket-pool` accepts any [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) implementation. For example, you can use the native implementation in browsers or [`ws`](https://npmjs.com/package/ws) in Node. 53 | 54 | The `createScheduler` function must implement the [`abstract-scheduler` interface](https://github.com/derhuerst/abstract-scheduler). 55 | 56 | 57 | ## Related 58 | 59 | - [`reconnecting-websocket`](https://www.npmjs.com/package/reconnecting-websocket) – If want to connect to only one server. 60 | - [`node-pool`](https://github.com/coopernurse/node-pool) – Generic pool. You have to write the adapter to the resource. 61 | 62 | 63 | ## Contributing 64 | 65 | If you have a question or need support using `websocket-pool`, please double-check your code and setup first. If you think you have found a bug or want to propose a feature, refer to [the issues page](https://github.com/derhuerst/websocket-pool/issues). 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {EventEmitter} = require('events') 4 | const {operation} = require('retry') 5 | const debug = require('debug')('websocket-pool') 6 | 7 | const NORMAL_CLOSE = 1000 8 | 9 | const noConnectionAvailable = new Error('no connection available') 10 | noConnectionAvailable.code = 'noConnectionAvailable' 11 | 12 | const defaults = { 13 | retry: {} 14 | // todo: chooseConnection(msg, connections) => connection 15 | } 16 | 17 | const createPool = (WebSocket, createScheduler, opt = {}) => { 18 | opt = Object.assign({}, defaults, opt) 19 | 20 | const pool = new EventEmitter() 21 | const scheduler = createScheduler([]) 22 | 23 | const connections = [] 24 | let connectionsI = 0 25 | let nrOfOpenConnections = 0 26 | const ops = [] 27 | 28 | const add = (url) => { 29 | const i = connectionsI++ 30 | debug('adding', url, 'as', i) 31 | const op = operation(Object.assign({}, opt.retry)) 32 | ops[i] = op 33 | 34 | op.attempt((attemptNr) => { 35 | debug(i, 'reconnect', attemptNr) 36 | pool.emit('connection-retry', url, attemptNr) 37 | open(url, i, (err) => { 38 | debug(i, 'closed') 39 | const willRetry = op.retry(err) 40 | if (!willRetry && err.code !== NORMAL_CLOSE) { 41 | pool.emit('error', op.mainError()) 42 | } 43 | }) 44 | }) 45 | 46 | return () => remove(i) 47 | } 48 | 49 | const remove = (i) => { 50 | const ws = connections[i] 51 | if (!ws) throw new Error('unknown connection ' + i) 52 | debug('removing', i, ws.url) 53 | 54 | const op = ops[i] 55 | ops[i] = null 56 | op.stop() 57 | connections[i] = null 58 | ws.close(NORMAL_CLOSE, 'removed from pool') 59 | } 60 | 61 | const open = (url, i, onClose) => { 62 | const ws = new WebSocket(url) 63 | connections[i] = ws 64 | ws.addEventListener('message', (msg) => { 65 | pool.emit('message', msg, ws) 66 | }) 67 | 68 | const onceOpen = () => { 69 | ws.removeEventListener('open', onceOpen) 70 | debug(i, 'open') 71 | 72 | scheduler.add(i) 73 | 74 | pool.emit('connection-open', ws) 75 | nrOfOpenConnections++ 76 | if (nrOfOpenConnections === 1) pool.emit('open') 77 | } 78 | ws.addEventListener('open', onceOpen) 79 | 80 | const onceClosed = (ev) => { 81 | ws.removeEventListener('close', onceClosed) 82 | debug(i, 'close') 83 | 84 | scheduler.remove(i) 85 | connections[i] = null 86 | 87 | pool.emit('connection-close', ev.target, ev.code, ev.reason) 88 | nrOfOpenConnections-- 89 | if (nrOfOpenConnections === 0) pool.emit('close') 90 | 91 | const err = new Error(ev.reason) 92 | err.code = ev.code 93 | onClose(err) 94 | } 95 | ws.addEventListener('close', onceClosed) 96 | 97 | ws.addEventListener('error', (ev) => { 98 | debug(i, 'error', ev.error) 99 | pool.emit('connection-error', ev.target, ev.error) 100 | }) 101 | 102 | ws.addEventListener('ping', data => ws.pong(data)) 103 | } 104 | 105 | const send = (msg) => { 106 | const i = scheduler.get() 107 | const ws = connections[i] 108 | if (!ws) throw noConnectionAvailable // todo: wait 109 | debug(i, 'send', msg) 110 | ws.send(msg) 111 | } 112 | 113 | const close = () => { 114 | for (let i = 0; i <= connectionsI; i++) { 115 | if (connections[i]) remove(i) 116 | } 117 | } 118 | 119 | pool.add = add 120 | pool.remove = remove 121 | pool.send = send 122 | pool.close = close 123 | return pool 124 | } 125 | 126 | createPool.noConnectionAvailable = noConnectionAvailable 127 | module.exports = createPool 128 | --------------------------------------------------------------------------------