├── 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 | ///