new readme and some small fixes
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
[](https://www.npmjs.com/package/rpclibrary)
|
||||
[](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
|
||||
```
|
||||
@@ -16,147 +16,303 @@ npm i rpclibrary
|
||||
```typescript
|
||||
import {RPCServer, RPCSocket} from 'rpclibrary'
|
||||
|
||||
const port = 1234
|
||||
const host = 'locahost'
|
||||
// TL;DR
|
||||
const echo = (text) => text
|
||||
const add = (a, b) => a + b
|
||||
|
||||
const echo = (x) => x
|
||||
|
||||
const server = new RPCServer(port, [{
|
||||
name: 'HelloWorldRPCGroup',
|
||||
new RPCServer(20000, [{
|
||||
name: 'MyRPCGroup1',
|
||||
exportRPCs: () => [
|
||||
echo, //named function variable
|
||||
function echof(x){ return x }, //named function
|
||||
echo,
|
||||
add,
|
||||
]
|
||||
}])
|
||||
|
||||
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)
|
||||
}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
|
||||
call: async (x) => x
|
||||
name: 'getCallback',
|
||||
hook: getCallback,
|
||||
}
|
||||
]
|
||||
}])
|
||||
|
||||
const client = new RPCSocket(port, host)
|
||||
|
||||
client.connect().then(async () => {
|
||||
const r0 = await client['HelloWorldRPCGroup'].echo('Hello')
|
||||
const r1 = await client['HelloWorldRPCGroup'].echof('World')
|
||||
const r2 = await client['HelloWorldRPCGroup'].echoExplicit('RPC!')
|
||||
|
||||
console.log(r0,r1,r2) //Hello World RPC!
|
||||
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))
|
||||
}
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
# 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**.
|
||||
|
||||
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.
|
||||
There are a many things you can hook into to manage your connections
|
||||
|
||||
```typescript
|
||||
import {RPCServer, RPCSocket} from 'rpclibrary'
|
||||
|
||||
const port = 1234
|
||||
const host = 'locahost'
|
||||
|
||||
const callbacks:Map<string, Function> = new Map()
|
||||
|
||||
new RPCServer(port, [{
|
||||
name: 'HelloWorldRPCGroup',
|
||||
new RPCServer(20001, [{
|
||||
name: 'MyRPCGroup1',
|
||||
exportRPCs: () => [
|
||||
function triggerCallbacks(...messages){ callbacks.forEach(cb => cb.apply({}, messages)) },
|
||||
echo,
|
||||
add,
|
||||
getAsync,
|
||||
{
|
||||
name: 'subscribe',
|
||||
hook: async (callback) => {
|
||||
const randStr = 'generate_a_random_string_here'
|
||||
callbacks.set(randStr, callback);
|
||||
return { result: 'Success', uuid: randStr}
|
||||
name: 'getCallback',
|
||||
hook: getCallback,
|
||||
onClose: (response, rpc) => { /* client disconnected */ },
|
||||
onCallback: (...callbackArgs) => { /* callback triggered */ }
|
||||
}
|
||||
},{
|
||||
name: 'unsubscribe',
|
||||
call: async (uuid) => { callbacks.delete(uuid) }
|
||||
}
|
||||
]
|
||||
}])
|
||||
|
||||
const client = new RPCSocket(port, host)
|
||||
client.connect().then(async () => {
|
||||
const res = await client['HelloWorldRPCGroup'].subscribe(async (...args:any) => {
|
||||
console.log.apply(console, args)
|
||||
|
||||
/* close the callbacks once you're done */
|
||||
await client['HelloWorldRPCGroup'].unsubscribe(res.uuid)
|
||||
client.unhook(res.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 */ },
|
||||
})
|
||||
|
||||
await client['HelloWorldRPCGroup'].triggerCallbacks("Hello", "World", "Callbacks")
|
||||
})
|
||||
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(_ => { /* ... */})
|
||||
|
||||
```
|
||||
|
||||
If you need to include further response data into your `SubscriptionResponse` you can extend it using the server's first generic parameter `SubResType`
|
||||
|
||||
# Restricting access
|
||||
|
||||
rpclibrary offers some minimalistic permission management
|
||||
|
||||
```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
|
||||
|
||||
//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))
|
||||
}
|
||||
}]}
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
```
|
||||
|
||||
#Experimental typing support
|
||||
It is possible to declare pseudo-interfaces for servers and clients by using server's second generic parameter.
|
||||
This feature is currently still in development and considered **unstable and untested**. Use with caution.
|
||||
|
||||
# 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 = {
|
||||
Group1: {
|
||||
triggerCallbacks: (...args:any[]) => Promise<void>,
|
||||
subscribe: (param:string, callback:Function) => Promise<SubscriptionResponse<{a: string}>>,
|
||||
unsubscribe: (uuid:string) => Promise<void>
|
||||
},
|
||||
Group2: {
|
||||
echo: (x:string) => Promise<string>
|
||||
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,
|
||||
}
|
||||
```
|
||||
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 */
|
||||
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))
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
# A class-based scalable pattern for APIs
|
||||
|
||||
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
|
||||
new RPCServer<{a:string}, MyInterface>(port,
|
||||
[{
|
||||
interface IMyImplementation {
|
||||
echo: (x: string) => string
|
||||
add: (a: number, b: number) => number
|
||||
getAsync: () => Promise<{ topic: string, message: string }>
|
||||
getCallback: (callback:Function) => string
|
||||
}
|
||||
|
||||
type MyInterface = {
|
||||
MyRPCGroup1: {
|
||||
echo: IMyImplementation['echo']
|
||||
add: IMyImplementation['add']
|
||||
getAsync: IMyImplementation['getAsync']
|
||||
getCallback: IMyImplementation['getCallback']
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// ...
|
||||
},{
|
||||
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
|
||||
}]
|
||||
}]
|
||||
)
|
||||
;
|
||||
|
||||
new RPCServer<ProjectInterface>(20004, [new MyImplementation() /*, new MyOtherImplementation(), new MyOtherOtherImplementation() */])
|
||||
|
||||
new RPCSocket<ProjectInterface>(20004, 'localhost').connect().then(async sock => {
|
||||
// ...
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
|
||||
# [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 I from './Interfaces';
|
||||
|
||||
|
||||
/**
|
||||
* A Websocket-server-on-steroids with built-in RPC capabilities
|
||||
*/
|
||||
export class RPCServer<
|
||||
InterfaceT extends T.RPCInterface = T.RPCInterface,
|
||||
> 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{
|
||||
|
||||
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)
|
||||
return await socket.connect<T>(sesame)
|
||||
const socket = new RPCSocket<T>(port, server, conf)
|
||||
return await socket.connect(sesame)
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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.errorHandlers.forEach(h => this.socket.on('error', 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].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<
|
||||
Ifc extends T.RPCInterface = T.RPCInterface,
|
||||
Name extends keyof Ifc = string,
|
||||
Name extends keyof Ifc = keyof Ifc,
|
||||
> = {
|
||||
name: Name
|
||||
exportRPCs() : T.RPCDefinitions<Ifc>[Name]
|
||||
|
||||
+7
-4
@@ -3,13 +3,12 @@ import { RPCSocket } from "./Frontend";
|
||||
|
||||
export type AnyFunction = (...args:any) => any
|
||||
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 ConnectionHandler = (socket:I.Socket) => void
|
||||
export type ErrorHandler = (socket:I.Socket, error:any, rpcName: string, args: any[]) => void
|
||||
export type CloseHandler = (socket:I.Socket) => void
|
||||
export type SesameFunction = (sesame : string) => boolean
|
||||
export type ExceptionHandling = 'local' | 'remote'
|
||||
export type SesameConf = {
|
||||
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 ConnectedSocket<T extends RPCInterface = RPCInterface> = RPCSocket & T
|
||||
export type ConnectedSocket<T extends RPCInterface = RPCInterface> = RPCSocket & AsyncIfc<T>
|
||||
|
||||
export type ServerConf<InterfaceT extends RPCInterface> = {
|
||||
accessFilter?: AccessFilter<InterfaceT>
|
||||
@@ -28,7 +27,6 @@ export type ServerConf<InterfaceT extends RPCInterface> = {
|
||||
errorHandler?: ErrorHandler
|
||||
closeHandler?: CloseHandler
|
||||
visibility?: Visibility
|
||||
exceptionHandling?: ExceptionHandling
|
||||
} & SesameConf
|
||||
|
||||
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 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) => {
|
||||
const sock = new RPCSocket(21004, "localhost")
|
||||
sock.connect<SesameTestIfc>( /* no sesame */).then(async (cli) => {
|
||||
sock.connect( /* no sesame */).then(async (cli) => {
|
||||
if (!cli.test)
|
||||
done()
|
||||
else {
|
||||
@@ -343,7 +343,7 @@ describe('Sesame should unlock the socket', () => {
|
||||
|
||||
it('should fail with wrong sesame', (done) => {
|
||||
const sock = new RPCSocket(21004, "localhost")
|
||||
sock.connect<SesameTestIfc>('abasd').then(async (cli) => {
|
||||
sock.connect('abasd').then(async (cli) => {
|
||||
if (!cli.test)
|
||||
done()
|
||||
else {
|
||||
@@ -586,8 +586,8 @@ describe("Class binding", () => {
|
||||
})
|
||||
|
||||
beforeEach((done) => {
|
||||
const s = new RPCSocket(21004, 'localhost')
|
||||
s.connect<myExporterIfc>("xxx").then(conn => {
|
||||
const s = new RPCSocket<myExporterIfc>(21004, 'localhost')
|
||||
s.connect("xxx").then(conn => {
|
||||
sock = conn
|
||||
done()
|
||||
})
|
||||
@@ -645,7 +645,7 @@ describe("Class binding", () => {
|
||||
|
||||
describe("attaching handlers before connecting", () => {
|
||||
it("fires error if server is unreachable", (done) => {
|
||||
const sock = new RPCSocket(21004, 'localhost')
|
||||
const sock = new RPCSocket<myExporterIfc>(21004, 'localhost')
|
||||
let errorHandleCount = 0
|
||||
|
||||
sock.on('error', (err) => {
|
||||
|
||||
+1
-1
@@ -9,6 +9,6 @@
|
||||
"strict": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts", "Index.ts"],
|
||||
"include": ["src/**/*.ts", "test/**/*.ts", "Index.ts", "demo.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user