├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── browser.js ├── client.js ├── index.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@penalosa/epsilon` 2 | 3 | > Fast, fun & simple communication over websockets 4 | 5 | Makes communication over websockets as easy as calling an `async` local function, using Proxies to "proxy" function calls to a node server. 6 | 7 | Install with `npm install @penalosa/epsilon` or `yarn add @penalosa/epsilon`. It works, but is in a very early stage of development, so expect bugs. 8 | 9 | ## QuickStart 10 | 11 | ```javascript 12 | // On the server, node.js code 13 | import { server } from '@penalosa/epsilon' 14 | 15 | const app = server() 16 | 17 | app.helloWorld = ({ name }) => { 18 | return `Hello ${name}` 19 | } 20 | ``` 21 | 22 | ```javascript 23 | // Clientside 24 | import { client } from '@penalosa/epsilon' 25 | 26 | const api = client(`ws://localhost:8090`) 27 | 28 | api.helloWorld({ name: `Jane Smith` }).then(console.log) 29 | // Will output "Hello Jane Smith" 30 | ``` 31 | 32 | ## API reference 33 | 34 | ```javascript 35 | // server :: ({ port: number | undefined, debug: boolean | undefined } | undefined) -> Proxy 36 | import { server } from '@penalosa/epsilon' 37 | ``` 38 | 39 | The `port` parameter describes the port on which the websocket server will be served, and the `debug` parameter enables debug logging to the console. 40 | 41 | The `server` function returns a proxy that can be assigned to in order to register handlers. The property that you assign to becomes the endpoint name. The function definition can be `async`, and when called is given three parameters: 42 | 43 | - `payload`, wich is whatever was sent by the client 44 | - `persisted`, any data that's been persisted for this specific connection 45 | - `api`, of the form `{ persist, publish}`, both of which are functions: 46 | - `persist` - Takes a single parameter of any type, and persists it onto the specific connection. Any subsequent handler invocations will receive it as `persisted` 47 | - `publish` - Publish some data to multiple clients. Takes three parameters, `(subscription, match, payload)`. `subscription` is a string that identifies this for clients to know where to send it. `match` is a function that decides which clients this data will be sent to. It takes a single parameter of the persisted data on a specific client, and returns a boolean. Payload is what should be sent to clients. 48 | 49 | ```javascript 50 | import { server } from '@penalosa/epsilon' 51 | 52 | const app = server() 53 | 54 | app.hello = async ({ name }, _, { persist, publish }) => { 55 | persist({ name }) 56 | publish('say_hi', () => true, `${name} was greeted`) 57 | return `Hello ${name}` 58 | } 59 | app.whoami = async (_, { name }) => { 60 | return name 61 | } 62 | ``` 63 | 64 | ```javascript 65 | // client :: (string, { debug: boolean | undefined } | undefined) -> Proxy 66 | import { client } from '@penalosa/epsilon' 67 | ``` 68 | 69 | The first parameter is the websocket connection string to use, a la `ws://localhost:3000`, where `3000` is the port defined in `server()` (default is `8090`),and the `debug` parameter enables debug logging to the console. 70 | 71 | Calling `client` returns a proxy that works in a similar manner to the server. Calling a property triggers the corresponding handler on the server, and is resolved with the handler's return value (or rejected if the handler rejects): 72 | 73 | ```javascript 74 | // client :: (string, { debug: boolean | undefined } | undefined) -> Proxy 75 | import { client } from '@penalosa/epsilon' 76 | 77 | const api = client(`ws://localhost:8090`) 78 | 79 | api 80 | .hello({ name: `Jane Smith` }) 81 | .then(() => api.whoami()) 82 | .then(console.log) 83 | //Logs `Jane Smith` 84 | ``` 85 | 86 | Listening for server published events is also really simple - instead of calling a property of the proxy, you assign to the property: 87 | 88 | ```javascript 89 | import { client } from '@penalosa/epsilon' 90 | 91 | const api = client(`ws://localhost:8090`) 92 | 93 | api.say_hi = console.log 94 | 95 | // Using the examples above, this could log `Jane Smith was greeted` 96 | ``` 97 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@penalosa/epsilon", 3 | "version": "1.0.5", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@rollup/plugin-commonjs": { 8 | "version": "11.1.0", 9 | "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.1.0.tgz", 10 | "integrity": "sha512-Ycr12N3ZPN96Fw2STurD21jMqzKwL9QuFhms3SD7KKRK7oaXUsBU9Zt0jL/rOPHiPYisI21/rXGO3jr9BnLHUA==", 11 | "dev": true, 12 | "requires": { 13 | "@rollup/pluginutils": "^3.0.8", 14 | "commondir": "^1.0.1", 15 | "estree-walker": "^1.0.1", 16 | "glob": "^7.1.2", 17 | "is-reference": "^1.1.2", 18 | "magic-string": "^0.25.2", 19 | "resolve": "^1.11.0" 20 | } 21 | }, 22 | "@rollup/plugin-node-resolve": { 23 | "version": "7.1.3", 24 | "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz", 25 | "integrity": "sha512-RxtSL3XmdTAE2byxekYLnx+98kEUOrPHF/KRVjLH+DEIHy6kjIw7YINQzn+NXiH/NTrQLAwYs0GWB+csWygA9Q==", 26 | "dev": true, 27 | "requires": { 28 | "@rollup/pluginutils": "^3.0.8", 29 | "@types/resolve": "0.0.8", 30 | "builtin-modules": "^3.1.0", 31 | "is-module": "^1.0.0", 32 | "resolve": "^1.14.2" 33 | } 34 | }, 35 | "@rollup/pluginutils": { 36 | "version": "3.0.10", 37 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.10.tgz", 38 | "integrity": "sha512-d44M7t+PjmMrASHbhgpSbVgtL6EFyX7J4mYxwQ/c5eoaE6N2VgCgEcWVzNnwycIloti+/MpwFr8qfw+nRw00sw==", 39 | "dev": true, 40 | "requires": { 41 | "@types/estree": "0.0.39", 42 | "estree-walker": "^1.0.1", 43 | "picomatch": "^2.2.2" 44 | } 45 | }, 46 | "@types/estree": { 47 | "version": "0.0.39", 48 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", 49 | "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", 50 | "dev": true 51 | }, 52 | "@types/node": { 53 | "version": "14.0.1", 54 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.1.tgz", 55 | "integrity": "sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA==", 56 | "dev": true 57 | }, 58 | "@types/resolve": { 59 | "version": "0.0.8", 60 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", 61 | "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", 62 | "dev": true, 63 | "requires": { 64 | "@types/node": "*" 65 | } 66 | }, 67 | "acorn": { 68 | "version": "7.2.0", 69 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", 70 | "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", 71 | "dev": true 72 | }, 73 | "balanced-match": { 74 | "version": "1.0.0", 75 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 76 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 77 | "dev": true 78 | }, 79 | "brace-expansion": { 80 | "version": "1.1.11", 81 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 82 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 83 | "dev": true, 84 | "requires": { 85 | "balanced-match": "^1.0.0", 86 | "concat-map": "0.0.1" 87 | } 88 | }, 89 | "builtin-modules": { 90 | "version": "3.1.0", 91 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", 92 | "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", 93 | "dev": true 94 | }, 95 | "commondir": { 96 | "version": "1.0.1", 97 | "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", 98 | "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", 99 | "dev": true 100 | }, 101 | "concat-map": { 102 | "version": "0.0.1", 103 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 104 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 105 | "dev": true 106 | }, 107 | "estree-walker": { 108 | "version": "1.0.1", 109 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", 110 | "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", 111 | "dev": true 112 | }, 113 | "fs.realpath": { 114 | "version": "1.0.0", 115 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 116 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 117 | "dev": true 118 | }, 119 | "glob": { 120 | "version": "7.1.6", 121 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 122 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 123 | "dev": true, 124 | "requires": { 125 | "fs.realpath": "^1.0.0", 126 | "inflight": "^1.0.4", 127 | "inherits": "2", 128 | "minimatch": "^3.0.4", 129 | "once": "^1.3.0", 130 | "path-is-absolute": "^1.0.0" 131 | } 132 | }, 133 | "inflight": { 134 | "version": "1.0.6", 135 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 136 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 137 | "dev": true, 138 | "requires": { 139 | "once": "^1.3.0", 140 | "wrappy": "1" 141 | } 142 | }, 143 | "inherits": { 144 | "version": "2.0.4", 145 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 146 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 147 | "dev": true 148 | }, 149 | "is-module": { 150 | "version": "1.0.0", 151 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 152 | "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", 153 | "dev": true 154 | }, 155 | "is-reference": { 156 | "version": "1.1.4", 157 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.1.4.tgz", 158 | "integrity": "sha512-uJA/CDPO3Tao3GTrxYn6AwkM4nUPJiGGYu5+cB8qbC7WGFlrKZbiRo7SFKxUAEpFUfiHofWCXBUNhvYJMh+6zw==", 159 | "dev": true, 160 | "requires": { 161 | "@types/estree": "0.0.39" 162 | } 163 | }, 164 | "magic-string": { 165 | "version": "0.25.7", 166 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", 167 | "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", 168 | "dev": true, 169 | "requires": { 170 | "sourcemap-codec": "^1.4.4" 171 | } 172 | }, 173 | "minimatch": { 174 | "version": "3.0.4", 175 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 176 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 177 | "dev": true, 178 | "requires": { 179 | "brace-expansion": "^1.1.7" 180 | } 181 | }, 182 | "once": { 183 | "version": "1.4.0", 184 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 185 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 186 | "dev": true, 187 | "requires": { 188 | "wrappy": "1" 189 | } 190 | }, 191 | "path-is-absolute": { 192 | "version": "1.0.1", 193 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 194 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 195 | "dev": true 196 | }, 197 | "path-parse": { 198 | "version": "1.0.6", 199 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 200 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 201 | "dev": true 202 | }, 203 | "picomatch": { 204 | "version": "2.2.2", 205 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", 206 | "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", 207 | "dev": true 208 | }, 209 | "resolve": { 210 | "version": "1.17.0", 211 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", 212 | "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", 213 | "dev": true, 214 | "requires": { 215 | "path-parse": "^1.0.6" 216 | } 217 | }, 218 | "rollup": { 219 | "version": "1.32.1", 220 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz", 221 | "integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==", 222 | "dev": true, 223 | "requires": { 224 | "@types/estree": "*", 225 | "@types/node": "*", 226 | "acorn": "^7.1.0" 227 | } 228 | }, 229 | "sourcemap-codec": { 230 | "version": "1.4.8", 231 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 232 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 233 | "dev": true 234 | }, 235 | "wrappy": { 236 | "version": "1.0.2", 237 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 238 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 239 | "dev": true 240 | }, 241 | "ws": { 242 | "version": "7.3.0", 243 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", 244 | "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==" 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@penalosa/epsilon", 3 | "version": "1.0.5", 4 | "description": "Simple, fast & fun communication over Websockets", 5 | "main": "dist/epsilon.cjs.js", 6 | "module": "dist/epsilon.esm.js", 7 | "browser": "dist/epsilon.umd.js", 8 | "scripts": { 9 | "build": "rollup -c", 10 | "dev": "rollup -c -w" 11 | }, 12 | "files": [ 13 | "dist" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/penalosa/epsilon.git" 18 | }, 19 | "keywords": [ 20 | "websockets", 21 | "rpc" 22 | ], 23 | "dependencies": { 24 | "ws": "^7.3.0" 25 | }, 26 | "devDependencies": { 27 | "@rollup/plugin-commonjs": "^11.0.1", 28 | "@rollup/plugin-node-resolve": "^7.0.0", 29 | "rollup": "^1.29.0" 30 | }, 31 | "author": "Samuel Macleod", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/penalosa/epsilon/issues" 35 | }, 36 | "homepage": "https://github.com/penalosa/epsilon#readme" 37 | } 38 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | // import replace from '@rollup/plugin-replace' 4 | import pkg from './package.json' 5 | 6 | export default [ 7 | // browser-friendly UMD build 8 | { 9 | input: 'src/browser.js', 10 | output: { 11 | name: 'epsilon', 12 | file: pkg.browser, 13 | format: 'umd', 14 | }, 15 | plugins: [ 16 | resolve(), // so Rollup can find `ms` 17 | commonjs(), // so Rollup can convert `ms` to an ES module 18 | ], 19 | }, 20 | 21 | // CommonJS (for Node) and ES module (for bundlers) build. 22 | // (We could have three entries in the configuration array 23 | // instead of two, but it's quicker to generate multiple 24 | // builds from a single configuration where possible, using 25 | // an array for the `output` option, where we can specify 26 | // `file` and `format` for each target) 27 | { 28 | input: 'src/index.js', 29 | external: ['ws'], 30 | output: [ 31 | { file: pkg.main, format: 'cjs' }, 32 | { file: pkg.module, format: 'es' }, 33 | ], 34 | }, 35 | ] 36 | -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | export { default as client } from './client.js' 2 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | export default (url, { debug = false } = { debug: false }) => { 2 | const setup = () => { 3 | let ws = new WebSocket(url) 4 | debug && console.log(`Constructing event listeners`) 5 | ws.onopen = onOpen 6 | ws.onclose = onClose 7 | ws.onmessage = onMessage 8 | return ws 9 | } 10 | const heartbeat = setInterval(() => { 11 | requestQueue.push( 12 | JSON.stringify({ 13 | method: '__heartbeat', 14 | }) 15 | ) 16 | kickoffRequests() 17 | }, 5000) 18 | let ws = setup() 19 | let requestQueue = [] 20 | const kickoffRequests = () => { 21 | let req = requestQueue.shift() 22 | if (!req) { 23 | return 24 | } 25 | try { 26 | ws.send(req) 27 | if (requestQueue.length > 0) { 28 | Promise.resolve().then(kickoffRequests) 29 | } 30 | } catch (e) { 31 | requestQueue.push(req) 32 | } 33 | } 34 | let inProgress = { 35 | subscriptions: { 36 | __heartbeat: (payload) => { 37 | debug && console.log(`Heartbeat: ${payload}`) 38 | }, 39 | }, 40 | rpc: {}, 41 | } 42 | let requestId = 1 43 | let proxy = new Proxy(inProgress, { 44 | set: (target, event, handler) => { 45 | target.subscriptions[event] = handler 46 | }, 47 | get: (target, method) => { 48 | return (payload) => { 49 | return new Promise((resolve, reject) => { 50 | target.rpc[requestId] = { resolve, reject } 51 | 52 | requestQueue.push( 53 | JSON.stringify({ 54 | method, 55 | requestId, 56 | payload, 57 | }) 58 | ) 59 | kickoffRequests() 60 | requestId++ 61 | }) 62 | } 63 | }, 64 | }) 65 | function onOpen() { 66 | debug && console.log(`Connection opened`) 67 | 68 | kickoffRequests() 69 | } 70 | const reconnect = () => { 71 | try { 72 | ws = setup() 73 | } catch (e) { 74 | setTimeout(reconnect, 1000) 75 | } 76 | } 77 | function onClose() { 78 | debug && console.warn(`Connection closed`) 79 | reconnect() 80 | } 81 | 82 | function onMessage({ data }) { 83 | const { payload, success, subscription, requestId } = JSON.parse(data) 84 | debug && 85 | console.log(`Message received from server:`, { 86 | payload, 87 | success, 88 | subscription, 89 | requestId, 90 | }) 91 | 92 | if (subscription) { 93 | let handler = inProgress.subscriptions[subscription] 94 | if (handler) { 95 | return handler(payload) 96 | } else { 97 | debug && console.warn(`No handler for subscription: ${subscription}`) 98 | return 99 | } 100 | } 101 | 102 | let handler = inProgress.rpc[requestId] 103 | 104 | if (handler) { 105 | if (success) { 106 | handler.resolve(payload) 107 | } else { 108 | handler.reject(payload) 109 | } 110 | 111 | delete inProgress.rpc[requestId] 112 | } else { 113 | console.error(`No handler for RPC return: ${requestId}`) 114 | } 115 | } 116 | return proxy 117 | } 118 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as server } from './server.js' 2 | export { default as client } from './client.js' 3 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws' 2 | export default ( 3 | { port = 8090, debug = false } = { port: 8090, debug: false } 4 | ) => { 5 | const persistData = Symbol('persist') 6 | const identifier = Symbol('identifier') 7 | let id = 1 8 | const wss = new WebSocket.Server({ port }) 9 | 10 | let connection = { 11 | heartbeat: (payload) => payload, 12 | } 13 | 14 | let proxy = new Proxy(connection, { 15 | set: (target, method, definition) => { 16 | if (target[method]) 17 | debug && 18 | console.warn(`Overriding defined method ${method}`, target[method]) 19 | target[method] = definition 20 | 21 | return true 22 | }, 23 | get: (target, method) => { 24 | if (!target[method]) throw new Error("Method doesn't exist") 25 | return target[method] 26 | }, 27 | }) 28 | 29 | const respond = ({ requestId, payload }) => 30 | JSON.stringify({ 31 | success: true, 32 | requestId, 33 | payload, 34 | }) 35 | const error = ({ requestId, payload }) => 36 | JSON.stringify({ 37 | success: false, 38 | requestId, 39 | payload, 40 | }) 41 | wss.on('connection', (ws) => { 42 | ws[identifier] = id++ 43 | ws[persistData] = {} 44 | ws.on('message', async (message) => { 45 | try { 46 | let { method, payload, requestId } = JSON.parse(message) 47 | 48 | try { 49 | if (method == `__heartbeat`) { 50 | return ws.send(JSON.stringify({ subscription: '__heartbeat' })) 51 | } 52 | if (!method) { 53 | throw new Error('Method not supplied') 54 | } 55 | if (!requestId) { 56 | throw new Error('requestId not supplied') 57 | } 58 | let handler = proxy[method] 59 | let result = await handler(payload || {}, ws[persistData], { 60 | persist: (data) => (ws[persistData] = data), 61 | publish: (subscription, match, payload) => { 62 | wss.clients.forEach((client) => { 63 | if ( 64 | client.readyState === WebSocket.OPEN && 65 | match(client[persistData]) 66 | ) { 67 | client.send( 68 | JSON.stringify({ 69 | success: true, 70 | subscription, 71 | payload, 72 | }) 73 | ) 74 | } 75 | }) 76 | }, 77 | }) 78 | return ws.send(respond({ requestId, payload: result })) 79 | } catch (e) { 80 | return ws.send(error({ requestId, payload: e.message })) 81 | } 82 | } catch (e) { 83 | debug && console.error('Malformed payload received:', e.message) 84 | } 85 | }) 86 | }) 87 | return proxy 88 | } 89 | --------------------------------------------------------------------------------