├── .gitignore ├── README.md ├── examples ├── bi-directional.ts └── traditional.ts ├── index.ts ├── package.json ├── tests └── bench.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | dist/ 3 | node_modules/ 4 | yarn.lock 5 | 6 | *.d.ts 7 | *.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Procedurem ![npm bundle size](https://img.shields.io/bundlephobia/minzip/procedurem) [![npm](https://img.shields.io/npm/v/procedurem)](https://www.npmjs.com/package/procedurem) 2 | A small (2kb) and performant isomorphic RPC library for TypeScript. 3 | 4 | # Benchmarks 5 | AWS T2.Small 6 | 7 | | Clients | Payload | Throughput | Average | 8 | |---------|---------|------------|-----------| 9 | | 1 | 1 kb | 3030 kbps | 0.33 ms | 10 | | 1 | 10 kb | 17857 kbps | 0.56 ms | 11 | | 1 | 100 kb | 29069 kbps | 3.44 ms | 12 | | 10 | 1 kb | 680 kbps | 1.47 ms | 13 | | 10 | 10 kb | 2638 kbps | 3.79 ms | 14 | | 10 | 100 kb | 3175 kbps | 31.49 ms | 15 | | 100 | 1 kb | 71 kbps | 14.03 ms | 16 | | 100 | 10 kb | 252 kbps | 39.59 ms | 17 | | 100 | 100 kb | 314 kbps | 318.45 ms | 18 | -------------------------------------------------------------------------------- /examples/bi-directional.ts: -------------------------------------------------------------------------------- 1 | import { Client, remote, Server } from 'procedurem'; 2 | 3 | class DataServer extends Server { 4 | @remote 5 | request(input: string) { 6 | new Promise(resolve => setTimeout(resolve, 1000)).then(() => { 7 | this.call('receive', 'Got you data!: ' + input.split('').reverse().join('')) 8 | }) 9 | 10 | return 'Fetching data!'; 11 | } 12 | } 13 | 14 | class DataRequestor extends Client { 15 | @remote 16 | receive(input: string) { 17 | console.log('Data received from server!', input); 18 | } 19 | } 20 | 21 | new DataServer().listen(8080); 22 | 23 | let c = new DataRequestor(); 24 | 25 | c.connect('ws://127.0.0.1:8080').then(async () => { 26 | console.log(await c.call('request', 'ycnaf oOoo')) 27 | }); -------------------------------------------------------------------------------- /examples/traditional.ts: -------------------------------------------------------------------------------- 1 | import { Client, remote, Server } from 'procedurem'; 2 | 3 | class Handler extends Server { 4 | @remote 5 | capitalize(input: string) { 6 | return input.toUpperCase(); 7 | } 8 | } 9 | 10 | new Handler().listen(8080); 11 | 12 | let c = new Client(); 13 | 14 | c.connect('ws://127.0.0.1:8080').then(async () => { 15 | console.log(await c.call('capitalize', 'this should be upper case!')) 16 | }); -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import '@abraham/reflection'; 2 | import WebSocket from 'isomorphic-ws'; 3 | 4 | const METADATA_KEY = 'rpc:methods'; 5 | 6 | interface Call { 7 | id: string; 8 | method: string; 9 | params: any[]; 10 | } 11 | 12 | interface CallResponse { 13 | id: string; 14 | ok: boolean; 15 | value: any; 16 | } 17 | 18 | interface DeferredPromise { 19 | resolve: any; 20 | reject: any; 21 | } 22 | 23 | class Executable { 24 | private methods?: string[] = Reflect.getMetadata(METADATA_KEY, Object.create(this)); 25 | 26 | public execute(instance: any, call: Call) { 27 | const cr = {} as CallResponse; 28 | cr.id = call.id; 29 | 30 | if (!this.methods || !this.methods.includes(call.method)) { 31 | cr.ok = false; 32 | } else { 33 | cr.value = instance[call.method].apply(instance, call.params); 34 | cr.ok = true; 35 | } 36 | 37 | return cr; 38 | } 39 | } 40 | 41 | class WSRPC extends Executable { 42 | callbacks = new Map(); 43 | 44 | public invoke(ws: WebSocket, method: string, params: any[]) { 45 | let id = Math.random().toString(36).substring(2) + Date.now().toString(36); 46 | 47 | let p = new Promise((resolve, reject) => this.callbacks.set(id, { resolve, reject })); 48 | 49 | ws.send(JSON.stringify({ method, params, id })) 50 | 51 | return p; 52 | } 53 | 54 | public handle(msg: WebSocket.Data, ws: WebSocket, instance: any) { 55 | let pmsg = JSON.parse(msg.toString()); 56 | 57 | if (pmsg.method) { 58 | let req = pmsg as Call; 59 | ws.send(JSON.stringify(this.execute(instance, req))); 60 | } else { 61 | let rall = pmsg as CallResponse; 62 | 63 | let p = this.callbacks.get(rall.id) as DeferredPromise; 64 | 65 | if (rall.ok) p.resolve(rall.value); 66 | else p.reject(); 67 | 68 | this.callbacks.delete(rall.id); 69 | } 70 | } 71 | } 72 | 73 | export class Server extends WSRPC { 74 | private wss!: WebSocket.Server; 75 | 76 | private client!: WebSocket; 77 | 78 | public listen(port: number) { 79 | this.wss = new WebSocket.Server({ port }) 80 | 81 | this.wss.on('connection', (ws) => { 82 | const instance = Object.create(this); 83 | instance.client = ws; 84 | 85 | ws.onmessage = (msg) => { this.handle(msg.data, ws, instance) } 86 | }) 87 | } 88 | 89 | public call(method: string, ...params: any[]): Promise { 90 | return super.invoke(this.client, method, params) as Promise; 91 | } 92 | } 93 | 94 | 95 | export class Client extends WSRPC { 96 | private cws!: WebSocket; 97 | 98 | public connect(address: string) { 99 | return new Promise((resolve, reject) => { 100 | this.cws = new WebSocket(address); 101 | 102 | this.cws.onopen = () => { 103 | this.cws.onmessage = (msg) => { this.handle(msg.data, this.cws, this) } 104 | resolve(); 105 | }; 106 | this.cws.onerror = function (err) { 107 | reject(err); 108 | }; 109 | }); 110 | } 111 | 112 | public call(method: string, ...params: any[]): Promise { 113 | return super.invoke(this.cws, method, params) as Promise; 114 | } 115 | } 116 | 117 | export function remote(target: any, key: string) { 118 | const methods: string[] = Reflect.getOwnMetadata(METADATA_KEY, target) || []; 119 | 120 | methods.push(key); 121 | 122 | Reflect.defineMetadata(METADATA_KEY, methods, target); 123 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "procedurem", 3 | "version": "1.0.6", 4 | "description": "A Small (2kb) And Performant Type-Safe RPC Library Using WebSockets", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/ImVexed/Procedurem" 8 | }, 9 | "types": "index.d.ts", 10 | "main": "index.js", 11 | "keywords": [ 12 | "rpc", 13 | "websockets", 14 | "typescript", 15 | "bi-directional" 16 | ], 17 | "author": "ImVexed", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@abraham/reflection": "^0.5.1", 21 | "isomorphic-ws": "^4.0.1", 22 | "ws": "^7.1.2" 23 | }, 24 | "devDependencies": { 25 | "easy-table": "^1.1.1", 26 | "procedurem": "^1.0.6", 27 | "@types/ws": "^6.0.2", 28 | "typescript": "^3.5.3" 29 | }, 30 | "files": [ 31 | "index.js", 32 | "index.d.ts" 33 | ] 34 | } -------------------------------------------------------------------------------- /tests/bench.ts: -------------------------------------------------------------------------------- 1 | import { Client, remote, Server } from '../'; 2 | const { performance } = require('perf_hooks'); 3 | var Table = require('easy-table') 4 | 5 | class TestS extends Server { 6 | @remote 7 | echo(data: string) { 8 | return data; 9 | } 10 | } 11 | 12 | let server = new TestS(); 13 | console.log('listening on :8080...'); 14 | server.listen(8080); 15 | 16 | (async () => { 17 | let data = [ 18 | await benchmark(1, 1, 1000), 19 | await benchmark(1, 10, 1000), 20 | await benchmark(1, 100, 1000), 21 | await benchmark(10, 1, 1000), 22 | await benchmark(10, 10, 1000), 23 | await benchmark(10, 100, 1000), 24 | await benchmark(100, 1, 1000), 25 | await benchmark(100, 10, 1000), 26 | await benchmark(100, 100, 1000) 27 | ] 28 | 29 | let t = new Table 30 | 31 | data.forEach((b: any) => { 32 | t.cell('Clients', b.clients) 33 | t.cell('Payload', b.kbs + ' kb', (val: any, w: any) => w ? Table.padLeft(val, w) : val) 34 | t.cell('Throughput', b.throughput + ' kbps', (val: any, w: any) => w ? Table.padLeft(val, w) : val) 35 | t.cell('Average', b.average + ' ms', (val: any, w: any) => w ? Table.padLeft(val, w) : val) 36 | t.newRow() 37 | }) 38 | 39 | console.log(t.toString()) 40 | })(); 41 | 42 | function benchmark(clients: number, kbs: number, invocations: number) { 43 | const data = ' '.repeat(1024 * kbs) 44 | let averages: number[] = []; 45 | 46 | return new Promise((resolve) => { 47 | 48 | for (let c = 0; c < clients; c++) { 49 | let client = new Client(); 50 | client.connect("ws://127.0.0.1:8080").then(async () => { 51 | for (let i = 0; i < invocations; i++) { 52 | 53 | const t0 = performance.now(); 54 | 55 | if (await client.call('echo', data) != data) { 56 | console.error('data mismatch!') 57 | } 58 | 59 | const t1 = performance.now(); 60 | 61 | averages.push(t1 - t0) 62 | 63 | if (averages.length == invocations * clients) { 64 | const total = averages.reduce((acc, c) => acc + c, 0); 65 | const average = Math.round((total / averages.length) * 100) / 100; 66 | 67 | resolve({ clients, kbs, average: average, throughput: Math.trunc((1000 / average) * kbs) }) 68 | } 69 | 70 | } 71 | }) 72 | } 73 | }) 74 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es5", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "jsx": "preserve", 9 | "lib": [ 10 | "es2017", 11 | "dom" 12 | ], 13 | "strict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | "esModuleInterop": true 21 | }, 22 | "exclude": [ 23 | ".git", 24 | "node_modules" 25 | ] 26 | } --------------------------------------------------------------------------------