├── examples ├── painter │ ├── client │ │ ├── canvas.js │ │ ├── contents │ │ │ ├── index.json │ │ │ ├── style.css │ │ │ └── paint.ts │ │ ├── config.json │ │ └── templates │ │ │ └── index.html │ ├── tsconfig.json │ ├── README.md │ ├── package.json │ ├── Makefile │ ├── protocol │ │ └── service.proto │ ├── shared │ │ ├── paint.ts │ │ └── brush.json │ └── server │ │ └── server.ts └── ping │ ├── package.json │ ├── service.proto │ ├── server.js │ └── ping.js ├── docs ├── README.md ├── assets │ └── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png ├── interfaces │ ├── irpcmessage.html │ ├── iprotobuftype.html │ ├── iserverevents.html │ ├── iclientevents.html │ └── iserveroptions.html ├── index.html └── globals.html ├── ws-browser.js ├── .gitignore ├── src ├── index.ts ├── utils.ts ├── server.ts └── client.ts ├── test ├── tsconfig.json └── index.ts ├── .travis.yml ├── tslint.json ├── tsconfig.json ├── protocol ├── test.proto └── rpc.proto ├── package.json ├── LICENSE ├── Makefile └── README.md /examples/painter/client/canvas.js: -------------------------------------------------------------------------------- 1 | module.exports = window 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Served at 2 | -------------------------------------------------------------------------------- /ws-browser.js: -------------------------------------------------------------------------------- 1 | module.exports = window.WebSocket || window.MozWebSocket -------------------------------------------------------------------------------- /examples/painter/client/contents/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "index.html" 3 | } 4 | -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnordberg/wsrpc/HEAD/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnordberg/wsrpc/HEAD/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnordberg/wsrpc/HEAD/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnordberg/wsrpc/HEAD/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib3/ 3 | lib6/ 4 | coverage/ 5 | **/protocol/* 6 | !**/protocol/*.proto 7 | .nyc_output/ 8 | -------------------------------------------------------------------------------- /examples/ping/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wsrpc-example-node", 3 | "private": true, 4 | "dependencies": { 5 | "wsrpc": ">=1.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {Server, Connection, IServerEvents, IServerOptions} from './server' 2 | export {Client, IClientEvents, IClientOptions} from './client' 3 | export const version: string = require('../package').version 4 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015"], 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "target": "es6" 7 | }, 8 | "include": [ 9 | "*.ts" 10 | ] 11 | } -------------------------------------------------------------------------------- /examples/painter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "es2015" 6 | ], 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "target": "es3" 10 | } 11 | } -------------------------------------------------------------------------------- /examples/ping/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | service PingService { 4 | rpc Ping (Ping) returns (Pong) {} 5 | } 6 | 7 | message Ping { 8 | bytes nonce = 1; 9 | } 10 | 11 | message Pong { 12 | bytes nonce = 1; 13 | uint32 time = 2; 14 | } 15 | -------------------------------------------------------------------------------- /examples/painter/README.md: -------------------------------------------------------------------------------- 1 | 2 | # wsrpc-example-painter 3 | 4 | Live version [up here](https://johan-nordberg.com/wspainter) 5 | 6 | ## Develop 7 | 8 | In two separate terminals run: 9 | 10 | ``` 11 | make preview 12 | ``` 13 | 14 | and 15 | 16 | ``` 17 | make server 18 | ``` 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | after_success: 6 | - "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls" 7 | env: 8 | - CXX=g++-4.8 9 | addons: 10 | apt: 11 | sources: 12 | - ubuntu-toolchain-r-test 13 | packages: 14 | - g++-4.8 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "rules": { 5 | "max-classes-per-file": false, 6 | "no-bitwise": false, 7 | "no-var-requires": false, 8 | "quotemark": [true, "single", "avoid-escape"], 9 | "semicolon": [true, "never"] 10 | } 11 | } -------------------------------------------------------------------------------- /examples/painter/client/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "browserify": { 3 | "watchify": false, 4 | "extensions": [".js", ".ts"], 5 | "plugins": ["tsify"] 6 | }, 7 | "nunjucks": { 8 | "autoescape": false 9 | }, 10 | "plugins": [ 11 | "wintersmith-browserify", 12 | "wintersmith-nunjucks", 13 | "wintersmith-livereload" 14 | ] 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "lib": ["es2015"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "noImplicitAny": true, 8 | "noImplicitThis": true, 9 | "outDir": "lib", 10 | "strictNullChecks": true, 11 | "target": "es6" 12 | }, 13 | "include": [ 14 | "src/*.ts" 15 | ] 16 | } -------------------------------------------------------------------------------- /protocol/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Empty {} 4 | 5 | service TestService { 6 | rpc Echo (TextMessage) returns (TextMessage) {} 7 | rpc Upper (TextMessage) returns (TextMessage) {} 8 | rpc NotImplemented (EmptyMessage) returns (EmptyMessage) {} 9 | } 10 | 11 | message EmptyMessage {} 12 | 13 | message TextMessage { 14 | required string text = 1; 15 | } 16 | 17 | message EventRequest { 18 | required string name = 1; 19 | required uint32 delay = 2; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /examples/painter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wsrpc-example-painter", 3 | "private": true, 4 | "browser": { 5 | "canvas": "./client/canvas.js" 6 | }, 7 | "dependencies": { 8 | "@types/lru-cache": "^5.1.0", 9 | "@types/sharp": "^0.23.0", 10 | "canvas": "^2.6.0", 11 | "lru-cache": "^5.1.1", 12 | "protobufjs": "^6.8.8", 13 | "sharp": "^0.23.1", 14 | "ts-node": "^8.4.1", 15 | "tsify": "^4.0.0", 16 | "typescript": "^3.6.4", 17 | "wintersmith": "^2.5.0", 18 | "wintersmith-browserify": "^1.3.0", 19 | "wintersmith-livereload": "^1.0.0", 20 | "wintersmith-nunjucks": "^2.0.0", 21 | "wsrpc": "^1.4.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /protocol/rpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Message { 4 | enum Type { REQUEST = 1; RESPONSE = 2; EVENT = 3; } 5 | required Type type = 1; 6 | optional Request request = 2; 7 | optional Response response = 3; 8 | optional Event event = 4; 9 | } 10 | 11 | message Request { 12 | required string method = 1; 13 | required uint32 seq = 2; 14 | optional bytes payload = 3; 15 | } 16 | 17 | message Response { 18 | required uint32 seq = 1; 19 | required bool ok = 2; 20 | optional bytes payload = 3; 21 | optional string error = 4; 22 | } 23 | 24 | message Event { 25 | required string name = 1; 26 | optional bytes payload = 2; 27 | } 28 | -------------------------------------------------------------------------------- /examples/painter/Makefile: -------------------------------------------------------------------------------- 1 | 2 | PATH := $(PATH):$(PWD)/node_modules/.bin 3 | SHELL := env PATH=$(PATH) /bin/bash 4 | 5 | .PHONY: preview 6 | preview: node_modules protocol/service.d.ts protocol/service.js 7 | wintersmith preview --chdir client 8 | 9 | .PHONY: server 10 | preview: node_modules protocol/service.d.ts protocol/service.js 11 | ts-node server/server.ts 12 | 13 | protocol/service.js: node_modules protocol/service.proto 14 | pbjs -t static-module -w commonjs protocol/service.proto -o protocol/service.js 15 | 16 | protocol/service.d.ts: node_modules protocol/service.js 17 | pbts -o protocol/service.d.ts protocol/service.js 18 | 19 | node_modules: 20 | npm install 21 | 22 | .PHONY: clean 23 | clean: 24 | rm -f protocol/service.js 25 | rm -f protocol/service.d.ts 26 | 27 | .PHONY: distclean 28 | distclean: clean 29 | rm -rf node_modules 30 | -------------------------------------------------------------------------------- /examples/painter/protocol/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | service Painter { 4 | rpc GetCanvas (CanvasRequest) returns (CanvasResponse) {} 5 | rpc Paint (PaintEvent) returns (Empty) {} 6 | } 7 | 8 | message Empty {} 9 | 10 | message Position { 11 | required int32 x = 1; 12 | required int32 y = 2; 13 | } 14 | 15 | message PaintEvent { 16 | required Position pos = 1; 17 | required uint32 size = 3; 18 | required uint32 color = 4; 19 | } 20 | 21 | message StatusEvent { 22 | required uint32 users = 1; 23 | } 24 | 25 | message CanvasRequest { 26 | enum Encoding { 27 | PNG = 1; 28 | JPEG = 2; 29 | WEBP = 3; 30 | } 31 | required Position offset = 1; 32 | required uint32 width = 2; 33 | required uint32 height = 3; 34 | required Encoding encoding = 4; 35 | } 36 | 37 | message CanvasResponse { 38 | required bytes image = 1; 39 | } 40 | -------------------------------------------------------------------------------- /examples/ping/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const wsrpc = require('wsrpc') 4 | const protobuf = require('protobufjs') 5 | 6 | const proto = protobuf.loadSync('service.proto') 7 | const service = proto.lookupService('PingService') 8 | 9 | const server = new wsrpc.Server(service, {port: 4242}) 10 | 11 | server.implement('ping', async (request) => { 12 | return {nonce: request.nonce, time: Date.now()} 13 | }) 14 | /* 15 | // node <7.6 version 16 | server.implement('ping', (request) => { 17 | return Promise.resolve({nonce: request.nonce, time: Date.now()}) 18 | }) 19 | */ 20 | 21 | server.on('listening', () => { 22 | console.log(`listening on ${ server.options.port }`) 23 | }) 24 | 25 | server.on('error', (error) => { 26 | console.warn('error', error) 27 | }) 28 | 29 | server.on('connection', (connection) => { 30 | console.log(`connection ${ connection.id }`) 31 | connection.once('close', () => { 32 | console.log(`connection ${ connection.id } closed`) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wsrpc", 3 | "version": "1.4.1", 4 | "description": "node.js/browser protobuf rpc over binary websockets", 5 | "author": "Johan Nordberg", 6 | "license": "BSD-3-Clause", 7 | "main": "./lib6/index", 8 | "typings": "./lib6/index", 9 | "browser": { 10 | "./lib6/index": "./lib3/client.js", 11 | "ws": "./ws-browser.js" 12 | }, 13 | "files": [ 14 | "lib3/*", 15 | "lib6/*", 16 | "protocol/*", 17 | "ws-browser.js" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/jnordberg/wsrpc" 22 | }, 23 | "scripts": { 24 | "prepublishOnly": "make lint && make test && make docs && make lib", 25 | "test": "make ci-test" 26 | }, 27 | "keywords": [ 28 | "protobuf", 29 | "protobuffer", 30 | "protocol buffer", 31 | "rpc", 32 | "websocket", 33 | "websockets", 34 | "ws" 35 | ], 36 | "dependencies": { 37 | "protobufjs": "^6.8.8", 38 | "ws": "^7.2.0", 39 | "verror": "^1.10.0" 40 | }, 41 | "devDependencies": { 42 | "@types/mocha": "^5.2.7", 43 | "@types/node": "^12.11.5", 44 | "@types/verror": "^1.9.3", 45 | "@types/ws": "^6.0.3", 46 | "coveralls": "^3.0.7", 47 | "mocha": "^6.2.2", 48 | "nyc": "^14.1.1", 49 | "ts-node": "^8.4.1", 50 | "tslint": "^5.20.0", 51 | "typedoc": "^0.15.0", 52 | "typescript": "^3.6.4" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2017 Johan Nordberg. All Rights Reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistribution of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistribution in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 21 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 22 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 23 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 26 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 27 | OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | You acknowledge that this software is not designed, licensed or intended for use 30 | in the design, construction, operation or maintenance of any military facility. 31 | -------------------------------------------------------------------------------- /examples/painter/client/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | wsrpc - painter example 6 | 7 | 8 | 9 | {{ livereloadScript }} 10 | 11 | 12 | wsrpc on GitHub 42 | 43 | 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | PATH := $(PATH):$(PWD)/node_modules/.bin 3 | SHELL := env PATH=$(PATH) /bin/bash 4 | 5 | PROTO_FILES := $(wildcard protocol/*.proto) 6 | PROTO_DEFS := $(PROTO_FILES:.proto=.d.ts) 7 | SRC_FILES := $(wildcard src/*.ts) 8 | 9 | .PHONY: lib 10 | lib: lib3 lib6 11 | 12 | .PHONY: proto 13 | proto: $(PROTO_DEFS) 14 | 15 | .PHONY: coverage 16 | coverage: node_modules 17 | nyc -r html -r text -e .ts -i ts-node/register -n "src/*.ts" mocha --reporter nyan --require ts-node/register test/*.ts 18 | 19 | .PHONY: test 20 | test: node_modules $(PROTO_DEFS) 21 | mocha --require ts-node/register test/*.ts 22 | 23 | .PHONY: ci-test 24 | ci-test: node_modules $(PROTO_DEFS) 25 | tslint -p tsconfig.json -c tslint.json 26 | nyc -r lcov -e .ts -i ts-node/register -n "src/*.ts" mocha --reporter tap --require ts-node/register test/*.ts 27 | 28 | .PHONY: lint 29 | lint: node_modules 30 | tslint -p tsconfig.json -c tslint.json -t stylish --fix 31 | 32 | lib6: $(PROTO_DEFS) $(SRC_FILES) node_modules 33 | tsc -p tsconfig.json -t es6 --outDir lib6 34 | touch lib6 35 | 36 | lib3: $(PROTO_DEFS) $(SRC_FILES) node_modules 37 | tsc -p tsconfig.json -t es3 --outDir lib3 38 | touch lib3 39 | 40 | protocol/%.d.ts: protocol/%.js node_modules 41 | pbts -o $@ $< 42 | 43 | .PRECIOUS: protocol/%.js 44 | protocol/%.js: protocol/%.proto node_modules 45 | pbjs -r $(basename $(notdir $<)) -t static-module -w commonjs -o $@ $< 46 | 47 | node_modules: 48 | npm install 49 | 50 | .PHONY: docs 51 | docs: node_modules 52 | typedoc --gitRevision master --target ES6 --mode file --out docs src 53 | find docs -name "*.html" | xargs sed -i '' 's~$(shell pwd)~.~g' 54 | echo "Served at " > docs/README.md 55 | 56 | .PHONY: clean 57 | clean: 58 | rm -rf lib3/ 59 | rm -rf lib6/ 60 | rm -f protocol/*.js 61 | rm -f protocol/*.d.ts 62 | 63 | .PHONY: distclean 64 | distclean: clean 65 | rm -rf node_modules 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # [wsrpc](https://github.com/jnordberg/wsrpc) [![Build Status](https://img.shields.io/travis/jnordberg/wsrpc.svg?style=flat-square)](https://travis-ci.org/jnordberg/wsrpc) [![Coverage Status](https://img.shields.io/coveralls/jnordberg/wsrpc.svg?style=flat-square)](https://coveralls.io/github/jnordberg/wsrpc?branch=master) [![Package Version](https://img.shields.io/npm/v/wsrpc.svg?style=flat-square)](https://www.npmjs.com/package/wsrpc) ![License](https://img.shields.io/npm/l/wsrpc.svg?style=flat-square) 3 | 4 | node.js/browser protobuf rpc over binary websockets. 5 | 6 | * **[Demo](https://johan-nordberg.com/wspainter)** ([source](https://github.com/jnordberg/wsrpc/tree/master/examples/painter)) 7 | * [Documentation](https://jnordberg.github.io/wsrpc/) 8 | * [Issues](https://github.com/jnordberg/wsrpc/issues) 9 | 10 | --- 11 | 12 | Minimal example 13 | --------------- 14 | 15 | my-service.proto 16 | ```protobuf 17 | service MyService { 18 | rpc SayHello (HelloRequest) returns (HelloResponse) {} 19 | } 20 | 21 | message HelloRequest { 22 | required string name = 1; 23 | } 24 | 25 | message HelloResponse { 26 | required string text = 1; 27 | } 28 | ``` 29 | 30 | server.js 31 | ```typescript 32 | const wsrpc = require('wsrpc') 33 | const protobuf = require('protobufjs') 34 | 35 | const proto = protobuf.loadSync('my-service.proto') 36 | 37 | const server = new wsrpc.Server(proto.lookupService('MyService'), { port: 4242 }) 38 | 39 | server.implement('sayHello', async (request) => { 40 | return {text: `Hello ${ request.name }!`} 41 | }) 42 | ``` 43 | 44 | client.js 45 | ```typescript 46 | const wsrpc = require('wsrpc') 47 | const protobuf = require('protobufjs') 48 | 49 | const proto = protobuf.loadSync('my-service.proto') 50 | 51 | const client = new wsrpc.Client('ws://localhost:4242', proto.lookupService('MyService')) 52 | 53 | const response = await client.service.sayHello({name: 'world'}) 54 | console.log(response) // Hello world! 55 | ``` 56 | -------------------------------------------------------------------------------- /examples/painter/client/contents/style.css: -------------------------------------------------------------------------------- 1 | 2 | * { 3 | margin: 0; padding: 0; border: 0; line-height: 1; 4 | user-select: none; -webkit-user-select: none; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif; 10 | font-size: 14px; 11 | font-weight: 300; 12 | background: black; 13 | color: #f5f5f5; 14 | } 15 | 16 | .connected body { 17 | background: #ffffff; 18 | } 19 | 20 | a { 21 | color: #f5f5f5; 22 | } 23 | 24 | a, .status { 25 | background: rgba(0, 0, 0, 0.45); 26 | top: 0; 27 | right: 0; 28 | padding: 0.6em 0.8em; 29 | z-index: 1; 30 | } 31 | 32 | .status { 33 | right: auto; 34 | left: 0; 35 | } 36 | 37 | body, html, canvas { 38 | width: 100%; 39 | height: 100%; 40 | } 41 | 42 | canvas, div, a { 43 | position: absolute; 44 | } 45 | 46 | .picker { 47 | bottom: 0; 48 | } 49 | 50 | .picker span { 51 | display: block; 52 | position: relative; 53 | width: 2em; 54 | height: 2em; 55 | } 56 | 57 | .picker span.active { 58 | outline: 0.2em solid white; 59 | border-left: 0.2em solid black; 60 | z-index: 1; 61 | } 62 | 63 | .info { 64 | text-align: center; 65 | left: 50%; 66 | top: 50%; 67 | width: 10em; 68 | height: 2em; 69 | line-height: 2em; 70 | margin-left: -5em; 71 | margin-top: -1em; 72 | background: rgba(0, 0, 0, 0.45); 73 | display: none; 74 | } 75 | 76 | .info.pan { 77 | top: auto; 78 | bottom: 0; 79 | } 80 | 81 | html.loading .info.loading { 82 | display: block; 83 | } 84 | 85 | html.pan .info.pan { 86 | display: block; 87 | } 88 | 89 | .pan-handle { 90 | display: block; 91 | position: absolute; 92 | bottom: 0; 93 | right: 0; 94 | background: rgba(0, 0, 0, 0.45); 95 | width: 1.5em; 96 | height: 1.5em; 97 | line-height: 1.5em; 98 | text-align: center; 99 | font-size: 2em; 100 | cursor: move; 101 | } 102 | 103 | html.pan canvas, html.pan-move canvas { 104 | cursor: move; 105 | opacity: 0.75; 106 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Misc utility functions. 3 | * @author Johan Nordberg 4 | * @license 5 | * Copyright (c) 2017 Johan Nordberg. All Rights Reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without modification, 8 | * are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistribution of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 13 | * 2. Redistribution in binary form must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * 3. Neither the name of the copyright holder nor the names of its contributors 18 | * may be used to endorse or promote products derived from this software without 19 | * specific prior written permission. 20 | * 21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 24 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 25 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 29 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 30 | * OF THE POSSIBILITY OF SUCH DAMAGE. 31 | * 32 | * You acknowledge that this software is not designed, licensed or intended for use 33 | * in the design, construction, operation or maintenance of any military facility. 34 | */ 35 | 36 | import {EventEmitter} from 'events' 37 | 38 | /** 39 | * Return a promise that will resove when a specific event is emitted. 40 | */ 41 | export function waitForEvent(emitter: EventEmitter, eventName: string|symbol): Promise { 42 | return new Promise((resolve, reject) => { 43 | emitter.once(eventName, resolve) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /examples/painter/shared/paint.ts: -------------------------------------------------------------------------------- 1 | import {IPaintEvent} from './../protocol/service' 2 | const Canvas = require('canvas') 3 | import * as LRUCache from 'lru-cache' 4 | 5 | export const canvasWidth = process.env['CANVAS_WIDTH'] ? parseInt(process.env['CANVAS_WIDTH']) : 4096 6 | export const canvasHeight = process.env['CANVAS_HEIGHT'] ? parseInt(process.env['CANVAS_HEIGHT']) :4096 7 | export const brushSize = 124 8 | 9 | const brushCache = new LRUCache({max: 20}) 10 | const brushImage = new Canvas.Image() 11 | brushImage.src = require('./brush') 12 | 13 | let brushData: Uint8ClampedArray 14 | 15 | function createCanvas(width: number, height: number):HTMLCanvasElement { 16 | if (process.title === 'browser') { 17 | const rv = document.createElement('canvas') 18 | rv.width = width 19 | rv.height = height 20 | return rv 21 | } else { 22 | return new Canvas.createCanvas(width, height) 23 | } 24 | } 25 | 26 | function getBrush(color: number):HTMLCanvasElement { 27 | if (brushCache.has(color)) { 28 | return brushCache.get(color) 29 | } 30 | 31 | const r = (color >> 16) & 0xff 32 | const g = (color >> 8) & 0xff 33 | const b = color & 0xff 34 | 35 | const brush = createCanvas(brushSize, brushSize) 36 | const ctx = brush.getContext('2d') 37 | 38 | if (!brushData) { 39 | ctx.drawImage(brushImage, 0, 0) 40 | brushData = ctx.getImageData(0, 0, brushSize, brushSize).data 41 | } 42 | 43 | const imageData = ctx.createImageData(brushSize, brushSize) 44 | 45 | for (let i = 0; i < brushData.length; i+=4) { 46 | imageData.data[i] = r 47 | imageData.data[i+1] = g 48 | imageData.data[i+2] = b 49 | imageData.data[i+3] = brushData[i+3] 50 | } 51 | 52 | ctx.putImageData(imageData, 0, 0) 53 | 54 | brushCache.set(color, brush) 55 | return brush 56 | } 57 | 58 | export function paint(event: IPaintEvent, ctx: CanvasRenderingContext2D) { 59 | ctx.globalAlpha = 0.4 60 | ctx.globalCompositeOperation = 'source-over' 61 | 62 | if (event.size < 1 || event.size > brushSize) { 63 | throw new Error('Invalid size') 64 | } 65 | 66 | if (Math.abs(event.pos.x) > 0xffff || Math.abs(event.pos.y) > 0xffff) { 67 | throw new Error('Invalid position') 68 | } 69 | 70 | if (event.color > 0xffffff) { 71 | throw new Error('Invalid color') 72 | } 73 | 74 | const offset = ~~(event.size / 2) 75 | const x = ~~(event.pos.x - offset) 76 | const y = ~~(event.pos.y - offset) 77 | const s = ~~event.size 78 | 79 | ctx.drawImage(getBrush(event.color), x, y, s, s) 80 | } 81 | -------------------------------------------------------------------------------- /examples/ping/ping.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // usage: [address] [-s packetsize] [-i wait] [-W waittime] 4 | 5 | const wsrpc = require('wsrpc') 6 | const protobuf = require('protobufjs') 7 | const crypto = require('crypto') 8 | const assert = require('assert') 9 | 10 | const proto = protobuf.loadSync('service.proto') 11 | const argv = process.argv 12 | 13 | const address = (argv[2] && argv[2].match(/^ws(s)?:\/\//i)) ? argv[2] : 'ws://localhost:4242' 14 | const payloadSize = argv.indexOf('-s')>0 ? Number(argv[argv.indexOf('-s')+1]) : 1024 15 | const sendTimeout = argv.indexOf('-W')>0 ? Number(argv[argv.indexOf('-W')+1]) : 1000 16 | const pingInterval = argv.indexOf('-i')>0 ? Number(argv[argv.indexOf('-i')+1]) : 1000 17 | 18 | const client = new wsrpc.Client(address, proto.lookupService('PingService'), {sendTimeout}) 19 | client.on('error', (error) => {}) 20 | 21 | async function ping(size) { 22 | const nonce = crypto.randomBytes(size) 23 | const start = process.hrtime() 24 | const response = await client.service.ping({nonce}) 25 | const diff = process.hrtime(start) 26 | assert.deepEqual(nonce, response.nonce, 'nonce should be the same') 27 | return diff[0] * 1e4 + (diff[1] / 1e6) 28 | } 29 | 30 | async function sleep(duration) { 31 | await new Promise((resolve) => setTimeout(resolve, duration)) 32 | } 33 | 34 | let seq = 0 35 | let transmitted = 0 36 | let times = [] 37 | 38 | async function main() { 39 | console.log(`ping ${ address } with a payload size of ${ payloadSize } bytes`) 40 | let lastPrint = Date.now() 41 | while (true) { 42 | try { 43 | transmitted++ 44 | const time = await ping(payloadSize) 45 | times.push(time) 46 | console.log(`seq=${ seq } time=${ time.toFixed(3) } ms`) 47 | } catch (error) { 48 | if (error.name !== 'TimeoutError') { 49 | throw error 50 | } 51 | console.log(`Timeout for seq ${ seq }`) 52 | } 53 | seq++ 54 | await sleep(pingInterval) 55 | } 56 | } 57 | 58 | process.on('exit', () => { 59 | const n = times.length 60 | const loss = 1 - (n / transmitted) 61 | console.log(`\n--- ${ address } ping statistics ---`) 62 | console.log(`${ transmitted } transmitted, ${ n } received, ${ (loss*100).toFixed(1) }% loss`) 63 | if (n < 1) return 64 | const avg = times.reduce((v,n)=>v+n,0)/n 65 | const stddev = Math.sqrt(times.map((v)=>Math.pow(v-avg, 2)).reduce((v,n)=>v+n,0)/n) 66 | const min = Math.min.apply(null, times) 67 | const max = Math.max.apply(null, times) 68 | console.log(`round-trip min/avg/max/stddev = ${ [min,avg,max,stddev].map((v)=>v.toFixed(2)).join('/') } ms`) 69 | }) 70 | process.on('SIGINT', () => process.exit()) 71 | 72 | main().catch((error) => { 73 | console.error('fatal error', error) 74 | process.exit(1) 75 | }) 76 | -------------------------------------------------------------------------------- /examples/painter/server/server.ts: -------------------------------------------------------------------------------- 1 | import * as wsrpc from 'wsrpc' 2 | import * as protobuf from 'protobufjs' 3 | import * as zlib from 'zlib' 4 | import * as Canvas from 'canvas' 5 | import * as fs from 'fs' 6 | import * as path from 'path' 7 | import * as sharp from 'sharp' 8 | 9 | import {PaintEvent, StatusEvent, CanvasRequest} from './../protocol/service' 10 | import * as shared from './../shared/paint' 11 | 12 | const proto = protobuf.loadSync(`${ __dirname }/../protocol/service.proto`) 13 | 14 | const canvas = Canvas.createCanvas(shared.canvasWidth, shared.canvasHeight) 15 | const ctx = canvas.getContext('2d') 16 | ctx.patternQuality = 'fast' 17 | ctx.filter = 'fast' 18 | ctx.antialias = 'none' 19 | 20 | try { 21 | const img = new Canvas.Image() 22 | img.src = fs.readFileSync('canvas.jpeg') 23 | ctx.drawImage(img, 0, 0) 24 | } catch (error) { 25 | if (error.code !== 'ENOENT') { 26 | throw error 27 | } 28 | } 29 | 30 | async function saveCanvas() { 31 | const width = shared.canvasWidth 32 | const height = shared.canvasHeight 33 | const imageData = ctx.getImageData(0, 0, width, height) 34 | const imageBuffer = Buffer.from(imageData.data.buffer) 35 | await sharp(imageBuffer, {raw: {channels: 4, width, height}}) 36 | .flatten({ background: '#ffffff'}) 37 | .jpeg({quality: 90, chromaSubsampling: '4:4:4'}) 38 | .toFile('canvas.jpeg') 39 | } 40 | 41 | process.on('SIGINT', async () => { 42 | console.log('saving canvas...') 43 | await saveCanvas() 44 | console.log('saved') 45 | process.exit() 46 | }) 47 | 48 | let canvasDirty = false 49 | if (process.env['SAVE_INTERVAL'] && process.env['SAVE_DIR']) { 50 | const interval = parseInt(process.env['SAVE_INTERVAL']) 51 | const dir = process.env['SAVE_DIR'] 52 | console.log(`saving canvas to ${ dir } every ${ interval } seconds`) 53 | const save = async () => { 54 | if (!canvasDirty) { 55 | return 56 | } 57 | const filename = path.join(dir, `canvas-${ new Date().toISOString() }.jpeg`) 58 | console.log(`saving canvas to ${ filename }`) 59 | await saveCanvas() 60 | fs.createReadStream('canvas.jpeg').pipe(fs.createWriteStream(filename)); 61 | canvasDirty = false 62 | } 63 | setInterval(save, interval * 1000) 64 | } 65 | 66 | const server = new wsrpc.Server(proto.lookupService('Painter') as any, { 67 | port: 4242, 68 | }) 69 | 70 | server.implement('paint', async (event: PaintEvent, sender) => { 71 | shared.paint(event, ctx) 72 | canvasDirty = true 73 | const broadcast = PaintEvent.encode(event).finish() 74 | for (const connection of server.connections) { 75 | if (connection === sender) { 76 | continue 77 | } 78 | connection.send('paint', broadcast) 79 | } 80 | return {} 81 | }) 82 | 83 | server.implement('getCanvas', async (request: CanvasRequest) => { 84 | let {offset, width, height} = request 85 | if (offset.x < 0 || offset.y < 0 || 86 | offset.x + width > shared.canvasWidth || 87 | offset.y + height > shared.canvasHeight) { 88 | throw new Error('Out of bounds') 89 | } 90 | const imageData = ctx.getImageData(offset.x, offset.y, width, height) 91 | const imageBuffer = Buffer.from(imageData.data.buffer) 92 | const image = sharp(imageBuffer, {raw: {width, height, channels: 4}}) 93 | let responseImage: Buffer 94 | switch (request.encoding) { 95 | case CanvasRequest.Encoding.JPEG: 96 | responseImage = await image 97 | .flatten({ background: '#ffffff' }).jpeg().toBuffer() 98 | break 99 | case CanvasRequest.Encoding.WEBP: 100 | responseImage = await image.webp().toBuffer() 101 | break 102 | case CanvasRequest.Encoding.PNG: 103 | responseImage = await image.png().toBuffer() 104 | break 105 | default: 106 | throw new Error('Invalid encoding') 107 | } 108 | return {image: responseImage} 109 | }) 110 | 111 | const broadcastStatus = () => { 112 | const data = StatusEvent.encode({users: server.connections.length}).finish() 113 | server.broadcast('status', data) 114 | } 115 | 116 | server.on('connection', (connection) => { 117 | broadcastStatus() 118 | connection.once('close', broadcastStatus) 119 | }) 120 | 121 | server.on('error', (error) => { 122 | console.log('error', error.message) 123 | }) 124 | 125 | server.on('listening', () => { 126 | console.log(`listening on ${ server.options.port }`) 127 | }) 128 | -------------------------------------------------------------------------------- /examples/painter/shared/brush.json: -------------------------------------------------------------------------------- 1 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAH0AAAB8CAYAAABE3L+AAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNS4xIE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4QjlEQzM5MEMzQTAxMUUwQkU3QUFBOUY2M0QzMUI1MyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4QjlEQzM5MUMzQTAxMUUwQkU3QUFBOUY2M0QzMUI1MyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkRBMjhBRDU3QzM2RjExRTBCRTdBQUE5RjYzRDMxQjUzIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkRBMjhBRDU4QzM2RjExRTBCRTdBQUE5RjYzRDMxQjUzIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+VTkSEgAAGLFJREFUeNrsXe1y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" 2 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import 'mocha' 3 | 4 | import * as protobuf from 'protobufjs' 5 | import * as assert from 'assert' 6 | 7 | import * as path from 'path' 8 | import * as crypto from 'crypto' 9 | import {Server, Client} from './../src' 10 | import * as wsrpc_client from './../src/client' 11 | import {waitForEvent} from './../src/utils' 12 | import {TestService, TextMessage} from './../protocol/test' 13 | import * as rpcproto from './../protocol/rpc' 14 | import * as WebSocket from 'ws' 15 | 16 | const testPort = 1234 17 | const testAddr = `ws://localhost:${ testPort }` 18 | const testProtoPath = path.join(__dirname, './../protocol/test.proto') 19 | const testProto = protobuf.loadSync(testProtoPath) 20 | 21 | const serverService = testProto.lookupService('TestService') 22 | const serverOpts = { 23 | port: testPort, 24 | pingInterval: 0.05, 25 | } 26 | 27 | describe('rpc', () => { 28 | 29 | let planError = false 30 | let unplannedError = false 31 | 32 | let server = new Server(serverService, serverOpts) 33 | 34 | server.implement('echo', async (request: TextMessage) => { 35 | if (request.text === 'throw-string') { 36 | throw 'You should always trow an error object' 37 | } 38 | if (request.text === 'throw') { 39 | throw new Error('Since you asked for it') 40 | } 41 | return {text: request.text} 42 | }) 43 | 44 | server.implement(serverService.methods['Upper'], (request: TextMessage) => { 45 | return new Promise((resolve, reject) => { 46 | const text = request.text.toUpperCase() 47 | setTimeout(() => { 48 | resolve({text}) 49 | }, 50) 50 | }) 51 | }) 52 | 53 | server.on('error', (error: Error) => { 54 | if (planError) { 55 | return 56 | } 57 | 58 | unplannedError = true 59 | console.warn('unplanned server error', error.message) 60 | }) 61 | 62 | const client = new Client(testAddr, TestService, { 63 | sendTimeout: 100, 64 | eventTypes: { 65 | 'text': TextMessage 66 | } 67 | }) 68 | 69 | client.on('error', (error: Error) => { 70 | if (planError) { 71 | return 72 | } 73 | 74 | unplannedError = true 75 | console.warn('unplanned client error', error.message) 76 | }) 77 | after(async () => await client.disconnect()) 78 | 79 | it('should throw when implementing invalid method', function() { 80 | assert.throws(() => { 81 | server.implement('kek', async () => { return {}}) 82 | }) 83 | assert.throws(() => { 84 | const orphanMethod = new protobuf.Method('Keke', 'foo', 'bar', 'baz') 85 | server.implement(orphanMethod, async () => { return {}}) 86 | }) 87 | }) 88 | 89 | it('should run echo rpc method', async function() { 90 | const response = await client.service.echo({text: 'hello world'}) 91 | assert.equal(response.text, 'hello world') 92 | }) 93 | 94 | it('should run upper rpc method', async function() { 95 | this.slow(150) 96 | const response = await client.service.upper({text: 'hello world'}) 97 | assert.equal(response.text, 'HELLO WORLD') 98 | }) 99 | 100 | it('should handle thrown errors in implementation handler', async function() { 101 | planError = true 102 | try { 103 | await client.service.echo({text: 'throw'}) 104 | assert(false, 'should not be reached') 105 | } catch (error) { 106 | assert.equal(error.name, 'RPCError') 107 | assert.equal(error.message, 'Since you asked for it') 108 | } 109 | }) 110 | 111 | it('should handle thrown strings in implementation handler', async function() { 112 | try { 113 | await client.service.echo({text: 'throw-string'}) 114 | assert(false, 'should not be reached') 115 | } catch (error) { 116 | assert.equal(error.name, 'RPCError') 117 | assert.equal(error.message, 'You should always trow an error object') 118 | } 119 | }) 120 | 121 | it('should handle unimplemented methods', async function() { 122 | try { 123 | await client.service.notImplemented({}) 124 | assert(false, 'should throw') 125 | } catch (error) { 126 | assert.equal(error.name, 'RPCError') 127 | assert.equal(error.message, 'Not implemented') 128 | } 129 | }) 130 | 131 | it('should handle bogus request message', function(done) { 132 | const c = client as any 133 | const msg = rpcproto.Message.encode({ 134 | type: rpcproto.Message.Type.REQUEST, 135 | request: { 136 | seq: 0, 137 | method: crypto.pseudoRandomBytes(1e4).toString('utf8'), 138 | } 139 | }).finish() 140 | c.socket.send(msg) 141 | server.once('error', (error: any) => { 142 | assert.equal(error.message, 'connection error: Invalid method') 143 | done() 144 | }) 145 | }) 146 | 147 | it('should handle bogus message', function(done) { 148 | const c = client as any 149 | const msg = rpcproto.Message.encode({ 150 | type: rpcproto.Message.Type.EVENT, 151 | response: { 152 | seq: -100, 153 | ok: false, 154 | payload: crypto.pseudoRandomBytes(1e6), 155 | } 156 | }).finish() 157 | c.socket.send(msg) 158 | server.once('error', (error: any) => { 159 | assert.equal(error.message, 'connection error: could not decode message: Invalid message type') 160 | done() 161 | }) 162 | }) 163 | 164 | 165 | it('should handle garbled data from client', function(done) { 166 | planError = true 167 | const c = client as any 168 | c.socket.send(crypto.pseudoRandomBytes(512)) 169 | server.once('error', (error: any) => { 170 | assert.equal(error.jse_cause.name, 'RequestError') 171 | assert.equal(error.jse_cause.jse_shortmsg, 'could not decode message') 172 | done() 173 | }) 174 | }) 175 | 176 | it('should handle garbled data from server', function(done) { 177 | assert.equal(server.connections.length, 1) 178 | let conn = server.connections[0] as any 179 | conn.socket.send(crypto.pseudoRandomBytes(1024)) 180 | client.once('error', (error: any) => { 181 | assert.equal(error.name, 'MessageError') 182 | assert.equal(error.jse_shortmsg, 'got invalid message') 183 | done() 184 | }) 185 | }) 186 | 187 | it('should emit event', function(done) { 188 | planError = false 189 | assert.equal(server.connections.length, 1) 190 | const data = crypto.pseudoRandomBytes(42) 191 | server.connections[0].send('marvin', data) 192 | client.once('event', (name: string, payload?: Uint8Array) => { 193 | assert.equal(name, 'marvin') 194 | assert.deepEqual(payload, data) 195 | done() 196 | }) 197 | }) 198 | 199 | it('should emit typed event', function(done) { 200 | const text = 'I like les turlos' 201 | server.broadcast('text', TextMessage.encode({text}).finish()) 202 | client.once('event', (name: string, payload: TextMessage) => { 203 | assert.equal(name, 'text') 204 | assert.equal(payload.text, text) 205 | done() 206 | }) 207 | }) 208 | 209 | it('should handle garbled event data', function(done) { 210 | planError = true 211 | server.broadcast('text', crypto.pseudoRandomBytes(42)) 212 | client.once('error', (error: any) => { 213 | assert.equal(error.name, 'EventError') 214 | assert.equal(error.jse_shortmsg, 'could not decode event payload') 215 | done() 216 | }) 217 | }) 218 | 219 | it('should timeout messages', async function() { 220 | this.slow(300) 221 | const response = client.service.echo({text: 'foo'}) 222 | await client.disconnect() 223 | try { 224 | await response 225 | assert(false, 'should throw') 226 | } catch (error) { 227 | assert.equal(error.name, 'TimeoutError') 228 | } 229 | }) 230 | 231 | it('should reconnect', async function() { 232 | planError = false 233 | await client.connect() 234 | const response = await client.service.echo({text: 'baz'}) 235 | assert(response.text, 'baz') 236 | }) 237 | 238 | it('should handle server disconnection', async function() { 239 | this.slow(300) 240 | const c = client as any 241 | c.sendTimeout = 1000 242 | 243 | assert.equal(server.connections.length, 1) 244 | server.connections[0].close() 245 | await waitForEvent(client, 'close') 246 | 247 | const buzz = client.service.echo({text: 'fizz'}) 248 | const fizz = client.service.echo({text: 'buzz'}) 249 | const response = await Promise.all([buzz, fizz]) 250 | assert.deepEqual(response.map((msg) => msg.text), ['fizz', 'buzz']) 251 | }) 252 | 253 | it('should retry', async function() { 254 | this.slow(300) 255 | server.close() 256 | await waitForEvent(client, 'close') 257 | planError = true 258 | // force a connection failure to simulate server being down for a bit 259 | await client.connect() 260 | planError = false 261 | server = new Server(serverService, serverOpts) 262 | await waitForEvent(client, 'open') 263 | }) 264 | 265 | it('should handle failed writes', async function() { 266 | ( client).socket.send = () => { throw new Error('boom') } 267 | try { 268 | await client.service.echo({text: 'boom'}) 269 | assert(false, 'should not be reached') 270 | } catch (error) { 271 | assert.equal(error.message, 'boom') 272 | } 273 | }) 274 | 275 | it('should close server', async function() { 276 | server.close() 277 | await waitForEvent(client, 'close') 278 | }) 279 | 280 | it('should not have any unplanned error', async function() { 281 | assert.equal(false, unplannedError) 282 | }) 283 | }) 284 | 285 | describe('rpc browser client', function() { 286 | // simulated browser test using the ws module 287 | 288 | let server: Server 289 | let client: Client 290 | 291 | before(async function() { 292 | (wsrpc_client).WS = WebSocket 293 | process.title = 'browser' 294 | server = new Server(serverService, serverOpts) 295 | server.implement('echo', async (request: TextMessage) => { 296 | return {text: request.text} 297 | }) 298 | client = new Client(testAddr, TestService) 299 | }) 300 | 301 | after(async function() { 302 | await client.disconnect() 303 | server.close() 304 | }) 305 | 306 | it('should work', async function() { 307 | const response = await client.service.echo({text: 'foo'}) 308 | assert.equal(response.text, 'foo') 309 | }) 310 | }) 311 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file RPC Server implementation. 3 | * @author Johan Nordberg 4 | * @license 5 | * Copyright (c) 2017 Johan Nordberg. All Rights Reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without modification, 8 | * are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistribution of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 13 | * 2. Redistribution in binary form must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * 3. Neither the name of the copyright holder nor the names of its contributors 18 | * may be used to endorse or promote products derived from this software without 19 | * specific prior written permission. 20 | * 21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 24 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 25 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 29 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 30 | * OF THE POSSIBILITY OF SUCH DAMAGE. 31 | * 32 | * You acknowledge that this software is not designed, licensed or intended for use 33 | * in the design, construction, operation or maintenance of any military facility. 34 | */ 35 | 36 | import {EventEmitter} from 'events' 37 | import * as protobuf from 'protobufjs/minimal' 38 | import {VError} from 'verror' 39 | import * as WebSocket from 'ws' 40 | import * as RPC from './../protocol/rpc' 41 | import {waitForEvent} from './utils' 42 | 43 | /** 44 | * RPC Server options 45 | * ------------------ 46 | * Server options, extends the WebSocket server options. 47 | * Note that `WebSocket.IServerOptions.perMessageDeflate` defaults 48 | * to `false` if omitted. 49 | */ 50 | export interface IServerOptions extends WebSocket.ServerOptions { 51 | /** 52 | * How often to send a ping frame, in seconds. Set to 0 to disable. Default = 10. 53 | */ 54 | pingInterval?: number 55 | } 56 | 57 | export interface IServerEvents { 58 | on(event: 'connection', listener: (connection: Connection) => void): void 59 | on(event: 'error', listener: (error: Error) => void): void 60 | } 61 | 62 | export type Message = protobuf.Message<{}>|{[k: string]: any} 63 | export type Handler = (request: Message, connection: Connection) => Promise 64 | 65 | /** 66 | * RPC Server 67 | * ---------- 68 | */ 69 | export class Server extends EventEmitter implements IServerEvents { 70 | 71 | /** 72 | * List of clients currently connected to server. 73 | */ 74 | public readonly connections: Connection[] = [] 75 | 76 | /** 77 | * Implemented RPC method handlers, read-only. {@see Service.implement} 78 | */ 79 | public readonly handlers: {[name: string]: Handler} = {} 80 | 81 | /** 82 | * Server options, read-only. 83 | */ 84 | public readonly options: IServerOptions 85 | 86 | /** 87 | * The protobuf Service instance, internal. 88 | */ 89 | public readonly service: protobuf.Service 90 | 91 | /** 92 | * The underlying uWebSocket server, internal. 93 | */ 94 | public readonly server: WebSocket.Server 95 | 96 | private connectionCounter: number = 0 97 | private pingInterval: number 98 | 99 | /** 100 | * @param service The protocol buffer service class to serve. 101 | * @param options Options, see {@link IServerOptions}. 102 | */ 103 | constructor(service: protobuf.Service, options: IServerOptions = {}) { 104 | super() 105 | 106 | this.service = service 107 | this.options = options 108 | 109 | options.clientTracking = false 110 | if (options.perMessageDeflate === undefined) { 111 | options.perMessageDeflate = false 112 | } 113 | 114 | this.pingInterval = options.pingInterval || 10 115 | 116 | this.server = new WebSocket.Server(options) 117 | this.server.on('listening', () => { this.emit('listening') }) 118 | this.server.on('error', (cause: any) => { 119 | this.emit('error', new VError({name: 'WebSocketError', cause}, 'server error')) 120 | }) 121 | this.server.on('connection', this.connectionHandler) 122 | this.server.on('headers', (headers) => { this.emit('headers', headers) }) 123 | } 124 | 125 | /** 126 | * Implement a RPC method defined in the protobuf service. 127 | */ 128 | public implement(method: protobuf.Method|string, handler: Handler) { 129 | if (typeof method === 'string') { 130 | const methodName = method[0].toUpperCase() + method.substring(1) 131 | method = this.service.methods[methodName] 132 | if (!method) { 133 | throw new Error('Invalid method') 134 | } 135 | } else if (this.service.methodsArray.indexOf(method) === -1) { 136 | throw new Error('Invalid method') 137 | } 138 | method.resolve() 139 | this.handlers[method.name] = handler 140 | } 141 | 142 | /** 143 | * Send event to all connected clients. {@see Connection.send} 144 | */ 145 | public async broadcast(name: string, payload?: Uint8Array) { 146 | const promises = this.connections.map((connection) => { 147 | return connection.send(name, payload) 148 | }) 149 | await Promise.all(promises) 150 | } 151 | 152 | /** 153 | * Stop listening and close all connections. 154 | */ 155 | public close() { 156 | this.connections.forEach((connection) => { 157 | connection.close() 158 | }) 159 | this.server.close() 160 | } 161 | 162 | private connectionHandler = (socket: WebSocket) => { 163 | const connection = new Connection(socket, this, ++this.connectionCounter) 164 | this.connections.push(connection) 165 | 166 | connection.on('error', (cause: Error) => { 167 | const error: Error = new VError({name: 'ConnectionError', cause}, 'connection error') 168 | this.emit('error', error) 169 | }) 170 | 171 | let pingTimer: NodeJS.Timer 172 | if (this.pingInterval !== 0) { 173 | pingTimer = setInterval(() => { socket.ping() }, this.pingInterval * 1000) 174 | } 175 | 176 | connection.once('close', () => { 177 | clearInterval(pingTimer) 178 | const idx = this.connections.indexOf(connection) 179 | if (idx !== -1) { 180 | this.connections.splice(idx, 1) 181 | } 182 | }) 183 | 184 | this.emit('connection', connection) 185 | } 186 | } 187 | 188 | /** 189 | * Class representing a connection to the server, i.e. client. 190 | */ 191 | export class Connection extends EventEmitter { 192 | 193 | /** 194 | * Unique identifier for this connection. 195 | */ 196 | public readonly id: number 197 | 198 | /** 199 | * The underlying WebSocket instance. 200 | */ 201 | public readonly socket: WebSocket 202 | 203 | private server: Server 204 | 205 | constructor(socket: WebSocket, server: Server, id: number) { 206 | super() 207 | this.socket = socket 208 | this.server = server 209 | this.id = id 210 | socket.on('message', this.messageHandler) 211 | socket.on('close', () => { this.emit('close') }) 212 | socket.on('error', (error) => { this.emit('error', error) }) 213 | } 214 | 215 | /** 216 | * Send event to client with optional payload. 217 | */ 218 | public send(name: string, payload?: Uint8Array): Promise { 219 | return new Promise((resolve, reject) => { 220 | const event: RPC.IEvent = {name} 221 | if (payload) { 222 | event.payload = payload 223 | } 224 | const message = RPC.Message.encode({ 225 | event, type: RPC.Message.Type.EVENT, 226 | }).finish() 227 | this.socket.send(message, (error) => { 228 | if (error) { reject(error) } else { resolve() } 229 | }) 230 | }) 231 | } 232 | 233 | /** 234 | * Close the connection to the client. 235 | */ 236 | public close() { 237 | this.socket.close() 238 | } 239 | 240 | private async requestHandler(request: RPC.Request): Promise { 241 | const methodName = request.method[0].toUpperCase() + request.method.substring(1) 242 | 243 | const method = this.server.service.methods[methodName] 244 | if (!method) { 245 | throw new Error('Invalid method') 246 | } 247 | 248 | const impl = this.server.handlers[methodName] 249 | if (!impl) { 250 | throw new Error('Not implemented') 251 | } 252 | 253 | if (!method.resolvedRequestType || !method.resolvedResponseType) { 254 | throw new Error('Unable to resolve method types') 255 | } 256 | 257 | const requestData = method.resolvedRequestType.decode(request.payload) 258 | let responseData: Message 259 | try { 260 | responseData = await impl(requestData, this) 261 | } catch (error) { 262 | if (!(error instanceof Error)) { 263 | error = new Error(String(error)) 264 | } 265 | throw error 266 | } 267 | 268 | const response = new RPC.Response({seq: request.seq, ok: true}) 269 | response.payload = method.resolvedResponseType.encode(responseData).finish() 270 | 271 | return response 272 | } 273 | 274 | private messageHandler = (data: any) => { 275 | let request: RPC.Request 276 | try { 277 | const message = RPC.Message.decode(new Uint8Array(data)) 278 | if (message.type !== RPC.Message.Type.REQUEST) { 279 | throw new Error('Invalid message type') 280 | } 281 | if (!message.request) { 282 | throw new Error('Message request missing') 283 | } 284 | request = new RPC.Request(message.request) 285 | } catch (cause) { 286 | const error = new VError({name: 'RequestError', cause}, 'could not decode message') 287 | this.emit('error', error) 288 | return 289 | } 290 | this.requestHandler(request).then((response) => { 291 | const message = RPC.Message.encode({type: RPC.Message.Type.RESPONSE, response}).finish() 292 | return new Promise((resolve, reject) => { 293 | this.socket.send(message, (error) => { 294 | if (error) { reject(error) } else { resolve ()} 295 | }) 296 | }) 297 | }).catch((error: Error) => { 298 | const message = RPC.Message.encode({ 299 | response: { 300 | error: error.message, 301 | ok: false, 302 | seq: request.seq, 303 | }, 304 | type: RPC.Message.Type.RESPONSE, 305 | }).finish() 306 | if (this.socket.readyState === WebSocket.OPEN) { 307 | this.socket.send(message) 308 | } 309 | setImmediate(() => { 310 | // this avoids the promise swallowing the error thrown 311 | // by emit 'error' when no listeners are present 312 | this.emit('error', error) 313 | }) 314 | }) 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file RPC Client implementation. 3 | * @author Johan Nordberg 4 | * @license 5 | * Copyright (c) 2017 Johan Nordberg. All Rights Reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without modification, 8 | * are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistribution of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 13 | * 2. Redistribution in binary form must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * 3. Neither the name of the copyright holder nor the names of its contributors 18 | * may be used to endorse or promote products derived from this software without 19 | * specific prior written permission. 20 | * 21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 24 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 25 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 29 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 30 | * OF THE POSSIBILITY OF SUCH DAMAGE. 31 | * 32 | * You acknowledge that this software is not designed, licensed or intended for use 33 | * in the design, construction, operation or maintenance of any military facility. 34 | */ 35 | 36 | import {EventEmitter} from 'events' 37 | import * as protobuf from 'protobufjs/minimal' 38 | import {VError} from 'verror' 39 | import * as WebSocket from 'ws' 40 | import * as RPC from '../protocol/rpc' 41 | import {waitForEvent} from './utils' 42 | 43 | export let WS = WebSocket 44 | 45 | export interface IProtobufType { 46 | encode(message: any, writer?: protobuf.Writer): protobuf.Writer 47 | decode(reader: (protobuf.Reader|Uint8Array), length?: number): any 48 | } 49 | 50 | interface IRPCMessage { 51 | callback: protobuf.RPCImplCallback, 52 | message?: RPC.IMessage, 53 | seq: number, 54 | timer?: NodeJS.Timer, 55 | } 56 | 57 | /** 58 | * RPC Client options 59 | * ------------------ 60 | * *Note* - The options inherited from `WebSocket.IClientOptions` are only 61 | * valid when running in node.js, they have no effect in the browser. 62 | */ 63 | export interface IClientOptions extends WebSocket.ClientOptions { 64 | /** 65 | * Event names to protobuf types, any event assigned a type will have 66 | * its payload decoded before the event is posted. 67 | */ 68 | eventTypes?: {[name: string]: IProtobufType} 69 | /** 70 | * Retry backoff function, returns milliseconds. Default = {@link defaultBackoff}. 71 | */ 72 | backoff?: (tries: number) => number 73 | /** 74 | * Whether to connect when {@link Client} instance is created. Default = `true`. 75 | */ 76 | autoConnect?: boolean 77 | /** 78 | * How long in milliseconds before a message times out, set to `0` to disable. 79 | * Default = `5 * 1000`. 80 | */ 81 | sendTimeout?: number 82 | } 83 | 84 | /** 85 | * RPC Client events 86 | * ----------------- 87 | */ 88 | export interface IClientEvents { 89 | /** 90 | * Emitted when the connection closes/opens. 91 | */ 92 | on(event: 'open' | 'close', listener: () => void): this 93 | /** 94 | * Emitted on error, throws if there is no listener. 95 | */ 96 | on(event: 'error', listener: (error: Error) => void): this 97 | /** 98 | * RPC event sent by the server. If the event name is given a type 99 | * constructor in {@link IClientOptions.eventTypes} the data will 100 | * be decoded before the event is emitted. 101 | */ 102 | on(event: 'event', listener: (name: string, data?: Uint8Array|{[k: string]: any}) => void): this 103 | on(event: 'event ', listener: (data?: Uint8Array|{[k: string]: any}) => void): this 104 | } 105 | 106 | /** 107 | * RPC Client 108 | * ---------- 109 | * Can be used in both node.js and the browser. Also see {@link IClientOptions}. 110 | */ 111 | export class Client extends EventEmitter implements IClientEvents { 112 | 113 | /** 114 | * Client options, *readonly*. 115 | */ 116 | public readonly options: IClientOptions 117 | 118 | /** 119 | * The protobuf service instance which holds all the rpc methods defined in your protocol. 120 | */ 121 | public readonly service: T 122 | 123 | private active: boolean = false 124 | private address: string 125 | private backoff: (tries: number) => number 126 | private eventTypes: {[name: string]: IProtobufType} 127 | private messageBuffer: {[seq: number]: IRPCMessage} = {} 128 | private nextSeq: number = 0 129 | private numRetries: number = 0 130 | private sendTimeout: number 131 | private socket?: WebSocket 132 | private writeMessage: (message: RPC.IMessage) => Promise 133 | 134 | /** 135 | * @param address The address to the {@link Server}, eg `ws://example.com:8042`. 136 | * @param service The protocol buffer service class to use, an instance of this 137 | * will be available as {@link Client.service}. 138 | */ 139 | constructor(address: string, service: {create(rpcImpl: protobuf.RPCImpl): T}, options: IClientOptions = {}) { 140 | super() 141 | 142 | this.address = address 143 | this.options = options 144 | this.service = service.create(this.rpcImpl) 145 | 146 | this.eventTypes = options.eventTypes || {} 147 | this.backoff = options.backoff || defaultBackoff 148 | this.writeMessage = process.title === 'browser' ? this.writeMessageBrowser : this.writeMessageNode 149 | this.sendTimeout = options.sendTimeout || 5 * 1000 150 | 151 | if (options.autoConnect === undefined || options.autoConnect === true) { 152 | this.connect() 153 | } 154 | } 155 | 156 | /** 157 | * Return `true` if the client is connected, otherwise `false`. 158 | */ 159 | public isConnected(): boolean { 160 | return (this.socket !== undefined && this.socket.readyState === WS.OPEN) 161 | } 162 | 163 | /** 164 | * Connect to the server. 165 | */ 166 | public async connect() { 167 | this.active = true 168 | if (this.socket) { return } 169 | if (process.title === 'browser') { 170 | this.socket = new WS(this.address) 171 | this.socket.addEventListener('message', this.messageHandler) 172 | this.socket.addEventListener('open', this.openHandler) 173 | this.socket.addEventListener('close', this.closeHandler) 174 | this.socket.addEventListener('error', this.errorHandler) 175 | } else { 176 | let didOpen = false 177 | this.socket = new WS(this.address, this.options) 178 | this.socket.onmessage = this.messageHandler 179 | this.socket.onopen = () => { 180 | didOpen = true 181 | this.openHandler() 182 | } 183 | this.socket.onclose = this.closeHandler 184 | this.socket.onerror = (error) => { 185 | if (!didOpen) { this.closeHandler() } 186 | this.errorHandler(error) 187 | } 188 | } 189 | (this.socket as any).binaryType = 'arraybuffer' 190 | await new Promise((resolve) => { 191 | const done = () => { 192 | this.removeListener('open', done) 193 | this.removeListener('close', done) 194 | resolve() 195 | } 196 | this.on('open', done) 197 | this.on('close', done) 198 | }) 199 | } 200 | 201 | /** 202 | * Disconnect from the server. 203 | */ 204 | public async disconnect() { 205 | this.active = false 206 | if (!this.socket) { return } 207 | if (this.socket.readyState !== WS.CLOSED) { 208 | this.socket.close() 209 | await waitForEvent(this, 'close') 210 | } 211 | } 212 | 213 | private retryHandler = () => { 214 | if (this.active) { 215 | this.connect() 216 | } 217 | } 218 | 219 | private closeHandler = () => { 220 | this.emit('close') 221 | this.socket = undefined 222 | if (this.active) { 223 | setTimeout(this.retryHandler, this.backoff(++this.numRetries)) 224 | } 225 | } 226 | 227 | private errorHandler = (error: any) => { 228 | this.emit('error', error) 229 | } 230 | 231 | private openHandler = () => { 232 | this.numRetries = 0 233 | this.emit('open') 234 | this.flushMessageBuffer().catch(this.errorHandler) 235 | } 236 | 237 | private rpcImpl: protobuf.RPCImpl = (method, requestData, callback) => { 238 | const seq = this.nextSeq 239 | this.nextSeq = (this.nextSeq + 1) & 0xffff 240 | 241 | const message: RPC.IMessage = { 242 | request: { 243 | method: method.name, 244 | payload: requestData, 245 | seq, 246 | }, 247 | type: RPC.Message.Type.REQUEST, 248 | } 249 | 250 | let timer: NodeJS.Timer|undefined 251 | if (this.sendTimeout > 0) { 252 | timer = setTimeout(() => { 253 | const error = new VError({name: 'TimeoutError'}, `Timed out after ${ this.sendTimeout }ms`) 254 | this.rpcCallback(seq, error) 255 | }, this.sendTimeout) 256 | } 257 | this.messageBuffer[seq] = {seq, callback, timer} 258 | 259 | if (this.isConnected()) { 260 | this.writeMessage(message).catch((error: Error) => { 261 | this.rpcCallback(seq, error) 262 | }) 263 | } else { 264 | this.messageBuffer[seq].message = message 265 | } 266 | } 267 | 268 | private rpcCallback = (seq: number, error: Error|null, response?: (Uint8Array|null)) => { 269 | if (!this.messageBuffer[seq]) { 270 | this.errorHandler(new VError({cause: error}, `Got response for unknown seqNo: ${ seq }`)) 271 | return 272 | } 273 | const {callback, timer} = this.messageBuffer[seq] 274 | if (timer) { clearTimeout(timer) } 275 | delete this.messageBuffer[seq] 276 | callback(error, response) 277 | } 278 | 279 | private writeMessageNode = async (message: RPC.IMessage) => { 280 | await new Promise((resolve, reject) => { 281 | if (!this.socket) { throw new Error('No socket') } 282 | const data = RPC.Message.encode(message).finish() 283 | this.socket.send(data, (error: Error) => { 284 | if (error) { reject(error) } else { resolve() } 285 | }) 286 | }) 287 | } 288 | 289 | private writeMessageBrowser = async (message: RPC.IMessage) => { 290 | if (!this.socket) { throw new Error('No socket') } 291 | const data = RPC.Message.encode(message).finish() 292 | this.socket.send(data) 293 | } 294 | 295 | private async flushMessageBuffer() { 296 | const messages: IRPCMessage[] = [] 297 | for (const seq in this.messageBuffer) { 298 | if (this.messageBuffer[seq].message) { 299 | messages.push(this.messageBuffer[seq]) 300 | } 301 | } 302 | messages.sort((a, b) => a.seq - b.seq) 303 | while (messages.length > 0) { 304 | const message = messages.shift() as IRPCMessage 305 | try { 306 | await this.writeMessage(message.message as RPC.IMessage) 307 | message.message = undefined 308 | } catch (error) { 309 | this.rpcCallback(message.seq, error) 310 | } 311 | } 312 | } 313 | 314 | private messageHandler = (event: {data: any, type: string, target: WebSocket}) => { 315 | try { 316 | let data = event.data 317 | if (event.data instanceof ArrayBuffer) { 318 | data = new Uint8Array(event.data) 319 | } 320 | const message = RPC.Message.decode(data) 321 | switch (message.type) { 322 | case RPC.Message.Type.RESPONSE: 323 | const response = message.response 324 | if (!response) { throw new Error('Response data missing') } 325 | this.responseHandler(response) 326 | break 327 | case RPC.Message.Type.EVENT: 328 | const eventData = message.event 329 | if (!eventData) { throw new Error('Event data missing') } 330 | this.eventHandler(eventData) 331 | break 332 | } 333 | } catch (cause) { 334 | const error = new VError({cause, name: 'MessageError'}, 'got invalid message') 335 | this.errorHandler(error) 336 | } 337 | } 338 | 339 | private async responseHandler(response: RPC.IResponse) { 340 | if (!response.ok) { 341 | this.rpcCallback(response.seq, new VError({name: 'RPCError'}, response.error || 'Unknown error')) 342 | } else { 343 | this.rpcCallback(response.seq, null, response.payload) 344 | } 345 | } 346 | 347 | private eventHandler(event: RPC.IEvent) { 348 | const type = this.eventTypes[event.name] 349 | let payload: protobuf.Message<{}> | Uint8Array | undefined 350 | if (event.payload && event.payload.length > 0) { 351 | if (type) { 352 | try { 353 | payload = type.decode(event.payload) 354 | } catch (cause) { 355 | const error = new VError({cause, name: 'EventError'}, 'could not decode event payload') 356 | this.errorHandler(error) 357 | return 358 | } 359 | } else { 360 | payload = event.payload 361 | } 362 | } 363 | this.emit('event', event.name, payload) 364 | this.emit(`event ${ event.name }`, payload) 365 | } 366 | 367 | } 368 | 369 | /** 370 | * Default backoff function. 371 | * ```min(tries*10^2, 10 seconds)``` 372 | */ 373 | const defaultBackoff = (tries: number): number => { 374 | return Math.min(Math.pow(tries * 10, 2), 10 * 1000) 375 | } 376 | -------------------------------------------------------------------------------- /docs/interfaces/irpcmessage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IRPCMessage | wsrpc 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Interface IRPCMessage

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Hierarchy

71 |
    72 |
  • 73 | IRPCMessage 74 |
  • 75 |
76 |
77 |
78 |

Index

79 |
80 |
81 |
82 |

Properties

83 | 89 |
90 |
91 |
92 |
93 |
94 |

Properties

95 |
96 | 97 |

callback

98 |
callback: protobuf.RPCImplCallback
99 | 104 |
105 |
106 | 107 |

Optional message

108 |
message: RPC.IMessage
109 | 114 |
115 |
116 | 117 |

seq

118 |
seq: number
119 | 124 |
125 |
126 | 127 |

Optional timer

128 |
timer: NodeJS.Timer
129 | 134 |
135 |
136 |
137 | 213 |
214 |
215 |
216 |
217 |

Legend

218 |
219 |
    220 |
  • Module
  • 221 |
  • Object literal
  • 222 |
  • Variable
  • 223 |
  • Function
  • 224 |
  • Function with type parameter
  • 225 |
  • Index signature
  • 226 |
  • Type alias
  • 227 |
  • Type alias with type parameter
  • 228 |
229 |
    230 |
  • Enumeration
  • 231 |
  • Enumeration member
  • 232 |
  • Property
  • 233 |
  • Method
  • 234 |
235 |
    236 |
  • Interface
  • 237 |
  • Interface with type parameter
  • 238 |
  • Constructor
  • 239 |
  • Property
  • 240 |
  • Method
  • 241 |
  • Index signature
  • 242 |
243 |
    244 |
  • Class
  • 245 |
  • Class with type parameter
  • 246 |
  • Constructor
  • 247 |
  • Property
  • 248 |
  • Method
  • 249 |
  • Accessor
  • 250 |
  • Index signature
  • 251 |
252 |
    253 |
  • Inherited constructor
  • 254 |
  • Inherited property
  • 255 |
  • Inherited method
  • 256 |
  • Inherited accessor
  • 257 |
258 |
    259 |
  • Protected property
  • 260 |
  • Protected method
  • 261 |
  • Protected accessor
  • 262 |
263 |
    264 |
  • Private property
  • 265 |
  • Private method
  • 266 |
  • Private accessor
  • 267 |
268 |
    269 |
  • Static property
  • 270 |
  • Static method
  • 271 |
272 |
273 |
274 |
275 |
276 |

Generated using TypeDoc

277 |
278 |
279 | 280 | 281 | 282 | 283 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wsrpc 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 59 |

wsrpc

60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | 68 |

wsrpc Build Status Coverage Status Package Version License

69 | 70 |

node.js/browser protobuf rpc over binary websockets.

71 | 76 |
77 | 78 |

Minimal example

79 |
80 |

my-service.proto

81 |
service MyService {
 82 |     rpc SayHello (HelloRequest) returns (HelloResponse) {}
 83 | }
 84 | 
 85 | message HelloRequest {
 86 |     required string name = 1;
 87 | }
 88 | 
 89 | message HelloResponse {
 90 |     required string text = 1;
 91 | }
92 |

server.js

93 |
const wsrpc = require('wsrpc')
 94 | const protobuf = require('protobufjs')
 95 | 
 96 | const proto = protobuf.loadSync('my-service.proto')
 97 | 
 98 | const server = new wsrpc.Server(proto.lookupService('MyService'), { port: 4242 })
 99 | 
100 | server.implement('sayHello', async (request) => {
101 |     return {text: `Hello ${ request.name }!`}
102 | })
103 |

client.js

104 |
const wsrpc = require('wsrpc')
105 | const protobuf = require('protobufjs')
106 | 
107 | const proto = protobuf.loadSync('my-service.proto')
108 | 
109 | const client = new wsrpc.Client('ws://localhost:4242', proto.lookupService('MyService'))
110 | 
111 | const response = await client.service.sayHello({name: 'world'})
112 | console.log(response) // Hello world!
113 |
114 |
115 | 173 |
174 |
175 |
176 |
177 |

Legend

178 |
179 |
    180 |
  • Module
  • 181 |
  • Object literal
  • 182 |
  • Variable
  • 183 |
  • Function
  • 184 |
  • Function with type parameter
  • 185 |
  • Index signature
  • 186 |
  • Type alias
  • 187 |
  • Type alias with type parameter
  • 188 |
189 |
    190 |
  • Enumeration
  • 191 |
  • Enumeration member
  • 192 |
  • Property
  • 193 |
  • Method
  • 194 |
195 |
    196 |
  • Interface
  • 197 |
  • Interface with type parameter
  • 198 |
  • Constructor
  • 199 |
  • Property
  • 200 |
  • Method
  • 201 |
  • Index signature
  • 202 |
203 |
    204 |
  • Class
  • 205 |
  • Class with type parameter
  • 206 |
  • Constructor
  • 207 |
  • Property
  • 208 |
  • Method
  • 209 |
  • Accessor
  • 210 |
  • Index signature
  • 211 |
212 |
    213 |
  • Inherited constructor
  • 214 |
  • Inherited property
  • 215 |
  • Inherited method
  • 216 |
  • Inherited accessor
  • 217 |
218 |
    219 |
  • Protected property
  • 220 |
  • Protected method
  • 221 |
  • Protected accessor
  • 222 |
223 |
    224 |
  • Private property
  • 225 |
  • Private method
  • 226 |
  • Private accessor
  • 227 |
228 |
    229 |
  • Static property
  • 230 |
  • Static method
  • 231 |
232 |
233 |
234 |
235 |
236 |

Generated using TypeDoc

237 |
238 |
239 | 240 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /docs/interfaces/iprotobuftype.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IProtobufType | wsrpc 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Interface IProtobufType

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Hierarchy

71 |
    72 |
  • 73 | IProtobufType 74 |
  • 75 |
76 |
77 |
78 |

Index

79 |
80 |
81 |
82 |

Methods

83 | 87 |
88 |
89 |
90 |
91 |
92 |

Methods

93 |
94 | 95 |

decode

96 |
    97 |
  • decode(reader: Reader | Uint8Array, length?: undefined | number): any
  • 98 |
99 |
    100 |
  • 101 | 106 |

    Parameters

    107 |
      108 |
    • 109 |
      reader: Reader | Uint8Array
      110 |
    • 111 |
    • 112 |
      Optional length: undefined | number
      113 |
    • 114 |
    115 |

    Returns any

    116 |
  • 117 |
118 |
119 |
120 | 121 |

encode

122 |
    123 |
  • encode(message: any, writer?: protobuf.Writer): Writer
  • 124 |
125 |
    126 |
  • 127 | 132 |

    Parameters

    133 |
      134 |
    • 135 |
      message: any
      136 |
    • 137 |
    • 138 |
      Optional writer: protobuf.Writer
      139 |
    • 140 |
    141 |

    Returns Writer

    142 |
  • 143 |
144 |
145 |
146 |
147 | 217 |
218 |
219 |
220 |
221 |

Legend

222 |
223 |
    224 |
  • Module
  • 225 |
  • Object literal
  • 226 |
  • Variable
  • 227 |
  • Function
  • 228 |
  • Function with type parameter
  • 229 |
  • Index signature
  • 230 |
  • Type alias
  • 231 |
  • Type alias with type parameter
  • 232 |
233 |
    234 |
  • Enumeration
  • 235 |
  • Enumeration member
  • 236 |
  • Property
  • 237 |
  • Method
  • 238 |
239 |
    240 |
  • Interface
  • 241 |
  • Interface with type parameter
  • 242 |
  • Constructor
  • 243 |
  • Property
  • 244 |
  • Method
  • 245 |
  • Index signature
  • 246 |
247 |
    248 |
  • Class
  • 249 |
  • Class with type parameter
  • 250 |
  • Constructor
  • 251 |
  • Property
  • 252 |
  • Method
  • 253 |
  • Accessor
  • 254 |
  • Index signature
  • 255 |
256 |
    257 |
  • Inherited constructor
  • 258 |
  • Inherited property
  • 259 |
  • Inherited method
  • 260 |
  • Inherited accessor
  • 261 |
262 |
    263 |
  • Protected property
  • 264 |
  • Protected method
  • 265 |
  • Protected accessor
  • 266 |
267 |
    268 |
  • Private property
  • 269 |
  • Private method
  • 270 |
  • Private accessor
  • 271 |
272 |
    273 |
  • Static property
  • 274 |
  • Static method
  • 275 |
276 |
277 |
278 |
279 |
280 |

Generated using TypeDoc

281 |
282 |
283 | 284 | 285 | 286 | 287 | -------------------------------------------------------------------------------- /docs/interfaces/iserverevents.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IServerEvents | wsrpc 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Interface IServerEvents

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Hierarchy

71 |
    72 |
  • 73 | IServerEvents 74 |
  • 75 |
76 |
77 |
78 |

Implemented by

79 | 82 |
83 |
84 |

Index

85 |
86 |
87 |
88 |

Methods

89 |
    90 |
  • on
  • 91 |
92 |
93 |
94 |
95 |
96 |
97 |

Methods

98 |
99 | 100 |

on

101 |
    102 |
  • on(event: "connection", listener: function): void
  • 103 |
  • on(event: "error", listener: function): void
  • 104 |
105 |
    106 |
  • 107 | 112 |

    Parameters

    113 |
      114 |
    • 115 |
      event: "connection"
      116 |
    • 117 |
    • 118 |
      listener: function
      119 |
        120 |
      • 121 | 124 |
          125 |
        • 126 |

          Parameters

          127 | 132 |

          Returns void

          133 |
        • 134 |
        135 |
      • 136 |
      137 |
    • 138 |
    139 |

    Returns void

    140 |
  • 141 |
  • 142 | 147 |

    Parameters

    148 |
      149 |
    • 150 |
      event: "error"
      151 |
    • 152 |
    • 153 |
      listener: function
      154 |
        155 |
      • 156 |
          157 |
        • (error: Error): void
        • 158 |
        159 |
          160 |
        • 161 |

          Parameters

          162 |
            163 |
          • 164 |
            error: Error
            165 |
          • 166 |
          167 |

          Returns void

          168 |
        • 169 |
        170 |
      • 171 |
      172 |
    • 173 |
    174 |

    Returns void

    175 |
  • 176 |
177 |
178 |
179 |
180 | 247 |
248 |
249 |
250 |
251 |

Legend

252 |
253 |
    254 |
  • Module
  • 255 |
  • Object literal
  • 256 |
  • Variable
  • 257 |
  • Function
  • 258 |
  • Function with type parameter
  • 259 |
  • Index signature
  • 260 |
  • Type alias
  • 261 |
  • Type alias with type parameter
  • 262 |
263 |
    264 |
  • Enumeration
  • 265 |
  • Enumeration member
  • 266 |
  • Property
  • 267 |
  • Method
  • 268 |
269 |
    270 |
  • Interface
  • 271 |
  • Interface with type parameter
  • 272 |
  • Constructor
  • 273 |
  • Property
  • 274 |
  • Method
  • 275 |
  • Index signature
  • 276 |
277 |
    278 |
  • Class
  • 279 |
  • Class with type parameter
  • 280 |
  • Constructor
  • 281 |
  • Property
  • 282 |
  • Method
  • 283 |
  • Accessor
  • 284 |
  • Index signature
  • 285 |
286 |
    287 |
  • Inherited constructor
  • 288 |
  • Inherited property
  • 289 |
  • Inherited method
  • 290 |
  • Inherited accessor
  • 291 |
292 |
    293 |
  • Protected property
  • 294 |
  • Protected method
  • 295 |
  • Protected accessor
  • 296 |
297 |
    298 |
  • Private property
  • 299 |
  • Private method
  • 300 |
  • Private accessor
  • 301 |
302 |
    303 |
  • Static property
  • 304 |
  • Static method
  • 305 |
306 |
307 |
308 |
309 |
310 |

Generated using TypeDoc

311 |
312 |
313 | 314 | 315 | 316 | 317 | -------------------------------------------------------------------------------- /examples/painter/client/contents/paint.ts: -------------------------------------------------------------------------------- 1 | import {Client} from 'wsrpc' 2 | import {Painter, PaintEvent, StatusEvent, IPaintEvent, CanvasRequest} from './../../protocol/service' 3 | import * as shared from './../../shared/paint' 4 | 5 | interface DrawPosition { 6 | x: number 7 | y: number 8 | timestamp: number 9 | } 10 | 11 | interface DrawEvent { 12 | pos: DrawPosition 13 | lastPos?: DrawPosition 14 | lastV?: number 15 | force?: number 16 | color: number 17 | } 18 | 19 | const palettes = [ 20 | [0x413E4A, 0x73626E, 0xB38184, 0xF0B49E, 0xF7E4BE, 0xFFFFFF], 21 | [0x00A8C6, 0x40C0CB, 0xF9F2E7, 0xFFFFFF, 0xAEE239, 0x8FBE00], 22 | [0x467588, 0xFFFFFF, 0xFCE5BC, 0xFDCD92, 0xFCAC96, 0xDD8193], 23 | [0xFFFFFF, 0xF8B195, 0xF67280, 0xC06C84, 0x6C5B7B, 0x355C7D], 24 | [0xFFFFFF, 0xCFF09E, 0xA8DBA8, 0x79BD9A, 0x3B8686, 0x0B486B], 25 | [0xFFFFFF, 0xEEE6AB, 0xC5BC8E, 0x696758, 0x45484B, 0x36393B], 26 | [0xFFFFFF, 0xFFED90, 0xA8D46F, 0x359668, 0x3C3251, 0x341139], 27 | [0x351330, 0x424254, 0x64908A, 0xE8CAA4, 0xFFFFFF, 0xCC2A41], 28 | [0xD9CEB2, 0x948C75, 0xD5DED9, 0x7A6A53, 0x99B2B7, 0xFFFFFF], 29 | [0x00A0B0, 0xFFFFFF, 0x6A4A3C, 0xCC333F, 0xEB6841, 0xEDC951], 30 | [0xFF4E50, 0xFC913A, 0xF9D423, 0xEDE574, 0xE1F5C4, 0xFFFFFF], 31 | [0xE94E77, 0xD68189, 0xC6A49A, 0xC6E5D9, 0xF4EAD5, 0xFFFFFF], 32 | [0xFFFFFF, 0xE8DDCB, 0xCDB380, 0x036564, 0x033649, 0x031634], 33 | [0x69D2E7, 0xA7DBD8, 0xE0E4CC, 0xFFFFFF, 0xF38630, 0xFA6900], 34 | [0x490A3D, 0xBD1550, 0xE97F02, 0xF8CA00, 0xFFFFFF, 0x8A9B0F], 35 | [0x8C2318, 0x5E8C6A, 0x88A65E, 0xBFB35A, 0xF2C45A, 0xFFFFFF], 36 | [0xFFFFFF, 0xEFFFCD, 0xDCE9BE, 0x555152, 0x2E2633, 0x99173C], 37 | ] 38 | 39 | function toHex(d: number): string { 40 | let hex = Number(d).toString(16) 41 | hex = '000000'.substr(0, 6 - hex.length) + hex 42 | return '#'+hex 43 | } 44 | 45 | function store(key: string, value: any) { 46 | try { 47 | const data = JSON.stringify(value) 48 | window.localStorage.setItem(key, data) 49 | } catch (error) { 50 | console.warn(`unable to store ${ key }`, error) 51 | } 52 | } 53 | 54 | function retrieve(key: string): any { 55 | try { 56 | const string = window.localStorage.getItem(key) 57 | return JSON.parse(string) 58 | } catch (error) { 59 | console.warn(`unable to retrieve ${ key }`, error) 60 | } 61 | } 62 | 63 | const now = window.performance ? () => window.performance.now() : () => Date.now() 64 | const day = () => Math.floor(Date.now() / (1000 * 60 * 60 * 24 * 2)) 65 | 66 | const client = new Client('ws://localhost:4242', Painter as any, { 67 | sendTimeout: 5 * 60 * 1000, 68 | eventTypes: { 69 | paint: PaintEvent, 70 | status: StatusEvent, 71 | } 72 | }) 73 | 74 | const service = client.service as Painter 75 | 76 | client.on('open', () => { 77 | document.documentElement.classList.add('connected') 78 | }) 79 | 80 | client.on('close', () => { 81 | document.documentElement.classList.remove('connected') 82 | }) 83 | 84 | window.addEventListener('DOMContentLoaded', async () => { 85 | const status = document.createElement('div') 86 | status.className = 'status' 87 | status.innerHTML = 'Connecting...' 88 | document.body.appendChild(status) 89 | 90 | client.on('event status', (event: StatusEvent) => { 91 | status.innerHTML = `Users: ${ event.users }` 92 | }) 93 | 94 | client.on('close', () => { 95 | status.innerHTML = 'Disconnected' 96 | }) 97 | 98 | client.on('error', (error) => { 99 | console.warn('client error', error) 100 | }) 101 | 102 | const colors = palettes[day() % palettes.length] 103 | 104 | let activeColor: number = colors[0] 105 | 106 | const colorWells: HTMLSpanElement[] = [] 107 | const colorPicker = document.createElement('div') 108 | colorPicker.className = 'picker' 109 | for (const color of colors) { 110 | const well = document.createElement('span') 111 | const cssColor = toHex(color) 112 | well.style.backgroundColor = cssColor 113 | well.style.outlineColor = cssColor 114 | well.addEventListener('click', (event) => { 115 | event.preventDefault() 116 | colorWells.forEach((el) => el.classList.remove('active')) 117 | well.classList.add('active') 118 | activeColor = color 119 | }) 120 | colorWells.push(well) 121 | colorPicker.appendChild(well) 122 | } 123 | document.body.appendChild(colorPicker) 124 | 125 | colorWells[0].classList.add('active') 126 | 127 | const canvas = document.querySelector('canvas') 128 | const ctx = canvas.getContext('2d') 129 | 130 | const pixelRatio = window.devicePixelRatio || 1 131 | 132 | canvas.width = window.innerWidth * pixelRatio 133 | canvas.height = window.innerHeight * pixelRatio 134 | 135 | let offset: {x: number, y: number}// = retrieve('offset') 136 | if (!offset) { 137 | offset = { 138 | x: Math.max(0, (shared.canvasWidth / 2) - (canvas.width / 2)), 139 | y: Math.max(0, (shared.canvasHeight / 2) - (canvas.height / 2)), 140 | } 141 | } 142 | 143 | function offsetPaint(event: IPaintEvent) { 144 | const {pos, color, size} = event 145 | shared.paint({ 146 | pos: { 147 | x: pos.x - offset.x, 148 | y: pos.y - offset.y, 149 | }, color, size 150 | }, ctx) 151 | } 152 | client.on('event paint', offsetPaint) 153 | 154 | 155 | const panHandle = document.createElement('div') 156 | panHandle.className = 'pan-handle' 157 | panHandle.innerHTML = '☩' 158 | document.body.appendChild(panHandle) 159 | 160 | const panInfo = document.createElement('div') 161 | panInfo.className = 'info pan' 162 | panInfo.innerHTML = 'Drag to move' 163 | document.body.appendChild(panInfo) 164 | 165 | let isPanning = false 166 | const enterPan = () => { 167 | isPanning = true 168 | document.documentElement.classList.add('pan') 169 | window.addEventListener('mousedown', startMousePan) 170 | } 171 | 172 | const exitPan = () => { 173 | isPanning = false 174 | document.documentElement.classList.remove('pan') 175 | window.removeEventListener('mousedown', startMousePan) 176 | } 177 | 178 | let panStartPos: {x: number, y: number} 179 | let panStartOffset: {x: number, y: number} 180 | let panData: ImageData 181 | 182 | const panStart = (pos: {x: number, y: number}) => { 183 | document.documentElement.classList.add('pan-move') 184 | panStartPos = {x: pos.x, y: pos.y} 185 | panStartOffset = {x: offset.x, y: offset.y} 186 | panData = ctx.getImageData(0, 0, canvas.width, canvas.height) 187 | } 188 | 189 | const panMove = (pos: {x: number, y: number}) => { 190 | const dx = pos.x - panStartPos.x 191 | const dy = pos.y - panStartPos.y 192 | 193 | offset.x = panStartOffset.x - dx 194 | offset.y = panStartOffset.y - dy 195 | 196 | if (offset.x < 0 || offset.y < 0 || 197 | offset.x > shared.canvasWidth - canvas.width || 198 | offset.y > shared.canvasHeight - canvas.height) 199 | { 200 | ctx.fillStyle = '#ffd4c6' 201 | } else { 202 | ctx.fillStyle = '#e9e9e9' 203 | } 204 | 205 | ctx.fillRect(0, 0, canvas.width, canvas.height) 206 | ctx.putImageData(panData, dx, dy) 207 | } 208 | 209 | const panEnd = () => { 210 | offset.x = Math.max(0, Math.min(offset.x, shared.canvasWidth - canvas.width)) 211 | offset.y = Math.max(0, Math.min(offset.y, shared.canvasHeight - canvas.height)) 212 | if (offset.x !== panStartOffset.x || offset.y !== panStartOffset.y) { 213 | fetchCanvas() 214 | } else { 215 | ctx.putImageData(panData, 0, 0) 216 | } 217 | store('offset', offset) 218 | document.documentElement.classList.remove('pan-move') 219 | panData = null 220 | } 221 | 222 | const startMousePan = (event: MouseEvent) => { 223 | event.preventDefault() 224 | panStart(event) 225 | const moveHandler = (event: MouseEvent) => { 226 | // event.preventDefault() 227 | panMove(event) 228 | } 229 | const endHandler = (event: MouseEvent) => { 230 | event.preventDefault() 231 | window.removeEventListener('mousemove', moveHandler) 232 | window.removeEventListener('mouseup', endHandler) 233 | window.removeEventListener('mouseleave', endHandler) 234 | panEnd() 235 | } 236 | window.addEventListener('mousemove', moveHandler) 237 | window.addEventListener('mouseup', endHandler) 238 | window.addEventListener('mouseleave', endHandler) 239 | } 240 | 241 | window.addEventListener('keydown', (event) => { 242 | if (event.keyCode === 32) { 243 | event.preventDefault() 244 | if (!isPanning) { enterPan() } 245 | } 246 | }) 247 | 248 | window.addEventListener('keyup', (event) => { 249 | if (event.keyCode === 32) { 250 | event.preventDefault() 251 | exitPan() 252 | } 253 | }) 254 | 255 | panHandle.addEventListener('touchstart', (event) => { 256 | event.preventDefault() 257 | enterPan() 258 | }) 259 | 260 | panHandle.addEventListener('touchcancel', (event) => { 261 | exitPan() 262 | }) 263 | 264 | panHandle.addEventListener('touchend', (event) => { 265 | exitPan() 266 | }) 267 | 268 | panHandle.addEventListener('mousedown', (event) => { 269 | event.preventDefault() 270 | startMousePan(event) 271 | }) 272 | 273 | const loadingEl = document.createElement('div') 274 | loadingEl.className = 'info loading' 275 | loadingEl.innerHTML = 'Loading canvas...' 276 | document.body.appendChild(loadingEl) 277 | 278 | async function fetchCanvas() { 279 | document.documentElement.classList.add('loading') 280 | 281 | offset.x = Math.max(0, Math.min(offset.x, shared.canvasWidth - canvas.width)) 282 | offset.y = Math.max(0, Math.min(offset.y, shared.canvasHeight - canvas.height)) 283 | store('offset', offset) 284 | 285 | let encoding = CanvasRequest.Encoding.JPEG 286 | const request = { 287 | offset, encoding, 288 | width: Math.min(window.innerWidth * pixelRatio, shared.canvasWidth - offset.x), 289 | height: Math.min(window.innerHeight * pixelRatio, shared.canvasHeight - offset.y), 290 | } 291 | console.log('loading canvas...', request) 292 | const response = await service.getCanvas(request) 293 | 294 | console.log(`response size: ${ ~~(response.image.length / 1024) }kb`) 295 | 296 | const arr = response.image 297 | let buffer = Buffer.from(arr.buffer) 298 | buffer = buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength) 299 | 300 | await new Promise((resolve, reject) => { 301 | const img = new Image() 302 | let type: string 303 | switch (encoding) { 304 | case CanvasRequest.Encoding.JPEG: 305 | type = 'image/jpeg' 306 | break 307 | case CanvasRequest.Encoding.WEBP: 308 | type = 'image/webp' 309 | break 310 | case CanvasRequest.Encoding.PNG: 311 | type = 'image/png' 312 | break 313 | default: 314 | throw new Error('Invalid encoding') 315 | } 316 | img.src = `data:${ type };base64,` + buffer.toString('base64') 317 | img.onload = () => { 318 | ctx.globalAlpha = 1.0 319 | ctx.fillStyle = 'white' 320 | ctx.fillRect(0, 0, canvas.width, canvas.height) 321 | ctx.drawImage(img, 0, 0) 322 | resolve() 323 | } 324 | img.onerror = (error) => { 325 | reject(error) 326 | } 327 | }) 328 | 329 | document.documentElement.classList.remove('loading') 330 | } 331 | 332 | let debounceTimer 333 | window.addEventListener('resize', () => { 334 | if (window.innerWidth * pixelRatio <= canvas.width && 335 | window.innerHeight * pixelRatio <= canvas.height) { 336 | const data = ctx.getImageData(0, 0, canvas.width, canvas.height) 337 | canvas.width = window.innerWidth * pixelRatio 338 | canvas.height = window.innerHeight * pixelRatio 339 | ctx.putImageData(data, 0, 0) 340 | } else { 341 | canvas.width = window.innerWidth * pixelRatio 342 | canvas.height = window.innerHeight * pixelRatio 343 | clearTimeout(debounceTimer) 344 | debounceTimer = setTimeout(fetchCanvas, 500) 345 | document.documentElement.classList.add('loading') 346 | } 347 | }) 348 | 349 | await fetchCanvas() 350 | 351 | const vF = 0.5 352 | const vMax = 8 353 | async function drawAsync(event: DrawEvent) { 354 | let msgs: IPaintEvent[] = [] 355 | let size = 20// * pixelRatio 356 | const color = event.color 357 | if (event.force) { 358 | size = Math.min(size + event.force * shared.brushSize, shared.brushSize) 359 | } 360 | if (event.lastPos) { 361 | const dx = event.lastPos.x - event.pos.x 362 | const dy = event.lastPos.y - event.pos.y 363 | const d = Math.sqrt(dx*dx + dy*dy) 364 | if (!event.force && event.pos.timestamp !== event.lastPos.timestamp) { 365 | const dt = event.pos.timestamp - event.lastPos.timestamp 366 | let v = Math.min(d / dt, vMax) 367 | if (event.lastV) { 368 | v = event.lastV * (1 - vF) + v * vF 369 | } 370 | if (v < 0) { v = 0 } 371 | event.lastV = v 372 | size = Math.min(size + size * v, shared.brushSize) 373 | } 374 | const interpSteps = ~~(d / (size / 4)) 375 | for (let i = 0; i < interpSteps; i++) { 376 | const p = (i + 1) / (interpSteps + 1) 377 | const x = event.lastPos.x * p + event.pos.x * (1 - p) 378 | const y = event.lastPos.y * p + event.pos.y * (1 - p) 379 | msgs.push({pos: {x: x + offset.x, y: y + offset.y}, color, size}) 380 | } 381 | } 382 | msgs.push({ 383 | pos: { 384 | x: event.pos.x + offset.x, 385 | y: event.pos.y + offset.y, 386 | }, 387 | color, size 388 | }) 389 | let drawCalls = [] 390 | for (const msg of msgs) { 391 | offsetPaint(msg) 392 | drawCalls.push(service.paint(msg)) 393 | } 394 | await Promise.all(drawCalls) 395 | } 396 | 397 | function draw(event: DrawEvent) { 398 | drawAsync(event).catch((error) => { 399 | console.warn('error drawing', error) 400 | }) 401 | } 402 | 403 | let mouseDraw: DrawEvent|undefined 404 | 405 | canvas.addEventListener('mousedown', (event) => { 406 | event.preventDefault() 407 | if (isPanning) { return } 408 | mouseDraw = { 409 | pos: { 410 | x: event.x * pixelRatio, 411 | y: event.y * pixelRatio, 412 | timestamp: event.timeStamp || now(), 413 | }, 414 | color: activeColor, 415 | } 416 | draw(mouseDraw) 417 | }) 418 | 419 | canvas.addEventListener('mousemove', (event) => { 420 | event.preventDefault() 421 | if (isPanning) { return } 422 | if (mouseDraw) { 423 | mouseDraw.lastPos = mouseDraw.pos 424 | mouseDraw.pos = { 425 | x: event.x * pixelRatio, 426 | y: event.y * pixelRatio, 427 | timestamp: event.timeStamp || now(), 428 | } 429 | draw(mouseDraw) 430 | } 431 | }) 432 | 433 | const mouseup = (event) => { 434 | mouseDraw = undefined 435 | } 436 | canvas.addEventListener('mouseup', mouseup) 437 | canvas.addEventListener('mouseleave', mouseup) 438 | 439 | let fingerDraw: {[id: number]: DrawEvent} = {} 440 | let panTouch: number|undefined 441 | 442 | canvas.addEventListener('touchstart', (event) => { 443 | event.preventDefault() 444 | if (isPanning) { 445 | if (!panTouch) { 446 | for (var i = 0; i < event.touches.length; i++) { 447 | const touch = event.touches[i] 448 | if (touch.target === panHandle) { continue } 449 | panTouch = touch.identifier 450 | console.log('tracking', panTouch) 451 | panStart({x: touch.screenX, y: touch.screenY}) 452 | break 453 | } 454 | } 455 | return 456 | } 457 | for (var i = 0; i < event.touches.length; i++) { 458 | const touch = event.touches[i] 459 | fingerDraw[touch.identifier] = { 460 | pos: { 461 | x: touch.screenX * pixelRatio, 462 | y: touch.screenY * pixelRatio, 463 | timestamp: event.timeStamp || now(), 464 | }, 465 | force: touch['force'], 466 | color: activeColor 467 | } 468 | draw(fingerDraw[touch.identifier]) 469 | } 470 | }) 471 | 472 | canvas.addEventListener('touchmove', (event) => { 473 | event.preventDefault() 474 | if (isPanning) { 475 | if (panTouch) { 476 | for (var i = 0; i < event.touches.length; i++) { 477 | const touch = event.touches[i] 478 | if (touch.identifier == panTouch) { 479 | panMove({x: touch.screenX, y: touch.screenY}) 480 | break 481 | } 482 | } 483 | } 484 | return 485 | } 486 | for (var i = 0; i < event.touches.length; i++) { 487 | const touch = event.touches[i] 488 | const drawEvent = fingerDraw[touch.identifier] 489 | if (drawEvent) { 490 | drawEvent.lastPos = drawEvent.pos 491 | drawEvent.pos = { 492 | x: touch.screenX * pixelRatio, 493 | y: touch.screenY * pixelRatio, 494 | timestamp: event.timeStamp || now(), 495 | } 496 | drawEvent.force = touch['force'] 497 | draw(drawEvent) 498 | } 499 | } 500 | }) 501 | 502 | const touchend = (event: TouchEvent) => { 503 | event.preventDefault() 504 | if (isPanning) { 505 | if (panTouch) { 506 | panEnd() 507 | panTouch = undefined 508 | } 509 | return 510 | } 511 | for (var i = 0; i < event.touches.length; i++) { 512 | const touch = event.touches[i] 513 | delete fingerDraw[touch.identifier] 514 | } 515 | } 516 | canvas.addEventListener('touchend', touchend) 517 | canvas.addEventListener('touchcancel', touchend) 518 | }) 519 | 520 | console.log(' ;-) ') 521 | window['client'] = client 522 | -------------------------------------------------------------------------------- /docs/globals.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wsrpc 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 59 |

wsrpc

60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |

Index

68 |
69 |
70 |
71 |

Classes

72 | 77 |
78 |
79 |

Interfaces

80 | 88 |
89 |
90 |

Type aliases

91 | 95 |
96 |
97 |

Variables

98 | 102 |
103 |
104 |

Functions

105 | 109 |
110 |
111 |
112 |
113 |
114 |

Type aliases

115 |
116 | 117 |

Handler

118 |
Handler: function
119 | 124 |
125 |

Type declaration

126 |
    127 |
  • 128 | 131 |
      132 |
    • 133 |

      Parameters

      134 | 142 |

      Returns Promise<Message>

      143 |
    • 144 |
    145 |
  • 146 |
147 |
148 |
149 |
150 | 151 |

Message

152 |
Message: Message<__type> | object
153 | 158 |
159 |
160 |
161 |

Variables

162 |
163 | 164 |

Let WS

165 |
WS: WebSocket = WebSocket
166 | 171 |
172 |
173 | 174 |

Const version

175 |
version: string = require('../package').version
176 | 181 |
182 |
183 |
184 |

Functions

185 |
186 | 187 |

Const defaultBackoff

188 |
    189 |
  • defaultBackoff(tries: number): number
  • 190 |
191 |
    192 |
  • 193 | 198 |
    199 |
    200 |

    Default backoff function. 201 | min(tries*10^2, 10 seconds)

    202 |
    203 |
    204 |

    Parameters

    205 |
      206 |
    • 207 |
      tries: number
      208 |
    • 209 |
    210 |

    Returns number

    211 |
  • 212 |
213 |
214 |
215 | 216 |

waitForEvent

217 |
    218 |
  • waitForEvent<T>(emitter: EventEmitter, eventName: string | symbol): Promise<T>
  • 219 |
220 |
    221 |
  • 222 | 227 |
    228 |
    229 |

    Return a promise that will resove when a specific event is emitted.

    230 |
    231 |
    232 |

    Type parameters

    233 |
      234 |
    • 235 |

      T

      236 |
    • 237 |
    238 |

    Parameters

    239 |
      240 |
    • 241 |
      emitter: EventEmitter
      242 |
    • 243 |
    • 244 |
      eventName: string | symbol
      245 |
    • 246 |
    247 |

    Returns Promise<T>

    248 |
  • 249 |
250 |
251 |
252 |
253 | 311 |
312 |
313 |
314 |
315 |

Legend

316 |
317 |
    318 |
  • Module
  • 319 |
  • Object literal
  • 320 |
  • Variable
  • 321 |
  • Function
  • 322 |
  • Function with type parameter
  • 323 |
  • Index signature
  • 324 |
  • Type alias
  • 325 |
  • Type alias with type parameter
  • 326 |
327 |
    328 |
  • Enumeration
  • 329 |
  • Enumeration member
  • 330 |
  • Property
  • 331 |
  • Method
  • 332 |
333 |
    334 |
  • Interface
  • 335 |
  • Interface with type parameter
  • 336 |
  • Constructor
  • 337 |
  • Property
  • 338 |
  • Method
  • 339 |
  • Index signature
  • 340 |
341 |
    342 |
  • Class
  • 343 |
  • Class with type parameter
  • 344 |
  • Constructor
  • 345 |
  • Property
  • 346 |
  • Method
  • 347 |
  • Accessor
  • 348 |
  • Index signature
  • 349 |
350 |
    351 |
  • Inherited constructor
  • 352 |
  • Inherited property
  • 353 |
  • Inherited method
  • 354 |
  • Inherited accessor
  • 355 |
356 |
    357 |
  • Protected property
  • 358 |
  • Protected method
  • 359 |
  • Protected accessor
  • 360 |
361 |
    362 |
  • Private property
  • 363 |
  • Private method
  • 364 |
  • Private accessor
  • 365 |
366 |
    367 |
  • Static property
  • 368 |
  • Static method
  • 369 |
370 |
371 |
372 |
373 |
374 |

Generated using TypeDoc

375 |
376 |
377 | 378 | 379 | 380 | 381 | -------------------------------------------------------------------------------- /docs/interfaces/iclientevents.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IClientEvents | wsrpc 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Interface IClientEvents

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | 76 |
77 |
78 |
79 |

Hierarchy

80 |
    81 |
  • 82 | IClientEvents 83 |
  • 84 |
85 |
86 |
87 |

Implemented by

88 | 91 |
92 |
93 |

Index

94 |
95 |
96 |
97 |

Methods

98 |
    99 |
  • on
  • 100 |
101 |
102 |
103 |
104 |
105 |
106 |

Methods

107 |
108 | 109 |

on

110 |
    111 |
  • on(event: "open" | "close", listener: function): this
  • 112 |
  • on(event: "error", listener: function): this
  • 113 |
  • on(event: "event", listener: function): this
  • 114 |
  • on(event: "event <name>", listener: function): this
  • 115 |
116 |
    117 |
  • 118 | 123 |
    124 |
    125 |

    Emitted when the connection closes/opens.

    126 |
    127 |
    128 |

    Parameters

    129 |
      130 |
    • 131 |
      event: "open" | "close"
      132 |
    • 133 |
    • 134 |
      listener: function
      135 |
        136 |
      • 137 |
          138 |
        • (): void
        • 139 |
        140 |
          141 |
        • 142 |

          Returns void

          143 |
        • 144 |
        145 |
      • 146 |
      147 |
    • 148 |
    149 |

    Returns this

    150 |
  • 151 |
  • 152 | 157 |
    158 |
    159 |

    Emitted on error, throws if there is no listener.

    160 |
    161 |
    162 |

    Parameters

    163 |
      164 |
    • 165 |
      event: "error"
      166 |
    • 167 |
    • 168 |
      listener: function
      169 |
        170 |
      • 171 |
          172 |
        • (error: Error): void
        • 173 |
        174 |
          175 |
        • 176 |

          Parameters

          177 |
            178 |
          • 179 |
            error: Error
            180 |
          • 181 |
          182 |

          Returns void

          183 |
        • 184 |
        185 |
      • 186 |
      187 |
    • 188 |
    189 |

    Returns this

    190 |
  • 191 |
  • 192 | 197 |
    198 |
    199 |

    RPC event sent by the server. If the event name is given a type 200 | constructor in IClientOptions.eventTypes the data will 201 | be decoded before the event is emitted.

    202 |
    203 |
    204 |

    Parameters

    205 |
      206 |
    • 207 |
      event: "event"
      208 |
    • 209 |
    • 210 |
      listener: function
      211 |
        212 |
      • 213 |
          214 |
        • (name: string, data?: Uint8Array | object): void
        • 215 |
        216 |
          217 |
        • 218 |

          Parameters

          219 |
            220 |
          • 221 |
            name: string
            222 |
          • 223 |
          • 224 |
            Optional data: Uint8Array | object
            225 |
          • 226 |
          227 |

          Returns void

          228 |
        • 229 |
        230 |
      • 231 |
      232 |
    • 233 |
    234 |

    Returns this

    235 |
  • 236 |
  • 237 | 242 |

    Parameters

    243 |
      244 |
    • 245 |
      event: "event <name>"
      246 |
    • 247 |
    • 248 |
      listener: function
      249 |
        250 |
      • 251 |
          252 |
        • (data?: Uint8Array | object): void
        • 253 |
        254 |
          255 |
        • 256 |

          Parameters

          257 |
            258 |
          • 259 |
            Optional data: Uint8Array | object
            260 |
          • 261 |
          262 |

          Returns void

          263 |
        • 264 |
        265 |
      • 266 |
      267 |
    • 268 |
    269 |

    Returns this

    270 |
  • 271 |
272 |
273 |
274 |
275 | 342 |
343 |
344 |
345 |
346 |

Legend

347 |
348 |
    349 |
  • Module
  • 350 |
  • Object literal
  • 351 |
  • Variable
  • 352 |
  • Function
  • 353 |
  • Function with type parameter
  • 354 |
  • Index signature
  • 355 |
  • Type alias
  • 356 |
  • Type alias with type parameter
  • 357 |
358 |
    359 |
  • Enumeration
  • 360 |
  • Enumeration member
  • 361 |
  • Property
  • 362 |
  • Method
  • 363 |
364 |
    365 |
  • Interface
  • 366 |
  • Interface with type parameter
  • 367 |
  • Constructor
  • 368 |
  • Property
  • 369 |
  • Method
  • 370 |
  • Index signature
  • 371 |
372 |
    373 |
  • Class
  • 374 |
  • Class with type parameter
  • 375 |
  • Constructor
  • 376 |
  • Property
  • 377 |
  • Method
  • 378 |
  • Accessor
  • 379 |
  • Index signature
  • 380 |
381 |
    382 |
  • Inherited constructor
  • 383 |
  • Inherited property
  • 384 |
  • Inherited method
  • 385 |
  • Inherited accessor
  • 386 |
387 |
    388 |
  • Protected property
  • 389 |
  • Protected method
  • 390 |
  • Protected accessor
  • 391 |
392 |
    393 |
  • Private property
  • 394 |
  • Private method
  • 395 |
  • Private accessor
  • 396 |
397 |
    398 |
  • Static property
  • 399 |
  • Static method
  • 400 |
401 |
402 |
403 |
404 |
405 |

Generated using TypeDoc

406 |
407 |
408 | 409 | 410 | 411 | 412 | -------------------------------------------------------------------------------- /docs/interfaces/iserveroptions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IServerOptions | wsrpc 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Interface IServerOptions

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | 73 |

RPC Server options

74 |
75 |

Server options, extends the WebSocket server options. 76 | Note that WebSocket.IServerOptions.perMessageDeflate defaults 77 | to false if omitted.

78 |
79 |
80 |
81 |
82 |

Hierarchy

83 |
    84 |
  • 85 | ServerOptions 86 |
      87 |
    • 88 | IServerOptions 89 |
    • 90 |
    91 |
  • 92 |
93 |
94 |
95 |

Index

96 |
97 |
98 |
99 |

Properties

100 | 114 |
115 |
116 |
117 |
118 |
119 |

Properties

120 |
121 | 122 |

Optional backlog

123 |
backlog: undefined | number
124 | 130 |
131 |
132 | 133 |

Optional clientTracking

134 |
clientTracking: undefined | false | true
135 | 141 |
142 |
143 | 144 |

Optional handleProtocols

145 |
handleProtocols: any
146 | 152 |
153 |
154 | 155 |

Optional host

156 |
host: undefined | string
157 | 163 |
164 |
165 | 166 |

Optional maxPayload

167 |
maxPayload: undefined | number
168 | 174 |
175 |
176 | 177 |

Optional noServer

178 |
noServer: undefined | false | true
179 | 185 |
186 |
187 | 188 |

Optional path

189 |
path: undefined | string
190 | 196 |
197 |
198 | 199 |

Optional perMessageDeflate

200 |
perMessageDeflate: boolean | PerMessageDeflateOptions
201 | 207 |
208 |
209 | 210 |

Optional pingInterval

211 |
pingInterval: undefined | number
212 | 217 |
218 |
219 |

How often to send a ping frame, in seconds. Set to 0 to disable. Default = 10.

220 |
221 |
222 |
223 |
224 | 225 |

Optional port

226 |
port: undefined | number
227 | 233 |
234 |
235 | 236 |

Optional server

237 |
server: Server | Server
238 | 244 |
245 |
246 | 247 |

Optional verifyClient

248 |
verifyClient: VerifyClientCallbackAsync | VerifyClientCallbackSync
249 | 255 |
256 |
257 |
258 | 358 |
359 |
360 |
361 |
362 |

Legend

363 |
364 |
    365 |
  • Module
  • 366 |
  • Object literal
  • 367 |
  • Variable
  • 368 |
  • Function
  • 369 |
  • Function with type parameter
  • 370 |
  • Index signature
  • 371 |
  • Type alias
  • 372 |
  • Type alias with type parameter
  • 373 |
374 |
    375 |
  • Enumeration
  • 376 |
  • Enumeration member
  • 377 |
  • Property
  • 378 |
  • Method
  • 379 |
380 |
    381 |
  • Interface
  • 382 |
  • Interface with type parameter
  • 383 |
  • Constructor
  • 384 |
  • Property
  • 385 |
  • Method
  • 386 |
  • Index signature
  • 387 |
388 |
    389 |
  • Class
  • 390 |
  • Class with type parameter
  • 391 |
  • Constructor
  • 392 |
  • Property
  • 393 |
  • Method
  • 394 |
  • Accessor
  • 395 |
  • Index signature
  • 396 |
397 |
    398 |
  • Inherited constructor
  • 399 |
  • Inherited property
  • 400 |
  • Inherited method
  • 401 |
  • Inherited accessor
  • 402 |
403 |
    404 |
  • Protected property
  • 405 |
  • Protected method
  • 406 |
  • Protected accessor
  • 407 |
408 |
    409 |
  • Private property
  • 410 |
  • Private method
  • 411 |
  • Private accessor
  • 412 |
413 |
    414 |
  • Static property
  • 415 |
  • Static method
  • 416 |
417 |
418 |
419 |
420 |
421 |

Generated using TypeDoc

422 |
423 |
424 | 425 | 426 | 427 | 428 | --------------------------------------------------------------------------------