├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .notbabelrc ├── LICENSE ├── README.md ├── examples └── chatroom-client-vue │ ├── .babelrc │ ├── .env │ ├── build │ ├── setup-dev-server.js │ ├── vue-loader.config.js │ ├── webpack.base.config.js │ ├── webpack.client.config.js │ └── webpack.server.config.js │ ├── manifest.json │ ├── package.json │ ├── public │ ├── logo-120.png │ ├── logo-144.png │ ├── logo-152.png │ ├── logo-192.png │ ├── logo-256.png │ ├── logo-384.png │ ├── logo-48.png │ └── logo-512.png │ ├── robots.txt │ ├── server.js │ ├── src │ ├── App.vue │ ├── api │ │ └── index.js │ ├── app.js │ ├── components │ │ └── ProgressBar.vue │ ├── entry-client.js │ ├── entry-server.js │ ├── index.template.html │ ├── router │ │ └── index.js │ ├── store │ │ ├── actions.js │ │ ├── getters.js │ │ ├── index.js │ │ └── mutations.js │ ├── util │ │ └── title.js │ └── views │ │ ├── MainView.vue │ │ └── SignInView.vue │ └── yarn.lock ├── lib └── webwire.js ├── package-lock.json ├── package.json ├── src ├── asciiArrayToStr.js ├── identifier.js ├── index.js ├── message.js ├── namelessReqMsg.js ├── parse.js ├── parseEndpointAddress.js ├── requestMessage.js ├── sessionKey.js ├── signalMessage.js ├── socket.js ├── strToUtf8Array.js ├── utf8ArrayToStr.js └── verifyProtocolVersion.js ├── webpack.config.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | '@vue/standard', 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'indent': ['error', 'tab'], 14 | 'no-tabs': 'off', 15 | 'space-before-function-paren': 'off', 16 | 'space-in-parens': ['error', 'never'], 17 | 'object-curly-spacing': ['error', 'never'], 18 | 'comma-dangle': ['error', 'always-multiline'], 19 | 'no-unused-expressions': 'error', 20 | 'max-len': ['error', {'code': 80}, {'ignoreComments': true}], 21 | 'switch-colon-spacing': ['error', {'before': false}] 22 | }, 23 | parserOptions: { 24 | parser: 'babel-eslint', 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | #.env 59 | 60 | -------------------------------------------------------------------------------- /.notbabelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 11"] 7 | } 8 | }], 9 | "@babel/preset-stage-2" 10 | ], 11 | "ignore": [ 12 | "./examples/**/*.js", 13 | "./lib" 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 QBEON 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 | 2 |

3 |
4 | WebWire 5 |
6 |
7 | WebWire for JavaScript 8 |
9 | An asynchronous duplex messaging library 10 |

11 |

12 | 13 | Licence: MIT 14 | 15 |

16 |

17 | 18 | OpenCollective 19 | 20 |

21 |
22 | 23 | 24 | [WebWire](https://github.com/qbeon/webwire-go) is a high-level asynchronous duplex messaging library built on top of [WebSockets](https://developer.mozilla.org/de/docs/WebSockets) and an open source binary message protocol with builtin sessions and support for UTF8 and UTF16 encoding. 25 | The [webwire-js](https://github.com/qbeon/webwire-js) library provides a client implementation for JavaScript environments. 26 | 27 | ## WebWire Binary Protocol 28 | WebWire is built for speed and portability implementing an open source binary protocol. 29 | ![Protocol Subset Diagram](https://github.com/qbeon/webwire-go/blob/master/docs/img/wwr_msgproto_diagram.svg) 30 | 31 | More information about the protocol is available at [WebWire](https://github.com/qbeon/webwire-go). 32 | 33 | ## Examples 34 | - **[Chat Room](https://github.com/qbeon/webwire-js/tree/master/examples/chatroom-client-vue)** - Demonstrates advanced use of the library. The corresponding [Golang Chat Room Server](https://github.com/qbeon/webwire-go/tree/master/examples/chatroom) implements the server-side part of the example. 35 | 36 | ## Features 37 | ### Request-Reply 38 | Clients can initiate multiple simultaneous requests and receive replies asynchronously. Requests are multiplexed through the connection similar to HTTP2 pipelining. 39 | 40 | ```javascript 41 | // Send a request to the server, will block the goroutine until replied 42 | const {reply, err} = await client.request("", "sudo rm -rf /") 43 | if (err != null) { 44 | // Oh oh, request failed for some reason! 45 | } 46 | reply // Here we go! 47 | ``` 48 | 49 | Timed requests will timeout and return an error if the server doesn't manage to reply within the specified time frame. 50 | 51 | ```javascript 52 | // Send a request to the server, will fail if no reply is received within 200ms 53 | const {reply, err} = await client.request("", "hurry up!", null, 200) 54 | if (err != null) { 55 | // Probably timed out! 56 | } 57 | reply // Just in time! 58 | ``` 59 | 60 | ### Client-side Signals 61 | Individual clients can send signals to the server. Signals are one-way messages guaranteed to arrive not requiring any reply though. 62 | 63 | ```javascript 64 | // Send signal to server 65 | const err = await client.signal("eventA", "something") 66 | ``` 67 | 68 | ### Server-side Signals 69 | The server also can send signals to individual connected clients. 70 | 71 | ```javascript 72 | const client = new WebWireClient(serverAddr, { 73 | onSignal: signal => { 74 | signal.payload // Handle server-side signal 75 | }, 76 | }) 77 | ``` 78 | 79 | ### Namespaces 80 | Different kinds of requests and signals can be differentiated using the builtin namespacing feature. 81 | 82 | ```javascript 83 | // Request authentication 84 | const { 85 | reply: authReply, 86 | err: authReqErr 87 | } = await client.request("auth", "user:pass") 88 | if (authReqErr != null) { 89 | // Oh oh, authentication failed! 90 | } 91 | 92 | // Request data query 93 | const { 94 | reply: queryReply, 95 | err: queryErr 96 | } = await client.request("query", "sudo get sandwich") 97 | if (queryErr != null) { 98 | // Oh oh, data query failed! 99 | } 100 | ``` 101 | 102 | ```javascript 103 | const {err: aErr} = await client.signal("eventA", "something happend") 104 | const {err: bErr} = await client.signal("eventB", "something else happened") 105 | ``` 106 | 107 | ### Sessions 108 | Individual connections can get sessions assigned to identify them. The state of the session is automagically synchronized between the client and the server. WebWire doesn't enforce any kind of authentication technique though, it just provides a way to authenticate a connection. 109 | 110 | ```javascript 111 | const client = new WebWireClient(serverAddr, { 112 | onSessionCreated: newSession => { 113 | // The newly created session was just synchronized to the client 114 | }, 115 | }) 116 | ``` 117 | 118 | ### Automatic Session Restoration 119 | WebWire clients persist their session to the local storage and try to restore it when connecting to the server repeatedly assuming the server didn't yet close this session. 120 | 121 | ```javascript 122 | const client = new WebWireClient(serverAddr) 123 | const err = await client.connect() 124 | if (err != null) { 125 | // Oh, oh! Connection failed 126 | } 127 | client.session // Won't be null, if a previous session was restored 128 | ``` 129 | 130 | ### Encodings 131 | Besides plain binary streams WebWire supports UTF8 and UTF16 encodings and will automatically transcode payloads into the explicitly specified encoding. If no encoding is explicitly specified - UTF16 is used for JavaScript strings and plain binary for Uint8Array instances by default. 132 | 133 | ```javascript 134 | // Cyrillic text in UTF16 135 | client.request("", "кириллица") 136 | 137 | // Cyrillic text in UTF8 automatically transcoded 138 | client.request("", "кириллица", "utf8") 139 | 140 | const binaryData = new Uint8Array(new ArrayBuffer(5)) 141 | binaryData.set([76, 97, 116, 105, 110], 0) // "Latin" 142 | client.request("", binaryData) // 7-bit ASCII text in binary 143 | ``` 144 | ---- 145 | 146 | © 2018 Roman Sharkov 147 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }] 4 | ], 5 | "plugins": [ 6 | "syntax-dynamic-import", 7 | ["transform-runtime", { 8 | "helpers": false, 9 | "polyfill": false, 10 | "regenerator": true, 11 | "moduleName": "babel-runtime" 12 | }] 13 | ], 14 | "ignore": [ 15 | "../../**/*.js" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/.env: -------------------------------------------------------------------------------- 1 | API_HOST=https://localhost:9090 2 | PORT=7070 3 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/build/setup-dev-server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const MFS = require('memory-fs') 4 | const webpack = require('webpack') 5 | const chokidar = require('chokidar') 6 | const clientConfig = require('./webpack.client.config') 7 | const serverConfig = require('./webpack.server.config') 8 | 9 | const readFile = (fs, file) => { 10 | try { 11 | return fs.readFileSync( 12 | path.join(clientConfig.output.path, file), 13 | 'utf-8' 14 | ) 15 | } catch (e) {} 16 | } 17 | 18 | module.exports = function setupDevServer (app, templatePath, cb) { 19 | let bundle 20 | let template 21 | let clientManifest 22 | 23 | let ready 24 | const readyPromise = new Promise(resolve => { 25 | ready = resolve 26 | }) 27 | const update = () => { 28 | if (bundle && clientManifest) { 29 | ready() 30 | cb(bundle, { 31 | template, 32 | clientManifest, 33 | }) 34 | } 35 | } 36 | 37 | // read template from disk and watch 38 | template = fs.readFileSync(templatePath, 'utf-8') 39 | chokidar.watch(templatePath).on('change', () => { 40 | template = fs.readFileSync(templatePath, 'utf-8') 41 | console.log('index.html template updated.') 42 | update() 43 | }) 44 | 45 | // modify client config to work with hot middleware 46 | clientConfig.entry.app = [ 47 | 'webpack-hot-middleware/client', 48 | clientConfig.entry.app, 49 | ] 50 | clientConfig.output.filename = '[name].js' 51 | clientConfig.plugins.push( 52 | new webpack.HotModuleReplacementPlugin(), 53 | new webpack.NoEmitOnErrorsPlugin() 54 | ) 55 | 56 | // dev middleware 57 | const clientCompiler = webpack(clientConfig) 58 | const devMiddleware = require('webpack-dev-middleware')(clientCompiler, { 59 | publicPath: clientConfig.output.publicPath, 60 | noInfo: true, 61 | }) 62 | app.use(devMiddleware) 63 | clientCompiler.plugin('done', stats => { 64 | stats = stats.toJson() 65 | stats.errors.forEach(err => console.error(err)) 66 | stats.warnings.forEach(err => console.warn(err)) 67 | if (stats.errors.length) return 68 | clientManifest = JSON.parse(readFile( 69 | devMiddleware.fileSystem, 70 | 'vue-ssr-client-manifest.json' 71 | )) 72 | update() 73 | }) 74 | 75 | // hot middleware 76 | app.use( 77 | require('webpack-hot-middleware')(clientCompiler, {heartbeat: 5000}) 78 | ) 79 | 80 | // watch and update server renderer 81 | const serverCompiler = webpack(serverConfig) 82 | const mfs = new MFS() 83 | serverCompiler.outputFileSystem = mfs 84 | serverCompiler.watch({}, (err, stats) => { 85 | if (err) throw err 86 | stats = stats.toJson() 87 | if (stats.errors.length) return 88 | 89 | // read bundle generated by vue-ssr-webpack-plugin 90 | bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json')) 91 | update() 92 | }) 93 | 94 | return readyPromise 95 | } 96 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/build/vue-loader.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extractCSS: process.env.NODE_ENV === 'production', 3 | preserveWhitespace: false, 4 | postcss: [ 5 | require('autoprefixer')({ 6 | browsers: ['last 3 versions'], 7 | }), 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const vueConfig = require('./vue-loader.config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 6 | 7 | const isProd = process.env.NODE_ENV === 'production' 8 | 9 | module.exports = { 10 | devtool: isProd ? false : '#cheap-module-source-map', 11 | output: { 12 | path: path.resolve(__dirname, '../dist'), 13 | publicPath: '/dist/', 14 | filename: '[name].[chunkhash].js', 15 | }, 16 | resolve: { 17 | alias: { 18 | 'public': path.resolve(__dirname, '../public'), 19 | }, 20 | }, 21 | module: { 22 | noParse: /es6-promise\.js$/, // avoid webpack shimming process 23 | rules: [{ 24 | test: /\.vue$/, 25 | loader: 'vue-loader', 26 | options: vueConfig, 27 | }, { 28 | test: /\.js$/, 29 | loader: 'babel-loader', 30 | exclude: /node_modules/, 31 | }, { 32 | test: /\.(png|jpg|gif|svg)$/, 33 | loader: 'url-loader', 34 | options: { 35 | limit: 10000, 36 | name: '[name].[ext]?[hash]', 37 | }, 38 | }, { 39 | test: /\.css$/, 40 | use: isProd 41 | ? ExtractTextPlugin.extract({ 42 | use: 'css-loader?minimize', 43 | fallback: 'vue-style-loader', 44 | }) 45 | : ['vue-style-loader', 'css-loader'], 46 | }], 47 | }, 48 | performance: { 49 | maxEntrypointSize: 300000, 50 | hints: isProd ? 'warning' : false, 51 | }, 52 | plugins: isProd ? [ 53 | new webpack.optimize.UglifyJsPlugin({ 54 | compress: {warnings: false}, 55 | }), 56 | new webpack.optimize.ModuleConcatenationPlugin(), 57 | new ExtractTextPlugin({filename: 'common.[chunkhash].css'}), 58 | ] : [ 59 | new FriendlyErrorsPlugin(), 60 | ], 61 | } 62 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/build/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const SWPrecachePlugin = require('sw-precache-webpack-plugin') 5 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 6 | 7 | const config = merge(base, { 8 | entry: { 9 | app: './src/entry-client.js', 10 | }, 11 | resolve: { 12 | alias: { 13 | 'create-api': './create-api-client.js', 14 | }, 15 | }, 16 | plugins: [ 17 | // strip dev-only code in Vue source 18 | new webpack.DefinePlugin({ 19 | 'process.env.NODE_ENV': JSON.stringify( 20 | process.env.NODE_ENV || 'development' 21 | ), 22 | 'process.env.VUE_ENV': '"client"', 23 | }), 24 | // extract vendor chunks for better caching 25 | new webpack.optimize.CommonsChunkPlugin({ 26 | name: 'vendor', 27 | minChunks: function (module) { 28 | // a module is extracted into the vendor chunk if... 29 | return ( 30 | // it's inside node_modules 31 | /node_modules/.test(module.context) && 32 | // and not a CSS file 33 | // (due to extract-text-webpack-plugin limitation) 34 | !/\.css$/.test(module.request) 35 | ) 36 | }, 37 | }), 38 | // extract webpack runtime & manifest 39 | // to avoid vendor chunk hash changing on every build. 40 | new webpack.optimize.CommonsChunkPlugin({ 41 | name: 'manifest', 42 | }), 43 | new VueSSRClientPlugin(), 44 | ], 45 | }) 46 | 47 | if (process.env.NODE_ENV === 'production') { 48 | config.plugins.push( 49 | // auto generate service worker 50 | new SWPrecachePlugin({ 51 | cacheId: 'webwire-examples-chatroom-vue', 52 | filename: 'service-worker.js', 53 | minify: true, 54 | dontCacheBustUrlsMatching: /./, 55 | staticFileGlobsIgnorePatterns: [/\.map$/, /\.json$/], 56 | runtimeCaching: [{ 57 | urlPattern: '/', 58 | handler: 'networkFirst', 59 | }, { 60 | urlPattern: /\/(top|new|show|ask|jobs)/, 61 | handler: 'networkFirst', 62 | }, { 63 | urlPattern: '/item/:id', 64 | handler: 'networkFirst', 65 | }, { 66 | urlPattern: '/user/:id', 67 | handler: 'networkFirst', 68 | }], 69 | }) 70 | ) 71 | } 72 | 73 | module.exports = config 74 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/build/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const nodeExternals = require('webpack-node-externals') 5 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 6 | 7 | module.exports = merge(base, { 8 | target: 'node', 9 | devtool: '#source-map', 10 | entry: './src/entry-server.js', 11 | output: { 12 | filename: 'server-bundle.js', 13 | libraryTarget: 'commonjs2', 14 | }, 15 | resolve: { 16 | alias: { 17 | 'create-api': './create-api-server.js', 18 | }, 19 | }, 20 | // https://webpack.js.org/configuration/externals/#externals 21 | // https://github.com/liady/webpack-node-externals 22 | externals: nodeExternals({ 23 | // do not externalize CSS files in case we need to import it from a dep 24 | whitelist: /\.css$/, 25 | }), 26 | plugins: [ 27 | new webpack.DefinePlugin({ 28 | 'process.env.NODE_ENV': JSON.stringify( 29 | process.env.NODE_ENV || 'development' 30 | ), 31 | 'process.env.VUE_ENV': '"server"', 32 | }), 33 | new VueSSRServerPlugin(), 34 | ], 35 | }) 36 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WebWire Examples: Chat Room Vue", 3 | "short_name": "WebWire ChatRoom", 4 | "icons": [{ 5 | "src": "/public/logo-120.png", 6 | "sizes": "120x120", 7 | "type": "image/png" 8 | }, { 9 | "src": "/public/logo-144.png", 10 | "sizes": "144x144", 11 | "type": "image/png" 12 | }, { 13 | "src": "/public/logo-152.png", 14 | "sizes": "152x152", 15 | "type": "image/png" 16 | }, { 17 | "src": "/public/logo-192.png", 18 | "sizes": "192x192", 19 | "type": "image/png" 20 | }, { 21 | "src": "/public/logo-256.png", 22 | "sizes": "256x256", 23 | "type": "image/png" 24 | }, { 25 | "src": "/public/logo-384.png", 26 | "sizes": "384x384", 27 | "type": "image/png" 28 | }, { 29 | "src": "/public/logo-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png" 32 | }], 33 | "start_url": "/", 34 | "background_color": "#f2f3f5", 35 | "display": "standalone", 36 | "theme_color": "#f60" 37 | } 38 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webwire-examples-chatroom-vue", 3 | "description": "A WebWire Vue client of the chatroom example", 4 | "author": "Roman Sharkov ", 5 | "private": true, 6 | "scripts": { 7 | "dev": "cross-env PORT=1010 node server", 8 | "start": "cross-env NODE_ENV=production node server", 9 | "build": "rimraf dist && npm run build:client && npm run build:server", 10 | "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules", 11 | "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules" 12 | }, 13 | "engines": { 14 | "node": ">=7.0", 15 | "npm": ">=4.0" 16 | }, 17 | "dependencies": { 18 | "babel-runtime": "^6.26.0", 19 | "compression": "^1.7.1", 20 | "cross-env": "^5.1.1", 21 | "es6-promise": "^4.1.1", 22 | "express": "^4.16.2", 23 | "extract-text-webpack-plugin": "^3.0.2", 24 | "lru-cache": "^4.1.1", 25 | "node-fetch": "^2.1.2", 26 | "route-cache": "0.4.3", 27 | "serve-favicon": "^2.4.5", 28 | "vue": "^2.5.17", 29 | "vue-router": "^3.0.1", 30 | "vue-server-renderer": "^2.5.17", 31 | "vuex": "^3.0.1", 32 | "vuex-router-sync": "^5.0.0", 33 | "ws": "^5.1.0" 34 | }, 35 | "devDependencies": { 36 | "autoprefixer": "^7.1.6", 37 | "babel-core": "^6.26.0", 38 | "babel-loader": "^7.1.2", 39 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 40 | "babel-plugin-transform-runtime": "^6.23.0", 41 | "babel-preset-env": "^1.6.1", 42 | "chokidar": "^1.7.0", 43 | "css-loader": "^0.28.7", 44 | "dotenv": "^5.0.1", 45 | "file-loader": "^1.1.5", 46 | "friendly-errors-webpack-plugin": "^1.6.1", 47 | "rimraf": "^2.6.2", 48 | "stylus": "^0.54.5", 49 | "stylus-loader": "^3.0.1", 50 | "sw-precache-webpack-plugin": "^0.11.4", 51 | "url-loader": "^0.6.2", 52 | "vue-loader": "^13.5.0", 53 | "vue-style-loader": "^4.1.2", 54 | "vue-template-compiler": "^2.5.17", 55 | "webpack": "^3.8.1", 56 | "webpack-dev-middleware": "^1.12.0", 57 | "webpack-hot-middleware": "^2.20.0", 58 | "webpack-merge": "^4.1.1", 59 | "webpack-node-externals": "^1.6.0" 60 | }, 61 | "optionalDependencies": { 62 | "bufferutil": "^3.0.3", 63 | "utf-8-validate": "^4.0.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/public/logo-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romshark/webwire-js/90b3a4e04b6016af3f9f761506c2401119780be0/examples/chatroom-client-vue/public/logo-120.png -------------------------------------------------------------------------------- /examples/chatroom-client-vue/public/logo-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romshark/webwire-js/90b3a4e04b6016af3f9f761506c2401119780be0/examples/chatroom-client-vue/public/logo-144.png -------------------------------------------------------------------------------- /examples/chatroom-client-vue/public/logo-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romshark/webwire-js/90b3a4e04b6016af3f9f761506c2401119780be0/examples/chatroom-client-vue/public/logo-152.png -------------------------------------------------------------------------------- /examples/chatroom-client-vue/public/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romshark/webwire-js/90b3a4e04b6016af3f9f761506c2401119780be0/examples/chatroom-client-vue/public/logo-192.png -------------------------------------------------------------------------------- /examples/chatroom-client-vue/public/logo-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romshark/webwire-js/90b3a4e04b6016af3f9f761506c2401119780be0/examples/chatroom-client-vue/public/logo-256.png -------------------------------------------------------------------------------- /examples/chatroom-client-vue/public/logo-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romshark/webwire-js/90b3a4e04b6016af3f9f761506c2401119780be0/examples/chatroom-client-vue/public/logo-384.png -------------------------------------------------------------------------------- /examples/chatroom-client-vue/public/logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romshark/webwire-js/90b3a4e04b6016af3f9f761506c2401119780be0/examples/chatroom-client-vue/public/logo-48.png -------------------------------------------------------------------------------- /examples/chatroom-client-vue/public/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romshark/webwire-js/90b3a4e04b6016af3f9f761506c2401119780be0/examples/chatroom-client-vue/public/logo-512.png -------------------------------------------------------------------------------- /examples/chatroom-client-vue/robots.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romshark/webwire-js/90b3a4e04b6016af3f9f761506c2401119780be0/examples/chatroom-client-vue/robots.txt -------------------------------------------------------------------------------- /examples/chatroom-client-vue/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const fs = require('fs') 3 | const path = require('path') 4 | const LRU = require('lru-cache') 5 | const express = require('express') 6 | const favicon = require('serve-favicon') 7 | const compression = require('compression') 8 | const microcache = require('route-cache') 9 | const resolve = file => path.resolve(__dirname, file) 10 | const { 11 | createBundleRenderer, 12 | } = require('vue-server-renderer') 13 | global.WebSocket = require('ws') 14 | 15 | const fetch = require('node-fetch') 16 | global.fetch = fetch 17 | 18 | const isProd = process.env.NODE_ENV === 'production' 19 | const useMicroCache = process.env.MICRO_CACHE !== 'false' 20 | const serverInfo = 21 | `express/${require('express/package.json').version} ` + 22 | `vue-server-renderer/${require('vue-server-renderer/package.json').version}` 23 | 24 | const app = express() 25 | 26 | function createRenderer (bundle, options) { 27 | // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer 28 | return createBundleRenderer(bundle, Object.assign(options, { 29 | // for component caching 30 | cache: LRU({ 31 | max: 1000, 32 | maxAge: 1000 * 60 * 15, 33 | }), 34 | // this is only needed when vue-server-renderer is npm-linked 35 | basedir: resolve('./dist'), 36 | // recommended for performance 37 | runInNewContext: false, 38 | })) 39 | } 40 | 41 | let renderer 42 | let readyPromise 43 | const templatePath = resolve('./src/index.template.html') 44 | if (isProd) { 45 | // In production: create server renderer 46 | // using template and built server bundle. 47 | // The server bundle is generated by vue-ssr-webpack-plugin. 48 | const template = fs.readFileSync(templatePath, 'utf-8') 49 | const bundle = require('./dist/vue-ssr-server-bundle.json') 50 | // The client manifests are optional, but it allows the renderer 51 | // to automatically infer preload/prefetch links and directly add 19 | 20 | 55 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/api/index.js: -------------------------------------------------------------------------------- 1 | import WebWireClient from '../../../../src' 2 | 3 | function init(host, handlers) { 4 | api.client = new WebWireClient(host, {handlers}) 5 | } 6 | 7 | const api = { 8 | client: null, 9 | init: init, 10 | } 11 | 12 | export default api 13 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import { 4 | createStore, 5 | } from './store' 6 | import { 7 | createRouter, 8 | } from './router' 9 | import { 10 | sync, 11 | } from 'vuex-router-sync' 12 | import titleMixin from './util/title' 13 | 14 | // mixin for handling title 15 | Vue.mixin(titleMixin) 16 | 17 | // Expose a factory function that creates a fresh set of store, router, 18 | // app instances on each call (which is called for each SSR request) 19 | export function createApp() { 20 | // create store and router instances 21 | const store = createStore() 22 | const router = createRouter() 23 | 24 | // sync the router with the vuex store. 25 | // this registers `store.state.route` 26 | sync(store, router) 27 | 28 | // create the app instance. 29 | // here we inject the router, store and ssr context to all child components, 30 | // making them available everywhere as `this.$router` and `this.$store`. 31 | const app = new Vue({ 32 | router, 33 | store, 34 | render: h => h(App), 35 | }) 36 | 37 | // expose the app, the router and the store. 38 | // note we are not mounting the app here, since bootstrapping will be 39 | // different depending on whether we are in a browser or on the server. 40 | return {app, router, store} 41 | } 42 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/components/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 88 | 89 | 103 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/entry-client.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import 'es6-promise/auto' 3 | import { 4 | createApp, 5 | } from './app' 6 | import ProgressBar from './components/ProgressBar.vue' 7 | 8 | // global progress bar 9 | const bar = Vue.prototype.$bar = new Vue(ProgressBar).$mount() 10 | document.body.appendChild(bar.$el) 11 | 12 | // a global mixin that calls `asyncData` when a route component's params change 13 | Vue.mixin({ 14 | beforeRouteUpdate (to, from, next) { 15 | const {asyncData} = this.$options 16 | if (asyncData) { 17 | asyncData({ 18 | store: this.$store, 19 | route: to, 20 | }) 21 | .then(next) 22 | .catch(next) 23 | } else next() 24 | }, 25 | }) 26 | 27 | const {app, router, store} = createApp() 28 | 29 | // prime the store with server-initialized state. 30 | // the state is determined during SSR and inlined in the page markup. 31 | if (window.__INITIAL_STATE__) { 32 | store.replaceState(window.__INITIAL_STATE__) 33 | } 34 | 35 | // wait until router has resolved all async before hooks 36 | // and async components... 37 | router.onReady(() => { 38 | // Add router hook for handling asyncData. 39 | // Doing it after initial route is resolved so that we don't double-fetch 40 | // the data that we already have. Using router.beforeResolve() so that all 41 | // async components are resolved. 42 | router.beforeResolve((to, from, next) => { 43 | const matched = router.getMatchedComponents(to) 44 | const prevMatched = router.getMatchedComponents(from) 45 | let diffed = false 46 | const activated = matched.filter((c, i) => { 47 | return diffed || (diffed = (prevMatched[i] !== c)) 48 | }) 49 | const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _) 50 | if (!asyncDataHooks.length) return next() 51 | 52 | bar.start() 53 | Promise.all(asyncDataHooks.map(hook => hook({store, route: to}))) 54 | .then(() => { 55 | bar.finish() 56 | next() 57 | }) 58 | .catch(next) 59 | }) 60 | 61 | // actually mount to DOM 62 | app.$mount('#app') 63 | }) 64 | 65 | // service worker 66 | if (location.protocol === 'https:' && navigator.serviceWorker) { 67 | navigator.serviceWorker.register('/service-worker.js') 68 | } 69 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/entry-server.js: -------------------------------------------------------------------------------- 1 | import { 2 | createApp, 3 | } from './app' 4 | 5 | const isDev = process.env.NODE_ENV !== 'production' 6 | 7 | // This exported function will be called by `bundleRenderer`. 8 | // This is where we perform data-prefetching to determine the 9 | // state of our application before actually rendering it. 10 | // Since data fetching is async, this function is expected to 11 | // return a Promise that resolves to the app instance. 12 | export default context => { 13 | return new Promise((resolve, reject) => { 14 | const s = isDev && Date.now() 15 | const {app, router, store} = createApp() 16 | 17 | const {url} = context 18 | const {fullPath} = router.resolve(url).route 19 | 20 | if (fullPath !== url) return reject(new Error({url: fullPath})) 21 | 22 | // set router's location 23 | router.push(url) 24 | 25 | // wait until router has resolved possible async hooks 26 | router.onReady(() => { 27 | const matchedComponents = router.getMatchedComponents() 28 | // no matched routes 29 | if (!matchedComponents.length) { 30 | return reject(new Error({code: 404})) 31 | } 32 | 33 | // Call fetchData hooks on components matched by the route. 34 | // A preFetch hook dispatches a store action and returns a Promise, 35 | // which is resolved when the action is complete 36 | // and store state has been updated. 37 | Promise.all(matchedComponents.map( 38 | ({asyncData}) => asyncData && asyncData({ 39 | store, 40 | route: router.currentRoute, 41 | }) 42 | )) 43 | .then(() => { 44 | isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`) 45 | // After all preFetch hooks are resolved, our store is now 46 | // filled with the state needed to render the app. 47 | // Expose the state on the render context, 48 | // and let the request handler inline the state 49 | // in the HTML response. This allows the client-side 50 | // store to pick-up the server-side state 51 | // without having to duplicate the initial data fetching 52 | // on the client. 53 | context.state = store.state 54 | resolve(app) 55 | }) 56 | .catch(reject) 57 | }, reject) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | 8 | 9 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | import MainView from '../views/MainView.vue' 5 | import SignInView from '../views/SignInView.vue' 6 | 7 | Vue.use(Router) 8 | 9 | export function createRouter () { 10 | return new Router({ 11 | mode: 'history', 12 | fallback: false, 13 | scrollBehavior: () => ({y: 0}), 14 | routes: [{ 15 | path: '/', 16 | name: 'MainView', 17 | component: MainView, 18 | }, { 19 | path: '/signin', 20 | name: 'SignInView', 21 | component: SignInView, 22 | }], 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/store/actions.js: -------------------------------------------------------------------------------- 1 | import Api from '../api' 2 | 3 | export default { 4 | // CONNECT connects the API client to the API server. 5 | // Returns an error if anything goes wrong 6 | async CONNECT({commit, state}) { 7 | Api.init( 8 | process.env.API_HOST || 'https://localhost:9090', 9 | { 10 | // onSignal 11 | onSignal: signal => { 12 | commit('PUSH_MESSAGE', JSON.parse(signal.payload)) 13 | }, 14 | onSessionCreated: newSession => { 15 | commit('SET_API_USER', newSession.info.username) 16 | }, 17 | onSessionClosed: () => { 18 | commit('SET_API_USER', null) 19 | }, 20 | onDisconnected: () => { 21 | commit('SET_API_CONNECTION_STATUS', false) 22 | }, 23 | onConnected: () => { 24 | commit('SET_API_CONNECTION_STATUS', true) 25 | if (Api.client.session != null) { 26 | commit( 27 | 'SET_API_USER', 28 | Api.client.session.info.username 29 | ) 30 | } 31 | }, 32 | } 33 | ) 34 | }, 35 | 36 | // SIGNIN tries to authenticate the API client using the provided credentials. 37 | // Automatically connects the client if no connection has yet been established. 38 | // Returns an error if anything goes wrong 39 | async SIGNIN({dispatch, state}, credentials) { 40 | if (!state.api.connected) { 41 | let err = await dispatch('CONNECT') 42 | if (err != null) return err 43 | } 44 | // Send an authentication request with default UTF16 encoding to test whether 45 | // the server will accept it. Set timeout to 1 second instead of the default 60 46 | let {err} = await Api.client.request( 47 | 'auth', 48 | JSON.stringify(credentials), 49 | null, 50 | 1000, 51 | ) 52 | if (err != null) return err 53 | }, 54 | 55 | // SIGNOUT tries to close the currently active API session. 56 | // Does nothing if there's no currently active session. 57 | // Returns an error if anything goes wrong 58 | async SIGNOUT({commit, state}) { 59 | if (state.api.user == null) return 60 | let err = await Api.client.closeSession() 61 | if (err != null) return err 62 | commit('SET_API_USER', null) 63 | }, 64 | 65 | // PUSH_MESSAGE pushes a new message to the server in a signal. 66 | // Returns an error if anything goes wrong 67 | async PUSH_MESSAGE({state, dispatch}, message) { 68 | if (message.length < 1 || message === '\n') return 69 | 70 | if (!state.api.connected) { 71 | const err = await dispatch('CONNECT') 72 | if (err != null) return err 73 | } 74 | // UTF8-encode the message in the payload 75 | // the server doesn't support standard UTF16 JavaScript strings 76 | const {err} = await Api.client.request('msg', message, 'utf8') 77 | if (err != null) return err 78 | }, 79 | } 80 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/store/getters.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import actions from './actions' 4 | import mutations from './mutations' 5 | import getters from './getters' 6 | 7 | Vue.use(Vuex) 8 | 9 | export function createStore () { 10 | return new Vuex.Store({ 11 | state: { 12 | messages: [], 13 | api: { 14 | user: null, 15 | connected: false, 16 | }, 17 | }, 18 | actions, 19 | mutations, 20 | getters, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/store/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SET_API_CONNECTION_STATUS(state, connectionStatus) { 3 | state.api.connected = connectionStatus 4 | }, 5 | SET_API_USER(state, user) { 6 | state.api.user = user 7 | }, 8 | PUSH_MESSAGE(state, message) { 9 | state.messages.push({ 10 | author: message.user, 11 | msg: message.msg, 12 | time: new Date(), 13 | }) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/util/title.js: -------------------------------------------------------------------------------- 1 | function getTitle(vm) { 2 | const {title} = vm.$options 3 | if (title) return typeof title === 'function' ? title.call(vm) : title 4 | } 5 | 6 | const serverTitleMixin = { 7 | created() { 8 | const title = getTitle(this) 9 | if (title) this.$ssrContext.title = `WebWire - ChatRoom | ${title}` 10 | }, 11 | } 12 | 13 | const clientTitleMixin = { 14 | mounted() { 15 | const title = getTitle(this) 16 | if (title) document.title = `WebWire - ChatRoom | ${title}` 17 | }, 18 | } 19 | 20 | export default process.env.VUE_ENV === 'server' 21 | ? serverTitleMixin : clientTitleMixin 22 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/views/MainView.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 75 | 76 | 149 | -------------------------------------------------------------------------------- /examples/chatroom-client-vue/src/views/SignInView.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 62 | 63 | 99 | -------------------------------------------------------------------------------- /lib/webwire.js: -------------------------------------------------------------------------------- 1 | !function(e,r){"object"==typeof exports&&"object"==typeof module?module.exports=r():"function"==typeof define&&define.amd?define([],r):"object"==typeof exports?exports["webwire-js"]=r():e["webwire-js"]=r()}(this,function(){return function(e){var r={};function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}return t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:n})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,r){if(1&r&&(e=t(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(t.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var o in e)t.d(n,o,function(r){return e[r]}.bind(null,o));return n},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=15)}([function(e,r,t){"use strict";t.d(r,"b",function(){return n}),t.d(r,"a",function(){return o});var n={AcceptConf:23,ErrorReply:0,ReplyShutdown:1,ReplyInternalError:2,SessionNotFound:3,MaxSessConnsReached:4,SessionsDisabled:5,ReplyProtocolError:6,SessionCreated:21,SessionClosed:22,CloseSession:31,RestoreSession:32,Heartbeat:33,SignalBinary:63,SignalUtf8:64,SignalUtf16:65,RequestBinary:127,RequestUtf8:128,RequestUtf16:129,ReplyBinary:191,ReplyUtf8:192,ReplyUtf16:193},o={AcceptConf:11,Signal:3,SignalUtf16:4,Request:11,RequestUtf16:12,Reply:9,ReplyUtf16:10,ErrorReply:11,ReplyShutdown:9,ReplyInternalError:9,SessionNotFound:9,MaxSessConnsReached:9,SessionsDisabled:9,ReplyProtocolError:9,RestoreSession:10,CloseSession:9,SessionCreated:2,SessionClosed:1}},function(e,r,t){e.exports=t(16)},function(e,r,t){"use strict";function n(e){var r,t,n,o,a,i;for(r="",n=e.length,t=0;t>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:r+=String.fromCharCode(o);break;case 12:case 13:a=e[t++],r+=String.fromCharCode((31&o)<<6|63&a);break;case 14:a=e[t++],i=e[t++],r+=String.fromCharCode((15&o)<<12|(63&a)<<6|(63&i)<<0)}return r}t.d(r,"a",function(){return n})},function(e,r,t){"use strict";function n(e){var r;r=new ArrayBuffer(e.length);for(var t=new Uint8Array(r),n=0;n126)throw new Error("Unsupported session key character (".concat(o,")"));t[n]=o}Object.defineProperty(this,"bytes",{get:function(){return t}}),Object.defineProperty(this,"string",{get:function(){return e}})}t.d(r,"a",function(){return n})},function(e,r,t){"use strict";t.d(r,"a",function(){return o});var n=new a(0,0);function o(){n.increment();var e=n.frags;return new a(e.front,e.end)}function a(e,r){var t=new ArrayBuffer(8),n=new Uint32Array(t,0,2);n.set([e,r],0),Object.defineProperty(this,"frags",{get:function(){return{front:n[0],end:n[1]}}}),Object.defineProperty(this,"bytes",{get:function(){return new Uint8Array(t)}}),Object.defineProperty(this,"increment",{value:function(){var e=n[0],r=n[1];r+1>4294967295?(e+1>4294967295&&(r=0,e=0),r=0,e++):r++,n.set([e,r],0)}})}},function(e,r,t){"use strict";function n(e){for(var r=[],t=0;t>6,128|63&n):n<55296||n>=57344?r.push(224|n>>12,128|n>>6&63,128|63&n):(t++,n=65536+((1023&n)<<10|1023&e.charCodeAt(t)),r.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n))}return r}t.d(r,"a",function(){return n})},function(e,r){var t,n,o=e.exports={};function a(){throw new Error("setTimeout has not been defined")}function i(){throw new Error("clearTimeout has not been defined")}function c(e){if(t===setTimeout)return setTimeout(e,0);if((t===a||!t)&&setTimeout)return t=setTimeout,setTimeout(e,0);try{return t(e,0)}catch(r){try{return t.call(null,e,0)}catch(r){return t.call(this,e,0)}}}!function(){try{t="function"==typeof setTimeout?setTimeout:a}catch(e){t=a}try{n="function"==typeof clearTimeout?clearTimeout:i}catch(e){n=i}}();var s,u=[],l=!1,f=-1;function p(){l&&s&&(l=!1,s.length?u=s.concat(u):f=-1,u.length&&h())}function h(){if(!l){var e=c(p);l=!0;for(var r=u.length;r;){for(s=u,u=[];++f1)for(var t=1;ta.a.AcceptConf?Object(n.a)(r.subarray(11)):null}}(e,r);break;case a.b.SessionCreated:t=function(e){return e.length0?{name:String.fromCharCode.apply(null,e.subarray(2,t)),payload:e.subarray(t)}:{payload:e.subarray(2)}}(r);break;case a.b.SignalUtf8:i="utf8",t=function(e){if(e.length0?{name:String.fromCharCode.apply(null,e.subarray(2,t)),payload:Object(n.a)(e.subarray(t))}:{payload:Object(n.a)(e.subarray(2))}}(r);break;case a.b.SignalUtf16:i="utf16",t=function(e){if(e.length0?{name:String.fromCharCode.apply(null,new Uint8Array(e,2,2+r)),payload:String.fromCharCode.apply(null,new Uint16Array(e.subarray(n)))}:{payload:String.fromCharCode.apply(null,new Uint16Array(e.subarray(2)))}}(r);break;case a.b.ReplyShutdown:t=function(e){if(e.lengtha.a.Reply&&(r=e.subarray(9)),{id:e.subarray(1,9),payload:r}}(r);break;case a.b.ReplyUtf8:i="utf8",t=function(e){if(e.lengtha.a.Reply&&(r=Object(n.a)(e.subarray(9))),{id:e.subarray(1,9),payload:r}}(r);break;case a.b.ReplyUtf16:i="utf16",t=function(e){if(e.lengtha.a.ReplyUtf16&&(r=String.fromCharCode.apply(null,new Uint16Array(e,10,e.length-5))),{id:e.subarray(1,9),payload:r}}(r);break;default:t={err:new Error("Unsupported message type: ".concat(c))}}return null!=t.err?{err:t.err}:{type:c,payloadEncoding:i,msg:t}}r.a=function(r){return new Promise(function(t,n){try{if(e.browser){var o=new FileReader;o.onerror=function(e){n(e.target.error)},o.onload=function(){t(i(this.result,new Uint8Array(this.result)))},o.readAsArrayBuffer(r)}else t(i(r.buffer,new Uint8Array(r.buffer,r.byteOffset,r.byteLength/Uint8Array.BYTES_PER_ELEMENT)))}catch(e){n(e)}})}}).call(this,t(6))},function(e,r,t){(function(r){r.browser?e.exports=function(e){var r=new WebSocket(e);Object.defineProperty(this,"onOpen",{value:function(e){r.onopen=e},writable:!1}),Object.defineProperty(this,"onError",{value:function(e){r.onerror=e},writable:!1}),Object.defineProperty(this,"onMessage",{value:function(e){r.onmessage=function(r){return e(r.data)}},writable:!1}),Object.defineProperty(this,"onClose",{value:function(e){r.onclose=function(r){return e(r.code)}},writable:!1}),Object.defineProperty(this,"send",{value:function(e){r.send(e)},writable:!1})}:e.exports=function(e){var r=new WebSocket(e);Object.defineProperty(this,"onOpen",{value:function(e){r.on("open",e)},writable:!1}),Object.defineProperty(this,"onError",{value:function(e){},writable:!1}),Object.defineProperty(this,"onMessage",{value:function(e){r.on("message",e)},writable:!1}),Object.defineProperty(this,"onClose",{value:function(e){r.on("close",e)},writable:!1}),Object.defineProperty(this,"send",{value:function(e){r.send(e)},writable:!1})}}).call(this,t(6))},function(e,r,t){"use strict";t.d(r,"a",function(){return c});var n=t(0),o=t(4),a=t(5);function i(e){return(i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function c(e,r,t){if(null==r)throw new Error("Missing request payload");if(null==e&&(e=""),e.length>255)throw new Error("Request name too long (".concat(e.length,"), max 255"));var c,s=Object(o.a)();if("string"==typeof r&&"utf8"===t)c=function(e,r,t){var o=Object(a.a)(t),i=10+r.length,c=new ArrayBuffer(i+o.length),s=new Uint8Array(c,0,i);s[0]=n.b.RequestUtf8;for(var u=e.bytes,l=1;l<9;l++)s[l]=u[l-1];s[9]=r.length;for(var f=0;f126)throw new Error("Unsupported name character (".concat(p,")"));s[10+f]=r.charCodeAt(f)}for(var h=new Uint8Array(c,i,o.length),d=0;d126)throw new Error("Unsupported name character (".concat(f,")"));c[10+l]=r.charCodeAt(l)}for(var p=new Uint16Array(i,a,t.length),h=0;h255)throw new Error("Missing or invalid message type ".concat(e));if(null!=r&&!(r instanceof Uint8Array))throw new Error("Invalid request payload: ".concat(o(r)));var t=null!=r?r.length:0,a=new ArrayBuffer(9+t),i=new Uint8Array(a,0,9+t);i[0]=e;for(var c=Object(n.a)(),s=c.bytes,u=1;u<9;u++)i[u]=s[u-1];if(null!=r)for(var l=0;l0&&setTimeout(function(){var r=new Error("Auto-connect attempt timed out");r.errType="timeout",e(r)},r),void A.then(e);A=new Promise(function(){var e=b(o.a.mark(function e(n){var a;return o.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(e.prev=0,!(r>0)){e.next=4;break}return setTimeout(function(){var e=new Error("Auto-reconnect attempt timed out");e.errType="timeout",n(e)},r),e.abrupt("return");case 4:return e.next=7,ee();case 7:if(null!=(a=e.sent)){e.next=14;break}return n(),A=null,e.abrupt("return");case 14:if("disconnected"!==a.errType){e.next=20;break}return e.next=17,W(k);case 17:return e.abrupt("continue",4);case 20:return n({err:a}),A=null,e.abrupt("return");case 23:e.next=4;break;case 25:e.next=30;break;case 27:e.prev=27,e.t0=e.catch(0),t(e.t0);case 30:case"end":return e.stop()}},e,this,[[0,27]])}));return function(r){return e.apply(this,arguments)}}())}));case 9:case"end":return e.stop()}},e,this)}))).apply(this,arguments)}function H(e,r,t,n,a){return new Promise(function(){var i=b(o.a.mark(function i(c,u){var l,f,h,d,y,b;return o.a.wrap(function(o){for(;;)switch(o.prev=o.next){case 0:return o.prev=0,l=e&&!r?new p.a(e,t):new s.a(r,t,n),f=l.id.bytes,h=a||C,d=setTimeout(function(){delete O[f],d=null;var e=new Error("Request timed out");e.errType="timeout",c({err:e})},h),o.next=7,Y(h);case 7:if(null==(y=o.sent)){o.next=10;break}return o.abrupt("return",c({err:y}));case 10:b={fulfill:function(e){null!=d&&(delete O[f],clearTimeout(d),c({reply:e}))},fail:function(e){null!=d&&(delete O[f],clearTimeout(d),c({err:e}))}},O[f]=b,U.send(l.bytes),X(),o.next=19;break;case 16:o.prev=16,o.t0=o.catch(0),u(o.t0);case 19:case"end":return o.stop()}},i,this,[[0,16]])}));return function(e,r){return i.apply(this,arguments)}}())}function K(e){return Q.apply(this,arguments)}function Q(){return(Q=b(o.a.mark(function e(r){var t,n,a,i;return o.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,H(c.b.RestoreSession,null,r);case 2:if(t=e.sent,n=t.reply,null==(a=t.err)){e.next=10;break}return console.warn("webwire client: couldn't restore session:",a),j=null,localStorage.removeItem(x),e.abrupt("return",a);case 10:i=JSON.parse(n.payload),j={key:new l.a(i.k),creationDate:new Date(i.c),info:i.i};case 12:case"end":return e.stop()}},e,this)}))).apply(this,arguments)}function X(){clearInterval(I),I=setInterval(function(){var e=new ArrayBuffer(1),r=new Uint8Array(e,0,1);r[0]=c.b.Heartbeat;try{U.send(r)}catch(e){console.error("couldn't send heartbeat message: ",m)}},L)}function ee(){return P===g.Connected?Promise.resolve():null!=T?new Promise(function(e,r){T.then(e).catch(r)}):T=new Promise(function(){var e=b(o.a.mark(function e(r,t){var n;return o.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:P=g.Connecting;try{n=setTimeout(function(){if(P!=g.Connected){T=null,console.error("webwire dial timeout error:",m);var e=new Error("dial timeout error");e.errType="dialTimeout",r(e)}},_),(U=new i.a(S)).onError(function(e){clearTimeout(n),T=null,console.error("webwire client error:",e);var t=new Error("WebSocket error: ".concat(e));t.errType="disconnected",r(t)}),U.onMessage(function(){var e=b(o.a.mark(function e(t){var a,i,s,u;return o.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return clearTimeout(n),e.next=3,Object(f.a)(t);case 3:if((a=e.sent).type==c.b.AcceptConf){e.next=10;break}return T=null,console.error("webwire protocol error:",u),(i=new Error("webwire protocol error: expected the server to send an accept-conf "+" message, got: ".concat(a.type))).errType="protocolError",e.abrupt("return",r(i));case 10:if(!(s=!Object(d.a)(a.majorProtocolVersion,a.minorProtocolVersion))){e.next=13;break}return e.abrupt("return",r(s));case 13:if(a.messageBufferSize,L=a.readTimeout-a.readTimeout/4,U.onMessage(function(e){return J(e)}),T=null,P=g.Connected,X(),null!=j){e.next=22;break}return M(),e.abrupt("return",r());case 22:return e.next=24,K(j.key.bytes);case 24:null!=(u=e.sent)&&console.warn("webwire: couldn't restore session on reconnection",u),M(),r();case 28:case"end":return e.stop()}},e,this)}));return function(r){return e.apply(this,arguments)}}()),U.onClose(function(){var e=b(o.a.mark(function e(r){var t;return o.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return P=g.Disconnected,1e3!==r&&1001!==r&&console.warn("webwire: abnormal closure error code:",r),clearInterval(I),I=null,B(),e.next=6,Y(0);case 6:null!=(t=e.sent)&&console.error("webwire: autoconnect failed:",t);case 8:case"end":return e.stop()}},e,this)}));return function(r){return e.apply(this,arguments)}}())}catch(e){t(e)}case 2:case"end":return e.stop()}},e,this)}));return function(r,t){return e.apply(this,arguments)}}())}function re(){return(re=b(o.a.mark(function e(r,t,n){var a,i;return o.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return a=new u.a(r,t,n),e.next=3,ee();case 3:if(null==(i=e.sent)){e.next=6;break}return e.abrupt("return",i);case 6:U.send(a.bytes),X();case 8:case"end":return e.stop()}},e,this)}))).apply(this,arguments)}function te(){return(te=b(o.a.mark(function e(r){var t,n;return o.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(r instanceof l.a){e.next=2;break}return e.abrupt("return",new Error("Expected session key to be an instance of SessionKey"));case 2:if(!j){e.next=4;break}return e.abrupt("return",new Error("Can't restore session if another one is already active"));case 4:return e.next=6,ee();case 6:if(null==(t=e.sent)){e.next=9;break}return e.abrupt("return",t);case 9:return e.next=11,K(r.bytes);case 11:null!=(n=e.sent)&&console.warn("webwire: couldn't restore session by key\n\t\t\t\t\t(".concat(r.string,") : ").concat(n));case 13:case"end":return e.stop()}},e,this)}))).apply(this,arguments)}function ne(){return(ne=b(o.a.mark(function e(){var r,t;return o.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(j&&!(P=0,a=o&&n.regeneratorRuntime;if(n.regeneratorRuntime=void 0,e.exports=t(17),o)n.regeneratorRuntime=a;else try{delete n.regeneratorRuntime}catch(e){n.regeneratorRuntime=void 0}},function(e,r){!function(r){"use strict";var t,n=Object.prototype,o=n.hasOwnProperty,a="function"==typeof Symbol?Symbol:{},i=a.iterator||"@@iterator",c=a.asyncIterator||"@@asyncIterator",s=a.toStringTag||"@@toStringTag",u="object"==typeof e,l=r.regeneratorRuntime;if(l)u&&(e.exports=l);else{(l=r.regeneratorRuntime=u?e.exports:{}).wrap=m;var f="suspendedStart",p="suspendedYield",h="executing",d="completed",y={},b={};b[i]=function(){return this};var g=Object.getPrototypeOf,v=g&&g(g(A([])));v&&v!==n&&o.call(v,i)&&(b=v);var w=C.prototype=E.prototype=Object.create(b);x.prototype=w.constructor=C,C.constructor=x,C[s]=x.displayName="GeneratorFunction",l.isGeneratorFunction=function(e){var r="function"==typeof e&&e.constructor;return!!r&&(r===x||"GeneratorFunction"===(r.displayName||r.name))},l.mark=function(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,C):(e.__proto__=C,s in e||(e[s]="GeneratorFunction")),e.prototype=Object.create(w),e},l.awrap=function(e){return{__await:e}},k(R.prototype),R.prototype[c]=function(){return this},l.AsyncIterator=R,l.async=function(e,r,t,n){var o=new R(m(e,r,t,n));return l.isGeneratorFunction(r)?o:o.next().then(function(e){return e.done?e.value:o.next()})},k(w),w[s]="Generator",w[i]=function(){return this},w.toString=function(){return"[object Generator]"},l.keys=function(e){var r=[];for(var t in e)r.push(t);return r.reverse(),function t(){for(;r.length;){var n=r.pop();if(n in e)return t.value=n,t.done=!1,t}return t.done=!0,t}},l.values=A,P.prototype={constructor:P,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=t,this.done=!1,this.delegate=null,this.method="next",this.arg=t,this.tryEntries.forEach(U),!e)for(var r in this)"t"===r.charAt(0)&&o.call(this,r)&&!isNaN(+r.slice(1))&&(this[r]=t)},stop:function(){this.done=!0;var e=this.tryEntries[0].completion;if("throw"===e.type)throw e.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var r=this;function n(n,o){return c.type="throw",c.arg=e,r.next=n,o&&(r.method="next",r.arg=t),!!o}for(var a=this.tryEntries.length-1;a>=0;--a){var i=this.tryEntries[a],c=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var s=o.call(i,"catchLoc"),u=o.call(i,"finallyLoc");if(s&&u){if(this.prev=0;--t){var n=this.tryEntries[t];if(n.tryLoc<=this.prev&&o.call(n,"finallyLoc")&&this.prev=0;--r){var t=this.tryEntries[r];if(t.finallyLoc===e)return this.complete(t.completion,t.afterLoc),U(t),y}},catch:function(e){for(var r=this.tryEntries.length-1;r>=0;--r){var t=this.tryEntries[r];if(t.tryLoc===e){var n=t.completion;if("throw"===n.type){var o=n.arg;U(t)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:A(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),y}}}function m(e,r,t,n){var o=r&&r.prototype instanceof E?r:E,a=Object.create(o.prototype),i=new P(n||[]);return a._invoke=function(e,r,t){var n=f;return function(o,a){if(n===h)throw new Error("Generator is already running");if(n===d){if("throw"===o)throw a;return T()}for(t.method=o,t.arg=a;;){var i=t.delegate;if(i){var c=O(i,t);if(c){if(c===y)continue;return c}}if("next"===t.method)t.sent=t._sent=t.arg;else if("throw"===t.method){if(n===f)throw n=d,t.arg;t.dispatchException(t.arg)}else"return"===t.method&&t.abrupt("return",t.arg);n=h;var s=S(e,r,t);if("normal"===s.type){if(n=t.done?d:p,s.arg===y)continue;return{value:s.arg,done:t.done}}"throw"===s.type&&(n=d,t.method="throw",t.arg=s.arg)}}}(e,t,i),a}function S(e,r,t){try{return{type:"normal",arg:e.call(r,t)}}catch(e){return{type:"throw",arg:e}}}function E(){}function x(){}function C(){}function k(e){["next","throw","return"].forEach(function(r){e[r]=function(e){return this._invoke(r,e)}})}function R(e){var r;this._invoke=function(t,n){function a(){return new Promise(function(r,a){!function r(t,n,a,i){var c=S(e[t],e,n);if("throw"!==c.type){var s=c.arg,u=s.value;return u&&"object"==typeof u&&o.call(u,"__await")?Promise.resolve(u.__await).then(function(e){r("next",e,a,i)},function(e){r("throw",e,a,i)}):Promise.resolve(u).then(function(e){s.value=e,a(s)},function(e){return r("throw",e,a,i)})}i(c.arg)}(t,n,r,a)})}return r=r?r.then(a,a):a()}}function O(e,r){var n=e.iterator[r.method];if(n===t){if(r.delegate=null,"throw"===r.method){if(e.iterator.return&&(r.method="return",r.arg=t,O(e,r),"throw"===r.method))return y;r.method="throw",r.arg=new TypeError("The iterator does not provide a 'throw' method")}return y}var o=S(n,e.iterator,r.arg);if("throw"===o.type)return r.method="throw",r.arg=o.arg,r.delegate=null,y;var a=o.arg;return a?a.done?(r[e.resultName]=a.value,r.next=e.nextLoc,"return"!==r.method&&(r.method="next",r.arg=t),r.delegate=null,y):a:(r.method="throw",r.arg=new TypeError("iterator result is not an object"),r.delegate=null,y)}function j(e){var r={tryLoc:e[0]};1 in e&&(r.catchLoc=e[1]),2 in e&&(r.finallyLoc=e[2],r.afterLoc=e[3]),this.tryEntries.push(r)}function U(e){var r=e.completion||{};r.type="normal",delete r.arg,e.completion=r}function P(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(j,this),this.reset(!0)}function A(e){if(e){var r=e[i];if(r)return r.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var n=-1,a=function r(){for(;++n 4294967295) { 38 | if (front + 1 > 4294967295) { 39 | // Overflow! 40 | end = 0 41 | front = 0 42 | } 43 | end = 0 44 | front++ 45 | } else { 46 | end++ 47 | } 48 | 49 | _fragments.set([front, end], 0) 50 | }, 51 | }) 52 | } 53 | 54 | export function FromBytes(bytes) { 55 | if (bytes == null || !(bytes instanceof Uint8Array)) { 56 | throw new Error( 57 | `Missing or invalid binary representation of the identifier: ` + 58 | `${bytes}` 59 | ) 60 | } 61 | 62 | let front 63 | for (let i = 0; i < 4; i++) front *= (bytes[i] + 1) 64 | let end 65 | for (let i = 4; i < 9; i++) end *= (bytes[i] + 1) 66 | 67 | return new Identifier(front, end) 68 | } 69 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Socket from './socket' 2 | import { 3 | Type as MessageType, 4 | } from './message' 5 | import RequestMessage from './requestMessage' 6 | import SignalMessage from './signalMessage' 7 | import SessionKey from './sessionKey' 8 | import Parse from './parse' 9 | import NamelessRequestMessage from './namelessReqMsg' 10 | import ParseEndpointAddress from './parseEndpointAddress' 11 | import VerifyProtocolVersion from './verifyProtocolVersion' 12 | 13 | function getCallbacks(opts) { 14 | let onSignal = function() {} 15 | let onSessionCreated = function() {} 16 | let onSessionClosed = function() {} 17 | let onDisconnected = function() {} 18 | let onConnected = function() {} 19 | 20 | if (opts == null || opts.handlers == null) { 21 | return { 22 | onSignal, 23 | onSessionCreated, 24 | onSessionClosed, 25 | onDisconnected, 26 | onConnected, 27 | } 28 | } 29 | 30 | const handlers = opts.handlers 31 | if (handlers.onSignal instanceof Function) { 32 | onSignal = handlers.onSignal 33 | } 34 | if (handlers.onSessionCreated instanceof Function) { 35 | onSessionCreated = handlers.onSessionCreated 36 | } 37 | if (handlers.onSessionClosed instanceof Function) { 38 | onSessionClosed = handlers.onSessionClosed 39 | } 40 | if (handlers.onDisconnected instanceof Function) { 41 | onDisconnected = handlers.onDisconnected 42 | } 43 | if (handlers.onConnected instanceof Function) { 44 | onConnected = handlers.onConnected 45 | } 46 | 47 | return { 48 | onSignal, 49 | onSessionCreated, 50 | onSessionClosed, 51 | onDisconnected, 52 | onConnected, 53 | } 54 | } 55 | 56 | const ClientStatus = { 57 | Disabled: 0, 58 | Disconnected: 1, 59 | Connected: 2, 60 | Connecting: 3, 61 | } 62 | 63 | function localStorageStateKey(hostname, port, endpoint) { 64 | const key = `${hostname}:${port}` 65 | if (endpoint) return `${key}${endpoint}` 66 | return key 67 | } 68 | 69 | function websocketEndpointUri(protocol, hostname, port, path) { 70 | // Determine websocket schema based on the endpoint protocol 71 | const wsSchema = protocol === 'https' ? 'wss' : 'ws' 72 | const base = `${wsSchema}://${hostname}:${port}` 73 | if (path) return `${base}${path}` 74 | return base 75 | } 76 | 77 | export default function WebWireClient(endpointAddress, options) { 78 | // Parse endpoint address 79 | const { 80 | protocol: _protocol, 81 | hostname: _hostname, 82 | port: _port, 83 | path: _endpointPath, 84 | err, 85 | } = ParseEndpointAddress(endpointAddress) 86 | if (err) throw err 87 | 88 | // Determine the websocket endpoint URI 89 | const _websocketEndpointUri = websocketEndpointUri( 90 | _protocol, 91 | _hostname, 92 | _port, 93 | _endpointPath, 94 | ) 95 | 96 | if (options == null) options = {} 97 | 98 | // Load client state for this server address if any 99 | const locStorKey = localStorageStateKey(_hostname, _port, _endpointPath) 100 | 101 | let state 102 | 103 | if (process.browser) state = localStorage.getItem(locStorKey) 104 | if (state != null) { 105 | state = JSON.parse(state) 106 | state.session = new SessionKey(state.session) 107 | } else { 108 | state = {} 109 | } 110 | 111 | // Default request timeout is 60 seconds by default 112 | const _defaultReqTimeout = options.defaultReqTimeout || 60000 113 | const _reconnInterval = options.reconnectionInterval || 2000 114 | const _autoconnect = options.autoconnect || true 115 | const _pendingRequests = {} 116 | let _session = state.session ? {key: state.session} : null 117 | let _conn = null 118 | let _status = ClientStatus.Disconnected 119 | let _reconnecting = null 120 | let _connecting = null 121 | let _heartbeatIntervalToken = null 122 | let _messageBufferSize = null 123 | let _readTimeout = 60000 124 | const _dialTimeout = options.dialTimeout || 60000 125 | 126 | const { 127 | onSignal: _onSignal, 128 | onSessionCreated: _onSessionCreated, 129 | onSessionClosed: _onSessionClosed, 130 | onDisconnected: _onDisconnected, 131 | onConnected: _onConnected, 132 | } = getCallbacks(options) 133 | 134 | // Define interface methods 135 | Object.defineProperty(this, 'connect', {value: connect}) 136 | Object.defineProperty(this, 'request', { 137 | value: function(name, payload, encoding, timeoutDuration) { 138 | return sendRequest(null, name, payload, encoding, timeoutDuration) 139 | }, 140 | }) 141 | Object.defineProperty(this, 'signal', {value: signal}) 142 | Object.defineProperty(this, 'restoreSession', {value: restoreSession}) 143 | Object.defineProperty(this, 'closeSession', {value: closeSession}) 144 | Object.defineProperty(this, 'close', {value: close}) 145 | 146 | // Define interface properties 147 | Object.defineProperty(this, 'status', { 148 | get() { 149 | switch (_status) { 150 | case ClientStatus.Disabled: return 'disabled' 151 | case ClientStatus.Disconnected: return 'disconnected' 152 | case ClientStatus.Connected: return 'connected' 153 | case ClientStatus.Connecting: return 'connecting' 154 | } 155 | return 'invalid_client_status' 156 | }, 157 | }) 158 | Object.defineProperty(this, 'session', { 159 | get() { 160 | if (_session) { 161 | return { 162 | key: _session.key, 163 | creationDate: _session.creationDate 164 | ? new Date(_session.creationDate.getTime()) : null, 165 | info: _session.info 166 | ? JSON.parse(JSON.stringify(_session.info)) : null, 167 | } 168 | } 169 | }, 170 | }) 171 | Object.defineProperty(this, 'pendingRequests', { 172 | get() { 173 | return _pendingRequests.length 174 | }, 175 | }) 176 | Object.freeze(this) 177 | 178 | // Autoconnect 179 | tryAutoconnect(0) 180 | .then(err => { 181 | if (err != null) console.error(`webwire: autoconnect failed:`, err) 182 | }) 183 | .catch(excep => { 184 | console.warn(`webwire: autoconnect failed:`, excep) 185 | }) 186 | 187 | function handleSessionCreated(session) { 188 | const sessionKey = new SessionKey(session.k) 189 | _session = { 190 | key: sessionKey, 191 | creationDate: new Date(session.c), 192 | info: session.i, 193 | } 194 | 195 | // Save session key to local storage for automatic restoration 196 | const str = JSON.stringify({session: sessionKey.string}) 197 | localStorage.setItem(locStorKey, str) 198 | 199 | // Provide copy of the actual session to preserve its immutability 200 | _onSessionCreated({ 201 | key: sessionKey, 202 | creationDate: new Date(session.c), 203 | info: session.i, 204 | }) 205 | } 206 | 207 | function handleSessionClosed() { 208 | _session = null 209 | _onSessionClosed() 210 | } 211 | 212 | function handleFailure(message) { 213 | const req = _pendingRequests[message.id] 214 | 215 | // Ignore unexpected failure replies 216 | if (!req) return 217 | 218 | // Fail the request 219 | req.fail({ 220 | code: message.error.code, 221 | message: message.error.message, 222 | }) 223 | } 224 | 225 | function handleReply(message) { 226 | const req = _pendingRequests[message.id] 227 | 228 | // Ignore unexpected replies 229 | if (!req) return 230 | 231 | // Fulfill the request 232 | req.fulfill({ 233 | encoding: message.encoding, 234 | payload: message.payload, 235 | }) 236 | } 237 | 238 | async function handleMessage(msgObj) { 239 | if (msgObj.size < 1) return 240 | 241 | const { 242 | type, 243 | msg, 244 | payloadEncoding, 245 | err, 246 | } = await Parse(msgObj) 247 | 248 | if (err != null) { 249 | console.error(`webwire: failed parsing message: ${err}`) 250 | return 251 | } 252 | 253 | // Handle message 254 | switch (type) { 255 | case MessageType.ReplyBinary: 256 | case MessageType.ReplyUtf8: 257 | case MessageType.ReplyUtf16: 258 | handleReply({ 259 | id: msg.id, 260 | encoding: payloadEncoding, 261 | payload: msg.payload, 262 | }) 263 | break 264 | case MessageType.ReplyShutdown: 265 | case MessageType.ReplyInternalError: 266 | case MessageType.SessionNotFound: 267 | case MessageType.MaxSessConnsReached: 268 | case MessageType.ErrorReply: 269 | handleFailure({ 270 | id: msg.id, 271 | error: msg.reqError, 272 | }) 273 | break 274 | case MessageType.SignalBinary: 275 | case MessageType.SignalUtf8: 276 | case MessageType.SignalUtf16: 277 | _onSignal({ 278 | name: msg.name, 279 | encoding: payloadEncoding, 280 | payload: msg.payload, 281 | }) 282 | break 283 | case MessageType.SessionCreated: 284 | handleSessionCreated(msg.session) 285 | break 286 | case MessageType.SessionClosed: 287 | handleSessionClosed() 288 | break 289 | default: 290 | console.warn(`webwire: strange message type received:`, type) 291 | break 292 | } 293 | } 294 | 295 | function sleep(duration) { 296 | return new Promise(resolve => setTimeout(resolve, duration)) 297 | } 298 | 299 | async function tryAutoconnect(timeoutDur) { 300 | if (_status === ClientStatus.Connecting) { 301 | return new Promise((resolve, reject) => { 302 | _connecting 303 | .then(resolve) 304 | .catch(reject) 305 | }) 306 | } else if (_status !== ClientStatus.Disconnected) { 307 | return Promise.resolve() 308 | } 309 | if (!_autoconnect) return connect() 310 | return new Promise((resolve, reject) => { 311 | // Simulate a dam by accumulating awaiting connection attempts 312 | // and resolving them when connected 313 | if (_reconnecting != null) { 314 | if (timeoutDur > 0) { 315 | setTimeout(() => { 316 | const err = new Error(`Auto-connect attempt timed out`) 317 | err.errType = 'timeout' 318 | resolve(err) 319 | }, timeoutDur) 320 | } 321 | _reconnecting.then(resolve) 322 | return 323 | } 324 | // Resolving the following promise will flush the dam 325 | _reconnecting = new Promise(async resolve => { 326 | try { 327 | if (timeoutDur > 0) { 328 | setTimeout(() => { 329 | const err = new Error( 330 | `Auto-reconnect attempt timed out` 331 | ) 332 | err.errType = 'timeout' 333 | resolve(err) 334 | }, timeoutDur) 335 | return 336 | } 337 | while (1) { 338 | const err = await connect() 339 | if (err == null) { 340 | resolve() 341 | _reconnecting = null 342 | return 343 | } else { 344 | if (err.errType === 'disconnected') { 345 | await sleep(_reconnInterval) 346 | continue 347 | } else { 348 | resolve({err}) 349 | _reconnecting = null 350 | return 351 | } 352 | } 353 | } 354 | } catch (excep) { 355 | reject(excep) 356 | } 357 | }) 358 | }) 359 | } 360 | 361 | // Sends a request containing the given payload to the server. 362 | // Returns a promise that is resolved when the server replies. 363 | // Automatically connects to the server 364 | // if no connection has yet been established. 365 | // Optionally takes a timeout, otherwise default timeout is applied 366 | function sendRequest( 367 | messageType, 368 | name, 369 | payload, 370 | encoding, 371 | timeoutDuration, 372 | ) { 373 | //TODO: ensure the message won't overflow the message buffer size 374 | 375 | // Connect before attempting to send the request 376 | return new Promise(async (resolve, reject) => { 377 | try { 378 | const reqMsg = messageType && !name 379 | ? new NamelessRequestMessage(messageType, payload) 380 | : new RequestMessage(name, payload, encoding) 381 | const reqIdBytes = reqMsg.id.bytes 382 | const timeoutDur = timeoutDuration || _defaultReqTimeout 383 | 384 | let timeout = setTimeout(() => { 385 | // Deregister request 386 | delete _pendingRequests[reqIdBytes] 387 | timeout = null 388 | 389 | let newErr = new Error(`Request timed out`) 390 | newErr.errType = 'timeout' 391 | resolve({err: newErr}) 392 | }, timeoutDur) 393 | 394 | let err = await tryAutoconnect(timeoutDur) 395 | if (err != null) return resolve({err: err}) 396 | 397 | const req = { 398 | fulfill(reply) { 399 | // If the request already timed out then drop the reply 400 | if (timeout == null) return 401 | 402 | delete _pendingRequests[reqIdBytes] 403 | clearTimeout(timeout) 404 | 405 | resolve({reply: reply}) 406 | }, 407 | fail(err) { 408 | // If the request already timed out then drop the reply 409 | if (timeout == null) return 410 | 411 | delete _pendingRequests[reqIdBytes] 412 | clearTimeout(timeout) 413 | 414 | resolve({err: err}) 415 | }, 416 | } 417 | 418 | // Register request 419 | _pendingRequests[reqIdBytes] = req 420 | 421 | // Send request 422 | _conn.send(reqMsg.bytes) 423 | 424 | // Reset heartbeat 425 | startHeartbeat() 426 | } catch (excep) { 427 | reject(excep) 428 | } 429 | }) 430 | } 431 | 432 | async function tryRestoreSession(sessionKey) { 433 | const {reply, err: reqErr} = await sendRequest( 434 | MessageType.RestoreSession, 435 | null, 436 | sessionKey 437 | ) 438 | if (reqErr != null) { 439 | // Just log a warning and still return null, 440 | // even if session restoration failed, 441 | // because we only care about the connection establishment 442 | // in this method 443 | console.warn(`webwire client: couldn't restore session:`, reqErr) 444 | 445 | // Reset the session 446 | _session = null 447 | localStorage.removeItem(locStorKey) 448 | return reqErr 449 | } 450 | const decodedSession = JSON.parse(reply.payload) 451 | _session = { 452 | key: new SessionKey(decodedSession.k), 453 | creationDate: new Date(decodedSession.c), 454 | info: decodedSession.i, 455 | } 456 | } 457 | 458 | function startHeartbeat() { 459 | clearInterval(_heartbeatIntervalToken) 460 | _heartbeatIntervalToken = setInterval(() => { 461 | const _buf = new ArrayBuffer(1) 462 | const msgBuf = new Uint8Array(_buf, 0, 1) 463 | msgBuf[0] = MessageType.Heartbeat 464 | try { 465 | _conn.send(msgBuf) 466 | } catch(excep) { 467 | console.error( 468 | 'couldn\'t send heartbeat message: ', 469 | err, 470 | ) 471 | } 472 | }, _readTimeout) 473 | } 474 | 475 | function stopHeartbeat() { 476 | clearInterval(_heartbeatIntervalToken) 477 | _heartbeatIntervalToken = null 478 | } 479 | 480 | // Connects the client to the configured server and 481 | // returns an error in case of a connection failure 482 | function connect() { 483 | if (_status === ClientStatus.Connected) return Promise.resolve() 484 | else if (_connecting != null) { 485 | // Client is already trying to connect, await connection 486 | return new Promise((resolve, reject) => { 487 | _connecting 488 | .then(resolve) 489 | .catch(reject) 490 | }) 491 | } 492 | // Try to connect 493 | _connecting = new Promise(async (resolve, reject) => { 494 | _status = ClientStatus.Connecting 495 | try { 496 | const dialTimeoutToken = setTimeout(() => { 497 | // If the dialing didn't yet succeed then fail the 498 | // connection attempt with a dialTimeout error, otherwise do 499 | // nothing 500 | if (_status != ClientStatus.Connected) { 501 | _connecting = null 502 | console.error('webwire dial timeout error:', err) 503 | const dialTimeoutErr = new Error(`dial timeout error`) 504 | dialTimeoutErr.errType = 'dialTimeout' 505 | resolve(dialTimeoutErr) 506 | } 507 | }, _dialTimeout) 508 | 509 | _conn = new Socket(_websocketEndpointUri) 510 | _conn.onError(err => { 511 | clearTimeout(dialTimeoutToken) 512 | _connecting = null 513 | console.error('webwire client error:', err) 514 | const connErr = new Error(`WebSocket error: ${err}`) 515 | connErr.errType = 'disconnected' 516 | resolve(connErr) 517 | }) 518 | _conn.onMessage(async msg => { 519 | // Stop the dialing timeout because a message was received 520 | // assuming that it's the accept-conf message 521 | clearTimeout(dialTimeoutToken) 522 | 523 | const acceptConfMsg = await Parse(msg) 524 | 525 | // Ensure the received message is an accept-conf one 526 | if (acceptConfMsg.type != MessageType.AcceptConf) { 527 | _connecting = null 528 | console.error('webwire protocol error:', err) 529 | const protocolErr = new Error( 530 | `webwire protocol error: ` + 531 | `expected the server to send an accept-conf ` + 532 | ` message, got: ${acceptConfMsg.type}` 533 | ) 534 | protocolErr.errType = 'protocolError' 535 | return resolve(protocolErr) 536 | } 537 | 538 | // Verify protocol version compatibility 539 | const protoVersionErr = !VerifyProtocolVersion( 540 | acceptConfMsg.majorProtocolVersion, 541 | acceptConfMsg.minorProtocolVersion, 542 | ) 543 | if (protoVersionErr) return resolve(protoVersionErr) 544 | 545 | // Set the message buffer size 546 | _messageBufferSize = acceptConfMsg.messageBufferSize 547 | 548 | // Determine server's read timeout (send heartbeat 25% 549 | // before actual timeout) 550 | _readTimeout = acceptConfMsg.readTimeout - ( 551 | acceptConfMsg.readTimeout / 4 552 | ) 553 | 554 | // Connected successfully, reset the message handler, update 555 | // the status and start heartbeating 556 | _conn.onMessage(msg => handleMessage(msg)) 557 | _connecting = null 558 | _status = ClientStatus.Connected 559 | 560 | startHeartbeat() 561 | 562 | // Check whether the session needs to be restored 563 | if (_session == null) { 564 | _onConnected() 565 | return resolve() 566 | } 567 | 568 | // Try to automatically restore the previous session 569 | const err = await tryRestoreSession(_session.key.bytes) 570 | if (err != null) { 571 | console.warn( 572 | `webwire: couldn't restore session on reconnection`, 573 | err, 574 | ) 575 | } 576 | _onConnected() 577 | resolve() 578 | }) 579 | _conn.onClose(async code => { 580 | _status = ClientStatus.Disconnected 581 | // See http://tools.ietf.org/html/rfc6455#section-7.4.1 582 | if (code !== 1000 && code !== 1001) { 583 | console.warn( 584 | 'webwire: abnormal closure error code:', 585 | code, 586 | ) 587 | } 588 | 589 | stopHeartbeat() 590 | _onDisconnected() 591 | 592 | // Auto-reconnect on connection loss 593 | const err = await tryAutoconnect(0) 594 | if (err != null) { 595 | console.error('webwire: autoconnect failed:', err) 596 | } 597 | }) 598 | } catch (excep) { 599 | reject(excep) 600 | } 601 | }) 602 | return _connecting 603 | } 604 | 605 | // Sends a signal containing the given payload to the server. 606 | // Automatically connects to the server if no connection has yet been established 607 | async function signal(name, payload, encoding) { 608 | //TODO: ensure the message won't overflow the message buffer size 609 | 610 | const sigMsg = new SignalMessage(name, payload, encoding) 611 | 612 | // Connect before attempting to send the signal 613 | const err = await connect() 614 | if (err != null) return err 615 | _conn.send(sigMsg.bytes) 616 | 617 | // Reset heartbeat 618 | startHeartbeat() 619 | } 620 | 621 | // Tries to restore the previously opened session. 622 | // Fails if a session is currently already active 623 | // Automatically connects to the server if no connection has yet been established 624 | async function restoreSession(sessionKey) { 625 | if (!(sessionKey instanceof SessionKey)) { 626 | return new Error( 627 | `Expected session key to be an instance of SessionKey` 628 | ) 629 | } 630 | 631 | if (_session) { 632 | return new Error( 633 | `Can't restore session if another one is already active` 634 | ) 635 | } 636 | // Connect before attempting to send the signal 637 | let connErr = await connect() 638 | if (connErr != null) return connErr 639 | const err = await tryRestoreSession(sessionKey.bytes) 640 | if (err != null) { 641 | console.warn( 642 | `webwire: couldn't restore session by key 643 | (${sessionKey.string}) : ${err}` 644 | ) 645 | } 646 | } 647 | 648 | // Closes the currently active session. 649 | // Does nothing if there's no active session 650 | async function closeSession() { 651 | if (!_session || _status < ClientStatus.Connected) { 652 | _session = null 653 | return 654 | } 655 | let {err: reqErr} = await sendRequest(MessageType.CloseSession) 656 | if (reqErr != null) return reqErr 657 | _session = null 658 | localStorage.removeItem(locStorKey) 659 | } 660 | 661 | // Gracefully closes the connection. 662 | // Does nothing if the client isn't connected 663 | function close() { 664 | _conn.close() 665 | _conn = null 666 | _status = ClientStatus.Disabled 667 | } 668 | } 669 | -------------------------------------------------------------------------------- /src/message.js: -------------------------------------------------------------------------------- 1 | export const Type = { 2 | // AcceptConf is a connection approval push-message sent only by the 3 | // server right after the handshake and includes the server configurations 4 | AcceptConf: 23, 5 | 6 | // ErrorReply is sent by the server 7 | // and represents an error-reply to a previously sent request 8 | ErrorReply: 0, 9 | 10 | // ReplyShutdown is sent by the server when a request 11 | // is received during server shutdown and can't therefore be processed 12 | ReplyShutdown: 1, 13 | 14 | // ReplyInternalError is sent by the server if an unexpected, 15 | // internal error arose during the processing of a request 16 | ReplyInternalError: 2, 17 | 18 | // SessionNotFound is sent by the server in response 19 | // to an unfilfilled session restoration request 20 | // due to the session not being found 21 | SessionNotFound: 3, 22 | 23 | // MaxSessConnsReached is sent by the server in response 24 | // to an authentication request when the maximum number 25 | // of concurrent connections for a certain session was reached 26 | MaxSessConnsReached: 4, 27 | 28 | // SessionsDisabled is sent by the server in response 29 | // to a session restoration request 30 | // if sessions are disabled for the target server 31 | SessionsDisabled: 5, 32 | 33 | // ReplyProtocolError is sent by the server in response to an invalid 34 | // message violating the protocol 35 | ReplyProtocolError: 6, 36 | 37 | // SERVER 38 | 39 | // SessionCreated is sent by the server 40 | // to notify the client about the session creation 41 | SessionCreated: 21, 42 | 43 | // SessionClosed is sent by the server 44 | // to notify the client about the session destruction 45 | SessionClosed: 22, 46 | 47 | // CLIENT 48 | 49 | // CloseSession is sent by the client 50 | // and represents a request for the destruction 51 | // of the currently active session 52 | CloseSession: 31, 53 | 54 | // RestoreSession is sent by the client 55 | // to request session restoration 56 | RestoreSession: 32, 57 | 58 | // Heartbeat is sent by the client to acknowledge the server about the 59 | // activity of the connection to prevent it from shutting the connection 60 | // down on read timeout 61 | Heartbeat: 33, 62 | 63 | // SIGNAL 64 | // Signals are sent by both the client and the server 65 | // and represents a one-way signal message that doesn't require a reply 66 | 67 | // SignalBinary represents a signal with binary payload 68 | SignalBinary: 63, 69 | 70 | // SignalUtf8 represents a signal with UTF8 encoded payload 71 | SignalUtf8: 64, 72 | 73 | // SignalUtf16 represents a signal with UTF16 encoded payload 74 | SignalUtf16: 65, 75 | 76 | // REQUEST 77 | // Requests are sent by the client 78 | // and represents a roundtrip to the server requiring a reply 79 | 80 | // RequestBinary represents a request with binary payload 81 | RequestBinary: 127, 82 | 83 | // RequestUtf8 represents a request with a UTF8 encoded payload 84 | RequestUtf8: 128, 85 | 86 | // RequestUtf16 represents a request with a UTF16 encoded payload 87 | RequestUtf16: 129, 88 | 89 | // REPLY 90 | // Replies are sent by the server 91 | // and represent a reply to a previously sent request 92 | 93 | // ReplyBinary represents a reply with a binary payload 94 | ReplyBinary: 191, 95 | 96 | // ReplyUtf8 represents a reply with a UTF8 encoded payload 97 | ReplyUtf8: 192, 98 | 99 | // ReplyUtf16 represents a reply with a UTF16 encoded payload 100 | ReplyUtf16: 193, 101 | } 102 | 103 | export const MinLen = { 104 | // AcceptConf represents the minimum length 105 | // of an endpoint metadata message. 106 | AcceptConf: 11, 107 | 108 | // Signal represents the minimum 109 | // binary/UTF8 encoded signal message length 110 | Signal: 3, 111 | 112 | // SignalUtf16 represents the minimum 113 | // UTF16 encoded signal message length 114 | SignalUtf16: 4, 115 | 116 | // Request represents the minimum 117 | // binary/UTF8 encoded request message length 118 | Request: 11, 119 | 120 | // RequestUtf16 represents the minimum 121 | // UTF16 encoded request message length 122 | RequestUtf16: 12, 123 | 124 | // Reply represents the minimum 125 | // binary/UTF8 encoded reply message length 126 | Reply: 9, 127 | 128 | // ReplyUtf16 represents the minimum 129 | // UTF16 encoded reply message length 130 | ReplyUtf16: 10, 131 | 132 | // ErrorReply represents the minimum error-reply message length 133 | ErrorReply: 11, 134 | 135 | // ReplyShutdown represents the minimum shutdown-reply message length 136 | ReplyShutdown: 9, 137 | 138 | // ReplyShutdown represents the minimum 139 | // internal-error-reply message length 140 | ReplyInternalError: 9, 141 | 142 | // SessionNotFound represents the minimum 143 | // session-not-found error message length 144 | SessionNotFound: 9, 145 | 146 | // MaxSessConnsReached represents the minimum 147 | // max-session-conns-reached-error message length 148 | MaxSessConnsReached: 9, 149 | 150 | // SessionsDisabled represents the minimum 151 | // sessions-disabled-error message length 152 | SessionsDisabled: 9, 153 | 154 | // ReplyProtocolError represents the minimum protocol-error message length 155 | ReplyProtocolError: 9, 156 | 157 | // RestoreSession represents the minimum 158 | // session-restoration-request message length 159 | RestoreSession: 10, 160 | 161 | // CloseSession represents the minimum 162 | // session-destruction-request message length 163 | CloseSession: 9, 164 | 165 | // SessionCreated represents the minimum 166 | // session-creation-notification message length 167 | SessionCreated: 2, 168 | 169 | // SessionClosed represents the minimum 170 | // session-creation-notification message length 171 | SessionClosed: 1, 172 | } 173 | -------------------------------------------------------------------------------- /src/namelessReqMsg.js: -------------------------------------------------------------------------------- 1 | import { 2 | New as NewIdentifier, 3 | } from './identifier' 4 | 5 | // NamelessRequestMessage represents a nameless, instantiatable 6 | // webwire request message. 7 | // payload must be a binary Uint8Array 8 | export default function NamelessRequestMessage(type, payload) { 9 | if (type == null || type < 0 || type > 255) { 10 | throw new Error(`Missing or invalid message type ${type}`) 11 | } 12 | if (payload != null && !(payload instanceof Uint8Array)) { 13 | throw new Error(`Invalid request payload: ${typeof payload}`) 14 | } 15 | 16 | // Determine total message size 17 | const payloadSize = payload != null ? payload.length : 0 18 | const _buf = new ArrayBuffer(9 + payloadSize) 19 | const writeBuf = new Uint8Array(_buf, 0, 9 + payloadSize) 20 | 21 | // Write type flag, default to RequestUtf8 22 | writeBuf[0] = type 23 | 24 | // Write identifier 25 | const id = NewIdentifier() 26 | const idBytes = id.bytes 27 | for (let i = 1; i < 9; i++) writeBuf[i] = idBytes[i - 1] 28 | 29 | // Write payload if any 30 | if (payload != null) { 31 | for (let i = 0; i < payloadSize; i++) writeBuf[9 + i] = payload[i] 32 | } 33 | 34 | Object.defineProperty(this, 'bytes', { 35 | get: function() { 36 | return new Uint8Array(_buf) 37 | }, 38 | }) 39 | 40 | Object.defineProperty(this, 'id', { 41 | get: function() { 42 | return id 43 | }, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/parse.js: -------------------------------------------------------------------------------- 1 | import utf8ArrayToStr from './utf8ArrayToStr' 2 | import asciiArrayToStr from './asciiArrayToStr' 3 | import { 4 | Type as MessageType, 5 | MinLen as MinMsgLen, 6 | } from './message' 7 | 8 | export default parse 9 | 10 | function parseSessionCreated(message) { 11 | if (message.length < MinMsgLen.SessionCreated) { 12 | return {err: new Error( 13 | `Invalid session creation notification message, ` + 14 | `too short (${message.length} / ${MinMsgLen.SessionCreated})` 15 | )} 16 | } 17 | 18 | return { 19 | // Read session from payload as UTF8 encoded JSON 20 | session: JSON.parse(utf8ArrayToStr(message.subarray(1))), 21 | } 22 | } 23 | 24 | function parseSessionClosed(message) { 25 | if (message.length !== MinMsgLen.SessionClosed) { 26 | return {err: new Error(`Invalid session closure notification message`)} 27 | } 28 | 29 | return {} 30 | } 31 | 32 | function parseSignalBinary(message) { 33 | // Minimum binary signal message structure: 34 | // 1. message type (1 byte) 35 | // 2. name length flag (1 byte) 36 | // 3. name (n bytes, required if name length flag is bigger zero) 37 | // 4. payload (n bytes, at least 1 byte) 38 | if (message.length < MinMsgLen.Signal) { 39 | return {err: new Error( 40 | `Invalid signal (Binary) message, too short ` + 41 | `(${message.length} / ${MinMsgLen.Signal})` 42 | )} 43 | } 44 | 45 | // Read name length 46 | const nameLen = message.subarray(1, 2)[0] 47 | const payloadOffset = 2 + nameLen 48 | 49 | // Verify total message size to prevent segmentation faults 50 | // caused by inconsistent flags, 51 | // this could happen if the specified name length doesn't correspond 52 | // to the actual name length 53 | if (message.length < MinMsgLen.Signal + nameLen) { 54 | return {err: new Error( 55 | `Invalid signal (Binary) message, ` + 56 | `too short for full name` + 57 | `(${nameLen}) and the minimum payload (1)`, 58 | )} 59 | } 60 | 61 | if (nameLen > 0) { 62 | // Take name into account 63 | return { 64 | name: String.fromCharCode.apply( 65 | null, 66 | message.subarray(2, payloadOffset) 67 | ), 68 | payload: message.subarray(payloadOffset), 69 | } 70 | } else { 71 | // No name present, just payload 72 | return { 73 | payload: message.subarray(2), 74 | } 75 | } 76 | } 77 | 78 | function parseSignalUtf8(message) { 79 | // Minimum UTF8 signal message structure: 80 | // 1. message type (1 byte) 81 | // 2. name length flag (1 byte) 82 | // 3. name (n bytes, required if name length flag is bigger zero) 83 | // 4. payload (n bytes, at least 1 byte) 84 | if (message.length < MinMsgLen.Signal) { 85 | return {err: new Error( 86 | `Invalid signal (UTF8) message, too short ` + 87 | `(${message.length} / ${MinMsgLen.Signal})` 88 | )} 89 | } 90 | 91 | // Read name length 92 | const nameLen = message.subarray(1, 2)[0] 93 | const payloadOffset = 2 + nameLen 94 | 95 | // Verify total message size to prevent segmentation faults 96 | // caused by inconsistent flags, 97 | // this could happen if the specified name length doesn't correspond 98 | // to the actual name length 99 | if (message.length < MinMsgLen.Signal + nameLen) { 100 | return {err: new Error( 101 | `Invalid signal (UTF8) message, ` + 102 | `too short for full name (${nameLen}) ` + 103 | `and the minimum payload (1)`, 104 | )} 105 | } 106 | 107 | if (nameLen > 0) { 108 | // Take name into account 109 | return { 110 | name: String.fromCharCode.apply( 111 | null, 112 | message.subarray(2, payloadOffset) 113 | ), 114 | payload: utf8ArrayToStr(message.subarray(payloadOffset)), 115 | } 116 | } else { 117 | // No name present, just payload 118 | return { 119 | payload: utf8ArrayToStr(message.subarray(2)), 120 | } 121 | } 122 | } 123 | 124 | function parseSignalUtf16(message) { 125 | // Minimum UTF16 signal message structure: 126 | // 1. message type (1 byte) 127 | // 2. name length flag (1 byte) 128 | // 3. name (n bytes, required if name length flag is bigger zero) 129 | // 4. header padding (1 byte, present if name length is odd) 130 | // 5. payload (n bytes, at least 2 bytes) 131 | if (message.length < MinMsgLen.SignalUtf16) { 132 | return {err: new Error( 133 | `Invalid signal (UTF16) message, too short ` + 134 | `(${message.length} / ${MinMsgLen.SignalUtf16})` 135 | )} 136 | } 137 | 138 | if (message.length % 2 !== 0) { 139 | return {err: new Error( 140 | `Unaligned UTF16 encoded signal message ` + 141 | `(length: ${message.length}, probably missing header padding)` 142 | )} 143 | } 144 | 145 | // Read name length 146 | const nameLen = message.subarray(1, 2)[0] 147 | 148 | // Determine minimum required message length 149 | let minMsgSize = MinMsgLen.SignalUtf16 + nameLen 150 | let payloadOffset = 2 + nameLen 151 | 152 | // Check whether a name padding byte is to be expected 153 | if (nameLen % 2 !== 0) { 154 | minMsgSize++ 155 | payloadOffset++ 156 | } 157 | 158 | // Verify total message size to prevent segmentation faults 159 | // caused by inconsistent flags, 160 | // this could happen if the specified name length doesn't correspond 161 | // to the actual name length 162 | if (message.length < minMsgSize) { 163 | return {err: new Error( 164 | `Invalid signal (UTF16) message, ` + 165 | `too short for full name ` + 166 | `(${nameLen}) and the minimum payload (2)` 167 | )} 168 | } 169 | 170 | if (nameLen > 0) { 171 | // Take name into account 172 | return { 173 | name: String.fromCharCode.apply( 174 | null, 175 | new Uint8Array(message, 2, 2 + nameLen) 176 | ), 177 | 178 | // Read payload as UTF16 encoded string 179 | payload: String.fromCharCode.apply( 180 | null, 181 | new Uint16Array(message.subarray(payloadOffset)), 182 | ), 183 | } 184 | } else { 185 | // No name present, just payload 186 | return { 187 | // Read payload as UTF16 encoded string 188 | payload: String.fromCharCode.apply( 189 | null, 190 | new Uint16Array(message.subarray(2)), 191 | ), 192 | } 193 | } 194 | } 195 | 196 | function parseErrorReply(message) { 197 | if (message.length < MinMsgLen.ErrorReply) { 198 | return {err: new Error( 199 | `Invalid error reply message, too short ` + 200 | `(${message.length} / ${MinMsgLen.ErrorReply})` 201 | )} 202 | } 203 | 204 | // Read error code length 205 | const errCodeLen = message.subarray(9, 10)[0] 206 | 207 | if (errCodeLen < 1) { 208 | return {err: new Error( 209 | `Invalid error code length in error reply message` 210 | )} 211 | } 212 | 213 | // Verify total message size to prevent segmentation faults 214 | // caused by inconsistent flags, this could happen if the specified 215 | // error code length doesn't correspond to the actual length. 216 | // Subtract 1 character already taken into account by MinMsgLen.ErrorReply 217 | if (message.length < MinMsgLen.ErrorReply + errCodeLen - 1) { 218 | return {err: new Error( 219 | `Invalid error reply message, ` + 220 | `too short for error code (${errCodeLen})` 221 | )} 222 | } 223 | 224 | // Read UTF8 encoded error message 225 | const err = new Error(utf8ArrayToStr(message.subarray(10 + errCodeLen))) 226 | 227 | // Read ASCII 7 bit encoded error code 228 | err.code = asciiArrayToStr(message.subarray(10, 10 + errCodeLen)) 229 | 230 | return { 231 | id: message.subarray(1, 9), 232 | reqError: err, 233 | } 234 | } 235 | 236 | function parseReplyShutdown(message) { 237 | if (message.length < MinMsgLen.ReplyShutdown) { 238 | return {err: new Error( 239 | `Invalid shutdown error message, too short ` + 240 | `(${message.length} / ${MinMsgLen.ReplyShutdown})` 241 | )} 242 | } 243 | 244 | const err = new Error( 245 | `Server is currently being shutdown and won't process the request` 246 | ) 247 | err.errType = 'shutdown' 248 | 249 | return { 250 | id: message.subarray(1, 9), 251 | reqError: err, 252 | } 253 | } 254 | 255 | function parseInternalError(message) { 256 | if (message.length < MinMsgLen.ReplyInternalError) { 257 | return {err: new Error( 258 | `Invalid internal error message, too short ` + 259 | `(${message.length} / ${MinMsgLen.ReplyInternalError})` 260 | )} 261 | } 262 | 263 | const err = new Error(`Request failed due to an internal server error`) 264 | err.errType = 'internal' 265 | 266 | return { 267 | id: message.subarray(1, 9), 268 | reqError: err, 269 | } 270 | } 271 | 272 | function parseProtocolError(message) { 273 | if (message.length < MinMsgLen.ReplyProtocolError) { 274 | return {err: new Error( 275 | `Invalid protocol error reply message, too short: ` + 276 | `(${message.length} / ${MinMsgLen.ReplyProtocolError})` 277 | )} 278 | } 279 | 280 | const err = new Error(`Protocol error`) 281 | err.errType = 'protocol_error' 282 | 283 | return { 284 | id: message.subarray(1, 9), 285 | reqError: err, 286 | } 287 | } 288 | 289 | function parseSessionNotFound(message) { 290 | if (message.length < MinMsgLen.SessionNotFound) { 291 | return {err: new Error( 292 | `Invalid session not found error message, too short ` + 293 | `(${message.length} / ${MinMsgLen.SessionNotFound})` 294 | )} 295 | } 296 | 297 | const err = new Error(`Requested session wasn't found`) 298 | err.errType = 'session_not_found' 299 | 300 | return { 301 | id: message.subarray(1, 9), 302 | reqError: err, 303 | } 304 | } 305 | 306 | function parseMaxSessConnsReached(message) { 307 | if (message.length < MinMsgLen.MaxSessConnsReached) { 308 | return {err: new Error( 309 | `Invalid max-session-connections-reached error message, ` + 310 | `too short ` + 311 | `(${message.length} / ${MinMsgLen.MaxSessConnsReached})` 312 | )} 313 | } 314 | 315 | // TODO: fix wrong error message 316 | const err = new Error( 317 | `Maximum concurrent connections reached for requested session` 318 | ) 319 | err.errType = 'max_sess_conns_reached' 320 | 321 | return { 322 | id: message.subarray(1, 9), 323 | reqError: err, 324 | } 325 | } 326 | 327 | function parseSessionsDisabled(message) { 328 | if (message.length < MinMsgLen.SessionsDisabled) { 329 | return {err: new Error( 330 | `Invalid sessions disabled message, too short ` + 331 | `(${message.length} / ${MinMsgLen.SessionsDisabled})` 332 | )} 333 | } 334 | 335 | const err = new Error(`Sessions are disabled for this server`) 336 | err.errType = 'sessions_disabled' 337 | 338 | return { 339 | id: message.subarray(1, 9), 340 | reqError: err, 341 | } 342 | } 343 | 344 | function parseReplyBinary(message) { 345 | // Minimum UTF8 reply message structure: 346 | // 1. message type (1 byte) 347 | // 2. message id (8 bytes) 348 | // 3. payload (n bytes, at least 1 byte) 349 | if (message.length < MinMsgLen.Reply) { 350 | return {err: new Error( 351 | `Invalid reply (Binary) message, too short ` + 352 | `(${message.length} / ${MinMsgLen.Reply})` 353 | )} 354 | } 355 | 356 | let payload = null 357 | if (message.length > MinMsgLen.Reply) { 358 | // Read payload as binary string 359 | payload = message.subarray(9) 360 | } 361 | 362 | return { 363 | id: message.subarray(1, 9), 364 | payload: payload, 365 | } 366 | } 367 | 368 | function parseReplyUtf8(message) { 369 | // Minimum UTF8 reply message structure: 370 | // 1. message type (1 byte) 371 | // 2. message id (8 bytes) 372 | // 3. payload (n bytes, at least 1 byte) 373 | if (message.length < MinMsgLen.Reply) { 374 | return {err: new Error( 375 | `Invalid reply (UTF8) message, too short ` + 376 | `(${message.length} / ${MinMsgLen.Reply})` 377 | )} 378 | } 379 | 380 | let payload = null 381 | if (message.length > MinMsgLen.Reply) { 382 | // Read payload as UTF8 encoded text 383 | payload = utf8ArrayToStr(message.subarray(9)) 384 | } 385 | 386 | return { 387 | id: message.subarray(1, 9), 388 | payload: payload, 389 | } 390 | } 391 | 392 | function parseReplyUtf16(message) { 393 | // Minimum UTF16 reply message structure: 394 | // 1. message type (1 byte) 395 | // 2. message id (8 bytes) 396 | // 3. header padding (1 byte) 397 | // 4. payload (n bytes, at least 2 bytes) 398 | if (message.length < MinMsgLen.ReplyUtf16) { 399 | return {err: new Error( 400 | `Invalid reply (UTF16) message, too short ` + 401 | `(${message.length} / ${MinMsgLen.ReplyUtf16})` 402 | )} 403 | } 404 | 405 | if (message.length % 2 !== 0) { 406 | return {err: new Error( 407 | `Unaligned UTF16 encoded reply message ` + 408 | `(length: ${message.length}, probably missing header padding)` 409 | )} 410 | } 411 | 412 | let payload = null 413 | if (message.length > MinMsgLen.ReplyUtf16) { 414 | // Read payload as UTF16 encoded text 415 | payload = String.fromCharCode.apply( 416 | null, new Uint16Array(message, 10, message.length - 10 / 2) 417 | ) 418 | } 419 | 420 | return { 421 | id: message.subarray(1, 9), 422 | 423 | // Read payload as UTF8 encoded string 424 | payload: payload, 425 | } 426 | } 427 | 428 | function parseAcceptConf(buffer, view8) { 429 | if (view8.length < MinMsgLen.AcceptConf) { 430 | return {err: new Error(`Invalid accept-conf message, too short` + 431 | `(${view8.length} / ${MinMsgLen.ReplyUtf16})` 432 | )} 433 | } 434 | 435 | const view = new DataView(buffer) 436 | 437 | return { 438 | majorProtocolVersion: view8[1], 439 | minorProtocolVersion: view8[2], 440 | readTimeout: view.getUint32(3, true), 441 | messageBufferSize: view.getUint32(7, true), 442 | subProtocolName: view8.length > MinMsgLen.AcceptConf ? 443 | utf8ArrayToStr(view8.subarray(11)) : null 444 | } 445 | } 446 | 447 | function parseMsg(buffer, view8) { 448 | if (view8.length < 1) { 449 | return {err: new Error(`Invalid message, too short`)} 450 | } 451 | let payloadEncoding = 'binary' 452 | 453 | // Read type 454 | const msgType = view8[0] 455 | let result 456 | 457 | switch (msgType) { 458 | // Accept-conf 459 | case MessageType.AcceptConf: 460 | result = parseAcceptConf(buffer, view8) 461 | break 462 | 463 | // Special notifications 464 | case MessageType.SessionCreated: 465 | result = parseSessionCreated(view8) 466 | break 467 | case MessageType.SessionClosed: 468 | result = parseSessionClosed(view8) 469 | break 470 | 471 | // Signals 472 | case MessageType.SignalBinary: 473 | result = parseSignalBinary(view8) 474 | break 475 | case MessageType.SignalUtf8: 476 | payloadEncoding = 'utf8' 477 | result = parseSignalUtf8(view8) 478 | break 479 | case MessageType.SignalUtf16: 480 | payloadEncoding = 'utf16' 481 | result = parseSignalUtf16(view8) 482 | break 483 | 484 | // Special request replies 485 | case MessageType.ReplyShutdown: 486 | result = parseReplyShutdown(view8) 487 | break 488 | case MessageType.ReplyInternalError: 489 | result = parseInternalError(view8) 490 | break 491 | case MessageType.SessionNotFound: 492 | result = parseSessionNotFound(view8) 493 | break 494 | case MessageType.MaxSessConnsReached: 495 | result = parseMaxSessConnsReached(view8) 496 | break 497 | case MessageType.SessionsDisabled: 498 | result = parseSessionsDisabled(view8) 499 | break 500 | case MessageType.ErrorReply: 501 | result = parseErrorReply(view8) 502 | break 503 | case MessageType.ReplyProtocolError: 504 | result = parseProtocolError(view8) 505 | break 506 | 507 | // Request replies 508 | case MessageType.ReplyBinary: 509 | result = parseReplyBinary(view8) 510 | break 511 | case MessageType.ReplyUtf8: 512 | payloadEncoding = 'utf8' 513 | result = parseReplyUtf8(view8) 514 | break 515 | case MessageType.ReplyUtf16: 516 | payloadEncoding = 'utf16' 517 | result = parseReplyUtf16(view8) 518 | break 519 | 520 | // Ignore messages of unsupported message type 521 | default: 522 | result = {err: new Error(`Unsupported message type: ${msgType}`)} 523 | } 524 | 525 | if (result.err != null) return {err: result.err} 526 | else { 527 | return { 528 | type: msgType, 529 | payloadEncoding: payloadEncoding, 530 | msg: result, 531 | } 532 | } 533 | } 534 | 535 | function parse(msg) { 536 | return new Promise((resolve, reject) => { 537 | try { 538 | if (process.browser) { 539 | const reader = new FileReader() 540 | reader.onerror = function(event) { 541 | reject(event.target.error) 542 | } 543 | reader.onload = function() { 544 | resolve(parseMsg( 545 | this.result, 546 | new Uint8Array(this.result) 547 | )) 548 | } 549 | reader.readAsArrayBuffer(msg) 550 | } else { 551 | resolve(parseMsg(msg.buffer, new Uint8Array( 552 | msg.buffer, 553 | msg.byteOffset, 554 | msg.byteLength / Uint8Array.BYTES_PER_ELEMENT 555 | ))) 556 | } 557 | } catch (excep) { 558 | reject(excep) 559 | } 560 | }) 561 | } 562 | -------------------------------------------------------------------------------- /src/parseEndpointAddress.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line max-len 2 | const regexp = /^((http|https):\/\/)?(localhost|((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){3}((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]))(:([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?\/*(\/([^/]+)\/?.*$)?$/ 3 | 4 | function fromString(hostAddress) { 5 | const parsed = regexp.exec(hostAddress) 6 | 7 | if (parsed == null) { 8 | throw new Error(`Invalid server host string: ${hostAddress}`) 9 | } 10 | 11 | const protocol = parsed[2] || 'http' 12 | const hostname = parsed[3] 13 | let port = parseInt(parsed[13] || 80) 14 | const path = parsed[14] 15 | 16 | if (protocol === 'https' && parsed[13] == null) port = 443 17 | 18 | return { 19 | protocol, 20 | hostname, 21 | port, 22 | path, 23 | } 24 | } 25 | 26 | export default function(hostAddress) { 27 | if (typeof hostAddress !== 'string' || hostAddress.length < 1) { 28 | throw new Error(`Invalid WebWire server host`) 29 | } 30 | return fromString(hostAddress) 31 | } 32 | -------------------------------------------------------------------------------- /src/requestMessage.js: -------------------------------------------------------------------------------- 1 | import { 2 | Type as MessageType, 3 | } from './message' 4 | import { 5 | New as NewIdentifier, 6 | } from './identifier' 7 | import strToUtf8Array from './strToUtf8Array' 8 | 9 | function stringUtf8(id, name, payload) { 10 | const encodedPayload = strToUtf8Array(payload) 11 | 12 | // Determine total message size 13 | const headerSize = 10 + name.length 14 | const buf = new ArrayBuffer(headerSize + encodedPayload.length) 15 | const headerBuf = new Uint8Array(buf, 0, headerSize) 16 | 17 | // Write type flag, default to RequestUtf8 18 | headerBuf[0] = MessageType.RequestUtf8 19 | 20 | // Write identifier 21 | const idBytes = id.bytes 22 | for (let i = 1; i < 9; i++) { 23 | headerBuf[i] = idBytes[i - 1] 24 | } 25 | 26 | // Write name length flag 27 | headerBuf[9] = name.length 28 | 29 | // Write name 30 | for (let i = 0; i < name.length; i++) { 31 | let charCode = name.charCodeAt(i) 32 | if (charCode < 32 || charCode > 126) { 33 | throw new Error(`Unsupported name character (${charCode})`) 34 | } 35 | headerBuf[10 + i] = name.charCodeAt(i) 36 | } 37 | 38 | // Write payload at an offset equal to the header size 39 | // (which includes the padding) 40 | const payloadBuf = new Uint8Array(buf, headerSize, encodedPayload.length) 41 | for (let i = 0; i < encodedPayload.length; i++) { 42 | payloadBuf[i] = encodedPayload[i] 43 | } 44 | 45 | return buf 46 | } 47 | 48 | function stringUtf16(id, name, payload) { 49 | // Decide padding byte for unaligned header 50 | // (offset of payload must be power 2) 51 | let headerPadding = 0 52 | if (name != null && name.length % 2 !== 0) headerPadding = 1 53 | 54 | // Determine total message size 55 | const headerSize = 10 + name.length + headerPadding 56 | const buf = new ArrayBuffer(headerSize + payload.length * 2) 57 | const headerBuf = new Uint8Array(buf, 0, headerSize) 58 | 59 | // Write type flag, default to RequestUtf16 60 | headerBuf[0] = MessageType.RequestUtf16 61 | 62 | // Write identifier 63 | const idBytes = id.bytes 64 | for (let i = 1; i < 9; i++) { 65 | headerBuf[i] = idBytes[i - 1] 66 | } 67 | 68 | // Write name length flag 69 | headerBuf[9] = name.length 70 | 71 | // Write name 72 | for (let i = 0; i < name.length; i++) { 73 | let charCode = name.charCodeAt(i) 74 | if (charCode < 32 || charCode > 126) { 75 | throw new Error(`Unsupported name character (${charCode})`) 76 | } 77 | headerBuf[10 + i] = name.charCodeAt(i) 78 | } 79 | 80 | // Write payload at an offset equal to the header size 81 | // (which includes the padding) 82 | const payloadBuf = new Uint16Array(buf, headerSize, payload.length) 83 | for (let i = 0; i < payload.length; i++) { 84 | payloadBuf[i] = payload.charCodeAt(i) 85 | } 86 | 87 | return buf 88 | } 89 | 90 | // RequestMessage represents an instantiatable webwire request message 91 | // name is optional and must be shorter 255 92 | // and must contain only ASCII characters (range 32-126) 93 | // if the payload is a string the encoding is undefined 94 | // then the payload will be encoded in UTF16 95 | export default function RequestMessage(name, payload, encoding) { 96 | if (payload == null) throw new Error(`Missing request payload`) 97 | if (name == null) name = '' 98 | if (name.length > 255) { 99 | throw new Error(`Request name too long (${name.length}), max 255`) 100 | } 101 | 102 | let buf 103 | const id = NewIdentifier() 104 | 105 | if (typeof payload === 'string' && encoding === 'utf8') { 106 | // Encode string into UTF8 payload 107 | buf = stringUtf8(id, name, payload) 108 | } else if (typeof payload === 'string' && encoding == null) { 109 | // Encode string into UTF16 payload 110 | buf = stringUtf16(id, name, payload) 111 | } else { 112 | throw new Error( 113 | `Unsupported request payload type: ${(typeof payload)}` 114 | ) 115 | } 116 | 117 | Object.defineProperty(this, 'bytes', { 118 | get: function() { 119 | return new Uint8Array(buf) 120 | }, 121 | }) 122 | 123 | Object.defineProperty(this, 'id', { 124 | get: function() { 125 | return id 126 | }, 127 | }) 128 | } 129 | -------------------------------------------------------------------------------- /src/sessionKey.js: -------------------------------------------------------------------------------- 1 | // SessionKey represents a valid session key in binary representation 2 | export default function SessionKey(_str) { 3 | let _buf 4 | 5 | // Determine total message size 6 | _buf = new ArrayBuffer(_str.length) 7 | 8 | // Write 9 | const bytes = new Uint8Array(_buf) 10 | for (let i = 0; i < _str.length; i++) { 11 | let charCode = _str.charCodeAt(i) 12 | if (charCode < 32 || charCode > 126) { 13 | throw new Error(`Unsupported session key character (${charCode})`) 14 | } 15 | bytes[i] = charCode 16 | } 17 | 18 | Object.defineProperty(this, 'bytes', { 19 | get: function() { 20 | return bytes 21 | }, 22 | }) 23 | 24 | Object.defineProperty(this, 'string', { 25 | get: function() { 26 | return _str 27 | }, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/signalMessage.js: -------------------------------------------------------------------------------- 1 | import { 2 | Type as MessageType, 3 | } from './message' 4 | import strToUtf8Array from './strToUtf8Array' 5 | 6 | // SignalMessage represents an instantiatable webwire signal message 7 | // name is optional and must be shorter 255 8 | // and must contain only ASCII characters (range 32-126) 9 | export default function SignalMessage(name, payload, encoding) { 10 | if (name == null) name = '' 11 | if (payload == null) throw new Error(`Missing signal payload`) 12 | 13 | let _buf 14 | 15 | if (typeof payload === 'string' && encoding === 'utf8') { 16 | // Encode string to UTF8 payload 17 | 18 | const encodedPayload = strToUtf8Array(payload) 19 | 20 | // Determine total message size 21 | const headerSize = 2 + name.length 22 | _buf = new ArrayBuffer(headerSize + encodedPayload.length) 23 | const headerBuf = new Uint8Array(_buf, 0, headerSize) 24 | 25 | // Write type flag 26 | // JavaScript strings are always UTF8 encoded 27 | // thus the payload must be UTF8 too 28 | headerBuf[0] = MessageType.SignalUtf8 29 | 30 | // Write name length flag 31 | headerBuf[1] = name.length 32 | 33 | // Write name 34 | for (let i = 0; i < name.length; i++) { 35 | headerBuf[2 + i] = name.charCodeAt(i) 36 | } 37 | 38 | // Write payload at an offset equal to the header size 39 | // (which includes the padding) 40 | const payloadBuf = new Uint8Array( 41 | _buf, 42 | headerSize, 43 | encodedPayload.length, 44 | ) 45 | for (let i = 0; i < encodedPayload.length; i++) { 46 | payloadBuf[i] = encodedPayload[i] 47 | } 48 | } else if (typeof payload === 'string' && encoding == null) { 49 | // Encode string into UTF16 payload 50 | 51 | // Decide padding byte for unaligned header 52 | // (offset of payload must be power 2) 53 | let headerPadding = false 54 | if (name.length % 2 !== 0) headerPadding = true 55 | 56 | // Determine total message size 57 | const headerSize = 2 + name.length + (headerPadding ? 1 : 0) 58 | _buf = new ArrayBuffer(headerSize + payload.length * 2) 59 | const headerBuf = new Uint8Array(_buf, 0, headerSize) 60 | 61 | // Write type flag 62 | // JavaScript strings are always UTF16 encoded 63 | // thus the payload must be UTF16 too 64 | headerBuf[0] = MessageType.SignalUtf16 65 | 66 | // Write name length flag 67 | headerBuf[1] = name.length 68 | 69 | // Write name 70 | for (let i = 0; i < name.length; i++) { 71 | headerBuf[2 + i] = name.charCodeAt(i) 72 | } 73 | 74 | // Write payload at an offset equal to the header size 75 | // (which includes the padding) 76 | const payloadBuf = new Uint16Array(_buf, headerSize, payload.length) 77 | for (let i = 0; i < payload.length; i++) { 78 | payloadBuf[i] = payload.charCodeAt(i) 79 | } 80 | } else { 81 | throw new Error(`Unsupported signal payload type: ${(typeof payload)}`) 82 | } 83 | 84 | Object.defineProperty(this, 'bytes', { 85 | get: function() { 86 | return new Uint8Array(_buf) 87 | }, 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /src/socket.js: -------------------------------------------------------------------------------- 1 | function NodeSocket(host) { 2 | let sock = new WebSocket(host) 3 | 4 | function onOpen(callback) { 5 | sock.on('open', callback) 6 | } 7 | 8 | function onError(callback) {} 9 | 10 | function onMessage(callback) { 11 | sock.on('message', callback) 12 | } 13 | 14 | function onClose(callback) { 15 | sock.on('close', callback) 16 | } 17 | 18 | function send(data) { 19 | sock.send(data) 20 | } 21 | 22 | Object.defineProperty(this, 'onOpen', { 23 | value: onOpen, writable: false, 24 | }) 25 | 26 | Object.defineProperty(this, 'onError', { 27 | value: onError, writable: false, 28 | }) 29 | 30 | Object.defineProperty(this, 'onMessage', { 31 | value: onMessage, writable: false, 32 | }) 33 | 34 | Object.defineProperty(this, 'onClose', { 35 | value: onClose, writable: false, 36 | }) 37 | 38 | Object.defineProperty(this, 'send', { 39 | value: send, writable: false, 40 | }) 41 | } 42 | 43 | function BrowserSocket(host) { 44 | let sock = new WebSocket(host) 45 | 46 | function onOpen(callback) { 47 | sock.onopen = callback 48 | } 49 | 50 | function onError(callback) { 51 | sock.onerror = callback 52 | } 53 | 54 | function onMessage(callback) { 55 | sock.onmessage = event => callback(event.data) 56 | } 57 | 58 | function onClose(callback) { 59 | sock.onclose = event => callback(event.code) 60 | } 61 | 62 | function send(data) { 63 | sock.send(data) 64 | } 65 | 66 | Object.defineProperty(this, 'onOpen', { 67 | value: onOpen, writable: false, 68 | }) 69 | 70 | Object.defineProperty(this, 'onError', { 71 | value: onError, writable: false, 72 | }) 73 | 74 | Object.defineProperty(this, 'onMessage', { 75 | value: onMessage, writable: false, 76 | }) 77 | 78 | Object.defineProperty(this, 'onClose', { 79 | value: onClose, writable: false, 80 | }) 81 | 82 | Object.defineProperty(this, 'send', { 83 | value: send, writable: false, 84 | }) 85 | } 86 | 87 | if (process.browser) module.exports = BrowserSocket 88 | else module.exports = NodeSocket 89 | -------------------------------------------------------------------------------- /src/strToUtf8Array.js: -------------------------------------------------------------------------------- 1 | // Borrowed from https://gist.github.com/joni/3760795 2 | 3 | export default function strToUtf8Array(str) { 4 | const utf8 = [] 5 | for (let i = 0; i < str.length; i++) { 6 | let charcode = str.charCodeAt(i) 7 | if (charcode < 0x80) utf8.push(charcode) 8 | else if (charcode < 0x800) { 9 | utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f)) 10 | } else if (charcode < 0xd800 || charcode >= 0xe000) { 11 | utf8.push( 12 | 0xe0 | (charcode >> 12), 13 | 0x80 | ((charcode >> 6) & 0x3f), 14 | 0x80 | (charcode & 0x3f), 15 | ) 16 | } else { 17 | // surrogate pair 18 | 19 | i++ 20 | // UTF-16 encodes 0x10000-0x10FFFF by 21 | // subtracting 0x10000 and splitting the 22 | // 20 bits of 0x0-0xFFFFF into two halves 23 | charcode = 0x10000 + ( 24 | ((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff) 25 | ) 26 | utf8.push( 27 | 0xf0 | (charcode >> 18), 28 | 0x80 | ((charcode >> 12) & 0x3f), 29 | 0x80 | ((charcode >> 6) & 0x3f), 30 | 0x80 | (charcode & 0x3f), 31 | ) 32 | } 33 | } 34 | return utf8 35 | } 36 | -------------------------------------------------------------------------------- /src/utf8ArrayToStr.js: -------------------------------------------------------------------------------- 1 | // Borrowed from https://ourcodeworld.com/articles/read/164/how-to-convert-an-uint8array-to-string-in-javascript 2 | // Copyright (C) 1999 Masanao Izumo 3 | 4 | export default function utf8ArrayToStr(byteArray) { 5 | let out, i, len, c 6 | let char2, char3 7 | out = '' 8 | len = byteArray.length 9 | i = 0 10 | while (i < len) { 11 | c = byteArray[i++] 12 | switch (c >> 4) { 13 | case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: 14 | // 0xxxxxxx 15 | out += String.fromCharCode(c) 16 | break 17 | case 12: case 13: 18 | // 110x xxxx 10xx xxxx 19 | char2 = byteArray[i++] 20 | out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)) 21 | break 22 | case 14: 23 | // 1110 xxxx 10xx xxxx 10xx xxxx 24 | char2 = byteArray[i++] 25 | char3 = byteArray[i++] 26 | out += String.fromCharCode( 27 | ((c & 0x0F) << 12) | 28 | ((char2 & 0x3F) << 6) | 29 | ((char3 & 0x3F) << 0) 30 | ) 31 | break 32 | } 33 | } 34 | return out 35 | } 36 | -------------------------------------------------------------------------------- /src/verifyProtocolVersion.js: -------------------------------------------------------------------------------- 1 | 2 | const supportedProtocolVersion = 2 3 | 4 | // verifyProtocolVersion returns an error if the given protovol version 5 | // isn't compatible with this version of the client, otherwise returns null 6 | export default function verifyProtocolVersion(major, minor) { 7 | // Initialize HTTP client 8 | if (major !== supportedProtocolVersion) { 9 | const err = new Error( 10 | `Unsupported protocol version: ${major}.${minor} ` + 11 | `(supported: ${supportedProtocolVersion}.0)` 12 | ) 13 | err.errType = 'incompatibleProtocolVersion' 14 | return err 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const isProd = process.env.NODE_ENV === 'production' 5 | 6 | module.exports = { 7 | mode: isProd ? 'production' : 'development', 8 | entry: './src/index.js', 9 | output: { 10 | path: path.resolve(__dirname, './lib'), 11 | filename: 'webwire.js', 12 | library: 'webwire-js', 13 | libraryTarget: 'umd', 14 | globalObject: 'this' 15 | }, 16 | externals: { 17 | http: 'http' 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | exclude: /node_modules/, 24 | include: path.resolve(__dirname, './src'), 25 | use: { 26 | loader: 'babel-loader', 27 | options: { 28 | presets: [ 29 | ["@babel/preset-env", { 30 | "targets": { 31 | "browsers": ["> 1%", "last 2 versions", "not ie <= 11"] 32 | } 33 | }] 34 | ], 35 | plugins: [ 36 | ["@babel/plugin-transform-runtime", { 37 | "helpers": false, 38 | "regenerator": true, 39 | }] 40 | ], 41 | ignore: [ 42 | "./examples/**/*.js", 43 | "./lib" 44 | ] 45 | } 46 | } 47 | } 48 | ] 49 | }, 50 | stats: { 51 | modules: false 52 | } 53 | } --------------------------------------------------------------------------------