rpcchannel
Version:
Easy RPC with permission controls
189 lines (162 loc) • 5.86 kB
Markdown
# rpcchannel
> A simple system for doing remote procedure calls (RPCs) in JS/TS.



**Note: This is experimental software ATM and is under active development. Use
with caution. Consider all APIs unstable.**
This assumes that there are only two peers per `RpcChannel`. An `RpcChannel` is
created with a send function that sends to whichever transport is being used.
This could literally just be a wrapper for a `MessagePort`'s `postMessage`
function. Messages are processed by calling the `recieve` function on the
`RpcChannel` object.
Each RPC function has a particular address, which is just an array of multiple
strings. They are determined using the Java package naming convention, like so:
```json
["net", "kb1rd", "mycoolprotocol"]
```
Here's the most basic example: (in Typescript. Just remove the type annotations
for normal JS)
```typescript
const {
RpcChannel,
RpcAddress,
EnforceMethodArgSchema
} = require('@kb1rd/rpcchannel')
// Just pretend these are in different browsing contexts :P
const a: RpcChannel = new RpcChannel((msg) =>
b.receive(JSON.parse(JSON.stringify(msg)))
)
const b = new RpcChannel((msg) => a.receive(JSON.parse(JSON.stringify(msg))))
b.register(['net', 'kb1rd', 'sayhi'], (): string => {
console.log('Hi!')
return 'hi'
})
// Send is unidirectional and does not wait for a response
a.send(['net', 'kb1rd', 'sayhi']) // Prints "Hi!"
async function test() {
// Prints "Hi!" and waits for the remote function to terminate
const value = await a.call(['net', 'kb1rd', 'sayhi'])
console.log(value) // Prints "hi"
}
test()
```
A part of the address can also be `undefined`, which acts as a wildcard, like so:
```typescript
const {
RpcChannel,
RpcAddress,
EnforceMethodArgSchema
} = require('@kb1rd/rpcchannel')
// Just pretend these are in different browsing contexts :P
const a: RpcChannel = new RpcChannel((msg) =>
b.receive(JSON.parse(JSON.stringify(msg)))
)
const b = new RpcChannel((msg) => a.receive(JSON.parse(JSON.stringify(msg))))
b.register(['net', 'kb1rd', 'say', undefined], (channel, wc) => {
console.log(wc[0])
})
a.send(['net', 'kb1rd', 'say', 'Hello!']) // Prints "Hello!"
```
Note the arguments of the function. `channel` is the RPC channel that recieved
the call, and `wc` is an array of values for wildcards. Wildcards can be
filtered by permissions, whereas arguments cannot be.
Here's an example of setting permissions to a particular endpoint:
`Permissions are being reworked, this will be updated.`
Functions can also be given arguments
```typescript
const {
RpcChannel,
RpcAddress,
EnforceArgumentSchema
} = require('@kb1rd/rpcchannel')
// Just pretend these are in different browsing contexts :P
const a: RpcChannel = new RpcChannel((msg) =>
b.receive(JSON.parse(JSON.stringify(msg)))
)
const b = new RpcChannel((msg) => a.receive(JSON.parse(JSON.stringify(msg))))
b.register(
['net', 'kb1rd', 'add'],
// This enforces a JSON schema on the function's arguments
EnforceArgumentSchema(
{
type: 'array',
items: [
{ type: 'object' },
{ type: 'array', items: { type: 'string' } },
{ type: 'number' },
{ type: 'number' }
]
},
// Arguments come after then channel and wildcards
(channel, wc, a: number, b: number) => (a + b)
)
)
async function test() {
// The last array is the arguments to pass to the function
const value = await a.call(['net', 'kb1rd', 'add'], [1, 2])
console.log(value) // Prints "3"
}
test()
```
Finally, here's a big example demonstrating `registerAll`:
```typescript
const {
RpcChannel,
RpcAddress,
EnforceMethodArgSchema
} = require('@kb1rd/rpcchannel')
const a: RpcChannel = new RpcChannel((msg) =>
b.receive(JSON.parse(JSON.stringify(msg)))
)
const b = new RpcChannel((msg) => a.receive(JSON.parse(JSON.stringify(msg))))
class TestClass {
test = 1234
// RpcAddress decorator always comes first since `EnforceMethodArgSchema`
// will overwrite it
@RpcAddress(['net', 'kb1rd', 'addto'])
@EnforceMethodArgSchema({
type: 'array',
items: [
{ type: 'object' },
{ type: 'array', items: { type: 'string' } },
{ type: 'number' }
]
})
addto(chan: RpcChannel, wc: string[], n: number): number {
return (this.test += n)
}
@RpcAddress(['net', 'kb1rd', 'greet', undefined])
@EnforceMethodArgSchema({
type: 'array',
items: [{ type: 'object' }, { type: 'array', items: { type: 'string' } }]
})
greet(chan: RpcChannel, wc: string[]): string {
return `Hello, ${wc[0]}`
}
}
// This registers all members of an object with `@RpcAddress` applied to them
// You can also set the RpcFunctionAddress (imported from `rpcchannel`) on a
// member function to the address you'd like and it will be seen by
// `registerAll` (@RpcAddress literally just does
// `func[RpcFunctionAddress] = address`)
b.registerAll(new TestClass())
async function test() {
a.send(['net', 'kb1rd', 'sayhi']) // "Hi!"
let data = await a.call(['net', 'kb1rd', 'addto'], [1])
console.log(data) // "1235"
// Access `call_obj` to get an object that will return a function for every
// string you can access. This is just a prettier way of doing `call`.
data = await a.call_obj.net.kb1rd.addto(20)
console.log(data) // "1255"
try {
const data = await a.call(['net', 'kb1rd', 'addto'], ['hi'])
console.log(data)
} catch (e) {
console.error('ERROR:', e) // "ERROR: ... validation failed"
}
data = await a.call_obj.net.kb1rd.greet['World!']()
console.log(data) // "Hello, World!"
}
test()
```