new readme and some small fixes
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
[](https://www.npmjs.com/package/rpclibrary)
|
[](https://www.npmjs.com/package/rpclibrary)
|
||||||
[](https://gitea.nitowa.xyz/docs/rpclibrary/src/branch/master/LICENSE.md)
|
[](https://gitea.nitowa.xyz/docs/rpclibrary/src/branch/master/LICENSE.md)
|
||||||
|
|
||||||
rpclibrary is a websocket on steroids!
|
rpclibrary is a simple to use websocket RPC library.
|
||||||
|
|
||||||
# How to install
|
# How to install
|
||||||
```
|
```
|
||||||
@@ -16,147 +16,303 @@ npm i rpclibrary
|
|||||||
```typescript
|
```typescript
|
||||||
import {RPCServer, RPCSocket} from 'rpclibrary'
|
import {RPCServer, RPCSocket} from 'rpclibrary'
|
||||||
|
|
||||||
const port = 1234
|
// TL;DR
|
||||||
const host = 'locahost'
|
const echo = (text) => text
|
||||||
|
const add = (a, b) => a + b
|
||||||
|
|
||||||
const echo = (x) => x
|
new RPCServer(20000, [{
|
||||||
|
name: 'MyRPCGroup1',
|
||||||
|
exportRPCs: () => [
|
||||||
|
echo,
|
||||||
|
add,
|
||||||
|
]
|
||||||
|
}])
|
||||||
|
|
||||||
const server = new RPCServer(port, [{
|
new RPCSocket(20000, 'localhost').connect().then(async sock => {
|
||||||
name: 'HelloWorldRPCGroup',
|
try{
|
||||||
exportRPCs: () => [
|
const RPCs = sock['MyRPCGroup1']
|
||||||
echo, //named function variable
|
await RPCs.echo("hello!").then(console.log)
|
||||||
function echof(x){ return x }, //named function
|
await RPCs.add(1, Math.PI).then(console.log)
|
||||||
|
}catch(e){
|
||||||
|
console.log(String(e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
# 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(20000, [{
|
||||||
|
name: 'MyRPCGroup1',
|
||||||
|
exportRPCs: () => [
|
||||||
|
getAsync,
|
||||||
{
|
{
|
||||||
name: 'echoExplicit', //describing object
|
name: 'getCallback',
|
||||||
call: async (x) => x
|
hook: getCallback,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}])
|
}])
|
||||||
|
|
||||||
const client = new RPCSocket(port, host)
|
new RPCSocket(20000, 'localhost').connect().then(async sock => {
|
||||||
|
try{
|
||||||
client.connect().then(async () => {
|
const RPCs = sock['MyRPCGroup1']
|
||||||
const r0 = await client['HelloWorldRPCGroup'].echo('Hello')
|
await RPCs.getAsync().then(console.log)
|
||||||
const r1 = await client['HelloWorldRPCGroup'].echof('World')
|
await RPCs.getCallback(console.log).then(console.log)
|
||||||
const r2 = await client['HelloWorldRPCGroup'].echoExplicit('RPC!')
|
}catch(e){
|
||||||
|
console.log(String(e))
|
||||||
console.log(r0,r1,r2) //Hello World RPC!
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Using callbacks
|
# Hooks and Events
|
||||||
|
|
||||||
rpclibrary offers a special type of call that can be used with callbacks. The callback **has to be the last argument** and **may be the only passed function**.
|
There are a many things you can hook into to manage your connections
|
||||||
|
|
||||||
In order to function, some metadata has to be included in the return value of hooks. On success, the function is expected to return a `{ result: 'Success', uuid: string }` (Types.SubscriptionResponse) or in case of errors a `{ result: 'Error' }`(Types.ErrorResponse).
|
|
||||||
|
|
||||||
The uuid, as the name implies, is used to uniquely identify the callback for a given invocation and also dictates the name given to the client-side RPC. Unless you got a preferred way of generating these (e.g. using some kind of unique information important to your task) we recommend [uuid](https://www.npmjs.com/package/uuid) for this purpose.
|
|
||||||
|
|
||||||
You should unhook the client socket once you're done with it as not to cause security or control flow issues.
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import {RPCServer, RPCSocket} from 'rpclibrary'
|
new RPCServer(20001, [{
|
||||||
|
name: 'MyRPCGroup1',
|
||||||
const port = 1234
|
exportRPCs: () => [
|
||||||
const host = 'locahost'
|
echo,
|
||||||
|
add,
|
||||||
const callbacks:Map<string, Function> = new Map()
|
getAsync,
|
||||||
|
|
||||||
new RPCServer(port, [{
|
|
||||||
name: 'HelloWorldRPCGroup',
|
|
||||||
exportRPCs: () => [
|
|
||||||
function triggerCallbacks(...messages){ callbacks.forEach(cb => cb.apply({}, messages)) },
|
|
||||||
{
|
{
|
||||||
name: 'subscribe',
|
name: 'getCallback',
|
||||||
hook: async (callback) => {
|
hook: getCallback,
|
||||||
const randStr = 'generate_a_random_string_here'
|
onClose: (response, rpc) => { /* client disconnected */ },
|
||||||
callbacks.set(randStr, callback);
|
onCallback: (...callbackArgs) => { /* callback triggered */ }
|
||||||
return { result: 'Success', uuid: randStr}
|
|
||||||
}
|
|
||||||
},{
|
|
||||||
name: 'unsubscribe',
|
|
||||||
call: async (uuid) => { callbacks.delete(uuid) }
|
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
}], {
|
||||||
|
visibility: '127.0.0.1', //0.0.0.0
|
||||||
|
closeHandler: (socket) => { /* global close handler */ },
|
||||||
|
connectionHandler: (socket) => { /* new connection made */ },
|
||||||
|
errorHandler: (socket, error, rpcname, argArr) => { /* An error occured inside a RPC */ },
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
|
||||||
|
//Restricting access
|
||||||
|
new RPCServer(20002, [{
|
||||||
|
name: 'MyRPCGroup1',
|
||||||
|
exportRPCs: () => [
|
||||||
|
echo,
|
||||||
|
add,
|
||||||
|
getAsync,
|
||||||
|
{
|
||||||
|
name: 'getCallback',
|
||||||
|
hook: getCallback,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}], {
|
||||||
|
sesame: "sesame open",
|
||||||
|
/*
|
||||||
|
OR check sesame dynamically
|
||||||
|
and refine permissioning with accessfilter (optional)
|
||||||
|
*/
|
||||||
|
|
||||||
|
//sesame: (sesame) => true
|
||||||
|
//accessFilter: (sesame, exporter) => { return exporter.name === "MyRPCGroup1" && sesame === "sesame open" },
|
||||||
|
})
|
||||||
|
|
||||||
|
new RPCSocket(20002, 'localhost').connect("sesame open").then(async sock => {
|
||||||
|
try{
|
||||||
|
const RPCs = sock['MyRPCGroup1']
|
||||||
|
await RPCs.echo("hello!").then(console.log)
|
||||||
|
await RPCs.add(1, Math.PI).then(console.log)
|
||||||
|
await RPCs.getAsync().then(console.log)
|
||||||
|
await RPCs.getCallback(console.log).then(console.log)
|
||||||
|
}catch(e){
|
||||||
|
console.log(String(e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# Typescript support
|
||||||
|
|
||||||
|
rpclibrary is a typescript-first project and offers full support for typing your RPCs.
|
||||||
|
**NOTE** that your function implementations have to be currectly typed to make the compiler agree.
|
||||||
|
Explicit typing is recommended.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```typescript
|
||||||
|
echo = (x) => x
|
||||||
|
/*becomes*/
|
||||||
|
echo = (x:string) : string => x
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type MyInterface = {
|
||||||
|
MyRPCGroup1: {
|
||||||
|
echo: (x: string) => string
|
||||||
|
add: (a: number, b: number) => number
|
||||||
|
getAsync: () => Promise<{ topic: string, message: string }>
|
||||||
|
getCallback: (callback:Function) => string
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
exportRPCs is now type safe. Try swapping echo for badEcho.
|
||||||
|
Sadly TSC's stack traces aren't the best, but try to scroll to the bottom of them to find useful info like
|
||||||
|
|
||||||
|
Type '(x: boolean) => number' is not assignable to type '(x: string) => string'
|
||||||
|
*/
|
||||||
|
|
||||||
|
const badEcho = (x: boolean) : number => 3
|
||||||
|
|
||||||
|
new RPCServer<MyInterface>(20003, [{
|
||||||
|
name: 'MyRPCGroup1',
|
||||||
|
exportRPCs: () => [
|
||||||
|
//badEcho,
|
||||||
|
echo,
|
||||||
|
add,
|
||||||
|
getAsync,
|
||||||
|
{
|
||||||
|
name: 'getCallback',
|
||||||
|
hook: getCallback,
|
||||||
|
}
|
||||||
|
],
|
||||||
}])
|
}])
|
||||||
|
|
||||||
const client = new RPCSocket(port, host)
|
new RPCSocket<MyInterface>(20003, 'localhost').connect().then(async sock => {
|
||||||
client.connect().then(async () => {
|
try{
|
||||||
const res = await client['HelloWorldRPCGroup'].subscribe(async (...args:any) => {
|
await sock.MyRPCGroup1.echo("hello!").then(console.log)
|
||||||
console.log.apply(console, args)
|
await sock.MyRPCGroup1.add(1, Math.PI).then(console.log)
|
||||||
|
await sock.MyRPCGroup1.getAsync().then(console.log)
|
||||||
/* close the callbacks once you're done */
|
await sock.MyRPCGroup1.getCallback(console.log).then(console.log)
|
||||||
await client['HelloWorldRPCGroup'].unsubscribe(res.uuid)
|
}catch(e){
|
||||||
client.unhook(res.uuid)
|
console.log(String(e))
|
||||||
})
|
}
|
||||||
|
|
||||||
await client['HelloWorldRPCGroup'].triggerCallbacks("Hello", "World", "Callbacks")
|
|
||||||
})
|
})
|
||||||
```
|
|
||||||
|
|
||||||
If you need to include further response data into your `SubscriptionResponse` you can extend it using the server's first generic parameter `SubResType`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
new RPCServer<{extension: string}>(port, [{
|
|
||||||
name: 'MyRPCGroup',
|
|
||||||
exportRPCs: () => [{
|
|
||||||
name: 'subscribe',
|
|
||||||
hook: async (callback) => {
|
|
||||||
return {
|
|
||||||
result: 'Success',
|
|
||||||
uuid: 'very_random_string',
|
|
||||||
extension: 'your_data_here' //tsc will demand this field
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]}
|
|
||||||
])
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#Experimental typing support
|
|
||||||
It is possible to declare pseudo-interfaces for servers and clients by using server's second generic parameter.
|
# A class-based scalable pattern for APIs
|
||||||
This feature is currently still in development and considered **unstable and untested**. Use with caution.
|
|
||||||
|
because long lists of functions quickly become unwieldy, it is smart to break up the RPCs into chunks or features.
|
||||||
|
A pattern I found to be useful is as follows:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type MyInterface = {
|
interface IMyImplementation {
|
||||||
Group1: {
|
echo: (x: string) => string
|
||||||
triggerCallbacks: (...args:any[]) => Promise<void>,
|
add: (a: number, b: number) => number
|
||||||
subscribe: (param:string, callback:Function) => Promise<SubscriptionResponse<{a: string}>>,
|
getAsync: () => Promise<{ topic: string, message: string }>
|
||||||
unsubscribe: (uuid:string) => Promise<void>
|
getCallback: (callback:Function) => string
|
||||||
},
|
}
|
||||||
Group2: {
|
|
||||||
echo: (x:string) => Promise<string>
|
type MyInterface = {
|
||||||
|
MyRPCGroup1: {
|
||||||
|
echo: IMyImplementation['echo']
|
||||||
|
add: IMyImplementation['add']
|
||||||
|
getAsync: IMyImplementation['getAsync']
|
||||||
|
getCallback: IMyImplementation['getCallback']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
|
||||||
Create a client using
|
|
||||||
```typescript
|
|
||||||
RPCSocket.makeSocket<MyInterface>(port, host).then((async (client) => {
|
|
||||||
const r = await client.Group2.echo("hee") //tsc knows about available RPCs
|
|
||||||
}))
|
|
||||||
|
|
||||||
/* OR */
|
class MyImplementation implements IMyImplementation, RPCExporter<MyInterface>{
|
||||||
|
//"X" as "X" syntax is required to satisfy the type system (as it assumes string to be the true type)
|
||||||
|
name = "MyRpcGroup11" as "MyRPCGroup1"
|
||||||
|
|
||||||
|
//List the functions you declared in MyInterface
|
||||||
|
exportRPCs = () => [
|
||||||
|
this.echo,
|
||||||
|
this.add,
|
||||||
|
this.getAsync,
|
||||||
|
this.getCallback
|
||||||
|
]
|
||||||
|
|
||||||
|
//Write your implementations as you normally would
|
||||||
|
echo = (text: string) => text
|
||||||
|
|
||||||
|
add = (a: number, b: number) : number => a + b
|
||||||
|
|
||||||
|
getAsync = async () : Promise<{topic: string, message:string}>=> await new Promise((res, _) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
res({
|
||||||
|
topic: "Hey!!",
|
||||||
|
message: "Hello World Async!"
|
||||||
|
})
|
||||||
|
}, 250)
|
||||||
|
})
|
||||||
|
|
||||||
|
getCallback = (callback: Function) : string => {
|
||||||
|
setTimeout(() => {
|
||||||
|
try{
|
||||||
|
callback({
|
||||||
|
topic: "Hey!!",
|
||||||
|
message: "Hello World Callback!"
|
||||||
|
})
|
||||||
|
}catch(e){
|
||||||
|
console.log(String(e))
|
||||||
|
}
|
||||||
|
}, 250)
|
||||||
|
return "Please wait for a callback :)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectInterface = MyInterface
|
||||||
|
//& MyOtherInterface
|
||||||
|
//& MyOtherOtherInterface
|
||||||
|
// ...
|
||||||
|
;
|
||||||
|
|
||||||
|
new RPCServer<ProjectInterface>(20004, [new MyImplementation() /*, new MyOtherImplementation(), new MyOtherOtherImplementation() */])
|
||||||
|
|
||||||
|
new RPCSocket<ProjectInterface>(20004, 'localhost').connect().then(async sock => {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
|
||||||
const client = new RPCSocket(port, host)
|
|
||||||
client.connect<MyInterface>().then((async (client) => {
|
|
||||||
const r = await client.Group2.echo("hee") //tsc knows about available RPCs
|
|
||||||
}))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a server using
|
|
||||||
```typescript
|
|
||||||
new RPCServer<{a:string}, MyInterface>(port,
|
|
||||||
[{
|
|
||||||
//...
|
|
||||||
},{
|
|
||||||
name: 'Group2', //Auto completion for names
|
|
||||||
exportRPCs: () => [{
|
|
||||||
name: 'echo', //this name too!
|
|
||||||
call: async (x) => x+"llo World!" //the paramter and return types are known by tsc
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
# [Full documentation](https://gitea.nitowa.xyz/docs/rpclibrary)
|
# [Full documentation](https://gitea.nitowa.xyz/docs/rpclibrary)
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import { RPCServer, RPCSocket } from './Index'
|
||||||
|
import { RPCExporter } from './src/Interfaces'
|
||||||
|
|
||||||
|
// TL;DR
|
||||||
|
const echo = (text: string) => text
|
||||||
|
const add = (a: number, b: number) : number => a + b
|
||||||
|
const getAsync = async () : Promise<{topic: string, message:string}>=> await new Promise((res, _) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
res({
|
||||||
|
topic: "Hey!!",
|
||||||
|
message: "Hello World Async!"
|
||||||
|
})
|
||||||
|
}, 250)
|
||||||
|
})
|
||||||
|
const getCallback = (callback: Function) : string => {
|
||||||
|
setTimeout(() => {
|
||||||
|
try{
|
||||||
|
callback({
|
||||||
|
topic: "Hey!!",
|
||||||
|
message: "Hello World Callback!"
|
||||||
|
})
|
||||||
|
}catch(e){
|
||||||
|
console.log(String(e))
|
||||||
|
}
|
||||||
|
}, 250)
|
||||||
|
return "Please wait for a callback :)"
|
||||||
|
}
|
||||||
|
new RPCServer(20000, [{
|
||||||
|
name: 'MyRPCGroup1',
|
||||||
|
exportRPCs: () => [
|
||||||
|
echo,
|
||||||
|
add,
|
||||||
|
getAsync,
|
||||||
|
{
|
||||||
|
name: 'getCallback',
|
||||||
|
hook: getCallback,
|
||||||
|
onClose: (response, rpc) => { /* ... */ },
|
||||||
|
onCallback: (...callbackArgs) => { /* ... */ }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}])
|
||||||
|
|
||||||
|
new RPCSocket(20000, 'localhost').connect().then(async sock => {
|
||||||
|
try{
|
||||||
|
const RPCs = sock['MyRPCGroup1']
|
||||||
|
await RPCs.echo("hello!").then(console.log)
|
||||||
|
await RPCs.add(1, Math.PI).then(console.log)
|
||||||
|
await RPCs.getAsync().then(console.log)
|
||||||
|
await RPCs.getCallback(console.log).then(console.log)
|
||||||
|
}catch(e){
|
||||||
|
console.log(String(e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//Hooks and events
|
||||||
|
new RPCServer(20001, [{
|
||||||
|
name: 'MyRPCGroup1',
|
||||||
|
exportRPCs: () => [
|
||||||
|
echo,
|
||||||
|
add,
|
||||||
|
getAsync,
|
||||||
|
{
|
||||||
|
name: 'getCallback',
|
||||||
|
hook: getCallback,
|
||||||
|
onClose: (response, rpc) => { /* client disconnected */ },
|
||||||
|
onCallback: (...callbackArgs) => { /* callback triggered */ }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}], {
|
||||||
|
visibility: '127.0.0.1', //0.0.0.0
|
||||||
|
closeHandler: (socket) => { /* global close handler */ },
|
||||||
|
connectionHandler: (socket) => { /* new connection made */ },
|
||||||
|
errorHandler: (socket, error, rpcname, argArr) => { /* An error occured inside a RPC */ },
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
new RPCServer(20002, [{
|
||||||
|
name: 'MyRPCGroup1',
|
||||||
|
exportRPCs: () => [
|
||||||
|
echo,
|
||||||
|
add,
|
||||||
|
getAsync,
|
||||||
|
{
|
||||||
|
name: 'getCallback',
|
||||||
|
hook: getCallback,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}], {
|
||||||
|
sesame: "sesame open",
|
||||||
|
/*
|
||||||
|
OR check sesame dynamically
|
||||||
|
and refine permissioning with accessfilter (optional)
|
||||||
|
*/
|
||||||
|
|
||||||
|
//sesame: (sesame) => true
|
||||||
|
//accessFilter: (sesame, exporter) => { return exporter.name === "MyRPCGroup1" && sesame === "sesame open" },
|
||||||
|
})
|
||||||
|
|
||||||
|
new RPCSocket(20002, 'localhost').connect("sesame open").then(async sock => {
|
||||||
|
try{
|
||||||
|
const RPCs = sock['MyRPCGroup1']
|
||||||
|
await RPCs.echo("hello!").then(console.log)
|
||||||
|
await RPCs.add(1, Math.PI).then(console.log)
|
||||||
|
await RPCs.getAsync().then(console.log)
|
||||||
|
await RPCs.getCallback(console.log).then(console.log)
|
||||||
|
}catch(e){
|
||||||
|
console.log(String(e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
//TypeScript and pseudo-interfaces
|
||||||
|
|
||||||
|
type MyInterface = {
|
||||||
|
MyRPCGroup1: {
|
||||||
|
echo: (x: string) => string
|
||||||
|
add: (a: number, b: number) => number
|
||||||
|
getAsync: () => Promise<{ topic: string, message: string }>
|
||||||
|
getCallback: (callback:Function) => string
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
exportRPCs is now type safe. Try swapping echo for badEcho.
|
||||||
|
Sadly TSC's stack traces aren't the best, but try to scroll to the bottom of them to find useful info like
|
||||||
|
|
||||||
|
Type '(x: boolean) => number' is not assignable to type '(x: string) => string'
|
||||||
|
*/
|
||||||
|
|
||||||
|
const badEcho = (x: boolean) : number => 3
|
||||||
|
|
||||||
|
new RPCServer<MyInterface>(20003, [{
|
||||||
|
name: 'MyRPCGroup1',
|
||||||
|
exportRPCs: () => [
|
||||||
|
//badEcho,
|
||||||
|
echo,
|
||||||
|
add,
|
||||||
|
getAsync,
|
||||||
|
{
|
||||||
|
name: 'getCallback',
|
||||||
|
hook: getCallback,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}])
|
||||||
|
|
||||||
|
new RPCSocket<MyInterface>(20003, 'localhost').connect().then(async sock => {
|
||||||
|
try{
|
||||||
|
await sock.MyRPCGroup1.echo("hello!").then(console.log)
|
||||||
|
await sock.MyRPCGroup1.add(1, Math.PI).then(console.log)
|
||||||
|
await sock.MyRPCGroup1.getAsync().then(console.log)
|
||||||
|
await sock.MyRPCGroup1.getCallback(console.log).then(console.log)
|
||||||
|
}catch(e){
|
||||||
|
console.log(String(e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
//Class-based pattern
|
||||||
|
|
||||||
|
interface IMyImplementation {
|
||||||
|
echo: (x: string) => string
|
||||||
|
add: (a: number, b: number) => number
|
||||||
|
getAsync: () => Promise<{ topic: string, message: string }>
|
||||||
|
getCallback: (callback:Function) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MyIfc = {
|
||||||
|
MyRPCGroup1: {
|
||||||
|
echo: IMyImplementation['echo']
|
||||||
|
add: IMyImplementation['add']
|
||||||
|
getAsync: IMyImplementation['getAsync']
|
||||||
|
getCallback: IMyImplementation['getCallback']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyImplementation implements IMyImplementation, RPCExporter<MyIfc>{
|
||||||
|
//"X" as "X" syntax is required to satisfy the type system (as it assumed string)
|
||||||
|
name = "MyRpcGroup11" as "MyRPCGroup1"
|
||||||
|
|
||||||
|
//List the functions you declared in MyIfc
|
||||||
|
exportRPCs = () => [
|
||||||
|
this.echo,
|
||||||
|
this.add,
|
||||||
|
this.getAsync,
|
||||||
|
this.getCallback
|
||||||
|
]
|
||||||
|
|
||||||
|
//Write your implementations as you normally would
|
||||||
|
echo = (text: string) => text
|
||||||
|
|
||||||
|
add = (a: number, b: number) : number => a + b
|
||||||
|
|
||||||
|
getAsync = async () : Promise<{topic: string, message:string}>=> await new Promise((res, _) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
res({
|
||||||
|
topic: "Hey!!",
|
||||||
|
message: "Hello World Async!"
|
||||||
|
})
|
||||||
|
}, 250)
|
||||||
|
})
|
||||||
|
|
||||||
|
getCallback = (callback: Function) : string => {
|
||||||
|
setTimeout(() => {
|
||||||
|
try{
|
||||||
|
callback({
|
||||||
|
topic: "Hey!!",
|
||||||
|
message: "Hello World Callback!"
|
||||||
|
})
|
||||||
|
}catch(e){
|
||||||
|
console.log(String(e))
|
||||||
|
}
|
||||||
|
}, 250)
|
||||||
|
return "Please wait for a callback :)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new RPCServer<MyIfc>(20004, [new MyImplementation(), /* ... other RPCExporters */])
|
||||||
|
|
||||||
|
new RPCSocket<MyIfc>(20004, 'localhost').connect().then(async sock => {
|
||||||
|
try{
|
||||||
|
await sock.MyRPCGroup1.echo("hello!").then(console.log)
|
||||||
|
await sock.MyRPCGroup1.add(1, Math.PI).then(console.log)
|
||||||
|
await sock.MyRPCGroup1.getAsync().then(console.log)
|
||||||
|
await sock.MyRPCGroup1.getCallback(console.log).then(console.log)
|
||||||
|
}catch(e){
|
||||||
|
console.log(String(e))
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -6,10 +6,6 @@ import * as T from './Types';
|
|||||||
import * as U from './Utils';
|
import * as U from './Utils';
|
||||||
import * as I from './Interfaces';
|
import * as I from './Interfaces';
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Websocket-server-on-steroids with built-in RPC capabilities
|
|
||||||
*/
|
|
||||||
export class RPCServer<
|
export class RPCServer<
|
||||||
InterfaceT extends T.RPCInterface = T.RPCInterface,
|
InterfaceT extends T.RPCInterface = T.RPCInterface,
|
||||||
> implements I.Destroyable {
|
> implements I.Destroyable {
|
||||||
|
|||||||
+4
-4
@@ -13,8 +13,8 @@ import { stripAfterEquals, appendComma } from './Utils';
|
|||||||
export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I.Socket{
|
export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I.Socket{
|
||||||
|
|
||||||
static async makeSocket<T extends T.RPCInterface = T.RPCInterface>(port:number, server: string, sesame?:string, conf?:T.SocketConf): Promise<T.ConnectedSocket<T>> {
|
static async makeSocket<T extends T.RPCInterface = T.RPCInterface>(port:number, server: string, sesame?:string, conf?:T.SocketConf): Promise<T.ConnectedSocket<T>> {
|
||||||
const socket = new RPCSocket(port, server, conf)
|
const socket = new RPCSocket<T>(port, server, conf)
|
||||||
return await socket.connect<T>(sesame)
|
return await socket.connect(sesame)
|
||||||
}
|
}
|
||||||
|
|
||||||
private socket: I.Socket
|
private socket: I.Socket
|
||||||
@@ -128,7 +128,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
|
|||||||
/**
|
/**
|
||||||
* Connects to the server and attaches available RPCs to this object
|
* Connects to the server and attaches available RPCs to this object
|
||||||
*/
|
*/
|
||||||
public async connect<T extends T.RPCInterface= Ifc>( sesame?: string ) : Promise<T.ConnectedSocket<T>> {
|
public async connect( sesame?: string ) : Promise<T.ConnectedSocket<Ifc>> {
|
||||||
this.socket = await bsock.connect(this.port, this.server, this.conf.tls?this.conf.tls:false)
|
this.socket = await bsock.connect(this.port, this.server, this.conf.tls?this.conf.tls:false)
|
||||||
this.errorHandlers.forEach(h => this.socket.on('error', h))
|
this.errorHandlers.forEach(h => this.socket.on('error', h))
|
||||||
this.closeHandlers.forEach(h => this.socket.on('close', h))
|
this.closeHandlers.forEach(h => this.socket.on('close', h))
|
||||||
@@ -153,7 +153,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
|
|||||||
this[i.owner][i.name] = f
|
this[i.owner][i.name] = f
|
||||||
this[i.owner][i.name].bind(this)
|
this[i.owner][i.name].bind(this)
|
||||||
})
|
})
|
||||||
return <RPCSocket & T.RPCInterface<T>> (this as any)
|
return <T.ConnectedSocket<Ifc>> (this as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import * as I from "./Interfaces"
|
|||||||
*/
|
*/
|
||||||
export type RPCExporter<
|
export type RPCExporter<
|
||||||
Ifc extends T.RPCInterface = T.RPCInterface,
|
Ifc extends T.RPCInterface = T.RPCInterface,
|
||||||
Name extends keyof Ifc = string,
|
Name extends keyof Ifc = keyof Ifc,
|
||||||
> = {
|
> = {
|
||||||
name: Name
|
name: Name
|
||||||
exportRPCs() : T.RPCDefinitions<Ifc>[Name]
|
exportRPCs() : T.RPCDefinitions<Ifc>[Name]
|
||||||
|
|||||||
+7
-4
@@ -3,13 +3,12 @@ import { RPCSocket } from "./Frontend";
|
|||||||
|
|
||||||
export type AnyFunction = (...args:any) => any
|
export type AnyFunction = (...args:any) => any
|
||||||
export type HookFunction = AnyFunction
|
export type HookFunction = AnyFunction
|
||||||
export type AccessFilter<InterfaceT extends RPCInterface = RPCInterface> = (sesame:string|undefined, exporter: I.RPCExporter<InterfaceT, keyof InterfaceT>) => Promise<boolean>
|
export type AccessFilter<InterfaceT extends RPCInterface = RPCInterface> = (sesame:string|undefined, exporter: I.RPCExporter<InterfaceT, keyof InterfaceT>) => Promise<boolean> | boolean
|
||||||
export type Visibility = "127.0.0.1" | "0.0.0.0"
|
export type Visibility = "127.0.0.1" | "0.0.0.0"
|
||||||
export type ConnectionHandler = (socket:I.Socket) => void
|
export type ConnectionHandler = (socket:I.Socket) => void
|
||||||
export type ErrorHandler = (socket:I.Socket, error:any, rpcName: string, args: any[]) => void
|
export type ErrorHandler = (socket:I.Socket, error:any, rpcName: string, args: any[]) => void
|
||||||
export type CloseHandler = (socket:I.Socket) => void
|
export type CloseHandler = (socket:I.Socket) => void
|
||||||
export type SesameFunction = (sesame : string) => boolean
|
export type SesameFunction = (sesame : string) => boolean
|
||||||
export type ExceptionHandling = 'local' | 'remote'
|
|
||||||
export type SesameConf = {
|
export type SesameConf = {
|
||||||
sesame?: string | SesameFunction
|
sesame?: string | SesameFunction
|
||||||
}
|
}
|
||||||
@@ -20,7 +19,7 @@ export type FrontEndHandlerType = {
|
|||||||
|
|
||||||
export type ExporterArray<InterfaceT extends RPCInterface = RPCInterface> = I.RPCExporter<RPCInterface<InterfaceT>, keyof InterfaceT>[]
|
export type ExporterArray<InterfaceT extends RPCInterface = RPCInterface> = I.RPCExporter<RPCInterface<InterfaceT>, keyof InterfaceT>[]
|
||||||
|
|
||||||
export type ConnectedSocket<T extends RPCInterface = RPCInterface> = RPCSocket & T
|
export type ConnectedSocket<T extends RPCInterface = RPCInterface> = RPCSocket & AsyncIfc<T>
|
||||||
|
|
||||||
export type ServerConf<InterfaceT extends RPCInterface> = {
|
export type ServerConf<InterfaceT extends RPCInterface> = {
|
||||||
accessFilter?: AccessFilter<InterfaceT>
|
accessFilter?: AccessFilter<InterfaceT>
|
||||||
@@ -28,7 +27,6 @@ export type ServerConf<InterfaceT extends RPCInterface> = {
|
|||||||
errorHandler?: ErrorHandler
|
errorHandler?: ErrorHandler
|
||||||
closeHandler?: CloseHandler
|
closeHandler?: CloseHandler
|
||||||
visibility?: Visibility
|
visibility?: Visibility
|
||||||
exceptionHandling?: ExceptionHandling
|
|
||||||
} & SesameConf
|
} & SesameConf
|
||||||
|
|
||||||
export type SocketConf = {
|
export type SocketConf = {
|
||||||
@@ -95,3 +93,8 @@ export type ExtendedRpcInfo = RpcInfo & { uniqueName: string }
|
|||||||
|
|
||||||
export type OnFunction = <T extends "error" | "close">(type: T, f: FrontEndHandlerType[T]) => void
|
export type OnFunction = <T extends "error" | "close">(type: T, f: FrontEndHandlerType[T]) => void
|
||||||
export type HookCloseFunction<T> = (res: T, rpc:HookRPC<any, any>) => any
|
export type HookCloseFunction<T> = (res: T, rpc:HookRPC<any, any>) => any
|
||||||
|
|
||||||
|
|
||||||
|
export type AsyncIfc<Ifc extends RPCInterface> = { [grp in keyof Ifc]: {[rpcname in keyof Ifc[grp]] : AsyncAnyFunction<Ifc[grp][rpcname]> } }
|
||||||
|
|
||||||
|
export type AsyncAnyFunction<F extends AnyFunction = AnyFunction> = F extends (...args: Parameters<F>) => infer R ? ((...args: Parameters<F>) => R extends Promise<any> ? R : Promise<R> ) : Promise<any>
|
||||||
+5
-5
@@ -330,7 +330,7 @@ describe('Sesame should unlock the socket', () => {
|
|||||||
|
|
||||||
it('should not work without sesame', (done) => {
|
it('should not work without sesame', (done) => {
|
||||||
const sock = new RPCSocket(21004, "localhost")
|
const sock = new RPCSocket(21004, "localhost")
|
||||||
sock.connect<SesameTestIfc>( /* no sesame */).then(async (cli) => {
|
sock.connect( /* no sesame */).then(async (cli) => {
|
||||||
if (!cli.test)
|
if (!cli.test)
|
||||||
done()
|
done()
|
||||||
else {
|
else {
|
||||||
@@ -343,7 +343,7 @@ describe('Sesame should unlock the socket', () => {
|
|||||||
|
|
||||||
it('should fail with wrong sesame', (done) => {
|
it('should fail with wrong sesame', (done) => {
|
||||||
const sock = new RPCSocket(21004, "localhost")
|
const sock = new RPCSocket(21004, "localhost")
|
||||||
sock.connect<SesameTestIfc>('abasd').then(async (cli) => {
|
sock.connect('abasd').then(async (cli) => {
|
||||||
if (!cli.test)
|
if (!cli.test)
|
||||||
done()
|
done()
|
||||||
else {
|
else {
|
||||||
@@ -586,8 +586,8 @@ describe("Class binding", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
const s = new RPCSocket(21004, 'localhost')
|
const s = new RPCSocket<myExporterIfc>(21004, 'localhost')
|
||||||
s.connect<myExporterIfc>("xxx").then(conn => {
|
s.connect("xxx").then(conn => {
|
||||||
sock = conn
|
sock = conn
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
@@ -645,7 +645,7 @@ describe("Class binding", () => {
|
|||||||
|
|
||||||
describe("attaching handlers before connecting", () => {
|
describe("attaching handlers before connecting", () => {
|
||||||
it("fires error if server is unreachable", (done) => {
|
it("fires error if server is unreachable", (done) => {
|
||||||
const sock = new RPCSocket(21004, 'localhost')
|
const sock = new RPCSocket<myExporterIfc>(21004, 'localhost')
|
||||||
let errorHandleCount = 0
|
let errorHandleCount = 0
|
||||||
|
|
||||||
sock.on('error', (err) => {
|
sock.on('error', (err) => {
|
||||||
|
|||||||
+1
-1
@@ -9,6 +9,6 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "test/**/*.ts", "Index.ts"],
|
"include": ["src/**/*.ts", "test/**/*.ts", "Index.ts", "demo.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user