# Overview [![Build Status](https://drone.nitowa.xyz/api/badges/npm-packages/rpclibrary/status.svg)](https://drone.nitowa.xyz/npm-packages/rpclibrary) [![Current Version](https://img.shields.io/npm/v/rpclibrary.svg)](https://www.npmjs.com/package/rpclibrary) [![Weekly Downloads](https://img.shields.io/npm/dw/rpclibrary?color=important)](https://www.npmjs.com/package/rpclibrary) [![License Type](https://img.shields.io/npm/l/rpclibrary?color=blueviolet)](https://gitea.nitowa.xyz/docs/rpclibrary/src/branch/master/LICENSE.md) rpclibrary is a simple to use websocket RPC library. # How to install ``` npm i rpclibrary ``` # Quickstart (TL;DR) ```typescript import {RPCServer, RPCSocket} from 'rpclibrary' const echo = (text) => text const add = (a, b) => a + b const subtract = (a, b) => a - b new RPCServer([ { name: 'MyRPCGroup1', RPCs: [ echo, add, ] }, name: 'MyOtherRPCGroup', RPCs: [ subtract ] } ]).listen(20000) new RPCSocket(20000, 'localhost').connect().then(async sock => { try{ await sock['MyRPCGroup1'].echo("hello!").then(console.log) const sum = await sock['MyRPCGroup1'].add(1, Math.PI) const result = await sock['MyOtherRPCGroup'].subtract(sum, 4) }catch(e){ console.log(String(e)) } }) ``` # Working with http.Server and express ```typescript import {RPCServer, RPCSocket} from 'rpclibrary' import {Server as httpServer} from 'http' import * as express from 'express' const app = express() //...do express things... const httpServer = new http.Server(app) const rpcServer = new RPCServer([ ... ]) rpcServer.attach(httpServer) rpcServer.listen(8080) //identical to httpServer.listen(8080) ``` # Async and Callbacks? rpclibrary offers full support for callbacks and Promises. Please note that **there may only be one callback per RPC and it has to be the last parameter** ```typescript const getAsync = async () => await new Promise((res, _) => { setTimeout(() => { res({ topic: "Hey!!", message: "Hello World Async!" }) }, 250) }) const getCallback = (callback) => { setTimeout(() => { try{ callback({ topic: "Hey!!", message: "Hello World Callback!" }) }catch(e){ console.log(String(e)) } }, 250) return "Please wait for a callback :)" } new RPCServer([{ name: 'MyRPCGroup1', RPCs: [ getAsync, { name: 'getCallback', hook: getCallback, } ] }]).listen(20000) new RPCSocket(20000, 'localhost').connect().then(async sock => { try{ const RPCs = sock['MyRPCGroup1'] await RPCs.getAsync().then(console.log) await RPCs.getCallback(console.log).then(console.log) }catch(e){ console.log(String(e)) } }) ``` # Hooks and Events There are a many things you can hook into to manage your connections ```typescript new RPCServer([{ name: 'MyRPCGroup1', RPCs: [ echo, { name: 'getCallback', hook: getCallback, onClose: (response, rpc) => { /* client disconnected */ }, onCallback: (...callbackArgs) => { /* callback triggered */ } } ], }], { closeHandler: (socket) => { /* global close handler */ }, connectionHandler: (socket) => { /* new connection made */ }, errorHandler: (socket, error, rpcname, argArr) => { /* An error occured inside a RPC */ }, }).listen(20001) const sock = new RPCSocket(20001, 'localhost') sock.on('error', (e) => { /* handle error */ }) sock.on('close', () => { /* handle close event */ }) sock.hook('RPCName', (/* arg0, arg1, ..., argN */) => { /* bind client-side RPCs */ }) //Retrieve the socket from connectionHandler (Server-side) and trigger with //socket.call('RPCName', arg0, arg1, ..., argN) sock.connect().then(_ => { /* ... */}) ``` # Restricting access rpclibrary offers some minimalistic permission management in the form of a parameter prefix. The ```sesame``` option will decorate all functions with a check for the sesame password. If the wrong string is provided, the RPC will not be executed or respond. ```typescript new RPCServer([ ... ], { sesame: "sesame open", }).listen(20002) new RPCSocket(20002, 'localhost').connect("sesame open").then(async sock => { /* use normally */ }) new RPCSocket(20002, 'localhost').connect("WRONG SESAME").then(async sock => { const RPCs = sock['MyRPCGroup1'] //undefined }) ``` The permissioning can also be performed dynamically with the ```accessfilter``` option and ```sesame``` function. ```typescript new RPCServer([ ... ], { sesame: (sesame) => checkIfSesameTokenShouldHaveAnyAccess(sesame), accessFilter: (sesame, rpcGroupName) => checkIfSesameTokenShouldSeeRPCGroup(sesame, rpcGroupName), }).listen(20002) ``` # Typescript support rpclibrary is a typescript-first project and offers full support for typing your RPCs. **NOTE** that your function implementations and Group Names will usually have to be manually typed to make the compiler infer the correct types. First apply types to your functions: ```typescript echo = (x) => x /*becomes*/ echo = (x:string) : string => x /* A faulty implementation */ badEcho = (x: boolean) : number => 3 ``` Next define a type matching the desired Group-to-RPC structure. This kind of type will be called ```pseudo-interface```: ```typescript type MyType = { MyRPCGroup1: { echo: (x: string) => string //...further RPC declarations }, //... further Group declarations }; ``` If ```MyType``` is provided as generic parameter to ```RPCServer```, ```RPCs``` becomes type-safe. TSC's stack traces may become very long when errors happen in this section. Don't be intimidated by this. The relevant information is usually found at the end in a clear manner. ```typescript new RPCServer([{ name: 'MyRPCGroup1' as 'MyRPCGroup1', //Otherwise the compiler may infer type 'string' RPCs: [ badEcho ], //Type '(x: boolean) => number' is not assignable to type '(x: string) => string' }]).listen(20003) ``` Finally also provide ```MyType``` to the client ```RPCSocket``` to gain type safety for the socket object. ```typescript new RPCSocket(20003, 'localhost').connect().then(async sock => { //TSC knows about MyRPCGroup1 and its members await sock.MyRPCGroup1.echo("hello!").then(console.log) }) ``` # A class-based pattern for modular APIs Define a regular Interface. This will function as the source of truth for type definitions. ```typescript interface IComponentA { echo: (x: string) => string add: (a: number, b: number) => number } interface IComponentB { getAsync: () => Promise<{ topic: string, message: string }> getCallback: (callback:Function) => string } ``` Create an RPC pseudo-interface type that references the proper interface. This will make sure the types declared by your RPCs are consistent with their implementations. ```typescript type TComponentA = { ComponentA: { echo: IComponentA['echo'] add: IComponentA['add'] } } type TComponentB = { ComponentB: { getAsync: IComponentB['getAsync'] getCallback: IComponentB['getCallback'] } } ``` Create classes that implement the interfaces you declared alongside ```RPCExporter```, where MyType is the RPC pseudo-interface. ```typescript class ComponentA implements IComponentA, RPCExporter { name = "ComponentA" as "ComponentA" RPCs = [ this.echo, this.add, ] echo = (text: string) => text add = (a: number, b: number) : number => a + b } class ComponentB implements IComponentB, RPCExporter { name = "ComponentB" as "ComponentB" RPCs = [ this.getAsync, { name: 'getCallback', hook: this.getCallback } ] getAsync = async () : Promise<{topic: string, message:string}> => {...} getCallback = (callback: Function) : string => {...} } ``` Finally, merge all RPC pseudo-interfaces into a project-wide one: ```typescript type TProject = TComponentA & TComponentB new RPCServer([new ComponentA(), new ComponentB()]).listen(port) new RPCSocket(port, host).connect().then(async sock => { }) ``` # [Full documentation](https://gitea.nitowa.xyz/docs/rpclibrary)