├── .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 |
5 |
6 |
7 | WebWire for JavaScript
8 |
9 | An asynchronous duplex messaging library
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 | 
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 |
4 |
10 |
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 |
2 |
3 |
6 |
Connecting...
7 |
{{$store.state.api.addr}}
8 |
9 |
10 |
29 |
30 | -
34 |
38 | {{message.msg}}
39 |
40 |
41 |
46 |
Sending Error {{sendError}}
47 |
48 |
49 |
50 |
75 |
76 |
149 |
--------------------------------------------------------------------------------
/examples/chatroom-client-vue/src/views/SignInView.vue:
--------------------------------------------------------------------------------
1 |
2 |
30 |
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 | }
--------------------------------------------------------------------------------