├── python ├── duplex │ ├── version.py │ ├── codecs │ │ ├── json.py │ │ ├── msgpack.py │ │ └── __init__.py │ ├── __init__.py │ ├── ws4py.py │ ├── protocol.py │ ├── test_sync.py │ ├── async.py │ ├── sync.py │ └── test_async.py ├── setup.cfg ├── TODO ├── README.md ├── Makefile ├── shell_async.py ├── demo │ └── demo.py └── setup.py ├── javascript ├── TODO ├── demo │ ├── package.json │ ├── index.html │ └── server.js ├── README.md ├── Makefile ├── tsconfig.json ├── package.json ├── package-lock.json ├── test │ ├── duplex_spec.js │ └── duplex_spec.ts ├── dist │ └── duplex.js └── src │ └── duplex.ts ├── Makefile ├── demo ├── Makefile └── index.html ├── golang ├── Makefile ├── demo │ └── demo.go ├── duplex.go └── duplex_test.go ├── docs ├── index.md └── getting-started │ └── python.md ├── .gitignore ├── LICENSE └── README.md /python/duplex/version.py: -------------------------------------------------------------------------------- 1 | version = "0.1.4" 2 | -------------------------------------------------------------------------------- /javascript/TODO: -------------------------------------------------------------------------------- 1 | * properly handle/bubble errors 2 | -------------------------------------------------------------------------------- /python/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /python/duplex/codecs/json.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | codec = ['json', json.dumps, json.loads] 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | make -C golang test 4 | make -C javascript test 5 | make -C python test 6 | -------------------------------------------------------------------------------- /python/TODO: -------------------------------------------------------------------------------- 1 | * decorator for rpc.register 2 | * More tests? Wrappers? 3 | * properly handle/bubble errors 4 | -------------------------------------------------------------------------------- /python/duplex/codecs/msgpack.py: -------------------------------------------------------------------------------- 1 | import msgpack 2 | 3 | codec = ['msgpack', msgpack.packb, msgpack.unpackb] 4 | -------------------------------------------------------------------------------- /demo/Makefile: -------------------------------------------------------------------------------- 1 | 2 | demo: 3 | make -C ../javascript build 4 | cp ../javascript/dist/duplex.js ./duplex.js 5 | python -m SimpleHTTPServer 6 | -------------------------------------------------------------------------------- /golang/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: demo 2 | 3 | test: 4 | go test -v 5 | 6 | race: 7 | go test -v -race 8 | 9 | demo: 10 | cd demo && go run demo.go 11 | -------------------------------------------------------------------------------- /javascript/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "duplex-demo", 3 | "version": "0.1.0", 4 | "dependencies" : { 5 | "nodejs-websocket": "*" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /python/duplex/codecs/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import importlib 3 | 4 | def load(name): 5 | return importlib.import_module( 6 | "."+name, "duplex.codecs").codec 7 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Duplex RPC for Python 2 | 3 | Full duplex RPC and service toolkit 4 | 5 | For more information, see [README](http://github.com/progrium/duplex). 6 | -------------------------------------------------------------------------------- /javascript/README.md: -------------------------------------------------------------------------------- 1 | # Duplex.js 2 | 3 | Full duplex RPC and service framework 4 | 5 | * Serialization and transport agnostic 6 | * Client and server combined into Peer 7 | * Callbacks, streams, and middleware 8 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Duplex RPC 2 | 3 | Modern full-duplex RPC 4 | 5 | ## Getting Started 6 | 7 | * [with Python](http://progrium.viewdocs.io/duplex/getting-started/python) 8 | * with Go 9 | * with JavaScript 10 | -------------------------------------------------------------------------------- /python/Makefile: -------------------------------------------------------------------------------- 1 | #export PYTHONASYNCIODEBUG=1 2 | export PYTHONWARNINGS=default 3 | 4 | test: 5 | python3 -m unittest 6 | 7 | release: 8 | python3 setup.py register -r pypi 9 | python3 setup.py sdist upload -r pypi 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | javascript/demo/node_modules 2 | javascript/node_modules 3 | javascript/src/duplex.js 4 | python/duplex.egg-info 5 | python/README 6 | python/**/__pycache__ 7 | python/dist 8 | demo/duplex.js 9 | **/*.pyc 10 | .DS_Store 11 | .idea/ 12 | *.map 13 | -------------------------------------------------------------------------------- /javascript/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test build 2 | 3 | build: 4 | tsc; mv ./src/duplex.js ./dist/duplex.js; 5 | cd test && tsc 6 | 7 | test: 8 | node_modules/.bin/jasmine-node --verbose test 9 | 10 | demo: build 11 | cd demo && node server.js 12 | 13 | dev: 14 | npm install 15 | -------------------------------------------------------------------------------- /python/shell_async.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pprint 3 | from asyncio import * 4 | def asynchook(v): 5 | import builtins 6 | if iscoroutine(v): 7 | builtins._ = get_event_loop().run_until_complete(v) 8 | pprint.pprint(builtins._) 9 | else: 10 | sys.__displayhook__(v) 11 | sys.displayhook = asynchook 12 | -------------------------------------------------------------------------------- /javascript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitAny": true, 5 | "removeComments": true, 6 | "preserveConstEnums": true, 7 | "sourceMap": true, 8 | "lib": ["es6", "dom"], 9 | "target": "es6" 10 | }, 11 | "files": [ 12 | "./src/duplex.ts", 13 | "./test/duplex_spec.ts" 14 | ] 15 | } -------------------------------------------------------------------------------- /python/duplex/__init__.py: -------------------------------------------------------------------------------- 1 | from . import protocol 2 | from . import async 3 | from . import sync 4 | from . import codecs 5 | 6 | __all__ = ( 7 | "RPC", 8 | "protocol", 9 | ) 10 | 11 | def RPC(*args, **kwargs): 12 | if "async" in kwargs: 13 | amode = kwargs["async"] 14 | del kwargs["async"] 15 | else: 16 | amode = True 17 | if amode: 18 | return async.RPC(*args, **kwargs) 19 | else: 20 | return sync.RPC(*args, **kwargs) 21 | 22 | 23 | from .version import version as __version__ 24 | -------------------------------------------------------------------------------- /javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "duplex.js", 3 | "description": "Duplex RPC and service framework for JS", 4 | "license": "MIT", 5 | "repository": "https://github.com/progrium/duplex", 6 | "version": "0.1.0", 7 | "devDependencies": { 8 | "@types/jasmine": "^2.8.6", 9 | "@types/node": "^9.6.0", 10 | "@types/ws": "^4.0.1", 11 | "atob": "*", 12 | "btoa": "*", 13 | "jasmine-node": "^2.0.0" 14 | }, 15 | "main": "./dist/duplex.js", 16 | "dependencies": { 17 | "typescript": "^2.8.1", 18 | "underscore": "^1.8.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /python/demo/demo.py: -------------------------------------------------------------------------------- 1 | 2 | import asyncio 3 | import websockets 4 | import duplex 5 | 6 | rpc = duplex.RPC("json") 7 | 8 | @asyncio.coroutine 9 | def echo(ch): 10 | obj, _ = yield from ch.recv() 11 | yield from ch.send(obj) 12 | rpc.register("echo", echo) 13 | 14 | @asyncio.coroutine 15 | def do_msgbox(ch): 16 | text, _ = yield from ch.recv() 17 | yield from ch.call("msgbox", text, async=True) 18 | rpc.register("doMsgbox", do_msgbox) 19 | 20 | @asyncio.coroutine 21 | def server(conn, path): 22 | peer = yield from rpc.accept(conn) 23 | yield from peer.route() 24 | 25 | start_server = websockets.serve(server, 'localhost', 8001) 26 | asyncio.get_event_loop().run_until_complete(start_server) 27 | asyncio.get_event_loop().run_forever() 28 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Open dev console... 7 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /javascript/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Open dev console... 7 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /python/duplex/ws4py.py: -------------------------------------------------------------------------------- 1 | import duplex 2 | import duplex.sync 3 | import queue 4 | import threading 5 | from ws4py.client.threadedclient import WebSocketClient 6 | 7 | def Client(url): 8 | event = threading.Event() 9 | conn = Conn(url, event) 10 | conn.connect() 11 | event.wait() 12 | return conn 13 | 14 | 15 | class Conn(WebSocketClient): 16 | def __init__(self, url, onopened): 17 | super().__init__(url) 18 | self.onopened = onopened 19 | self.inbox = queue.Queue() 20 | 21 | def opened(self): 22 | self.onopened.set() 23 | 24 | def received_message(self, m): 25 | self.inbox.put(m.data.decode("utf-8")) 26 | 27 | def recv(self): 28 | return self.inbox.get() 29 | 30 | def close(self, code=1000, reason=""): 31 | self.inbox.put("") 32 | super().close(code, reason) 33 | -------------------------------------------------------------------------------- /javascript/demo/server.js: -------------------------------------------------------------------------------- 1 | var ws = require("nodejs-websocket") 2 | var duplex = require("../dist/duplex.js").duplex 3 | var http = require('http'); 4 | var fs = require('fs'); 5 | var index = fs.readFileSync('index.html'); 6 | var duplexjs = fs.readFileSync('../dist/duplex.js'); 7 | 8 | // SERVE FILES 9 | http.createServer(function (req, res) { 10 | if (req.url == "/duplex.js") { 11 | res.writeHead(200, {'Content-Type': 'text/javascript'}); 12 | res.end(duplexjs); 13 | } else { 14 | res.writeHead(200, {'Content-Type': 'text/html'}); 15 | res.end(index); 16 | } 17 | }).listen(8000); 18 | console.log("HTTP on 8000...") 19 | 20 | // SETUP RPC 21 | var rpc = new duplex.RPC(duplex.JSON) 22 | rpc.register("echo", function(ch) { 23 | ch.onrecv = function(obj) { 24 | ch.send(obj) 25 | } 26 | }) 27 | rpc.register("doMsgbox", function(ch) { 28 | ch.onrecv = function(text) { 29 | ch.call("msgbox", text) 30 | } 31 | }) 32 | 33 | // WEBSOCKET SERVER 34 | var server = ws.createServer(function (conn) { 35 | rpc.accept(duplex.wrap["nodejs-websocket"](conn)) 36 | }).listen(8001) 37 | console.log("WS on 8001...") 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jeff Lindsay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /python/duplex/protocol.py: -------------------------------------------------------------------------------- 1 | 2 | name = "SIMPLEX" 3 | version = "1.0" 4 | 5 | class types: 6 | request = "req" 7 | reply = "rep" 8 | 9 | class handshake: 10 | accept = "+OK" 11 | 12 | def request(payload, method, id=None, more=None, ext=None): 13 | msg = dict( 14 | type=types.request, 15 | method=method, 16 | payload=payload, 17 | ) 18 | if id is not None: 19 | msg['id'] = id 20 | if more is True: 21 | msg['more'] = True 22 | if ext is not None: 23 | msg['ext'] = ext 24 | return msg 25 | 26 | def reply(id, payload, more=None, ext=None): 27 | msg = dict( 28 | type=types.reply, 29 | id=id, 30 | payload=payload, 31 | ) 32 | if more is True: 33 | msg['more'] = True 34 | if ext is not None: 35 | msg['ext'] = ext 36 | return msg 37 | 38 | def error(id, code, message, data=None, ext=None): 39 | msg = dict( 40 | type=types.reply, 41 | id=id, 42 | error=dict( 43 | code=code, 44 | message=message, 45 | ), 46 | ) 47 | if data is not None: 48 | msg['error']['data'] = data 49 | if ext is not None: 50 | msg['ext'] = ext 51 | return msg 52 | -------------------------------------------------------------------------------- /golang/demo/demo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/progrium/duplex/golang" 9 | "golang.org/x/net/websocket" 10 | ) 11 | 12 | var rpc = duplex.NewRPC(duplex.NewJSONCodec()) 13 | 14 | func init() { 15 | rpc.Register("echo", func(ch *duplex.Channel) error { 16 | var obj interface{} 17 | if _, err := ch.Recv(&obj); err != nil { 18 | return err 19 | } 20 | return ch.Send(obj, false) 21 | }) 22 | rpc.Register("doMsgbox", func(ch *duplex.Channel) error { 23 | var text string 24 | if _, err := ch.Recv(&text); err != nil { 25 | return err 26 | } 27 | return ch.Call("msgbox", text, nil) 28 | }) 29 | 30 | } 31 | 32 | func WebsocketServer(ws *websocket.Conn) { 33 | peer, err := rpc.Accept(ws) 34 | if err != nil { 35 | panic(err) 36 | } 37 | <-peer.CloseNotify() 38 | fmt.Println("Closed") 39 | } 40 | 41 | func main() { 42 | http.HandleFunc("/duplex.js", func(w http.ResponseWriter, r *http.Request) { 43 | http.ServeFile(w, r, "../../javascript/dist/duplex.js") 44 | }) 45 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 46 | http.ServeFile(w, r, "../../javascript/demo/index.html") 47 | }) 48 | go func() { 49 | fmt.Println("HTTP on 8000...") 50 | log.Fatal(http.ListenAndServe(":8000", nil)) 51 | }() 52 | 53 | ws := &http.Server{ 54 | Addr: ":8001", 55 | Handler: websocket.Handler(WebsocketServer), 56 | } 57 | fmt.Println("WS on 8001...") 58 | log.Fatal(ws.ListenAndServe()) 59 | } 60 | -------------------------------------------------------------------------------- /python/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import setuptools 5 | 6 | # Avoid polluting the .tar.gz with ._* files under Mac OS X 7 | os.putenv('COPYFILE_DISABLE', 'true') 8 | 9 | root = os.path.dirname(__file__) 10 | 11 | # Prevent distutils from complaining that a standard file wasn't found 12 | README = os.path.join(root, 'README') 13 | if not os.path.exists(README): 14 | os.symlink(README + '.md', README) 15 | 16 | with open(os.path.join(root, 'README'), encoding='utf-8') as f: 17 | long_description = '\n\n'.join(f.read().split('\n\n')[1:]) 18 | 19 | with open(os.path.join(root, 'duplex', 'version.py'), encoding='utf-8') as f: 20 | exec(f.read()) 21 | 22 | py_version = sys.version_info[:2] 23 | 24 | if py_version < (3, 3): 25 | raise Exception("duplex requires Python >= 3.3.") 26 | 27 | setuptools.setup( 28 | name='duplex', 29 | version=version, 30 | author='Jeff Lindsay', 31 | author_email='progrium@gmail.com', 32 | url='https://github.com/progrium/duplex', 33 | description="Full duplex RPC and service toolkit", 34 | long_description=long_description, 35 | #download_url='https://pypi.python.org/pypi/duplex', 36 | packages=setuptools.find_packages(), 37 | include_package_data = True, 38 | package_data = { 39 | '': ['*.md'], 40 | }, 41 | extras_require={ 42 | ':python_version=="3.3"': ['asyncio'], 43 | }, 44 | classifiers=[ 45 | # "Development Status :: 5 - Production/Stable", 46 | "Intended Audience :: Developers", 47 | "License :: OSI Approved :: MIT License", 48 | "Operating System :: OS Independent", 49 | "Programming Language :: Python", 50 | "Programming Language :: Python :: 3", 51 | "Programming Language :: Python :: 3.3", 52 | "Programming Language :: Python :: 3.4", 53 | "Programming Language :: Python :: 3.5", 54 | ], 55 | platforms='all', 56 | license='MIT' 57 | ) 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Duplex RPC 2 | 3 | Modern full-duplex RPC 4 | 5 | * Serialization and transport agnostic 6 | * Client and server combined into Peer object 7 | * Calls and callbacks in either direction 8 | * Optional streaming of results and arguments 9 | * Extensible with middleware (soon) 10 | * Easy to implement protocol and API 11 | * Ready-to-go implementations 12 | * Go 13 | * Python 3 14 | * JavaScript (Node.js and browser) 15 | * TODO: Ruby? 16 | 17 | ## Getting Started 18 | 19 | * [with Python](http://progrium.viewdocs.io/duplex/getting-started/python) 20 | * with Go 21 | * with JavaScript 22 | 23 | ## Tour 24 | 25 | ### Language and serialization agnostic 26 | 27 | Duplex is an RPC protocol designed for dynamic (and some statically-typed) languages that focuses on advanced RPC semantics and not object or frame serialization. This lets you pick how to marshal objects, whether with JSON, msgpack, protobuf, BERT, BSON, or anything custom. 28 | 29 | ```javascript 30 | // rpc setup using json 31 | ``` 32 | 33 | ```golang 34 | // rpc setup using gob 35 | ``` 36 | 37 | ### API combines client and server into "peer" 38 | 39 | While that alone is somehow already revolutionary for RPC protocols, it also combines client and server into a peer object, letting either side of the connection call or provide invocable service methods. This means you never have to worry about only having a client or only having a server library in your language. It also allows for flexible connection topologies (server calling client functions), callbacks, and plugin architectures. 40 | 41 | ```python 42 | # server with methods connects to client 43 | ``` 44 | 45 | ```javascript 46 | // method gets a callback and calls back to the client 47 | ``` 48 | 49 | ### Bi-directional object streaming 50 | 51 | Methods can also stream multiple results *and* accept multiple streaming arguments, letting you use Duplex for bi-directional object streaming. Among other patterns, this lets you implement real-time data synchronization, connection tunneling, and interactive consoles, all with the same mechanism as simple remote method calls. 52 | 53 | ```golang 54 | // calls an interactive method, attaches to terminal 55 | ``` 56 | 57 | ```ruby 58 | # call to subscribe to updates 59 | ``` 60 | 61 | ### Simple, extensible API and protocol 62 | 63 | Duplex has a simple protocol spec not much more complex than JSON-RPC. It also has an API guide that can be used for easy and consistent implementations in various languages. 64 | 65 | The protocol and API are also designed to eventually be extensible. Expect a middleware mechanism that lets you add tracing, authentication, policy, transactions, and more. This allows Duplex to remain simple but powerful. 66 | 67 | ### Transport agnostic, frame interface 68 | 69 | The API design has a simple framed transport interface. This means out of the box you can expect to use any transport that takes care of framing, for example WebSockets, UDP, ZeroMQ, AMQP. Wrapping streaming transports such as raw TCP or STDIO with length-prefix or newline framing lets you use them as well. By focusing on frames and making the API transport agnostic, as well as being serialization agnostic, implementations are very simple with no major dependencies. 70 | 71 | 72 | 73 | # TODO 74 | 75 | * document spec / api 76 | * demo 77 | * cross language, browser 78 | * topologies: client-server, server-client, gateway 79 | * transports: websocket, tcp, UDP 80 | * codecs: json, msgpack, protobuf 81 | * streaming: state sync, terminal 82 | * callbacks: async reply, events, plugins 83 | * gateway: behind firewall, client to client (browser) 84 | * patterns: identity, reflection (cli), self docs 85 | * implementation: code tour, api guide, protocol spec 86 | -------------------------------------------------------------------------------- /python/duplex/test_sync.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | import threading 4 | import queue 5 | import concurrent.futures as futures 6 | 7 | from . import RPC 8 | from . import protocol 9 | 10 | def spawn(fn, *args, **kwargs): 11 | f = futures.Future() 12 | def target(*args, **kwargs): 13 | f.set_result(fn(*args, **kwargs)) 14 | t = threading.Thread(target=target, args=args, kwargs=kwargs) 15 | t.start() 16 | return f 17 | 18 | class WaitGroup(object): 19 | def __init__(self): 20 | self.total = [] 21 | self.pending = [] 22 | 23 | def add(self, incr=1): 24 | for n in range(incr): 25 | f = futures.Future() 26 | self.total.append(f) 27 | self.pending.append(f) 28 | 29 | def done(self): 30 | if len(self.pending) == 0: 31 | return 32 | f = self.pending.pop() 33 | f.set_result(True) 34 | 35 | def wait(self): 36 | futures.wait(self.total) 37 | 38 | 39 | class MockConnection(object): 40 | def __init__(self): 41 | self.sent = [] 42 | self.closed = False 43 | self.pairedWith = None 44 | self.expectedSends = WaitGroup() 45 | self.expectedRecvs = WaitGroup() 46 | self.inbox = queue.Queue() 47 | 48 | def close(self): 49 | self.closed = True 50 | 51 | def send(self, frame): 52 | self.sent.append(frame) 53 | if self.pairedWith: 54 | self.pairedWith.inbox.put(frame) 55 | self.expectedSends.done() 56 | 57 | def recv(self): 58 | self.expectedRecvs.done() 59 | return self.inbox.get(timeout=1) 60 | 61 | def connection_pair(): 62 | conn1 = MockConnection() 63 | conn2 = MockConnection() 64 | conn1.pairedWith = conn2 65 | conn2.pairedWith = conn1 66 | return [conn1, conn2] 67 | 68 | def peer_pair(rpc): 69 | conn1, conn2 = connection_pair() 70 | tasks = [ 71 | spawn(rpc.accept, conn1, False), 72 | spawn(rpc.handshake, conn2, False), 73 | ] 74 | yield from futures.wait(tasks) 75 | return [tasks[0].result(), tasks[1].result()] 76 | 77 | def handshake(codec): 78 | return "{0}/{1};{2}".format( 79 | protocol.name, 80 | protocol.version, 81 | codec) 82 | 83 | def echo(ch): 84 | obj, _ = ch.recv() 85 | ch.send(obj) 86 | 87 | 88 | class DuplexSyncTests(unittest.TestCase): 89 | 90 | def setUp(self): 91 | pass 92 | 93 | def tearDown(self): 94 | pass 95 | 96 | def spawn(self, *args, **kwargs): 97 | return spawn(*args, **kwargs) 98 | 99 | def test_handshake(self): 100 | conn = MockConnection() 101 | rpc = RPC("json", async=False) 102 | conn.inbox.put(protocol.handshake.accept) 103 | rpc.handshake(conn, False) 104 | conn.close() 105 | self.assertEqual(conn.sent[0], handshake("json")) 106 | 107 | def test_accept(self): 108 | conn = MockConnection() 109 | rpc = RPC("json", async=False) 110 | conn.inbox.put(handshake("json")) 111 | rpc.accept(conn, False) 112 | conn.close() 113 | self.assertEqual(conn.sent[0], protocol.handshake.accept) 114 | 115 | def test_all_on_paired_peers(self): 116 | conns = connection_pair() 117 | rpc = RPC("json", async=False) 118 | def echo_tag(ch): 119 | obj, _ = ch.recv() 120 | obj["tag"] = True 121 | ch.send(obj) 122 | rpc.register("echo-tag", echo_tag) 123 | tasks = [ 124 | self.spawn(rpc.accept, conns[0], False), 125 | self.spawn(rpc.handshake, conns[1], False), 126 | ] 127 | futures.wait(tasks) 128 | peer1 = tasks[0].result() 129 | peer2 = tasks[1].result() 130 | tasks = [ 131 | self.spawn(peer1.call, "echo-tag", {"from": "peer1"}), 132 | self.spawn(peer2.call, "echo-tag", {"from": "peer2"}), 133 | self.spawn(peer1.route, 2), 134 | self.spawn(peer2.route, 2), 135 | ] 136 | futures.wait(tasks) 137 | conns[0].close() 138 | conns[1].close() 139 | self.assertEqual(tasks[0].result()["from"], "peer1") 140 | self.assertEqual(tasks[0].result()["tag"], True) 141 | self.assertEqual(tasks[1].result()["from"], "peer2") 142 | self.assertEqual(tasks[1].result()["tag"], True) 143 | -------------------------------------------------------------------------------- /python/duplex/async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from . import protocol 4 | from . import sync 5 | 6 | try: 7 | spawn = asyncio.ensure_future 8 | except: 9 | spawn = asyncio.async 10 | 11 | class Channel(sync.Channel): 12 | def __init__(self, peer, type_, method, ext=None): 13 | super().__init__(peer, type_, method, ext) 14 | self.inbox = asyncio.Queue(loop=peer.rpc.loop) 15 | 16 | @asyncio.coroutine 17 | def call(self, method, args=None, wait=True): 18 | ch = self.peer.open(method, self.ext) 19 | yield from ch.send(args) 20 | if wait: 21 | ret, _ = yield from ch.recv() 22 | # todo: if more is true, error 23 | return ret 24 | return None 25 | 26 | @asyncio.coroutine 27 | def close(self): 28 | yield from self.peer.close() 29 | 30 | @asyncio.coroutine 31 | def send(self, payload, more=False): 32 | if self.type == protocol.types.request: 33 | yield from self.peer.conn.send( 34 | self.peer.rpc.encode(protocol.request( 35 | payload, 36 | self.method, 37 | self.id, 38 | more, 39 | self.ext, 40 | ))) 41 | elif self.type == protocol.types.reply: 42 | yield from self.peer.conn.send( 43 | self.peer.rpc.encode(protocol.reply( 44 | self.id, 45 | payload, 46 | more, 47 | self.ext, 48 | ))) 49 | else: 50 | raise Exception("bad channel type") 51 | 52 | @asyncio.coroutine 53 | def senderr(self, code, message, data=None): 54 | if self.type != protocol.types.reply: 55 | raise Exception("not a reply channel") 56 | yield from self.peer.conn.send( 57 | self.peer.rpc.encode(protocol.error( 58 | self.id, 59 | code, 60 | message, 61 | data, 62 | self.ext, 63 | ))) 64 | 65 | @asyncio.coroutine 66 | def recv(self): 67 | obj, more = yield from self.inbox.get() 68 | return [obj, more] 69 | 70 | 71 | class Peer(sync.Peer): 72 | Channel = Channel 73 | 74 | def __init__(self, rpc, conn): 75 | super().__init__(rpc, conn) 76 | 77 | def _spawn(self, fn, *args, **kwargs): 78 | task = spawn(fn(*args, **kwargs), loop=self.rpc.loop) 79 | self.tasks.append(task) 80 | return task 81 | 82 | @asyncio.coroutine 83 | def route(self, loops=None): 84 | while self.routing: 85 | if loops is not None: 86 | if loops == 0: 87 | break 88 | loops -= 1 89 | frame = yield from self.conn.recv() 90 | self._handle_frame(frame) 91 | 92 | @asyncio.coroutine 93 | def close(self): 94 | yield from self.conn.close() 95 | for task in self.tasks: 96 | task.cancel() 97 | if len(self.tasks) > 0: 98 | yield from asyncio.wait(self.tasks) 99 | 100 | @asyncio.coroutine 101 | def call(self, method, args=None, wait=True): 102 | ch = self.open(method) 103 | yield from ch.send(args) 104 | if wait: 105 | ret, _ = yield from ch.recv() 106 | # todo: if more is true, error 107 | return ret 108 | return None 109 | 110 | 111 | class RPC(sync.RPC): 112 | Peer = Peer 113 | 114 | def __init__(self, codec, loop=None): 115 | super().__init__(codec) 116 | if loop is None: 117 | self.loop = asyncio.get_event_loop() 118 | else: 119 | self.loop = loop 120 | 121 | def register_func(self, method, func): 122 | @asyncio.coroutine 123 | def func_wrapped(ch): 124 | args, _ = yield from ch.recv() 125 | if asyncio.iscoroutinefunction(func): 126 | ret = yield from func(args, ch) 127 | else: 128 | ret = func(args, ch) 129 | yield from ch.send(ret) 130 | self.register(method, func_wrapped) 131 | 132 | @asyncio.coroutine 133 | def handshake(self, conn, route=True): 134 | peer = Peer(self, conn) 135 | yield from conn.send(self._handshake()) 136 | resp = yield from conn.recv() 137 | if resp[0] != "+": 138 | raise Exception("bad handshake") 139 | if route: 140 | peer._spawn(peer.route) 141 | return peer 142 | 143 | @asyncio.coroutine 144 | def accept(self, conn, route=True): 145 | peer = Peer(self, conn) 146 | handshake = yield from conn.recv() 147 | # TODO: check handshake 148 | yield from conn.send(protocol.handshake.accept) 149 | if route: 150 | peer._spawn(peer.route) 151 | return peer 152 | -------------------------------------------------------------------------------- /javascript/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "duplex.js", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/events": { 8 | "version": "1.2.0", 9 | "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", 10 | "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", 11 | "dev": true 12 | }, 13 | "@types/jasmine": { 14 | "version": "2.8.6", 15 | "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.6.tgz", 16 | "integrity": "sha512-clg9raJTY0EOo5pVZKX3ZlMjlYzVU73L71q5OV1jhE2Uezb7oF94jh4CvwrW6wInquQAdhOxJz5VDF2TLUGmmA==", 17 | "dev": true 18 | }, 19 | "@types/node": { 20 | "version": "9.6.0", 21 | "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.0.tgz", 22 | "integrity": "sha512-h3YZbOq2+ZoDFI1z8Zx0Ck/xRWkOESVaLdgLdd/c25mMQ1Y2CAkILu9ny5A15S5f32gGcQdaUIZ2jzYr8D7IFg==", 23 | "dev": true 24 | }, 25 | "@types/ws": { 26 | "version": "4.0.1", 27 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-4.0.1.tgz", 28 | "integrity": "sha512-J56Wn8j7ovzmlrkUSPXnVRH+YXUCGoVokiB49QIjz+yq0234guOrBvF/HHrqrJjnY4p5oq+q6xAxT/7An6SeWQ==", 29 | "dev": true, 30 | "requires": { 31 | "@types/events": "1.2.0", 32 | "@types/node": "9.6.0" 33 | } 34 | }, 35 | "atob": { 36 | "version": "2.0.3", 37 | "resolved": "https://registry.npmjs.org/atob/-/atob-2.0.3.tgz", 38 | "integrity": "sha1-GcenYEc3dEaPILLS0DNyrX1Mv10=", 39 | "dev": true 40 | }, 41 | "btoa": { 42 | "version": "1.1.2", 43 | "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.1.2.tgz", 44 | "integrity": "sha1-PkC4FmP4HS3WWWpMtxSo3BbPq+A=", 45 | "dev": true 46 | }, 47 | "gaze": { 48 | "version": "0.5.2", 49 | "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz", 50 | "integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=", 51 | "dev": true, 52 | "requires": { 53 | "globule": "0.1.0" 54 | } 55 | }, 56 | "glob": { 57 | "version": "3.1.21", 58 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", 59 | "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", 60 | "dev": true, 61 | "requires": { 62 | "graceful-fs": "1.2.3", 63 | "inherits": "1.0.2", 64 | "minimatch": "0.2.14" 65 | } 66 | }, 67 | "globule": { 68 | "version": "0.1.0", 69 | "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz", 70 | "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=", 71 | "dev": true, 72 | "requires": { 73 | "glob": "3.1.21", 74 | "lodash": "1.0.2", 75 | "minimatch": "0.2.14" 76 | } 77 | }, 78 | "graceful-fs": { 79 | "version": "1.2.3", 80 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", 81 | "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", 82 | "dev": true 83 | }, 84 | "growl": { 85 | "version": "1.7.0", 86 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", 87 | "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=", 88 | "dev": true 89 | }, 90 | "inherits": { 91 | "version": "1.0.2", 92 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", 93 | "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", 94 | "dev": true 95 | }, 96 | "jasmine-growl-reporter": { 97 | "version": "0.2.1", 98 | "resolved": "https://registry.npmjs.org/jasmine-growl-reporter/-/jasmine-growl-reporter-0.2.1.tgz", 99 | "integrity": "sha1-1fCje5L2qD/VxkgrgJSVyQqLVf4=", 100 | "dev": true, 101 | "requires": { 102 | "growl": "1.7.0" 103 | } 104 | }, 105 | "jasmine-node": { 106 | "version": "2.0.0", 107 | "resolved": "https://registry.npmjs.org/jasmine-node/-/jasmine-node-2.0.0.tgz", 108 | "integrity": "sha1-gXUacjJfVJdJCxQYGlUIfxsDcf8=", 109 | "dev": true, 110 | "requires": { 111 | "coffee-script": "1.7.1", 112 | "gaze": "0.5.2", 113 | "jasmine-growl-reporter": "0.2.1", 114 | "minimist": "0.0.8", 115 | "mkdirp": "0.3.5", 116 | "underscore": "1.6.0", 117 | "walkdir": "0.0.12" 118 | }, 119 | "dependencies": { 120 | "coffee-script": { 121 | "version": "1.7.1", 122 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz", 123 | "integrity": "sha1-YplqhheAx15tUGnROCJyO3NAS/w=", 124 | "dev": true, 125 | "requires": { 126 | "mkdirp": "0.3.5" 127 | } 128 | }, 129 | "underscore": { 130 | "version": "1.6.0", 131 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", 132 | "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", 133 | "dev": true 134 | } 135 | } 136 | }, 137 | "lodash": { 138 | "version": "1.0.2", 139 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", 140 | "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=", 141 | "dev": true 142 | }, 143 | "lru-cache": { 144 | "version": "2.7.3", 145 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", 146 | "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", 147 | "dev": true 148 | }, 149 | "minimatch": { 150 | "version": "0.2.14", 151 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", 152 | "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", 153 | "dev": true, 154 | "requires": { 155 | "lru-cache": "2.7.3", 156 | "sigmund": "1.0.1" 157 | } 158 | }, 159 | "minimist": { 160 | "version": "0.0.8", 161 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 162 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 163 | "dev": true 164 | }, 165 | "mkdirp": { 166 | "version": "0.3.5", 167 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", 168 | "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", 169 | "dev": true 170 | }, 171 | "sigmund": { 172 | "version": "1.0.1", 173 | "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", 174 | "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", 175 | "dev": true 176 | }, 177 | "typescript": { 178 | "version": "2.8.1", 179 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.8.1.tgz", 180 | "integrity": "sha512-Ao/f6d/4EPLq0YwzsQz8iXflezpTkQzqAyenTiw4kCUGr1uPiFLC3+fZ+gMZz6eeI/qdRUqvC+HxIJzUAzEFdg==" 181 | }, 182 | "underscore": { 183 | "version": "1.8.3", 184 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", 185 | "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" 186 | }, 187 | "walkdir": { 188 | "version": "0.0.12", 189 | "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.12.tgz", 190 | "integrity": "sha512-HFhaD4mMWPzFSqhpyDG48KDdrjfn409YQuVW7ckZYhW4sE87mYtWifdB/+73RA7+p4s4K18n5Jfx1kHthE1gBw==", 191 | "dev": true 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /python/duplex/sync.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import queue 3 | import threading 4 | 5 | from . import protocol 6 | from . import codecs 7 | 8 | class Channel(object): 9 | def __init__(self, peer, type_, method, ext=None): 10 | self.peer = peer 11 | self.ext = ext 12 | self.type = type_ 13 | self.method = method 14 | self.id = None 15 | self.inbox = queue.Queue() 16 | 17 | def call(self, method, args=None, wait=True): 18 | ch = self.peer.open(method, self.ext) 19 | ch.send(args) 20 | if wait: 21 | ret, _ = ch.recv() 22 | # todo: if more is true, error 23 | return ret 24 | return None 25 | 26 | def close(self): 27 | self.peer.close() 28 | 29 | def open(self, method): 30 | ch = self.peer.open(method, self.ext) 31 | return ch 32 | 33 | def send(self, payload, more=False): 34 | if self.type == protocol.types.request: 35 | self.peer.conn.send( 36 | self.peer.rpc.encode(protocol.request( 37 | payload, 38 | self.method, 39 | self.id, 40 | more, 41 | self.ext, 42 | ))) 43 | elif self.type == protocol.types.reply: 44 | self.peer.conn.send( 45 | self.peer.rpc.encode(protocol.reply( 46 | self.id, 47 | payload, 48 | more, 49 | self.ext, 50 | ))) 51 | else: 52 | raise Exception("bad channel type") 53 | 54 | def senderr(self, code, message, data=None): 55 | if self.type != protocol.types.reply: 56 | raise Exception("not a reply channel") 57 | self.peer.conn.send( 58 | self.peer.rpc.encode(protocol.error( 59 | self.id, 60 | code, 61 | message, 62 | data, 63 | self.ext, 64 | ))) 65 | 66 | def recv(self, timeout=None): 67 | return self.inbox.get(timeout=timeout) 68 | 69 | 70 | class Peer(object): 71 | Channel = Channel 72 | 73 | def __init__(self, rpc, conn): 74 | self.rpc = rpc 75 | self.conn = conn 76 | self.req_chans = {} 77 | self.rep_chans = {} 78 | self.counter = 0 79 | self.tasks = [] 80 | self.routing = True 81 | 82 | def _spawn(self, fn, *args, **kwargs): 83 | t = threading.Thread(target=fn, args=args, kwargs=kwargs) 84 | self.tasks.append(t) 85 | t.start() 86 | return t 87 | 88 | def route(self, loops=None): 89 | while self.routing: 90 | if loops is not None: 91 | if loops == 0: 92 | break 93 | loops -= 1 94 | frame = self.conn.recv() 95 | self._handle_frame(frame) 96 | 97 | def _handle_frame(self, frame): 98 | if frame == "": 99 | # ignore empty frames 100 | return 101 | try: 102 | msg = self.rpc.decode(frame) 103 | except Exception as e: 104 | # todo: handle decode error 105 | raise e 106 | if msg['type'] == protocol.types.request: 107 | if msg.get('id') in self.req_chans: 108 | ch = self.req_chans[msg['id']] 109 | if msg.get('more', False) is not True: 110 | del self.req_chans[msg['id']] 111 | else: 112 | ch = self.Channel(self, protocol.types.reply, msg['method']) 113 | if 'id' in msg: 114 | ch.id = msg['id'] 115 | if msg.get('more', False): 116 | self.req_chans[ch.id] = ch 117 | # TODO: locks 118 | if msg['method'] in self.rpc.registered: 119 | self._spawn(self.rpc.registered[msg['method']], ch) 120 | else: 121 | raise Exception("method missing") 122 | if 'ext' in msg: 123 | ch.ext = msg['ext'] 124 | ch.inbox.put_nowait([msg['payload'], msg.get('more', False)]) 125 | if msg.get('more', False) is not True: 126 | # ??? 127 | pass 128 | 129 | elif msg['type'] == protocol.types.reply: 130 | if 'error' in msg: 131 | ch = self.rep_chans[msg['id']] 132 | ch.inbox.put_nowait([msg['error'], False]) # TODO: wrap as exception? 133 | del self.rep_chans[msg['id']] 134 | else: 135 | ch = self.rep_chans[msg['id']] 136 | ch.inbox.put_nowait([msg['payload'], msg.get('more', False)]) 137 | if msg.get('more', False) is False: 138 | del self.rep_chans[msg['id']] 139 | else: 140 | raise Exception("bad msg type: {0}".format(msg.type)) 141 | 142 | 143 | def close(self): 144 | self.routing = False 145 | self.conn.close() 146 | for task in self.tasks: 147 | task.join() 148 | 149 | def call(self, method, args=None, wait=True): 150 | ch = self.open(method) 151 | ch.send(args) 152 | if wait: 153 | ret, _ = ch.recv() 154 | # todo: if more is true, error 155 | return ret 156 | return None 157 | 158 | def open(self, method, ext=None): 159 | ch = self.Channel(self, protocol.types.request, method, ext) 160 | self.counter += 1 161 | ch.id = self.counter 162 | self.rep_chans[ch.id] = ch 163 | return ch 164 | 165 | 166 | class RPC(object): 167 | Peer = Peer 168 | 169 | def __init__(self, codec): 170 | if isinstance(codec, str): 171 | self.codec = codecs.load(codec) 172 | else: 173 | self.codec = codec 174 | self.encode = self.codec[1] 175 | self.decode = self.codec[2] 176 | self.registered = {} 177 | 178 | def register(self, method, handler): 179 | self.registered[method] = handler 180 | 181 | def unregister(self, method): 182 | del self.registered[method] 183 | 184 | def register_func(self, method, func): 185 | def func_wrapped(ch): 186 | args, _ = ch.recv() 187 | ret = func(args, ch) 188 | ch.send(ret) 189 | self.register(method, func_wrapped) 190 | 191 | def callback_func(self, func): 192 | name = "_callback.{0}".format(uuid.uuid4()) 193 | self.register_func(name, func) 194 | return name 195 | 196 | def _handshake(self): 197 | return "{0}/{1};{2}".format( 198 | protocol.name, 199 | protocol.version, 200 | self.codec[0]) 201 | 202 | def handshake(self, conn, route=True): 203 | peer = self.Peer(self, conn) 204 | conn.send(self._handshake()) 205 | resp = conn.recv() 206 | if resp[0] != "+": 207 | raise Exception("bad handshake") 208 | if route: 209 | peer._spawn(peer.route) 210 | return peer 211 | 212 | def accept(self, conn, route=True): 213 | peer = self.Peer(self, conn) 214 | handshake = conn.recv() 215 | # TODO: check handshake 216 | conn.send(protocol.handshake.accept) 217 | if route: 218 | peer._spawn(peer.route) 219 | return peer 220 | -------------------------------------------------------------------------------- /golang/duplex.go: -------------------------------------------------------------------------------- 1 | package duplex 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "sync" 9 | 10 | "github.com/pborman/uuid" 11 | "golang.org/x/net/context" 12 | ) 13 | 14 | /* 15 | Notes 16 | 17 | The io.ReadWriteCloser is assumed to be a *framed* 18 | transport. So we expect each Read to be a full frame. 19 | The buffer provided to Read needs to be big enough, 20 | which means using a variable-sized Buffer. 21 | 22 | We also assume the io.ReadWriteCloser to handle locking 23 | on Writes. 24 | 25 | Potential requirement on spec: msg id has to be 1 or more 26 | 27 | TODO: replace panics 28 | 29 | */ 30 | 31 | var ( 32 | Version = "0.1.0" 33 | ProtocolName = "SIMPLEX" 34 | ProtocolVersion = "1.0" 35 | TypeRequest = "req" 36 | TypeReply = "rep" 37 | HandshakeAccept = "+OK" 38 | BacklogSize = 1024 39 | MaxFrameSize = 1 << 20 // 1mb 40 | ) 41 | 42 | type Message struct { 43 | Type string `json:"type"` 44 | Method string `json:"method,omitempty"` 45 | Payload interface{} `json:"payload,omitempty"` 46 | Error *Error `json:"error,omitempty"` 47 | Id int `json:"id,omitempty"` 48 | More bool `json:"more,omitempty"` 49 | Ext interface{} `json:"ext,omitempty"` 50 | } 51 | 52 | type Error struct { 53 | Code int `json:"code"` 54 | Message string `json:"message"` 55 | Data interface{} `json:"data,omitempty"` 56 | } 57 | 58 | func (err Error) Error() string { 59 | return err.Message 60 | } 61 | 62 | type Codec struct { 63 | Name string 64 | Encode func(obj interface{}) ([]byte, error) 65 | Decode func(frame []byte, obj interface{}) error 66 | } 67 | 68 | func NewJSONCodec() *Codec { 69 | return &Codec{ 70 | Name: "json", 71 | Encode: json.Marshal, 72 | Decode: json.Unmarshal, 73 | } 74 | } 75 | 76 | type RPC struct { 77 | sync.Mutex 78 | codec *Codec 79 | registered map[string]func(*Channel) error 80 | } 81 | 82 | func NewRPC(codec *Codec) *RPC { 83 | return &RPC{ 84 | codec: codec, 85 | registered: make(map[string]func(*Channel) error), 86 | } 87 | } 88 | 89 | func (rpc *RPC) Register(name string, handler func(*Channel) error) { 90 | rpc.Lock() 91 | defer rpc.Unlock() 92 | rpc.registered[name] = handler 93 | } 94 | 95 | func (rpc *RPC) Unregister(name string) { 96 | rpc.Lock() 97 | defer rpc.Unlock() 98 | delete(rpc.registered, name) 99 | } 100 | 101 | func (rpc *RPC) RegisterFunc(name string, fn func(interface{}, *Channel) (interface{}, error)) { 102 | rpc.Register(name, func(ch *Channel) error { 103 | var args interface{} 104 | _, err := ch.Recv(&args) 105 | if err != nil { 106 | return err 107 | } 108 | ret, err := fn(args, ch) 109 | if err != nil { 110 | return err 111 | } 112 | return ch.Send(ret, false) 113 | }) 114 | } 115 | 116 | func (rpc *RPC) CallbackFunc(fn func(interface{}, *Channel) (interface{}, error)) string { 117 | name := "_callback." + uuid.New() 118 | rpc.RegisterFunc(name, fn) 119 | return name 120 | } 121 | 122 | func (rpc *RPC) Handshake(conn io.ReadWriteCloser) (*Peer, error) { 123 | peer := NewPeer(rpc, conn, nil) 124 | handshake := []byte(fmt.Sprintf("%s/%s;%s", 125 | ProtocolName, ProtocolVersion, rpc.codec.Name)) 126 | _, err := conn.Write(handshake) 127 | if err != nil { 128 | return nil, err 129 | } 130 | buf := make([]byte, 32) 131 | _, err = conn.Read(buf) 132 | if err != nil { 133 | return nil, err 134 | } 135 | if buf[0] != '+' { 136 | panic(string(buf)) 137 | } 138 | go peer.route() 139 | return peer, nil 140 | } 141 | 142 | func (rpc *RPC) Accept(conn io.ReadWriteCloser) (*Peer, error) { 143 | return rpc.AcceptWith(conn, nil) 144 | } 145 | 146 | func (rpc *RPC) AcceptWith(conn io.ReadWriteCloser, ctx context.Context) (*Peer, error) { 147 | peer := NewPeer(rpc, conn, ctx) 148 | buf := make([]byte, 32) 149 | _, err := conn.Read(buf) 150 | if err != nil { 151 | return nil, err 152 | } 153 | // TODO: check handshake 154 | _, err = conn.Write([]byte(HandshakeAccept)) 155 | if err != nil { 156 | return nil, err 157 | } 158 | go peer.route() 159 | return peer, nil 160 | } 161 | 162 | type Peer struct { 163 | counter int 164 | reqCh map[int]*Channel 165 | repCh map[int]*Channel 166 | rpc *RPC 167 | conn io.ReadWriteCloser 168 | closeCh chan bool 169 | ctx context.Context 170 | } 171 | 172 | func NewPeer(rpc *RPC, conn io.ReadWriteCloser, ctx context.Context) *Peer { 173 | peer := &Peer{ 174 | rpc: rpc, 175 | conn: conn, 176 | ctx: ctx, 177 | reqCh: make(map[int]*Channel), 178 | repCh: make(map[int]*Channel), 179 | closeCh: make(chan bool), 180 | } 181 | return peer 182 | } 183 | 184 | func (peer *Peer) CloseNotify() <-chan bool { 185 | return peer.closeCh 186 | } 187 | 188 | func (peer *Peer) route() { 189 | // assumes closing will cause something 190 | // here to error and break loop. 191 | // TODO: double check 192 | for { 193 | frame := make([]byte, MaxFrameSize) 194 | n, err := peer.conn.Read(frame) 195 | if err != nil { 196 | // TODO: what happens on read error 197 | break 198 | } 199 | if n == 0 { 200 | // ignore empty frames 201 | continue 202 | } 203 | var msg Message 204 | err = peer.rpc.codec.Decode(frame[:n], &msg) 205 | if err != nil { 206 | // TODO: what happens on decode error 207 | panic(err) 208 | } 209 | switch msg.Type { 210 | case TypeRequest: 211 | ch, exists := peer.reqCh[msg.Id] 212 | if exists { 213 | if !msg.More { 214 | delete(peer.reqCh, msg.Id) 215 | } 216 | } else { 217 | ch = NewChannel(peer, TypeReply, msg.Method) 218 | if msg.Id != 0 { 219 | ch.id = msg.Id 220 | if msg.More { 221 | peer.reqCh[ch.id] = ch 222 | } 223 | } 224 | peer.rpc.Lock() 225 | fn, exists := peer.rpc.registered[msg.Method] 226 | peer.rpc.Unlock() 227 | if exists { 228 | go fn(ch) 229 | } else { 230 | // TODO: method missing 231 | panic(msg.Method) 232 | } 233 | } 234 | if msg.Ext != nil { 235 | ch.ext = msg.Ext 236 | } 237 | ch.inbox <- &msg 238 | if !msg.More { 239 | close(ch.inbox) 240 | } 241 | 242 | case TypeReply: 243 | if msg.Error != nil { 244 | // TODO: better handling of missing id 245 | ch := peer.repCh[msg.Id] 246 | ch.err = msg.Error 247 | ch.done <- ch 248 | close(ch.inbox) 249 | delete(peer.repCh, msg.Id) 250 | } else { 251 | // TODO: better handling of missing id 252 | ch := peer.repCh[msg.Id] 253 | ch.inbox <- &msg 254 | if !msg.More { 255 | ch.done <- ch 256 | close(ch.inbox) 257 | delete(peer.repCh, msg.Id) 258 | } 259 | } 260 | default: 261 | panic("bad msg type: " + msg.Type) 262 | } 263 | } 264 | peer.closeCh <- true 265 | } 266 | 267 | func (peer *Peer) Close() error { 268 | return peer.conn.Close() 269 | } 270 | 271 | func (peer *Peer) Call(method string, args interface{}, reply interface{}) error { 272 | ch := peer.Open(method) 273 | err := ch.Send(args, false) 274 | if err != nil { 275 | return err 276 | } 277 | if reply != nil { 278 | _, err = ch.Recv(reply) 279 | if err != nil { 280 | return err 281 | } 282 | return (<-ch.done).err 283 | } 284 | return nil 285 | } 286 | 287 | func (peer *Peer) Open(service string) *Channel { 288 | ch := NewChannel(peer, TypeRequest, service) 289 | peer.counter = peer.counter + 1 290 | ch.id = peer.counter 291 | peer.repCh[ch.id] = ch 292 | return ch 293 | } 294 | 295 | type Channel struct { 296 | *Peer 297 | 298 | inbox chan *Message 299 | done chan *Channel 300 | ext interface{} 301 | typ string 302 | method string 303 | id int 304 | err error 305 | } 306 | 307 | func NewChannel(peer *Peer, typ string, method string) *Channel { 308 | return &Channel{ 309 | Peer: peer, 310 | inbox: make(chan *Message, BacklogSize), 311 | done: make(chan *Channel, 1), // buffered 312 | ext: nil, 313 | typ: typ, 314 | method: method, 315 | id: 0, 316 | err: nil, 317 | } 318 | } 319 | 320 | func (ch *Channel) SetExt(ext interface{}) { 321 | ch.ext = ext 322 | } 323 | 324 | func (ch *Channel) SendErr(code int, message string, data interface{}) error { 325 | return ch.sendMsg(&Message{ 326 | Type: ch.typ, 327 | Method: ch.method, 328 | More: false, 329 | Id: ch.id, 330 | Ext: ch.ext, 331 | Error: &Error{code, message, data}, 332 | }) 333 | } 334 | 335 | // not convenient enough? we'll see 336 | func (ch *Channel) SendLast(obj interface{}) error { 337 | return ch.Send(obj, false) 338 | } 339 | 340 | func (ch *Channel) Send(obj interface{}, more bool) error { 341 | return ch.sendMsg(&Message{ 342 | Type: ch.typ, 343 | Method: ch.method, 344 | Payload: obj, 345 | More: more, 346 | Id: ch.id, 347 | Ext: ch.ext, 348 | }) 349 | } 350 | 351 | func (ch *Channel) sendMsg(msg *Message) error { 352 | frame, err := ch.rpc.codec.Encode(msg) 353 | if err != nil { 354 | return err 355 | } 356 | _, err = ch.conn.Write(frame) 357 | if ch.id == 0 { 358 | ch.done <- ch 359 | close(ch.inbox) // is this bad? 360 | } 361 | return err 362 | } 363 | 364 | func (ch *Channel) Recv(obj interface{}) (bool, error) { 365 | select { 366 | case msg, ok := <-ch.inbox: 367 | if !ok { 368 | return false, ch.err 369 | } 370 | payload := reflect.ValueOf(msg.Payload) 371 | if payload.IsValid() { 372 | reflect.ValueOf(obj).Elem().Set(payload) 373 | } 374 | return msg.More, nil 375 | } 376 | } 377 | 378 | func (ch *Channel) Context() context.Context { 379 | return ch.Peer.ctx 380 | } 381 | 382 | /* 383 | // convenience method to read []byte object 384 | func (ch *Channel) Read(p []byte) (n int, err error) { 385 | 386 | } 387 | 388 | // convenience method to write []byte object 389 | func (ch *Channel) Write(p []byte) (n int, err error) { 390 | 391 | } 392 | */ 393 | -------------------------------------------------------------------------------- /docs/getting-started/python.md: -------------------------------------------------------------------------------- 1 | # Getting Started in Python 2 | 3 | ## Overview 4 | 5 | Here we're going to use Duplex in Python for 3 examples: 6 | 7 | 1. A typical RPC service 8 | 1. A streaming RPC service 9 | 1. A callback RPC service 10 | 11 | Since Duplex peers are both RPC "clients" and "servers", we use the terms *caller* and *provider* when referring to a peer making a call or a peer providing functions, respectively. Client and server can then be used to refer to the transport topology, where the client establishes the connection and the server listens for connections. 12 | 13 | To keep things simple, in these examples we'll stick to the WebSocket client being the only caller, and the WebSocket server being the only provider. Thus resembling your typical RPC client-server scenario. 14 | 15 | ## Installing 16 | 17 | Duplex currently requires Python 3.3 or greater. Our examples will also use several Python libraries including `websockets` and `ws4py`. Install them with `pip3` (assuming you also have Python 2.x. If your primary Python is 3.x, drop the 3 as appropriate): 18 | 19 | ``` 20 | $ pip3 install duplex websockets ws4py 21 | ``` 22 | 23 | ## Typical RPC: Sum Service 24 | 25 | We're going to first write a traditional client and server using WebSockets and JSON that exposes an `add` function. 26 | 27 | ### Async Server 28 | 29 | Make a file called `server.py` like this: 30 | 31 | ```python 32 | import asyncio 33 | import websockets 34 | import duplex 35 | 36 | rpc = duplex.RPC("json") 37 | 38 | @asyncio.coroutine 39 | def add(args, ch): 40 | return sum(args) 41 | 42 | rpc.register_func("example.add", add) 43 | 44 | @asyncio.coroutine 45 | def handle(conn, path): 46 | peer = yield from rpc.accept(conn, route=False) 47 | yield from peer.route() 48 | 49 | start_server = websockets.serve(handle, 'localhost', 3000) 50 | asyncio.get_event_loop().run_until_complete(start_server) 51 | asyncio.get_event_loop().run_forever() 52 | ``` 53 | 54 | That's a lot of code for a simple add function. But most of it is for setting up the `asyncio` WebSocket server. We'd love to take a convenience module PR to simplify this in the future. 55 | 56 | We can run this and it will listen for connections on `localhost:3000`: 57 | 58 | ``` 59 | $ python3 server.py 60 | ``` 61 | 62 | ### Synchronous Interactive Client 63 | 64 | With the server running, let's use the interactive Python shell to connect and interact with our server instead of writing a script. 65 | 66 | Since `asyncio` is great for servers, but not fun to use interactively, we'll need a *different* WebSockets library. We'll use `ws4py` since we have a wrapper for it made specifically for using Duplex from the shell. 67 | 68 | Now let's jump into the shell by running `python3` and importing our modules and making our RPC object: 69 | 70 | ```python 71 | >>> import duplex 72 | >>> import duplex.ws4py 73 | >>> rpc = duplex.RPC("json", async=False) 74 | ``` 75 | 76 | Now we make a WebSocket connection using the builtin wrapper for `ws4py`. Then we give it to our RPC object to perform the Duplex handshake, returning a peer object. 77 | 78 | ```python 79 | >>> conn = duplex.ws4py.Client("ws://localhost:3000") 80 | >>> peer = rpc.handshake(conn) 81 | ``` 82 | 83 | We're ready to make calls! 84 | 85 | ```python 86 | >>> peer.call("example.add", [1,2,3]) 87 | 6 88 | ``` 89 | 90 | Some things to point out: 91 | 1. JSON could be switched with another codec like MessagePack. Just replace `"json"` with `"msgpack"`. 92 | 1. WebSocket could also be switched out, but we currently only have builtin wrappers for WebSocket. 93 | 1. The core implementation is not only neutral to codec and transport, but important for the Python world is being neutral to threaded and evented models. Here we used both. 94 | 95 | So far maybe not so interesting. Let's try the next example. 96 | 97 | ## Streaming RPC: Counting Service 98 | 99 | Next we'll make a streaming service that has a `count` method that streams back incrementing integers every second. Both sides of the connection can stream, but here we're only streaming the reply. 100 | 101 | ### Async Server 102 | 103 | Again, we'll use WebSocket and JSON. In fact, let's just add this streaming method to our existing `server.py`. It should look like this with two new added parts: 104 | 105 | ```python 106 | import asyncio 107 | import websockets 108 | import duplex 109 | 110 | rpc = duplex.RPC("json") 111 | 112 | # NEW 113 | @asyncio.coroutine 114 | def count(args, ch): 115 | n = 0 116 | while True: 117 | n += 1 118 | try: 119 | yield from ch.send(n, more=True) 120 | print(n) 121 | yield from asyncio.sleep(1) 122 | except Exception as e: 123 | print(e) 124 | return 125 | 126 | @asyncio.coroutine 127 | def add(args, ch): 128 | return sum(args) 129 | 130 | rpc.register_func("example.add", add) 131 | rpc.register_func("example.count", count) # NEW 132 | 133 | @asyncio.coroutine 134 | def handle(conn, path): 135 | peer = yield from rpc.accept(conn, route=False) 136 | yield from peer.route() 137 | 138 | start_server = websockets.serve(handle, 'localhost', 3000) 139 | asyncio.get_event_loop().run_until_complete(start_server) 140 | asyncio.get_event_loop().run_forever() 141 | ``` 142 | 143 | Similar to `add`, but a little more complex. We ignore the `args` argument as we expect this to be `None`. This is because the method is not invoked until we send something to it. This will make more sense with the client. 144 | 145 | Also, this time we're using the `ch` argument to work directly with the channel object. This lets us call `ch.send` multiple times to stream back multiple objects. It's necessary to set `more=True` when streaming. 146 | 147 | We can run this and it will listen for connections on `localhost:3000`: 148 | 149 | ``` 150 | $ python3 server.py 151 | ``` 152 | 153 | ### Threaded Client 154 | 155 | To avoid writing a lot of code, we'll write a synchronous threaded client with `ws4py` like before. This time as a daemon script instead of using the interactive Python shell. 156 | 157 | Make a file called `counter.py` like this: 158 | 159 | ```python 160 | import duplex 161 | import duplex.ws4py 162 | 163 | rpc = duplex.RPC("json", async=False) 164 | conn = duplex.ws4py.Client("ws://localhost:3000") 165 | peer = rpc.handshake(conn) 166 | ch = peer.open("example.count") 167 | ch.send(None) 168 | try: 169 | more = True 170 | while more: 171 | num, more = ch.recv() 172 | print(num) 173 | except KeyboardInterrupt: 174 | peer.close() 175 | 176 | ``` 177 | 178 | Pretty straightforward, but notice we do a `ch.send(None)`. This is because as mentioned before, methods are not invoked until a message is sent on a channel. Since we're not using the higher level `peer.call` API, we manually send on the channel to trigger the method. 179 | 180 | Another thing to notice is how we handle streaming responses. `ch.recv()` returns two values: the next object in the stream, and a boolean of whether to expect more values. We use this to loop until the provider is finished, though in this case it never will. 181 | 182 | If we run this, we'll get an incrementing number streamed to us until we `Ctrl-C` to close it down: 183 | 184 | ``` 185 | $ python3 counter.py 186 | 1 187 | 2 188 | 3 189 | ... 190 | ``` 191 | 192 | As mentioned, you can stream from the caller to the provider. Since there is a channel on both ends, the API is the same. Try modifying the `count` function to expect several objects sent to its channel before starting the counter stream. 193 | 194 | At this point the idea of an RPC method or service starts to break down. You could stream objects in either direction, with any order, pattern, or amount. For example, you could return two results instead of streaming many to model a function that returns two values. Working with channels is similar to working with ZeroMQ sockets, letting you build message-based "sub protocols" pretty quickly. 195 | 196 | ## Callback RPC: Alarm Service 197 | 198 | Finally we'll make a callback based service called `alarm` that takes N seconds and a callback, returns `True`, and triggers your callback after N seconds. Both sides of the connection can provide and call callbacks, but here we have the client providing a callback and the server calling it. 199 | 200 | ### Async Server 201 | 202 | Lets keep adding to our `server.py`. It should look like this with two new added parts: 203 | 204 | ```python 205 | import asyncio 206 | import websockets 207 | import duplex 208 | 209 | rpc = duplex.RPC("json") 210 | 211 | # NEW 212 | @asyncio.coroutine 213 | def alarm(args, ch): 214 | seconds = args.get("sec", 0) 215 | name = args.get("cb") 216 | if name is not None: 217 | @asyncio.coroutine 218 | def cb(): 219 | yield from asyncio.sleep(seconds) 220 | yield from ch.call(name, wait=False) 221 | asyncio.ensure_future(cb()) 222 | return True 223 | 224 | @asyncio.coroutine 225 | def count(args, ch): 226 | n = 0 227 | while True: 228 | n += 1 229 | try: 230 | yield from ch.send(n, more=True) 231 | print(n) 232 | yield from asyncio.sleep(1) 233 | except Exception as e: 234 | print(e) 235 | return 236 | 237 | @asyncio.coroutine 238 | def add(args, ch): 239 | return sum(args) 240 | 241 | rpc.register_func("example.add", add) 242 | rpc.register_func("example.count", count) 243 | rpc.register_func("example.alarm", alarm) # NEW 244 | 245 | @asyncio.coroutine 246 | def handle(conn, path): 247 | peer = yield from rpc.accept(conn, route=False) 248 | yield from peer.route() 249 | 250 | start_server = websockets.serve(handle, 'localhost', 3000) 251 | asyncio.get_event_loop().run_until_complete(start_server) 252 | asyncio.get_event_loop().run_forever() 253 | ``` 254 | 255 | Besides the `asyncio` nonsense, this probably reads obviously enough. We get the args object and pull the `sec` value from it, and also the `cb` value, which might not be obvious but is a string. Then we make an actual callback function in Python that sleeps for that many seconds and reuses the `ch` object to call the callback function named by the value of `cb`. We schedule that to run and then we return `True`. 256 | 257 | Now here is a client script that defines a callback, makes the call and waits for the callback. We'll call it `callback.py`: 258 | 259 | ```python 260 | import duplex 261 | import duplex.ws4py 262 | 263 | rpc = duplex.RPC("json", async=False) 264 | 265 | def alarm(args, ch): 266 | print("ALARM") 267 | 268 | conn = duplex.ws4py.Client("ws://localhost:3000") 269 | peer = rpc.handshake(conn) 270 | peer.call("example.alarm", {"sec": 3, "cb": rpc.callback_func(alarm)}) 271 | ``` 272 | 273 | If we run the server and then run our `callback.py` script, it should sit for 3 seconds, and sound the alarm, printing "ALARM". The script sits around instead of exiting because of the thread running in the background. 274 | 275 | There is nothing terribly magic happening here. The fact either side can make calls to the other is enough to implement callbacks, but we have an extra helper you may have noticed: `rpc.callback_func`. This works like `rpc.register_func` except it registers with a generated name and returns that name as a string. You just need to hand that to the other side and it can call that function back by that name whenever it wants. Although we don't here (hence `wait=False`), it can even return a value. 276 | 277 | ## What Next 278 | 279 | Now you can make Python call into remote Python with callbacks and streaming. Next you should try Duplex in another language and try calling into remote Python from Go or JavaScript. 280 | -------------------------------------------------------------------------------- /javascript/test/duplex_spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const { duplex } = require('../dist/duplex.js'); 4 | const btoa = require('btoa'); 5 | const atob = require('atob'); 6 | const testErrorCode = 1000; 7 | const testErrorMessage = "Test message"; 8 | class MockConnection { 9 | constructor() { 10 | this.sent = []; 11 | this.closed = false; 12 | this.pairedWith = null; 13 | this.onrecv = function () { }; 14 | } 15 | close() { 16 | return this.closed = true; 17 | } 18 | send(frame) { 19 | this.sent.push(frame); 20 | if (this.pairedWith) { 21 | return this.pairedWith.onrecv(frame); 22 | } 23 | } 24 | _recv(frame) { 25 | return this.onrecv(frame); 26 | } 27 | } 28 | const connectionPair = function () { 29 | const conn1 = new MockConnection(); 30 | const conn2 = new MockConnection(); 31 | conn1.pairedWith = conn2; 32 | conn2.pairedWith = conn1; 33 | return [conn1, conn2]; 34 | }; 35 | const peerPair = function (rpc, onready) { 36 | let peer2; 37 | const [conn1, conn2] = connectionPair(); 38 | const peer1 = rpc.accept(conn1); 39 | return peer2 = rpc.handshake(conn2, (peer2) => onready(peer1, peer2)); 40 | }; 41 | const handshake = function (codec) { 42 | const p = duplex.protocol; 43 | return `${p.name}/${p.version};${codec}`; 44 | }; 45 | const testServices = { 46 | echo(ch) { 47 | return ch.onrecv = (err, obj) => ch.send(obj); 48 | }, 49 | generator(ch) { 50 | return ch.onrecv = (err, count) => __range__(1, count, true).map((num) => ch.send({ num }, num !== count)); 51 | }, 52 | adder(ch) { 53 | let total = 0; 54 | return ch.onrecv = function (err, num, more) { 55 | total += num; 56 | if (!more) { 57 | return ch.send(total); 58 | } 59 | }; 60 | }, 61 | error(ch) { 62 | return ch.onrecv = () => ch.senderr(testErrorCode, testErrorMessage); 63 | }, 64 | errorAfter2(ch) { 65 | return ch.onrecv = (err, count) => (() => { 66 | const result = []; 67 | for (let num = 1; num <= count; num++) { 68 | ch.send({ num }, num !== count); 69 | if (num === 2) { 70 | ch.senderr(testErrorCode, testErrorMessage); 71 | break; 72 | } 73 | else { 74 | result.push(undefined); 75 | } 76 | } 77 | return result; 78 | })(); 79 | } 80 | }; 81 | const b64json = [ 82 | "b64json", 83 | (obj) => btoa(JSON.stringify(obj)), 84 | (str) => JSON.parse(atob(str)) 85 | ]; 86 | describe("duplex RPC", function () { 87 | it("handshakes", function () { 88 | const conn = new MockConnection(); 89 | const rpc = new duplex.RPC(duplex.JSON); 90 | return rpc.handshake(conn, () => expect(conn.sent[0]).toEqual(handshake("json"))); 91 | }); 92 | it("accepts handshakes", function () { 93 | const conn = new MockConnection(); 94 | const rpc = new duplex.RPC(duplex.JSON); 95 | rpc.accept(conn); 96 | conn._recv(handshake("json")); 97 | return expect(conn.sent[0]).toEqual(duplex.handshake.accept); 98 | }); 99 | it("handles registered function calls after accept", function () { 100 | const conn = new MockConnection(); 101 | const rpc = new duplex.RPC(duplex.JSON); 102 | rpc.register("echo", testServices.echo); 103 | rpc.accept(conn); 104 | conn._recv(handshake("json")); 105 | const req = { 106 | type: duplex.request, 107 | method: "echo", 108 | id: 1, 109 | payload: { 110 | foo: "bar" 111 | } 112 | }; 113 | conn._recv(JSON.stringify(req)); 114 | expect(conn.sent.length).toEqual(2); 115 | return expect(JSON.parse(conn.sent[1])).toEqual({ 116 | type: duplex.reply, 117 | id: 1, 118 | payload: { 119 | foo: "bar" 120 | } 121 | }); 122 | }); 123 | it("handles registered function calls after handshake", function () { 124 | const conn = new MockConnection(); 125 | const rpc = new duplex.RPC(duplex.JSON); 126 | rpc.register("echo", testServices.echo); 127 | const peer = rpc.handshake(conn); 128 | conn._recv(duplex.handshake.accept); 129 | const req = { 130 | type: duplex.request, 131 | method: "echo", 132 | id: 1, 133 | payload: { 134 | foo: "bar" 135 | } 136 | }; 137 | conn._recv(JSON.stringify(req)); 138 | expect(conn.sent.length).toEqual(2); 139 | return expect(JSON.parse(conn.sent[1])).toEqual({ 140 | type: duplex.reply, 141 | id: 1, 142 | payload: { 143 | foo: "bar" 144 | } 145 | }); 146 | }); 147 | it("calls remote peer functions after handshake", function (done) { 148 | const conn = new MockConnection(); 149 | const rpc = new duplex.RPC(duplex.JSON); 150 | const peer = rpc.handshake(conn); 151 | conn._recv(duplex.handshake.accept); 152 | const args = { foo: "bar" }; 153 | const reply = { baz: "qux" }; 154 | peer.call("callAfterHandshake", args, function (err, rep) { 155 | expect(rep).toEqual(reply); 156 | done(); 157 | }); 158 | conn._recv(JSON.stringify({ 159 | type: duplex.reply, 160 | id: 1, 161 | payload: reply 162 | })); 163 | }); 164 | it("calls remote peer functions after accept", function (done) { 165 | const conn = new MockConnection(); 166 | const rpc = new duplex.RPC(duplex.JSON); 167 | const peer = rpc.accept(conn); 168 | conn._recv(handshake("json")); 169 | const args = { foo: "bar" }; 170 | const reply = { baz: "qux" }; 171 | peer.call("callAfterAccept", args, function (err, rep) { 172 | expect(rep).toEqual(reply); 173 | done(); 174 | }); 175 | conn._recv(JSON.stringify({ 176 | type: duplex.reply, 177 | id: 1, 178 | payload: reply 179 | })); 180 | }); 181 | it("can do all handshake, accept, call, and handle", function (done) { 182 | const [conn1, conn2] = connectionPair(); 183 | const rpc = new duplex.RPC(duplex.JSON); 184 | rpc.register("echo-tag", (ch) => ch.onrecv = function (err, obj) { 185 | obj.tag = true; 186 | return ch.send(obj); 187 | }); 188 | let ready = 0; 189 | let [peer1, peer2] = [null, null]; 190 | peer1 = rpc.accept(conn1, () => ready++); 191 | peer2 = rpc.handshake(conn2, () => ready++); 192 | let replies = 0; 193 | setTimeout(function () { 194 | peer1.call("echo-tag", { from: "peer1" }, function (err, rep) { 195 | expect(rep).toEqual({ from: "peer1", tag: true }); 196 | replies++; 197 | if (replies === 2) { 198 | done(); 199 | } 200 | }); 201 | peer2.call("echo-tag", { from: "peer2" }, function (err, rep) { 202 | expect(rep).toEqual({ from: "peer2", tag: true }); 203 | replies++; 204 | if (replies === 2) { 205 | done(); 206 | } 207 | }); 208 | }, 1); 209 | }); 210 | it("streams multiple results", function (done) { 211 | const rpc = new duplex.RPC(duplex.JSON); 212 | rpc.register("count", testServices.generator); 213 | let count = 0; 214 | return peerPair(rpc, (client, _) => client.call("count", 5, function (err, rep) { 215 | count += rep.num; 216 | if (count === 15) { 217 | return done(); 218 | } 219 | })); 220 | }); 221 | it("streams multiple arguments", function (done) { 222 | const rpc = new duplex.RPC(duplex.JSON); 223 | rpc.register("adder", testServices.adder); 224 | return peerPair(rpc, function (client, _) { 225 | const ch = client.open("adder"); 226 | ch.onrecv = function (err, total) { 227 | expect(total).toEqual(15); 228 | return done(); 229 | }; 230 | return [1, 2, 3, 4, 5].map((num) => ch.send(num, num !== 5)); 231 | }); 232 | }); 233 | it("supports other codecs for serialization", function (done) { 234 | const rpc = new duplex.RPC(b64json); 235 | rpc.register("echo", testServices.echo); 236 | return peerPair(rpc, (client, server) => client.call("echo", { foo: "bar" }, function (err, rep) { 237 | expect(rep).toEqual({ foo: "bar" }); 238 | return done(); 239 | })); 240 | }); 241 | it("maintains optional ext from request to reply", function (done) { 242 | const rpc = new duplex.RPC(duplex.JSON); 243 | rpc.register("echo", testServices.echo); 244 | return peerPair(rpc, function (client, server) { 245 | const ch = client.open("echo"); 246 | ch.ext = { "hidden": "metadata" }; 247 | ch.onrecv = function (err, reply) { 248 | expect(reply).toEqual({ "foo": "bar" }); 249 | expect(JSON.parse(server.conn.sent[1])["ext"]) 250 | .toEqual({ "hidden": "metadata" }); 251 | return done(); 252 | }; 253 | return ch.send({ "foo": "bar" }, false); 254 | }); 255 | }); 256 | it("registers func for traditional RPC methods and callbacks", function (done) { 257 | const rpc = new duplex.RPC(duplex.JSON); 258 | rpc.registerFunc("callback", (args, reply, ch) => ch.call(args[0], args[1], (err, cbReply) => reply(cbReply))); 259 | return peerPair(rpc, function (client, server) { 260 | const upper = rpc.callbackFunc((s, r) => r(s.toUpperCase())); 261 | return client.call("callback", [upper, "hello"], function (err, rep) { 262 | expect(rep).toEqual("HELLO"); 263 | return done(); 264 | }); 265 | }); 266 | }); 267 | it("lets handlers return error", function (done) { 268 | const rpc = new duplex.RPC(duplex.JSON); 269 | rpc.register("error", testServices.error); 270 | return peerPair(rpc, (client, server) => client.call("error", { foo: "bar" }, function (err, rep) { 271 | expect(err["code"]).toEqual(testErrorCode); 272 | expect(err["message"]).toEqual(testErrorMessage); 273 | return done(); 274 | })); 275 | }); 276 | return it("lets handlers return error mid-stream", function (done) { 277 | const rpc = new duplex.RPC(duplex.JSON); 278 | rpc.register("count", testServices.errorAfter2); 279 | let count = 0; 280 | return peerPair(rpc, (client, server) => client.call("count", 5, function (err, rep) { 281 | if (err != null) { 282 | expect(err["code"]).toEqual(testErrorCode); 283 | expect(err["message"]).toEqual(testErrorMessage); 284 | expect(count).toEqual(2); 285 | done(); 286 | } 287 | return count += 1; 288 | })); 289 | }); 290 | }); 291 | function __range__(left, right, inclusive) { 292 | let range = []; 293 | let ascending = left < right; 294 | let end = !inclusive ? right : ascending ? right + 1 : right - 1; 295 | for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { 296 | range.push(i); 297 | } 298 | return range; 299 | } 300 | //# sourceMappingURL=duplex_spec.js.map -------------------------------------------------------------------------------- /javascript/test/duplex_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export {} 4 | 5 | const { duplex } = require('../dist/duplex.js'); 6 | const btoa = require('btoa'); 7 | const atob = require('atob'); 8 | 9 | const testErrorCode: number = 1000; 10 | const testErrorMessage: string = "Test message"; 11 | 12 | class MockConnection { 13 | constructor() { 14 | this.sent = []; 15 | this.closed = false; 16 | this.pairedWith = null; 17 | this.onrecv = function() {}; 18 | } 19 | 20 | sent: Array; 21 | closed: boolean; 22 | pairedWith: MockConnection; 23 | onrecv: (frame?: string) => object | void; 24 | 25 | close(): boolean { 26 | return this.closed = true; 27 | } 28 | 29 | send(frame: string): object | void { 30 | this.sent.push(frame); 31 | if (this.pairedWith) { 32 | return this.pairedWith.onrecv(frame); 33 | } 34 | } 35 | 36 | _recv(frame: string): object | void { 37 | return this.onrecv(frame); 38 | } 39 | } 40 | 41 | const connectionPair = function(): Array { 42 | const conn1 = new MockConnection(); 43 | const conn2 = new MockConnection(); 44 | conn1.pairedWith = conn2; 45 | conn2.pairedWith = conn1; 46 | return [conn1, conn2]; 47 | }; 48 | 49 | const peerPair = function(rpc: Duplex.RPC, onready: (p1: Duplex.Peer, p2: Duplex.Peer) => object | void): Duplex.Peer { 50 | let peer2; 51 | const [conn1, conn2] = connectionPair(); 52 | const peer1 = rpc.accept(conn1); 53 | return peer2 = rpc.handshake(conn2, (peer2: Duplex.Peer) => onready(peer1, peer2)); 54 | }; 55 | 56 | const handshake = function(codec: string): string { 57 | const p = duplex.protocol; 58 | return `${p.name}/${p.version};${codec}`; 59 | }; 60 | 61 | const testServices = { 62 | echo(ch: Duplex.Channel): any { 63 | return ch.onrecv = (err: object, obj: object) => ch.send(obj); 64 | }, 65 | 66 | generator(ch: Duplex.Channel): any { 67 | return ch.onrecv = (err: object, count: number) => 68 | __range__(1, count, true).map((num) => 69 | ch.send({num}, num !== count) 70 | ); 71 | }, 72 | 73 | adder(ch: Duplex.Channel): any { 74 | let total = 0; 75 | return ch.onrecv = function(err: object, num: number, more: boolean) { 76 | total += num; 77 | if (!more) { 78 | return ch.send(total); 79 | } 80 | }; 81 | }, 82 | 83 | error(ch: Duplex.Channel): any { 84 | return ch.onrecv = () => ch.senderr(testErrorCode, testErrorMessage); 85 | }, 86 | 87 | errorAfter2(ch: Duplex.Channel): any { 88 | return ch.onrecv = (err: object, count: number) => 89 | ((): Array => { 90 | const result = []; 91 | for (let num = 1; num <= count; num++) { 92 | ch.send({num}, num !== count); 93 | if (num === 2) { 94 | ch.senderr(testErrorCode, testErrorMessage); 95 | break; 96 | } else { 97 | result.push(undefined); 98 | } 99 | } 100 | return result; 101 | })() 102 | ; 103 | } 104 | }; 105 | 106 | const b64json = [ 107 | "b64json", 108 | (obj: object) => btoa(JSON.stringify(obj)), 109 | (str: string) => JSON.parse(atob(str)) 110 | ]; 111 | 112 | describe("duplex RPC", function() { 113 | it("handshakes", function() { 114 | const conn = new MockConnection(); 115 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 116 | return rpc.handshake(conn, () => expect(conn.sent[0]).toEqual(handshake("json"))); 117 | }); 118 | 119 | it("accepts handshakes", function() { 120 | const conn = new MockConnection(); 121 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 122 | rpc.accept(conn); 123 | conn._recv(handshake("json")); 124 | return expect(conn.sent[0]).toEqual(duplex.handshake.accept); 125 | }); 126 | 127 | it("handles registered function calls after accept", function() { 128 | const conn = new MockConnection(); 129 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 130 | rpc.register("echo", testServices.echo); 131 | rpc.accept(conn); 132 | conn._recv(handshake("json")); 133 | const req = { 134 | type: duplex.request, 135 | method: "echo", 136 | id: 1, 137 | payload: { 138 | foo: "bar" 139 | } 140 | }; 141 | conn._recv(JSON.stringify(req)); 142 | expect(conn.sent.length).toEqual(2); 143 | return expect(JSON.parse(conn.sent[1])).toEqual({ 144 | type: duplex.reply, 145 | id: 1, 146 | payload: { 147 | foo: "bar" 148 | } 149 | }); 150 | }); 151 | 152 | it("handles registered function calls after handshake", function() { 153 | const conn = new MockConnection(); 154 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 155 | rpc.register("echo", testServices.echo); 156 | const peer = rpc.handshake(conn); 157 | conn._recv(duplex.handshake.accept); 158 | const req = { 159 | type: duplex.request, 160 | method: "echo", 161 | id: 1, 162 | payload: { 163 | foo: "bar" 164 | } 165 | }; 166 | conn._recv(JSON.stringify(req)); 167 | expect(conn.sent.length).toEqual(2); 168 | return expect(JSON.parse(conn.sent[1])).toEqual({ 169 | type: duplex.reply, 170 | id: 1, 171 | payload: { 172 | foo: "bar" 173 | } 174 | }); 175 | }); 176 | 177 | it("calls remote peer functions after handshake", function(done: () => void) { 178 | const conn = new MockConnection(); 179 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 180 | const peer = rpc.handshake(conn); 181 | conn._recv(duplex.handshake.accept); 182 | const args = 183 | {foo: "bar"}; 184 | const reply = 185 | {baz: "qux"}; 186 | peer.call("callAfterHandshake", args, function(err: object, rep: object) { 187 | expect(rep).toEqual(reply); 188 | done(); 189 | }); 190 | conn._recv(JSON.stringify({ 191 | type: duplex.reply, 192 | id: 1, 193 | payload: reply 194 | }) 195 | ); 196 | }); 197 | 198 | it("calls remote peer functions after accept", function(done: () => void) { 199 | const conn = new MockConnection(); 200 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 201 | const peer = rpc.accept(conn); 202 | conn._recv(handshake("json")); 203 | const args = 204 | {foo: "bar"}; 205 | const reply = 206 | {baz: "qux"}; 207 | peer.call("callAfterAccept", args, function(err: object, rep: object) { 208 | expect(rep).toEqual(reply); 209 | done(); 210 | }); 211 | conn._recv(JSON.stringify({ 212 | type: duplex.reply, 213 | id: 1, 214 | payload: reply 215 | }) 216 | ); 217 | }); 218 | 219 | it("can do all handshake, accept, call, and handle", function(done: () => void) { 220 | const [conn1, conn2] = connectionPair(); 221 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 222 | rpc.register("echo-tag", (ch: Duplex.Channel) => 223 | ch.onrecv = function(err: object, obj: object) { 224 | (obj).tag = true; 225 | return ch.send(obj); 226 | } 227 | ); 228 | let ready = 0; 229 | let [peer1, peer2]: Array = [null, null]; 230 | peer1 = rpc.accept(conn1, () => ready++); 231 | peer2 = rpc.handshake(conn2, () => ready++); 232 | let replies = 0; 233 | setTimeout(function() { 234 | peer1.call("echo-tag", {from: "peer1"}, function(err: object, rep: object) { 235 | expect(rep).toEqual({from: "peer1", tag: true}); 236 | replies++; 237 | if (replies === 2) { 238 | done(); 239 | } 240 | }); 241 | peer2.call("echo-tag", {from: "peer2"}, function(err: object, rep: object) { 242 | expect(rep).toEqual({from: "peer2", tag: true}); 243 | replies++; 244 | if (replies === 2) { 245 | done(); 246 | } 247 | }); 248 | }, 1); 249 | }); 250 | 251 | it("streams multiple results", function(done: () => any) { 252 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 253 | rpc.register("count", testServices.generator); 254 | let count = 0; 255 | return peerPair(rpc, (client: Duplex.Peer, _: Duplex.Peer) => 256 | client.call("count", 5, function(err: object, rep: object) { 257 | count += (rep).num; 258 | if (count === 15) { 259 | return done(); 260 | } 261 | }) 262 | ); 263 | }); 264 | 265 | it("streams multiple arguments", function(done: () => any) { 266 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 267 | rpc.register("adder", testServices.adder); 268 | return peerPair(rpc, function(client: Duplex.Peer, _: Duplex.Peer) { 269 | const ch = client.open("adder"); 270 | ch.onrecv = function(err: object, total: number) { 271 | expect(total).toEqual(15); 272 | return done(); 273 | }; 274 | return [1, 2, 3, 4, 5].map((num) => 275 | ch.send(num, num !== 5)); 276 | }); 277 | }); 278 | 279 | it("supports other codecs for serialization", function(done: () => any) { 280 | const rpc: Duplex.RPC = new duplex.RPC(b64json); 281 | rpc.register("echo", testServices.echo); 282 | return peerPair(rpc, (client: Duplex.Peer, server: Duplex.Peer) => 283 | client.call("echo", {foo: "bar"}, function(err: object, rep: object) { 284 | expect(rep).toEqual({foo: "bar"}); 285 | return done(); 286 | }) 287 | ); 288 | }); 289 | 290 | it("maintains optional ext from request to reply", function(done: () => any) { 291 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 292 | rpc.register("echo", testServices.echo); 293 | return peerPair(rpc, function(client: Duplex.Peer, server: Duplex.Peer) { 294 | const ch = client.open("echo"); 295 | ch.ext = {"hidden": "metadata"}; 296 | ch.onrecv = function(err: object, reply: object) { 297 | expect(reply).toEqual({"foo": "bar"}); 298 | expect(JSON.parse(server.conn.sent[1])["ext"]) 299 | .toEqual({"hidden": "metadata"}); 300 | return done(); 301 | }; 302 | return ch.send({"foo": "bar"}, false); 303 | }); 304 | }); 305 | 306 | it("registers func for traditional RPC methods and callbacks", function(done: () => any) { 307 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 308 | rpc.registerFunc("callback", (args: Array, reply: (cbReply: string) => any | void, ch: Duplex.Channel) => 309 | ch.call(args[0], args[1], (err: object, cbReply: string) => reply(cbReply)) 310 | ); 311 | return peerPair(rpc, function(client: Duplex.Peer, server: Duplex.Peer) { 312 | const upper = rpc.callbackFunc((s: string, r: (s:string) => any) => r(s.toUpperCase())); 313 | return client.call("callback", [upper, "hello"], function(err: object, rep: string) { 314 | expect(rep).toEqual("HELLO"); 315 | return done(); 316 | }); 317 | }); 318 | }); 319 | 320 | interface Error { 321 | code: number, 322 | message: string 323 | } 324 | 325 | it("lets handlers return error", function(done: () => any) { 326 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 327 | rpc.register("error", testServices.error); 328 | return peerPair(rpc, (client: Duplex.Peer, server: Duplex.Peer) => 329 | client.call("error", {foo: "bar"}, function(err: Error, rep: any) { 330 | expect(err["code"]).toEqual(testErrorCode); 331 | expect(err["message"]).toEqual(testErrorMessage); 332 | return done(); 333 | }) 334 | ); 335 | }); 336 | 337 | return it("lets handlers return error mid-stream", function(done: () => any) { 338 | const rpc: Duplex.RPC = new duplex.RPC(duplex.JSON); 339 | rpc.register("count", testServices.errorAfter2); 340 | let count = 0; 341 | return peerPair(rpc, (client: Duplex.Peer, server: Duplex.Peer) => 342 | client.call("count", 5, function(err: Error, rep: string) { 343 | if (err != null) { 344 | expect(err["code"]).toEqual(testErrorCode); 345 | expect(err["message"]).toEqual(testErrorMessage); 346 | expect(count).toEqual(2); 347 | done(); 348 | } 349 | return count += 1; 350 | }) 351 | ); 352 | }); 353 | }); 354 | 355 | function __range__(left: any, right: any, inclusive: any): Array { 356 | let range = []; 357 | let ascending = left < right; 358 | let end = !inclusive ? right : ascending ? right + 1 : right - 1; 359 | for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { 360 | range.push(i); 361 | } 362 | return range; 363 | } 364 | -------------------------------------------------------------------------------- /javascript/dist/duplex.js: -------------------------------------------------------------------------------- 1 | var Duplex; 2 | (function (Duplex) { 3 | Duplex.version = "0.1.0"; 4 | Duplex.protocol = { 5 | name: "SIMPLEX", 6 | version: "1.0" 7 | }; 8 | Duplex.request = "req"; 9 | Duplex.reply = "rep"; 10 | Duplex.handshake = { 11 | accept: "+OK" 12 | }; 13 | Duplex.Json = [ 14 | "json", 15 | JSON.stringify, 16 | JSON.parse 17 | ]; 18 | Duplex.wrap = { 19 | "websocket"(ws) { 20 | const conn = { 21 | send(msg) { return ws.send(msg); }, 22 | close() { return ws.close(); } 23 | }; 24 | ws.onmessage = (event) => conn.onrecv(event.data); 25 | return conn; 26 | }, 27 | "nodejs-websocket"(ws) { 28 | const conn = { 29 | send(msg) { return ws.send(msg); }, 30 | close() { return ws.close(); } 31 | }; 32 | ws.on("text", (msg) => conn.onrecv(msg)); 33 | return conn; 34 | } 35 | }; 36 | ; 37 | class RPC { 38 | constructor(codec) { 39 | this.codec = codec; 40 | this.encode = this.codec[1]; 41 | this.decode = this.codec[2]; 42 | this.registered = {}; 43 | } 44 | register(method, handler) { 45 | return this.registered[method] = handler; 46 | } 47 | unregister(method) { 48 | return delete this.registered[method]; 49 | } 50 | registerFunc(method, func) { 51 | return this.register(method, (ch) => ch.onrecv = (err, args) => func(args, ((reply, more) => { if (more == null) { 52 | more = false; 53 | } return ch.send(reply, more); }), ch)); 54 | } 55 | callbackFunc(func) { 56 | const name = `_callback.${UUIDv4()}`; 57 | this.registerFunc(name, func); 58 | return name; 59 | } 60 | _handshake() { 61 | const p = Duplex.protocol; 62 | return `${p.name}/${p.version};${this.codec[0]}`; 63 | } 64 | handshake(conn, onready) { 65 | const peer = new Duplex.Peer(this, conn, onready); 66 | conn.onrecv = function (data) { 67 | if (data[0] === "+") { 68 | conn.onrecv = peer.onrecv; 69 | return peer._ready(peer); 70 | } 71 | else { 72 | return assert(`Bad handshake: ${data}`, false); 73 | } 74 | }; 75 | conn.send(this._handshake()); 76 | return peer; 77 | } 78 | accept(conn, onready) { 79 | const peer = new Duplex.Peer(this, conn, onready); 80 | conn.onrecv = function (data) { 81 | conn.onrecv = peer.onrecv; 82 | conn.send(Duplex.handshake.accept); 83 | return peer._ready(peer); 84 | }; 85 | return peer; 86 | } 87 | } 88 | Duplex.RPC = RPC; 89 | ; 90 | ; 91 | class Peer { 92 | constructor(rpc, conn, onready) { 93 | this.rpc = rpc; 94 | this.conn = conn; 95 | this.onrecv = this.onrecv.bind(this); 96 | if (onready == null) { 97 | onready = function ({}) { }; 98 | } 99 | this.onready = onready; 100 | assert("Peer expects an RPC", this.rpc.constructor.name === "RPC"); 101 | assert("Peer expects a connection", (this.conn != null)); 102 | this.lastId = 0; 103 | this.ext = null; 104 | this.reqChan = {}; 105 | this.repChan = {}; 106 | } 107 | _ready(peer) { 108 | return this.onready(peer); 109 | } 110 | close() { 111 | return this.conn.close(); 112 | } 113 | call(method, args, callback) { 114 | const ch = new Duplex.Channel(this, Duplex.request, method, this.ext); 115 | if (callback != null) { 116 | ch.id = ++this.lastId; 117 | ch.onrecv = callback; 118 | this.repChan[ch.id] = ch; 119 | } 120 | return ch.send(args); 121 | } 122 | open(method, callback) { 123 | const ch = new Duplex.Channel(this, Duplex.request, method, this.ext); 124 | ch.id = ++this.lastId; 125 | this.repChan[ch.id] = ch; 126 | if (callback != null) { 127 | ch.onrecv = callback; 128 | } 129 | return ch; 130 | } 131 | onrecv(frame) { 132 | let ch; 133 | if (frame === "") { 134 | return; 135 | } 136 | const msg = this.rpc.decode(frame); 137 | switch (msg.type) { 138 | case Duplex.request: 139 | if (this.reqChan[msg.id] != null) { 140 | ch = this.reqChan[msg.id]; 141 | if (msg.more === false) { 142 | delete this.reqChan[msg.id]; 143 | } 144 | } 145 | else { 146 | ch = new Duplex.Channel(this, Duplex.reply, msg.method); 147 | if (msg.id !== undefined) { 148 | ch.id = msg.id; 149 | if (msg.more === true) { 150 | this.reqChan[ch.id] = ch; 151 | } 152 | } 153 | assert("Method not registerd", (this.rpc.registered[msg.method] != null)); 154 | this.rpc.registered[msg.method](ch); 155 | } 156 | if (msg.ext != null) { 157 | ch.ext = msg.ext; 158 | } 159 | return ch.onrecv(null, msg.payload, msg.more); 160 | case Duplex.reply: 161 | if (msg.error != null) { 162 | if (this.repChan[msg.id] != null) { 163 | this.repChan[msg.id].onrecv(msg.error); 164 | } 165 | return delete this.repChan[msg.id]; 166 | } 167 | else { 168 | if (this.repChan[msg.id] != null) { 169 | this.repChan[msg.id].onrecv(null, msg.payload, msg.more); 170 | } 171 | if (msg.more === false) { 172 | return delete this.repChan[msg.id]; 173 | } 174 | } 175 | break; 176 | default: 177 | return assert("Invalid message", false); 178 | } 179 | } 180 | } 181 | Duplex.Peer = Peer; 182 | ; 183 | class Channel { 184 | constructor(peer, type, method, ext) { 185 | this.peer = peer; 186 | this.type = type; 187 | this.method = method; 188 | this.ext = ext; 189 | assert("Channel expects Peer", this.peer.constructor.name === "Peer"); 190 | this.id = null; 191 | this.onrecv = function () { }; 192 | } 193 | call(method, args, callback) { 194 | const ch = this.peer.open(method, callback); 195 | ch.ext = this.ext; 196 | return ch.send(args); 197 | } 198 | close() { 199 | return this.peer.close(); 200 | } 201 | open(method, callback) { 202 | const ch = this.peer.open(method, callback); 203 | ch.ext = this.ext; 204 | return ch; 205 | } 206 | send(payload, more) { 207 | if (more == null) { 208 | more = false; 209 | } 210 | switch (this.type) { 211 | case Duplex.request: 212 | return this.peer.conn.send(this.peer.rpc.encode(requestMsg(payload, this.method, this.id, more, this.ext))); 213 | case Duplex.reply: 214 | return this.peer.conn.send(this.peer.rpc.encode(replyMsg(this.id, payload, more, this.ext))); 215 | default: 216 | return assert("Bad channel type", false); 217 | } 218 | } 219 | senderr(code, message, data) { 220 | assert("Not reply channel", this.type === Duplex.reply); 221 | return this.peer.conn.send(this.peer.rpc.encode(errorMsg(this.id, code, message, data, this.ext))); 222 | } 223 | } 224 | Duplex.Channel = Channel; 225 | ; 226 | class API { 227 | constructor(endpoint) { 228 | const parts = endpoint.split(":"); 229 | assert("Invalid endpoint", parts.length > 1); 230 | const scheme = parts.shift(); 231 | let [protocol, codec] = scheme.split("+", 2); 232 | if (codec == null) { 233 | codec = "json"; 234 | } 235 | assert("JSON is only supported codec", codec === "json"); 236 | parts.unshift(protocol); 237 | const url = parts.join(":"); 238 | this.queued = []; 239 | this.rpc = new Duplex.RPC(Duplex.Json); 240 | var connect = (url) => { 241 | this.ws = new WebSocket(url); 242 | this.ws.onopen = () => { 243 | return this.rpc.handshake(Duplex.wrap.websocket(this.ws), (p) => { 244 | this.peer = p; 245 | return this.queued.map((args) => ((args) => this.call(...args || []))(args)); 246 | }); 247 | }; 248 | return this.ws.onclose = () => { 249 | return setTimeout((() => connect(url)), 2000); 250 | }; 251 | }; 252 | connect(url); 253 | } 254 | call(...args) { 255 | console.log(args); 256 | if (this.peer != null) { 257 | return this.peer.call(...args || []); 258 | } 259 | else { 260 | return this.queued.push(args); 261 | } 262 | } 263 | } 264 | Duplex.API = API; 265 | ; 266 | })(Duplex || (Duplex = {})); 267 | ; 268 | ; 269 | ; 270 | const assert = function (description, condition) { 271 | if (condition == null) { 272 | condition = false; 273 | } 274 | if (!condition) { 275 | throw Error(`Assertion: ${description}`); 276 | } 277 | }; 278 | const requestMsg = function (payload, method, id, more, ext) { 279 | const msg = { 280 | type: Duplex.request, 281 | method, 282 | payload 283 | }; 284 | if (id != null) { 285 | msg.id = id; 286 | } 287 | if (more === true) { 288 | msg.more = more; 289 | } 290 | if (ext != null) { 291 | msg.ext = ext; 292 | } 293 | return msg; 294 | }; 295 | const replyMsg = function (id, payload, more, ext) { 296 | const msg = { 297 | type: Duplex.reply, 298 | id, 299 | payload 300 | }; 301 | if (more === true) { 302 | msg.more = more; 303 | } 304 | if (ext != null) { 305 | msg.ext = ext; 306 | } 307 | return msg; 308 | }; 309 | const errorMsg = function (id, code, message, data, ext) { 310 | const msg = { 311 | type: Duplex.reply, 312 | id, 313 | error: { 314 | code, 315 | message 316 | } 317 | }; 318 | if (data != null) { 319 | msg.error.data = data; 320 | } 321 | if (ext != null) { 322 | msg.ext = ext; 323 | } 324 | return msg; 325 | }; 326 | const UUIDv4 = function () { 327 | let d = new Date().getTime(); 328 | if (typeof __guard__(typeof window !== 'undefined' && window !== null ? window.performance : undefined, (x) => x.now) === "function") { 329 | d += performance.now(); 330 | } 331 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 332 | let r = ((d + (Math.random() * 16)) % 16) | 0; 333 | d = Math.floor(d / 16); 334 | if (c !== 'x') { 335 | r = (r & 0x3) | 0x8; 336 | } 337 | return r.toString(16); 338 | }); 339 | }; 340 | let duplexInstance = { 341 | version: Duplex.version, 342 | protocol: Duplex.protocol, 343 | request: Duplex.request, 344 | reply: Duplex.reply, 345 | handshake: Duplex.handshake, 346 | JSON: Duplex.Json, 347 | wrap: Duplex.wrap, 348 | RPC: Duplex.RPC, 349 | Peer: Duplex.Peer, 350 | Channel: Duplex.Channel, 351 | API: Duplex.API 352 | }; 353 | if (typeof window !== 'undefined' && window !== null) { 354 | window.duplex = duplexInstance; 355 | } 356 | else { 357 | exports.duplex = duplexInstance; 358 | } 359 | function __guard__(value, transform) { 360 | return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; 361 | } 362 | //# sourceMappingURL=duplex.js.map -------------------------------------------------------------------------------- /python/duplex/test_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | import json 4 | import base64 5 | 6 | from . import RPC 7 | from . import protocol 8 | 9 | try: 10 | spawn = asyncio.ensure_future 11 | except: 12 | spawn = asyncio.async 13 | 14 | class WaitGroup(object): 15 | def __init__(self, loop): 16 | self.loop = loop 17 | self.total = [] 18 | self.pending = [] 19 | 20 | def add(self, incr=1): 21 | for n in range(incr): 22 | f = asyncio.Future(loop=self.loop) 23 | self.total.append(f) 24 | self.pending.append(f) 25 | 26 | def done(self): 27 | if len(self.pending) == 0: 28 | return 29 | f = self.pending.pop() 30 | f.set_result(True) 31 | 32 | @asyncio.coroutine 33 | def wait(self): 34 | yield from asyncio.wait(self.total) 35 | 36 | 37 | class MockConnection(object): 38 | def __init__(self, loop): 39 | self.sent = [] 40 | self.closed = False 41 | self.pairedWith = None 42 | self.expectedSends = WaitGroup(loop) 43 | self.expectedRecvs = WaitGroup(loop) 44 | self.inbox = asyncio.Queue(loop=loop) 45 | 46 | @asyncio.coroutine 47 | def close(self): 48 | self.closed = True 49 | 50 | @asyncio.coroutine 51 | def send(self, frame): 52 | self.sent.append(frame) 53 | if self.pairedWith: 54 | yield from self.pairedWith.inbox.put(frame) 55 | self.expectedSends.done() 56 | 57 | 58 | @asyncio.coroutine 59 | def recv(self): 60 | self.expectedRecvs.done() 61 | frame = yield from self.inbox.get() 62 | return frame 63 | 64 | def connection_pair(loop): 65 | conn1 = MockConnection(loop) 66 | conn2 = MockConnection(loop) 67 | conn1.pairedWith = conn2 68 | conn2.pairedWith = conn1 69 | return [conn1, conn2] 70 | 71 | @asyncio.coroutine 72 | def peer_pair(loop, rpc): 73 | conn1, conn2 = connection_pair(loop) 74 | tasks = [ 75 | spawn(rpc.accept(conn1, False), loop=loop), 76 | spawn(rpc.handshake(conn2, False), loop=loop), 77 | ] 78 | yield from asyncio.wait(tasks) 79 | return [tasks[0].result(), tasks[1].result()] 80 | 81 | def handshake(codec): 82 | return "{0}/{1};{2}".format( 83 | protocol.name, 84 | protocol.version, 85 | codec) 86 | 87 | @asyncio.coroutine 88 | def echo(ch): 89 | obj, _ = yield from ch.recv() 90 | yield from ch.send(obj) 91 | 92 | 93 | class DuplexAsyncTests(unittest.TestCase): 94 | 95 | def setUp(self): 96 | self.loop = asyncio.new_event_loop() 97 | asyncio.set_event_loop(self.loop) 98 | self.close = [self.loop] 99 | 100 | def tearDown(self): 101 | for obj in self.close: 102 | obj.close() 103 | 104 | def spawn(self, *args, **kwargs): 105 | kwargs['loop'] = self.loop 106 | return spawn(*args, **kwargs) 107 | 108 | def async(f): 109 | def wrapper(*args, **kwargs): 110 | coro = asyncio.coroutine(f) 111 | future = coro(*args, **kwargs) 112 | args[0].loop.run_until_complete( 113 | asyncio.wait_for(future, 5, loop=args[0].loop)) 114 | return wrapper 115 | 116 | @async 117 | def test_handshake(self): 118 | conn = MockConnection(self.loop) 119 | rpc = RPC("json", self.loop) 120 | yield from conn.inbox.put(protocol.handshake.accept) 121 | yield from rpc.handshake(conn, False) 122 | yield from conn.close() 123 | self.assertEqual(conn.sent[0], handshake("json")) 124 | 125 | @async 126 | def test_accept(self): 127 | conn = MockConnection(self.loop) 128 | rpc = RPC("json", self.loop) 129 | yield from conn.inbox.put(handshake("json")) 130 | yield from rpc.accept(conn, False) 131 | yield from conn.close() 132 | self.assertEqual(conn.sent[0], protocol.handshake.accept) 133 | 134 | @async 135 | def test_registered_func_after_accept(self): 136 | conn = MockConnection(self.loop) 137 | conn.expectedSends.add(2) 138 | rpc = RPC("json", self.loop) 139 | rpc.register("echo", echo) 140 | yield from conn.inbox.put(handshake("json")) 141 | peer = yield from rpc.accept(conn, False) 142 | req = { 143 | 'type': protocol.types.request, 144 | 'method': "echo", 145 | 'id': 1, 146 | 'payload': {"foo": "bar"} 147 | } 148 | frame = json.dumps(req) 149 | yield from conn.inbox.put(frame) 150 | yield from peer.route(1) 151 | yield from conn.expectedSends.wait() 152 | yield from conn.close() 153 | self.assertEqual(len(conn.sent), 2) 154 | 155 | @async 156 | def test_registered_func_after_handshake(self): 157 | conn = MockConnection(self.loop) 158 | conn.expectedSends.add(2) 159 | rpc = RPC("json", self.loop) 160 | rpc.register("echo", echo) 161 | yield from conn.inbox.put(protocol.handshake.accept) 162 | peer = yield from rpc.handshake(conn, False) 163 | req = { 164 | 'type': protocol.types.request, 165 | 'method': "echo", 166 | 'id': 1, 167 | 'payload': {"foo": "bar"} 168 | } 169 | frame = json.dumps(req) 170 | yield from conn.inbox.put(frame) 171 | yield from peer.route(1) 172 | yield from conn.expectedSends.wait() 173 | yield from conn.close() 174 | self.assertEqual(len(conn.sent), 2) 175 | 176 | @async 177 | def test_call_after_handshake(self): 178 | conn = MockConnection(self.loop) 179 | conn.expectedSends.add(2) 180 | rpc = RPC("json", self.loop) 181 | yield from conn.inbox.put(protocol.handshake.accept) 182 | peer = yield from rpc.handshake(conn, False) 183 | args = {"foo": "bar"} 184 | expectedReply = {"baz": "qux"} 185 | frame = json.dumps({ 186 | "type": protocol.types.reply, 187 | "id": 1, 188 | "payload": expectedReply, 189 | }) 190 | @asyncio.coroutine 191 | def inject_frame(): 192 | yield from conn.expectedSends.wait() 193 | yield from conn.inbox.put(frame) 194 | tasks = [ 195 | self.spawn(peer.call("foobar", args)), 196 | self.spawn(inject_frame()), 197 | peer.route(1), 198 | ] 199 | yield from asyncio.wait(tasks) 200 | reply = tasks[0].result() 201 | yield from conn.close() 202 | self.assertEqual(reply["baz"], expectedReply["baz"]) 203 | 204 | @async 205 | def test_call_after_accept(self): 206 | conn = MockConnection(self.loop) 207 | conn.expectedSends.add(2) 208 | rpc = RPC("json", self.loop) 209 | yield from conn.inbox.put(handshake("json")) 210 | peer = yield from rpc.accept(conn, False) 211 | args = {"foo": "bar"} 212 | expectedReply = {"baz": "qux"} 213 | frame = json.dumps({ 214 | "type": protocol.types.reply, 215 | "id": 1, 216 | "payload": expectedReply, 217 | }) 218 | @asyncio.coroutine 219 | def inject_frame(): 220 | yield from conn.expectedSends.wait() 221 | yield from conn.inbox.put(frame) 222 | tasks = [ 223 | self.spawn(peer.call("foobar", args)), 224 | self.spawn(inject_frame()), 225 | peer.route(1), 226 | ] 227 | yield from asyncio.wait(tasks) 228 | reply = tasks[0].result() 229 | yield from conn.close() 230 | self.assertEqual(reply["baz"], expectedReply["baz"]) 231 | 232 | @async 233 | def test_all_on_paired_peers(self): 234 | conns = connection_pair(self.loop) 235 | rpc = RPC("json", self.loop) 236 | @asyncio.coroutine 237 | def echo_tag(ch): 238 | obj, _ = yield from ch.recv() 239 | obj["tag"] = True 240 | yield from ch.send(obj) 241 | rpc.register("echo-tag", echo_tag) 242 | tasks = [ 243 | self.spawn(rpc.accept(conns[0], False)), 244 | self.spawn(rpc.handshake(conns[1], False)), 245 | ] 246 | yield from asyncio.wait(tasks) 247 | peer1 = tasks[0].result() 248 | peer2 = tasks[1].result() 249 | tasks = [ 250 | self.spawn(peer1.call("echo-tag", {"from": "peer1"})), 251 | self.spawn(peer2.call("echo-tag", {"from": "peer2"})), 252 | peer1.route(2), 253 | peer2.route(2), 254 | ] 255 | yield from asyncio.wait(tasks + peer1.tasks + peer2.tasks) 256 | yield from conns[0].close() 257 | yield from conns[1].close() 258 | self.assertEqual(tasks[0].result()["from"], "peer1") 259 | self.assertEqual(tasks[0].result()["tag"], True) 260 | self.assertEqual(tasks[1].result()["from"], "peer2") 261 | self.assertEqual(tasks[1].result()["tag"], True) 262 | 263 | @async 264 | def test_streaming_multiple_results(self): 265 | rpc = RPC("json", self.loop) 266 | @asyncio.coroutine 267 | def counter(ch): 268 | count, _ = yield from ch.recv() 269 | for i in range(count): 270 | n = i+1 271 | yield from ch.send({"num": n}, n != count) 272 | rpc.register("count", counter) 273 | client, server = yield from peer_pair(self.loop, rpc) 274 | ch = client.open("count") 275 | yield from ch.send(5) 276 | yield from server.route(1) 277 | @asyncio.coroutine 278 | def handle_results(): 279 | more = True 280 | loops = 0 281 | count = 0 282 | while more: 283 | reply, more = yield from ch.recv() 284 | count += reply['num'] 285 | loops += 1 286 | assert loops <= 5 287 | return count 288 | tasks = [ 289 | self.spawn(handle_results()), 290 | client.route(5), # kinda defeats the point 291 | ] 292 | yield from asyncio.wait(tasks + server.tasks) 293 | yield from client.close() 294 | yield from server.close() 295 | self.assertEqual(tasks[0].result(), 15) 296 | 297 | @async 298 | def test_streaming_multiple_arguments(self): 299 | rpc = RPC("json", self.loop) 300 | @asyncio.coroutine 301 | def adder(ch): 302 | more = True 303 | total = 0 304 | while more: 305 | count, more = yield from ch.recv() 306 | total += count 307 | yield from ch.send(total) 308 | rpc.register("adder", adder) 309 | client, server = yield from peer_pair(self.loop, rpc) 310 | ch = client.open("adder") 311 | @asyncio.coroutine 312 | def asyncio_sucks(): 313 | for i in range(5): 314 | n = i+1 315 | yield from ch.send(n, n != 5) 316 | total, _ = yield from ch.recv() 317 | return total 318 | tasks = [ 319 | self.spawn(asyncio_sucks()), 320 | server.route(5), 321 | client.route(1), 322 | ] 323 | yield from asyncio.wait(tasks + server.tasks) 324 | yield from client.close() 325 | yield from server.close() 326 | self.assertEqual(tasks[0].result(), 15) 327 | 328 | @async 329 | def test_custom_codec(self): 330 | b64json = [ 331 | 'b64json', 332 | lambda obj: base64.b64encode(json.dumps(obj).encode("utf-8")).decode("utf-8"), 333 | lambda s: json.loads(base64.b64decode(s.encode("utf-8")).decode("utf-8")), 334 | ] 335 | rpc = RPC(b64json, self.loop) 336 | rpc.register("echo", echo) 337 | client, server = yield from peer_pair(self.loop, rpc) 338 | args = {"foo": "bar"} 339 | tasks = [ 340 | self.spawn(client.call("echo", args)), 341 | server.route(1), 342 | client.route(1), 343 | ] 344 | yield from asyncio.wait(tasks + server.tasks) 345 | yield from client.close() 346 | yield from server.close() 347 | self.assertEqual(tasks[0].result(), args) 348 | 349 | @async 350 | def test_ext_fields(self): 351 | rpc = RPC("json", self.loop) 352 | rpc.register("echo", echo) 353 | client, server = yield from peer_pair(self.loop, rpc) 354 | args = {"foo": "bar"} 355 | ext = {"hidden": "metadata"} 356 | ch = client.open("echo") 357 | ch.ext = ext 358 | yield from ch.send(args) 359 | yield from server.route(1) 360 | yield from client.route(1) 361 | reply, _ = yield from ch.recv() 362 | self.assertEqual(args, reply) 363 | yield from client.close() 364 | yield from server.close() 365 | msg = json.loads(server.conn.sent[1]) 366 | self.assertEqual(msg['ext'], ext) 367 | 368 | @async 369 | def test_register_func_and_callback_func(self): 370 | rpc = RPC("json", self.loop) 371 | @asyncio.coroutine 372 | def callback(args, ch): 373 | ret = yield from ch.call(args[0], args[1]) 374 | return ret 375 | rpc.register_func("callback", callback) 376 | client, server = yield from peer_pair(self.loop, rpc) 377 | upper = rpc.callback_func(lambda arg,ch: arg.upper()) 378 | tasks = [ 379 | self.spawn(client.call("callback", [upper, "hello"])), 380 | server.route(2), 381 | client.route(2), 382 | ] 383 | yield from asyncio.wait(tasks) 384 | yield from client.close() 385 | yield from server.close() 386 | self.assertEqual(tasks[0].result(), "HELLO") 387 | -------------------------------------------------------------------------------- /javascript/src/duplex.ts: -------------------------------------------------------------------------------- 1 | 2 | namespace Duplex { 3 | 4 | export let version = "0.1.0"; 5 | export let protocol = { 6 | name: "SIMPLEX", 7 | version: "1.0" 8 | }; 9 | export let request = "req"; 10 | export let reply = "rep"; 11 | export let handshake = { 12 | accept: "+OK" 13 | }; 14 | 15 | // Builtin codecs 16 | export let Json = [ 17 | "json", 18 | JSON.stringify, 19 | JSON.parse 20 | ]; 21 | 22 | // Builtin connection wrappers 23 | export let wrap = { 24 | "websocket"(ws: WebSocket): Connection { 25 | const conn: Connection = { 26 | send(msg: string) { return ws.send(msg); }, 27 | close() { return ws.close(); } 28 | }; 29 | ws.onmessage = (event: { data: string | Array }) => conn.onrecv(event.data); 30 | return conn; 31 | }, 32 | 33 | "nodejs-websocket"(ws: any): Connection { 34 | const conn: Connection = { 35 | send(msg: string) { return ws.send(msg); }, 36 | close() { return ws.close(); } 37 | }; 38 | ws.on("text", (msg: string) => conn.onrecv(msg)); 39 | return conn; 40 | } 41 | }; 42 | 43 | interface Registered { 44 | [index: string]: (ch: Channel) => void; 45 | }; 46 | 47 | export class RPC { 48 | constructor(public codec: Array) { 49 | this.encode = this.codec[1]; 50 | this.decode = this.codec[2]; 51 | this.registered = {}; 52 | } 53 | 54 | encode: (obj: Message) => string; 55 | decode: (str: string) => Message; 56 | registered: Registered; 57 | 58 | register(method: string, handler: (ch: Channel) => void): (ch: Channel) => void { 59 | return this.registered[method] = handler; 60 | } 61 | 62 | unregister(method: string): boolean { 63 | return delete this.registered[method]; 64 | } 65 | 66 | registerFunc(method: string, func: (args: Array, f1: (reply: any, more?: boolean) => any | void, ch: Channel) => Message | void): (ch: Channel) => void { 67 | return this.register(method, (ch: Channel) => 68 | ch.onrecv = (err: object, args: Array) => func(args, ( (reply: any, more: boolean) => { if (more == null) { more = false; } return ch.send(reply, more); }), ch) 69 | ); 70 | } 71 | 72 | callbackFunc(func: (args: Array | string, f1: object, ch?: Channel) => Message | void): string { 73 | const name = `_callback.${UUIDv4()}`; 74 | this.registerFunc(name, func); 75 | return name; 76 | } 77 | 78 | _handshake(): string { 79 | const p = Duplex.protocol; 80 | return `${p.name}/${p.version};${this.codec[0]}`; 81 | } 82 | 83 | handshake(conn: Connection, onready?: (peer?: Peer) => object | boolean | number | void): Peer { 84 | const peer = new Duplex.Peer(this, conn, onready); 85 | conn.onrecv = function(data: string | Array) { 86 | if (data[0] === "+") { 87 | conn.onrecv = peer.onrecv; 88 | return peer._ready(peer); 89 | } else { 90 | return assert(`Bad handshake: ${data}`, false); 91 | } 92 | }; 93 | conn.send(this._handshake()); 94 | return peer; 95 | } 96 | 97 | accept(conn: Connection, onready?: (peer?: Peer) => object | number | void): Peer { 98 | const peer: Peer = new Duplex.Peer(this, conn, onready); 99 | conn.onrecv = function(data: string | Array) { 100 | // TODO: check handshake 101 | conn.onrecv = peer.onrecv; 102 | conn.send(Duplex.handshake.accept); 103 | return peer._ready(peer); 104 | }; 105 | return peer; 106 | } 107 | }; 108 | 109 | interface ChannelArray { 110 | [index: number]: Channel 111 | }; 112 | 113 | export class Peer { 114 | constructor(public rpc: RPC, public conn: Connection, onready: (peer: Peer) => object | boolean | number | void) { 115 | this.onrecv = this.onrecv.bind(this); 116 | if (onready == null) { onready = function({}) {}; } 117 | this.onready = onready; 118 | assert("Peer expects an RPC", (this.rpc.constructor).name === "RPC"); 119 | assert("Peer expects a connection", (this.conn != null)); 120 | this.lastId = 0; 121 | this.ext = null; 122 | this.reqChan = {}; 123 | this.repChan = {}; 124 | } 125 | 126 | onready: (peer: Peer) => object | boolean | number | void; 127 | lastId: number; 128 | ext: object; 129 | reqChan: ChannelArray; 130 | repChan: ChannelArray; 131 | 132 | _ready(peer: Peer): object | boolean | number | void { 133 | return this.onready(peer); 134 | } 135 | 136 | close(): void { 137 | return this.conn.close(); 138 | } 139 | 140 | call(method?: string, args?: any, callback?: object): void { 141 | const ch: Channel = new Duplex.Channel(this, Duplex.request, method, this.ext); 142 | if (callback != null) { 143 | ch.id = ++this.lastId; 144 | ch.onrecv = callback; 145 | this.repChan[ch.id] = ch; 146 | } 147 | return ch.send(args); 148 | } 149 | 150 | open(method: string, callback?: object): Channel { 151 | const ch: Channel = new Duplex.Channel(this, Duplex.request, method, this.ext); 152 | ch.id = ++this.lastId; 153 | this.repChan[ch.id] = ch; 154 | if (callback != null) { 155 | ch.onrecv = callback; 156 | } 157 | return ch; 158 | } 159 | 160 | onrecv(frame: string): any { 161 | let ch: Channel; 162 | if (frame === "") { 163 | // ignore empty frames 164 | return; 165 | } 166 | const msg: Message = this.rpc.decode(frame); 167 | // TODO: catch decode error 168 | switch (msg.type) { 169 | case Duplex.request: 170 | if (this.reqChan[msg.id] != null) { 171 | ch = this.reqChan[msg.id]; 172 | if (msg.more === false) { 173 | delete this.reqChan[msg.id]; 174 | } 175 | } else { 176 | ch = new Duplex.Channel(this, Duplex.reply, msg.method); 177 | if (msg.id !== undefined) { 178 | ch.id = msg.id; 179 | if (msg.more === true) { 180 | this.reqChan[ch.id] = ch; 181 | } 182 | } 183 | assert("Method not registerd", (this.rpc.registered[msg.method] != null)); 184 | this.rpc.registered[msg.method](ch); 185 | } 186 | if (msg.ext != null) { 187 | ch.ext = msg.ext; 188 | } 189 | return ch.onrecv(null, msg.payload, msg.more); 190 | case Duplex.reply: 191 | if (msg.error != null) { 192 | if (this.repChan[msg.id] != null) { 193 | this.repChan[msg.id].onrecv(msg.error); 194 | } 195 | return delete this.repChan[msg.id]; 196 | } else { 197 | if (this.repChan[msg.id] != null) { 198 | this.repChan[msg.id].onrecv(null, msg.payload, msg.more); 199 | } 200 | if (msg.more === false) { 201 | return delete this.repChan[msg.id]; 202 | } 203 | } 204 | break; 205 | default: 206 | return assert("Invalid message", false); 207 | } 208 | } 209 | }; 210 | 211 | export class Channel { 212 | constructor(public peer: Peer, public type: string, public method: string, public ext?: object) { 213 | assert("Channel expects Peer", (this.peer.constructor).name === "Peer"); 214 | this.id = null; 215 | this.onrecv = function() {}; 216 | } 217 | 218 | id: number; 219 | onrecv: any; 220 | 221 | call(method: string, args: Array, callback: object): Message | void { 222 | const ch: Channel = this.peer.open(method, callback); 223 | ch.ext = this.ext; 224 | return ch.send(args); 225 | } 226 | 227 | close(): void { 228 | return this.peer.close(); 229 | } 230 | 231 | open(method: string, callback: object): object { 232 | const ch: Channel = this.peer.open(method, callback); 233 | ch.ext = this.ext; 234 | return ch; 235 | } 236 | 237 | send(payload: object | number | string, more?: boolean): void { 238 | if (more == null) { more = false; } 239 | switch (this.type) { 240 | case Duplex.request: 241 | return this.peer.conn.send(this.peer.rpc.encode( 242 | requestMsg(payload, this.method, this.id, more, this.ext)) 243 | ); 244 | case Duplex.reply: 245 | return this.peer.conn.send(this.peer.rpc.encode( 246 | replyMsg(this.id, payload, more, this.ext)) 247 | ); 248 | default: 249 | return assert("Bad channel type", false); 250 | } 251 | } 252 | 253 | senderr(code: number, message: string, data?: string | Array): void { 254 | assert("Not reply channel", this.type === Duplex.reply); 255 | return this.peer.conn.send(this.peer.rpc.encode( 256 | errorMsg(this.id, code, message, data, this.ext)) 257 | ); 258 | } 259 | }; 260 | 261 | // high level wrapper for Duplex over websocket 262 | // * call buffering when not connected 263 | // * default retries (very basic right now) 264 | // * manages setting up peer / handshake 265 | export class API { 266 | constructor(endpoint: string) { 267 | const parts = endpoint.split(":"); 268 | assert("Invalid endpoint", parts.length > 1); 269 | const scheme = parts.shift(); 270 | let [protocol, codec] = scheme.split("+", 2); 271 | if (codec == null) { 272 | codec = "json"; 273 | } 274 | assert("JSON is only supported codec", codec === "json"); 275 | parts.unshift(protocol); 276 | const url = parts.join(":"); 277 | this.queued = []; 278 | this.rpc = new Duplex.RPC(Duplex.Json); 279 | var connect = (url: string): any => { 280 | this.ws = new WebSocket(url); 281 | this.ws.onopen = () => { 282 | return this.rpc.handshake(Duplex.wrap.websocket(this.ws), (p: any) => { 283 | this.peer = p; 284 | return this.queued.map((args: Array) => 285 | ((args: Array) => this.call(...args || []))(args)); 286 | }); 287 | }; 288 | return this.ws.onclose = () => { 289 | return setTimeout((() => connect(url)), 2000); 290 | }; 291 | }; 292 | connect(url); 293 | } 294 | 295 | queued: Array; 296 | rpc: RPC; 297 | ws: WebSocket; 298 | peer: Peer; 299 | 300 | call(...args: Array): void | number { 301 | console.log(args); 302 | if (this.peer != null) { 303 | return this.peer.call(...args || []); 304 | } else { 305 | return this.queued.push(args); 306 | } 307 | } 308 | }; 309 | 310 | }; 311 | 312 | interface Message { 313 | type: string, 314 | payload?: string | number | object, 315 | method?: string, 316 | id?: number, 317 | more?: boolean, 318 | ext?: object, 319 | error?: { code: number, message: string, data?: string | Array } 320 | }; 321 | 322 | interface Connection { 323 | onrecv?(data: string | Array): any, 324 | send(msg: string): void, 325 | close(): void, 326 | sent?: Array 327 | }; 328 | 329 | 330 | const assert = function(description: string, condition: boolean): void { 331 | // We assert assumed state so we can more easily catch bugs. 332 | // Do not assert if we *know* the user can get to it. 333 | // HOWEVER in development there are asserts instead of exceptions... 334 | if (condition == null) { condition = false; } 335 | if (!condition) { throw Error(`Assertion: ${description}`); } 336 | }; 337 | 338 | const requestMsg = function(payload: object | number | string, method: string, id: number, more: boolean, ext: object): Message { 339 | const msg: Message = { 340 | type: Duplex.request, 341 | method, 342 | payload 343 | }; 344 | if (id != null) { 345 | msg.id = id; 346 | } 347 | if (more === true) { 348 | msg.more = more; 349 | } 350 | if (ext != null) { 351 | msg.ext = ext; 352 | } 353 | return msg; 354 | }; 355 | 356 | const replyMsg = function(id: number, payload: object | number | string, more: boolean, ext: object): Message { 357 | const msg: Message = { 358 | type: Duplex.reply, 359 | id, 360 | payload 361 | }; 362 | if (more === true) { 363 | msg.more = more; 364 | } 365 | if (ext != null) { 366 | msg.ext = ext; 367 | } 368 | return msg; 369 | }; 370 | 371 | const errorMsg = function(id: number, code: number, message: string, data: string | Array, ext: object): Message { 372 | const msg: Message = { 373 | type: Duplex.reply, 374 | id, 375 | error: { 376 | code, 377 | message 378 | } 379 | }; 380 | if (data != null) { 381 | msg.error.data = data; 382 | } 383 | if (ext != null) { 384 | msg.ext = ext; 385 | } 386 | return msg; 387 | }; 388 | 389 | const UUIDv4 = function(): string { 390 | let d = new Date().getTime(); 391 | if (typeof __guard__(typeof window !== 'undefined' && window !== null ? window.performance : undefined, (x: object) => (x).now) === "function") { 392 | d += performance.now(); // use high-precision timer if available 393 | } 394 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 395 | let r = ((d + (Math.random()*16))%16) | 0; 396 | d = Math.floor(d/16); 397 | if (c !== 'x') { 398 | r = (r & 0x3) | 0x8; 399 | } 400 | return r.toString(16); 401 | }); 402 | }; 403 | 404 | // create duplex instance from namespace Duplex 405 | // This design pattern was used to allow classes as properties with TypeScript, 406 | // while still being abel to export the Duplex object as originialy designed 407 | let duplexInstance = { 408 | version: Duplex.version, 409 | protocol: Duplex.protocol, 410 | request: Duplex.request, 411 | reply: Duplex.reply, 412 | handshake: Duplex.handshake, 413 | JSON: Duplex.Json, // namespace property name changed because of colision with global 'JSON' object 414 | wrap: Duplex.wrap, 415 | RPC: Duplex.RPC, 416 | Peer: Duplex.Peer, 417 | Channel: Duplex.Channel, 418 | API: Duplex.API 419 | }; 420 | 421 | if (typeof window !== 'undefined' && window !== null) { 422 | // For use in the browser 423 | (window).duplex = duplexInstance; 424 | } else { 425 | // For Node / testing 426 | exports.duplex = duplexInstance; 427 | } 428 | 429 | function __guard__(value: any, transform: any): void { 430 | return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; 431 | } -------------------------------------------------------------------------------- /golang/duplex_test.go: -------------------------------------------------------------------------------- 1 | package duplex 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "strings" 10 | "sync" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | const ( 16 | TestErrorCode = 1000 17 | TestErrorMessage = "Test Message" 18 | ) 19 | 20 | type MockConn struct { 21 | sync.Mutex 22 | 23 | sent []*bytes.Buffer 24 | closed bool 25 | inbox chan string 26 | paired *MockConn 27 | writes sync.WaitGroup 28 | expectedWrites int 29 | reads sync.WaitGroup 30 | expectedReads int 31 | } 32 | 33 | func NewMockConn() *MockConn { 34 | return &MockConn{ 35 | closed: false, 36 | inbox: make(chan string, 1024), 37 | } 38 | } 39 | 40 | func (conn *MockConn) ExpectReads(n int) { 41 | conn.expectedReads = n 42 | conn.reads.Add(n) 43 | } 44 | 45 | func (conn *MockConn) ExpectWrites(n int) { 46 | conn.expectedWrites = n 47 | conn.writes.Add(n) 48 | } 49 | 50 | func (conn *MockConn) Close() error { 51 | conn.closed = true 52 | close(conn.inbox) 53 | return nil 54 | } 55 | 56 | func (conn *MockConn) Write(p []byte) (int, error) { 57 | conn.Lock() 58 | defer conn.Unlock() 59 | buf := bytes.NewBuffer(p) 60 | conn.sent = append(conn.sent, buf) 61 | if conn.paired != nil { 62 | conn.paired.inbox <- buf.String() 63 | } 64 | if os.Getenv("DEBUG") != "" { 65 | fmt.Println(buf.String()) 66 | } 67 | if conn.expectedWrites > 0 { 68 | conn.writes.Done() 69 | } 70 | return buf.Len(), nil 71 | } 72 | 73 | func (conn *MockConn) Read(p []byte) (int, error) { 74 | if conn.expectedReads > 0 { 75 | defer conn.reads.Done() 76 | } 77 | v, ok := <-conn.inbox 78 | if !ok { 79 | return 0, fmt.Errorf("Inbox closed") 80 | } 81 | if os.Getenv("DEBUG") != "" { 82 | defer fmt.Println(v) 83 | } 84 | return copy(p, v), nil 85 | } 86 | 87 | func NewConnPair() (*MockConn, *MockConn) { 88 | conn1 := NewMockConn() 89 | conn2 := NewMockConn() 90 | conn1.paired = conn2 91 | conn2.paired = conn1 92 | return conn1, conn2 93 | } 94 | 95 | func NewPeerPair(rpc *RPC) (*Peer, *Peer) { 96 | conn1, conn2 := NewConnPair() 97 | var wg sync.WaitGroup 98 | var peer1, peer2 *Peer 99 | wg.Add(2) 100 | go func() { 101 | peer1, _ = rpc.Accept(conn1) 102 | wg.Done() 103 | }() 104 | go func() { 105 | peer2, _ = rpc.Handshake(conn2) 106 | wg.Done() 107 | }() 108 | wg.Wait() 109 | return peer1, peer2 110 | } 111 | 112 | func Echo(ch *Channel) error { 113 | var obj interface{} 114 | if _, err := ch.Recv(&obj); err != nil { 115 | return err 116 | } 117 | return ch.Send(obj, false) 118 | } 119 | 120 | func Generator(ch *Channel) error { 121 | var count float64 122 | if _, err := ch.Recv(&count); err != nil { 123 | return err 124 | } 125 | var i float64 126 | for i = 1; i <= count; i++ { 127 | m := map[string]float64{"num": i} 128 | if err := ch.Send(m, i != count); err != nil { 129 | return err 130 | } 131 | } 132 | return nil 133 | } 134 | 135 | func GeneratorThatErrorsAfterSecond(ch *Channel) error { 136 | var count float64 137 | if _, err := ch.Recv(&count); err != nil { 138 | return err 139 | } 140 | var i float64 141 | for i = 1; i <= count; i++ { 142 | m := map[string]float64{"num": i} 143 | if err := ch.Send(m, i != count); err != nil { 144 | return err 145 | } 146 | if i == 2 { 147 | if err := ch.SendErr(TestErrorCode, TestErrorMessage, nil); err != nil { 148 | return err 149 | } 150 | break 151 | } 152 | } 153 | return nil 154 | } 155 | 156 | func Adder(ch *Channel) error { 157 | var total = 0.0 158 | var more = true 159 | var err error 160 | var count float64 161 | for more { 162 | if more, err = ch.Recv(&count); err != nil { 163 | return err 164 | } 165 | total += count 166 | } 167 | return ch.Send(total, false) 168 | } 169 | 170 | func ReturnError(ch *Channel) error { 171 | return ch.SendErr(TestErrorCode, TestErrorMessage, nil) 172 | } 173 | 174 | func Handshake(codec string) string { 175 | return fmt.Sprintf("%s/%s;%s", ProtocolName, ProtocolVersion, codec) 176 | } 177 | 178 | func Fatal(err error, t *testing.T) { 179 | if err != nil { 180 | t.Fatal(err) 181 | } 182 | } 183 | 184 | // Actual tests 185 | 186 | func TestHandshake(t *testing.T) { 187 | conn := NewMockConn() 188 | defer conn.Close() 189 | rpc := NewRPC(NewJSONCodec()) 190 | conn.inbox <- HandshakeAccept 191 | rpc.Handshake(conn) 192 | if conn.sent[0].String() != Handshake("json") { 193 | t.Fatal("Unexpected handshake frame:", conn.sent[0].String()) 194 | } 195 | } 196 | 197 | func TestAccept(t *testing.T) { 198 | conn := NewMockConn() 199 | defer conn.Close() 200 | rpc := NewRPC(NewJSONCodec()) 201 | conn.inbox <- Handshake("json") 202 | rpc.Accept(conn) 203 | if conn.sent[0].String() != HandshakeAccept { 204 | t.Fatal("Unexpected handshake response frame:", conn.sent[0].String()) 205 | } 206 | } 207 | 208 | func TestRegisteredFuncAfterAccept(t *testing.T) { 209 | conn := NewMockConn() 210 | defer conn.Close() 211 | conn.ExpectWrites(2) 212 | rpc := NewRPC(NewJSONCodec()) 213 | rpc.Register("echo", Echo) 214 | conn.inbox <- Handshake("json") 215 | rpc.Accept(conn) 216 | req := Message{ 217 | Type: TypeRequest, 218 | Method: "echo", 219 | Id: 1, 220 | Payload: map[string]string{"foo": "bar"}, 221 | } 222 | b, err := json.Marshal(req) 223 | Fatal(err, t) 224 | conn.inbox <- string(b) 225 | conn.writes.Wait() 226 | if len(conn.sent) != 2 { 227 | t.Fatal("Unexpected sent frame count:", len(conn.sent)) 228 | } 229 | } 230 | 231 | func TestRegisteredFuncAfterHandshake(t *testing.T) { 232 | conn := NewMockConn() 233 | defer conn.Close() 234 | conn.ExpectWrites(2) 235 | rpc := NewRPC(NewJSONCodec()) 236 | rpc.Register("echo", Echo) 237 | conn.inbox <- HandshakeAccept 238 | rpc.Handshake(conn) 239 | req := Message{ 240 | Type: TypeRequest, 241 | Method: "echo", 242 | Id: 1, 243 | Payload: map[string]string{"foo": "bar"}, 244 | } 245 | b, err := json.Marshal(req) 246 | Fatal(err, t) 247 | conn.inbox <- string(b) 248 | conn.writes.Wait() 249 | if len(conn.sent) != 2 { 250 | t.Fatal("Unexpected sent frame count:", len(conn.sent)) 251 | } 252 | } 253 | 254 | func TestCallAfterHandshake(t *testing.T) { 255 | conn := NewMockConn() 256 | defer conn.Close() 257 | conn.ExpectWrites(2) 258 | rpc := NewRPC(NewJSONCodec()) 259 | conn.inbox <- HandshakeAccept 260 | peer, err := rpc.Handshake(conn) 261 | Fatal(err, t) 262 | args := map[string]string{"foo": "bar"} 263 | expectedReply := map[string]string{"baz": "qux"} 264 | var reply map[string]interface{} 265 | b, err := json.Marshal(map[string]interface{}{ 266 | "type": TypeReply, 267 | "id": 1, 268 | "payload": expectedReply, 269 | }) 270 | Fatal(err, t) 271 | go func() { 272 | conn.writes.Wait() 273 | conn.inbox <- string(b) 274 | }() 275 | err = peer.Call("foobar", args, &reply) 276 | Fatal(err, t) 277 | if reply["baz"] != expectedReply["baz"] { 278 | t.Fatal("Unexpected reply:", reply) 279 | } 280 | } 281 | 282 | func TestCallAfterAccept(t *testing.T) { 283 | conn := NewMockConn() 284 | defer conn.Close() 285 | conn.ExpectWrites(2) 286 | rpc := NewRPC(NewJSONCodec()) 287 | conn.inbox <- Handshake("json") 288 | peer, err := rpc.Accept(conn) 289 | Fatal(err, t) 290 | args := map[string]string{"foo": "bar"} 291 | expectedReply := map[string]string{"baz": "qux"} 292 | var reply map[string]interface{} 293 | b, err := json.Marshal(map[string]interface{}{ 294 | "type": TypeReply, 295 | "id": 1, 296 | "payload": expectedReply, 297 | }) 298 | Fatal(err, t) 299 | go func() { 300 | conn.writes.Wait() 301 | conn.inbox <- string(b) 302 | }() 303 | err = peer.Call("foobar", args, &reply) 304 | Fatal(err, t) 305 | if reply["baz"] != expectedReply["baz"] { 306 | t.Fatal("Unexpected reply:", reply) 307 | } 308 | } 309 | 310 | func TestAllOnPairedPeers(t *testing.T) { 311 | conn1, conn2 := NewConnPair() 312 | defer conn1.Close() 313 | defer conn2.Close() 314 | rpc := NewRPC(NewJSONCodec()) 315 | rpc.Register("echo-tag", func(ch *Channel) error { 316 | var obj map[string]interface{} 317 | if _, err := ch.Recv(&obj); err != nil { 318 | return err 319 | } 320 | obj["tag"] = true 321 | return ch.Send(obj, false) 322 | }) 323 | var wg sync.WaitGroup 324 | var peer1, peer2 *Peer 325 | var err1, err2 error 326 | wg.Add(2) 327 | go func() { 328 | peer1, err1 = rpc.Accept(conn1) 329 | Fatal(err1, t) 330 | wg.Done() 331 | }() 332 | go func() { 333 | peer2, err2 = rpc.Handshake(conn2) 334 | Fatal(err2, t) 335 | wg.Done() 336 | }() 337 | wg.Wait() 338 | wg.Add(2) 339 | go func() { 340 | var reply map[string]interface{} 341 | err := peer1.Call("echo-tag", map[string]string{"from": "peer1"}, &reply) 342 | Fatal(err, t) 343 | if reply["from"] != "peer1" || reply["tag"] != true { 344 | t.Fatal("Unexpected reply to peer1:", reply) 345 | } 346 | wg.Done() 347 | }() 348 | go func() { 349 | var reply map[string]interface{} 350 | err := peer2.Call("echo-tag", map[string]string{"from": "peer2"}, &reply) 351 | Fatal(err, t) 352 | if reply["from"] != "peer2" || reply["tag"] != true { 353 | t.Fatal("Unexpected reply to peer2:", reply) 354 | } 355 | wg.Done() 356 | }() 357 | wg.Wait() 358 | } 359 | 360 | func TestStreamingMultipleResults(t *testing.T) { 361 | rpc := NewRPC(NewJSONCodec()) 362 | rpc.Register("count", Generator) 363 | client, _ := NewPeerPair(rpc) 364 | ch := client.Open("count") 365 | err := ch.Send(5, false) 366 | Fatal(err, t) 367 | var reply map[string]interface{} 368 | var more = true 369 | var count = 0.0 370 | loops := 0 371 | for more { 372 | more, err = ch.Recv(&reply) 373 | Fatal(err, t) 374 | count += reply["num"].(float64) 375 | loops++ 376 | if loops > 5 { 377 | t.Fatal("Too many loops") 378 | } 379 | } 380 | if count != 15 { 381 | t.Fatal("Unexpected final count:", count) 382 | } 383 | } 384 | 385 | func TestStreamingMultipleArguments(t *testing.T) { 386 | rpc := NewRPC(NewJSONCodec()) 387 | rpc.Register("adder", Adder) 388 | client, _ := NewPeerPair(rpc) 389 | ch := client.Open("adder") 390 | for i := 1; i <= 5; i++ { 391 | err := ch.Send(i, i != 5) 392 | Fatal(err, t) 393 | } 394 | var reply float64 395 | _, err := ch.Recv(&reply) 396 | Fatal(err, t) 397 | if reply != 15 { 398 | t.Fatal("Unexpected reply:", reply) 399 | } 400 | } 401 | 402 | func TestCustomCodec(t *testing.T) { 403 | b64json := &Codec{ 404 | Name: "b64json", 405 | Encode: func(obj interface{}) ([]byte, error) { 406 | j, err := json.Marshal(obj) 407 | if err != nil { 408 | return nil, err 409 | } 410 | encoded := make([]byte, base64.StdEncoding.EncodedLen(len(j))) 411 | base64.StdEncoding.Encode(encoded, j) 412 | return encoded, nil 413 | }, 414 | Decode: func(frame []byte, obj interface{}) error { 415 | decoded := make([]byte, base64.StdEncoding.DecodedLen(len(frame))) 416 | n, err := base64.StdEncoding.Decode(decoded, frame) 417 | if err != nil { 418 | return err 419 | } 420 | return json.Unmarshal(decoded[:n], obj) 421 | }, 422 | } 423 | rpc := NewRPC(b64json) 424 | rpc.Register("echo", Echo) 425 | client, _ := NewPeerPair(rpc) 426 | var reply map[string]interface{} 427 | err := client.Call("echo", map[string]string{"foo": "bar"}, &reply) 428 | Fatal(err, t) 429 | if reply["foo"] != "bar" { 430 | t.Fatal("Unexpected reply:", reply) 431 | } 432 | } 433 | 434 | func TestHiddenExt_EXPERIMENTAL(t *testing.T) { 435 | rpc := NewRPC(NewJSONCodec()) 436 | rpc.Register("echo", Echo) 437 | client, server := NewPeerPair(rpc) 438 | ch := client.Open("echo") 439 | ch.SetExt(map[string]string{"hidden": "metadata"}) 440 | err := ch.Send(map[string]string{"foo": "bar"}, false) 441 | Fatal(err, t) 442 | var reply map[string]interface{} 443 | _, err = ch.Recv(&reply) 444 | Fatal(err, t) 445 | if reply["foo"] != "bar" { 446 | t.Fatal("Unexpected reply:", reply) 447 | } 448 | var msg map[string]interface{} 449 | err = rpc.codec.Decode(server.conn.(*MockConn).sent[1].Bytes(), &msg) 450 | Fatal(err, t) 451 | if msg["ext"].(map[string]interface{})["hidden"] != "metadata" { 452 | t.Fatal("Unexpected ext:", msg["ext"]) 453 | } 454 | } 455 | 456 | func TestRegisterFuncAndCallbackFunc(t *testing.T) { 457 | rpc := NewRPC(NewJSONCodec()) 458 | rpc.RegisterFunc("callback", func(arg interface{}, ch *Channel) (interface{}, error) { 459 | args := arg.([]interface{}) 460 | var ret interface{} 461 | err := ch.Call(args[0].(string), args[1].(string), &ret) 462 | if err != nil { 463 | return nil, err 464 | } 465 | return ret, nil 466 | }) 467 | client, _ := NewPeerPair(rpc) 468 | upper := rpc.CallbackFunc(func(args interface{}, _ *Channel) (interface{}, error) { 469 | return strings.ToUpper(args.(string)), nil 470 | }) 471 | var reply string 472 | err := client.Call("callback", []string{upper, "hello"}, &reply) 473 | Fatal(err, t) 474 | if reply != "HELLO" { 475 | t.Fatal("Unexpected reply:", reply) 476 | } 477 | } 478 | 479 | func TestCallAsyncWhenReplyNil(t *testing.T) { 480 | rpc := NewRPC(NewJSONCodec()) 481 | received := make(chan bool, 1) 482 | sent := make(chan bool, 1) 483 | rpc.Register("noreply", func(ch *Channel) error { 484 | received <- true 485 | return nil 486 | }) 487 | client, _ := NewPeerPair(rpc) 488 | go func() { 489 | client.Call("noreply", nil, nil) 490 | sent <- true 491 | }() 492 | done := make(chan bool) 493 | go func() { 494 | <-received 495 | <-sent 496 | done <- true 497 | }() 498 | select { 499 | case <-done: 500 | return 501 | case <-time.After(1 * time.Second): 502 | t.Fatal("blocking Call timeout") 503 | } 504 | } 505 | 506 | func TestErrorReplyCall(t *testing.T) { 507 | rpc := NewRPC(NewJSONCodec()) 508 | rpc.Register("errorReply", ReturnError) 509 | client, _ := NewPeerPair(rpc) 510 | var reply interface{} 511 | err := client.Call("errorReply", nil, &reply) 512 | if err != nil { 513 | rpcError, ok := err.(*Error) 514 | if !ok { 515 | Fatal(err, t) 516 | } 517 | if rpcError.Code != TestErrorCode { 518 | t.Fatal("Unexpected error code:", rpcError) 519 | } 520 | if rpcError.Message != TestErrorMessage { 521 | t.Fatal("Unexpected error message:", rpcError) 522 | } 523 | } else { 524 | t.Fatal("No error returned") 525 | } 526 | } 527 | 528 | func TestErrorInStream(t *testing.T) { 529 | rpc := NewRPC(NewJSONCodec()) 530 | rpc.Register("errorInStream", GeneratorThatErrorsAfterSecond) 531 | client, _ := NewPeerPair(rpc) 532 | ch := client.Open("errorInStream") 533 | err := ch.Send(5, false) 534 | Fatal(err, t) 535 | var reply map[string]interface{} 536 | var more = true 537 | var count = 0.0 538 | var rpcError *Error 539 | var ok bool 540 | loops := 0 541 | for more { 542 | more, err = ch.Recv(&reply) 543 | if err != nil { 544 | rpcError, ok = err.(*Error) 545 | if !ok { 546 | Fatal(err, t) 547 | } 548 | break 549 | } 550 | count = reply["num"].(float64) 551 | loops++ 552 | if loops > 5 { 553 | t.Fatal("Too many loops") 554 | } 555 | } 556 | if rpcError.Code != TestErrorCode { 557 | t.Fatal("Unexpected error code:", rpcError) 558 | } 559 | if rpcError.Message != TestErrorMessage { 560 | t.Fatal("Unexpected error message:", rpcError) 561 | } 562 | if count != 2 { 563 | t.Fatal("Unexpected final count:", count) 564 | } 565 | } 566 | --------------------------------------------------------------------------------