|
@@ -5,7 +5,7 @@
|
5
|
5
|
[![Weekly Downloads](https://img.shields.io/npm/dw/rpclibrary?color=important)](https://www.npmjs.com/package/rpclibrary)
|
6
|
6
|
[![License Type](https://img.shields.io/npm/l/rpclibrary?color=blueviolet)](https://gitea.nitowa.xyz/docs/rpclibrary/src/branch/master/LICENSE.md)
|
7
|
7
|
|
8
|
|
-rpclibrary is a websocket on steroids!
|
|
8
|
+rpclibrary is a simple to use websocket RPC library.
|
9
|
9
|
|
10
|
10
|
# How to install
|
11
|
11
|
```
|
|
@@ -16,147 +16,303 @@ npm i rpclibrary
|
16
|
16
|
```typescript
|
17
|
17
|
import {RPCServer, RPCSocket} from 'rpclibrary'
|
18
|
18
|
|
19
|
|
-const port = 1234
|
20
|
|
-const host = 'locahost'
|
|
19
|
+// TL;DR
|
|
20
|
+const echo = (text) => text
|
|
21
|
+const add = (a, b) => a + b
|
21
|
22
|
|
22
|
|
-const echo = (x) => x
|
|
23
|
+new RPCServer(20000, [{
|
|
24
|
+ name: 'MyRPCGroup1',
|
|
25
|
+ exportRPCs: () => [
|
|
26
|
+ echo,
|
|
27
|
+ add,
|
|
28
|
+ ]
|
|
29
|
+}])
|
|
30
|
+
|
|
31
|
+new RPCSocket(20000, 'localhost').connect().then(async sock => {
|
|
32
|
+ try{
|
|
33
|
+ const RPCs = sock['MyRPCGroup1']
|
|
34
|
+ await RPCs.echo("hello!").then(console.log)
|
|
35
|
+ await RPCs.add(1, Math.PI).then(console.log)
|
|
36
|
+ }catch(e){
|
|
37
|
+ console.log(String(e))
|
|
38
|
+ }
|
|
39
|
+})
|
|
40
|
+```
|
|
41
|
+
|
|
42
|
+# Async and Callbacks?
|
|
43
|
+
|
|
44
|
+rpclibrary offers full support for callbacks and Promises.
|
|
45
|
+Please note that **there may only be one callback per RPC and it has to be the last parameter**
|
|
46
|
+
|
|
47
|
+```typescript
|
23
|
48
|
|
24
|
|
-const server = new RPCServer(port, [{
|
25
|
|
- name: 'HelloWorldRPCGroup',
|
26
|
|
- exportRPCs: () => [
|
27
|
|
- echo, //named function variable
|
28
|
|
- function echof(x){ return x }, //named function
|
|
49
|
+const getAsync = async () => await new Promise((res, _) => {
|
|
50
|
+ setTimeout(() => {
|
|
51
|
+ res({
|
|
52
|
+ topic: "Hey!!",
|
|
53
|
+ message: "Hello World Async!"
|
|
54
|
+ })
|
|
55
|
+ }, 250)
|
|
56
|
+})
|
|
57
|
+
|
|
58
|
+const getCallback = (callback) => {
|
|
59
|
+ setTimeout(() => {
|
|
60
|
+ try{
|
|
61
|
+ callback({
|
|
62
|
+ topic: "Hey!!",
|
|
63
|
+ message: "Hello World Callback!"
|
|
64
|
+ })
|
|
65
|
+ }catch(e){
|
|
66
|
+ console.log(String(e))
|
|
67
|
+ }
|
|
68
|
+ }, 250)
|
|
69
|
+ return "Please wait for a callback :)"
|
|
70
|
+}
|
|
71
|
+
|
|
72
|
+new RPCServer(20000, [{
|
|
73
|
+ name: 'MyRPCGroup1',
|
|
74
|
+ exportRPCs: () => [
|
|
75
|
+ getAsync,
|
29
|
76
|
{
|
30
|
|
- name: 'echoExplicit', //describing object
|
31
|
|
- call: async (x) => x
|
|
77
|
+ name: 'getCallback',
|
|
78
|
+ hook: getCallback,
|
32
|
79
|
}
|
33
|
80
|
]
|
34
|
81
|
}])
|
35
|
82
|
|
36
|
|
-const client = new RPCSocket(port, host)
|
37
|
|
-
|
38
|
|
-client.connect().then(async () => {
|
39
|
|
- const r0 = await client['HelloWorldRPCGroup'].echo('Hello')
|
40
|
|
- const r1 = await client['HelloWorldRPCGroup'].echof('World')
|
41
|
|
- const r2 = await client['HelloWorldRPCGroup'].echoExplicit('RPC!')
|
42
|
|
-
|
43
|
|
- console.log(r0,r1,r2) //Hello World RPC!
|
|
83
|
+new RPCSocket(20000, 'localhost').connect().then(async sock => {
|
|
84
|
+ try{
|
|
85
|
+ const RPCs = sock['MyRPCGroup1']
|
|
86
|
+ await RPCs.getAsync().then(console.log)
|
|
87
|
+ await RPCs.getCallback(console.log).then(console.log)
|
|
88
|
+ }catch(e){
|
|
89
|
+ console.log(String(e))
|
|
90
|
+ }
|
44
|
91
|
})
|
45
|
92
|
|
46
|
93
|
```
|
47
|
94
|
|
48
|
|
-# Using callbacks
|
|
95
|
+# Hooks and Events
|
|
96
|
+
|
|
97
|
+There are a many things you can hook into to manage your connections
|
49
|
98
|
|
50
|
|
-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**.
|
|
99
|
+```typescript
|
|
100
|
+new RPCServer(20001, [{
|
|
101
|
+ name: 'MyRPCGroup1',
|
|
102
|
+ exportRPCs: () => [
|
|
103
|
+ echo,
|
|
104
|
+ add,
|
|
105
|
+ getAsync,
|
|
106
|
+ {
|
|
107
|
+ name: 'getCallback',
|
|
108
|
+ hook: getCallback,
|
|
109
|
+ onClose: (response, rpc) => { /* client disconnected */ },
|
|
110
|
+ onCallback: (...callbackArgs) => { /* callback triggered */ }
|
|
111
|
+ }
|
|
112
|
+ ],
|
|
113
|
+}], {
|
|
114
|
+ visibility: '127.0.0.1', //0.0.0.0
|
|
115
|
+ closeHandler: (socket) => { /* global close handler */ },
|
|
116
|
+ connectionHandler: (socket) => { /* new connection made */ },
|
|
117
|
+ errorHandler: (socket, error, rpcname, argArr) => { /* An error occured inside a RPC */ },
|
|
118
|
+})
|
51
|
119
|
|
52
|
|
-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).
|
|
120
|
+const sock = new RPCSocket(20001, 'localhost')
|
|
121
|
+sock.on('error', (e) => { /* handle error */ })
|
|
122
|
+sock.on('close', () => { /* handle close event */ })
|
53
|
123
|
|
54
|
|
-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.
|
|
124
|
+sock.hook('RPCName', (/* arg0, arg1, ..., argN */) => { /* bind client-side RPCs */ })
|
|
125
|
+//Retrieve the socket from connectionHandler (Server-side) and trigger with
|
|
126
|
+//socket.call('RPCName', arg0, arg1, ..., argN)
|
55
|
127
|
|
56
|
|
-You should unhook the client socket once you're done with it as not to cause security or control flow issues.
|
|
128
|
+sock.connect().then(_ => { /* ... */})
|
|
129
|
+
|
|
130
|
+```
|
57
|
131
|
|
58
|
|
-```typescript
|
59
|
|
-import {RPCServer, RPCSocket} from 'rpclibrary'
|
60
|
132
|
|
61
|
|
-const port = 1234
|
62
|
|
-const host = 'locahost'
|
|
133
|
+# Restricting access
|
63
|
134
|
|
64
|
|
-const callbacks:Map<string, Function> = new Map()
|
|
135
|
+rpclibrary offers some minimalistic permission management
|
65
|
136
|
|
66
|
|
-new RPCServer(port, [{
|
67
|
|
- name: 'HelloWorldRPCGroup',
|
68
|
|
- exportRPCs: () => [
|
69
|
|
- function triggerCallbacks(...messages){ callbacks.forEach(cb => cb.apply({}, messages)) },
|
|
137
|
+```typescript
|
|
138
|
+
|
|
139
|
+//Restricting access
|
|
140
|
+new RPCServer(20002, [{
|
|
141
|
+ name: 'MyRPCGroup1',
|
|
142
|
+ exportRPCs: () => [
|
|
143
|
+ echo,
|
|
144
|
+ add,
|
|
145
|
+ getAsync,
|
70
|
146
|
{
|
71
|
|
- name: 'subscribe',
|
72
|
|
- hook: async (callback) => {
|
73
|
|
- const randStr = 'generate_a_random_string_here'
|
74
|
|
- callbacks.set(randStr, callback);
|
75
|
|
- return { result: 'Success', uuid: randStr}
|
76
|
|
- }
|
77
|
|
- },{
|
78
|
|
- name: 'unsubscribe',
|
79
|
|
- call: async (uuid) => { callbacks.delete(uuid) }
|
|
147
|
+ name: 'getCallback',
|
|
148
|
+ hook: getCallback,
|
80
|
149
|
}
|
81
|
|
- ]
|
82
|
|
-}])
|
|
150
|
+ ],
|
|
151
|
+}], {
|
|
152
|
+ sesame: "sesame open",
|
|
153
|
+ /*
|
|
154
|
+ OR check sesame dynamically
|
|
155
|
+ and refine permissioning with accessfilter (optional)
|
|
156
|
+ */
|
|
157
|
+
|
|
158
|
+ //sesame: (sesame) => true
|
|
159
|
+ //accessFilter: (sesame, exporter) => { return exporter.name === "MyRPCGroup1" && sesame === "sesame open" },
|
|
160
|
+})
|
83
|
161
|
|
84
|
|
-const client = new RPCSocket(port, host)
|
85
|
|
-client.connect().then(async () => {
|
86
|
|
- const res = await client['HelloWorldRPCGroup'].subscribe(async (...args:any) => {
|
87
|
|
- console.log.apply(console, args)
|
|
162
|
+new RPCSocket(20002, 'localhost').connect("sesame open").then(async sock => {
|
|
163
|
+ try{
|
|
164
|
+ const RPCs = sock['MyRPCGroup1']
|
|
165
|
+ await RPCs.echo("hello!").then(console.log)
|
|
166
|
+ await RPCs.add(1, Math.PI).then(console.log)
|
|
167
|
+ await RPCs.getAsync().then(console.log)
|
|
168
|
+ await RPCs.getCallback(console.log).then(console.log)
|
|
169
|
+ }catch(e){
|
|
170
|
+ console.log(String(e))
|
|
171
|
+ }
|
|
172
|
+})
|
88
|
173
|
|
89
|
|
- /* close the callbacks once you're done */
|
90
|
|
- await client['HelloWorldRPCGroup'].unsubscribe(res.uuid)
|
91
|
|
- client.unhook(res.uuid)
|
92
|
|
- })
|
93
|
174
|
|
94
|
|
- await client['HelloWorldRPCGroup'].triggerCallbacks("Hello", "World", "Callbacks")
|
95
|
|
-})
|
96
|
175
|
```
|
97
|
176
|
|
98
|
|
-If you need to include further response data into your `SubscriptionResponse` you can extend it using the server's first generic parameter `SubResType`
|
|
177
|
+
|
|
178
|
+# Typescript support
|
|
179
|
+
|
|
180
|
+rpclibrary is a typescript-first project and offers full support for typing your RPCs.
|
|
181
|
+**NOTE** that your function implementations have to be currectly typed to make the compiler agree.
|
|
182
|
+Explicit typing is recommended.
|
|
183
|
+
|
|
184
|
+Example:
|
|
185
|
+```typescript
|
|
186
|
+echo = (x) => x
|
|
187
|
+ /*becomes*/
|
|
188
|
+echo = (x:string) : string => x
|
|
189
|
+```
|
|
190
|
+
|
99
|
191
|
|
100
|
192
|
```typescript
|
101
|
|
-new RPCServer<{extension: string}>(port, [{
|
102
|
|
- name: 'MyRPCGroup',
|
103
|
|
- exportRPCs: () => [{
|
104
|
|
- name: 'subscribe',
|
105
|
|
- hook: async (callback) => {
|
106
|
|
- return {
|
107
|
|
- result: 'Success',
|
108
|
|
- uuid: 'very_random_string',
|
109
|
|
- extension: 'your_data_here' //tsc will demand this field
|
110
|
|
- }
|
|
193
|
+type MyInterface = {
|
|
194
|
+ MyRPCGroup1: {
|
|
195
|
+ echo: (x: string) => string
|
|
196
|
+ add: (a: number, b: number) => number
|
|
197
|
+ getAsync: () => Promise<{ topic: string, message: string }>
|
|
198
|
+ getCallback: (callback:Function) => string
|
|
199
|
+ }
|
|
200
|
+};
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+/*
|
|
204
|
+exportRPCs is now type safe. Try swapping echo for badEcho.
|
|
205
|
+Sadly TSC's stack traces aren't the best, but try to scroll to the bottom of them to find useful info like
|
|
206
|
+
|
|
207
|
+Type '(x: boolean) => number' is not assignable to type '(x: string) => string'
|
|
208
|
+*/
|
|
209
|
+
|
|
210
|
+const badEcho = (x: boolean) : number => 3
|
|
211
|
+
|
|
212
|
+new RPCServer<MyInterface>(20003, [{
|
|
213
|
+ name: 'MyRPCGroup1',
|
|
214
|
+ exportRPCs: () => [
|
|
215
|
+ //badEcho,
|
|
216
|
+ echo,
|
|
217
|
+ add,
|
|
218
|
+ getAsync,
|
|
219
|
+ {
|
|
220
|
+ name: 'getCallback',
|
|
221
|
+ hook: getCallback,
|
111
|
222
|
}
|
112
|
|
- }]}
|
113
|
|
-])
|
|
223
|
+ ],
|
|
224
|
+}])
|
|
225
|
+
|
|
226
|
+new RPCSocket<MyInterface>(20003, 'localhost').connect().then(async sock => {
|
|
227
|
+ try{
|
|
228
|
+ await sock.MyRPCGroup1.echo("hello!").then(console.log)
|
|
229
|
+ await sock.MyRPCGroup1.add(1, Math.PI).then(console.log)
|
|
230
|
+ await sock.MyRPCGroup1.getAsync().then(console.log)
|
|
231
|
+ await sock.MyRPCGroup1.getCallback(console.log).then(console.log)
|
|
232
|
+ }catch(e){
|
|
233
|
+ console.log(String(e))
|
|
234
|
+ }
|
|
235
|
+})
|
114
|
236
|
|
115
|
237
|
```
|
116
|
238
|
|
117
|
|
-#Experimental typing support
|
118
|
|
-It is possible to declare pseudo-interfaces for servers and clients by using server's second generic parameter.
|
119
|
|
-This feature is currently still in development and considered **unstable and untested**. Use with caution.
|
|
239
|
+
|
|
240
|
+# A class-based scalable pattern for APIs
|
|
241
|
+
|
|
242
|
+because long lists of functions quickly become unwieldy, it is smart to break up the RPCs into chunks or features.
|
|
243
|
+A pattern I found to be useful is as follows:
|
120
|
244
|
|
121
|
245
|
```typescript
|
122
|
|
-type MyInterface = {
|
123
|
|
- Group1: {
|
124
|
|
- triggerCallbacks: (...args:any[]) => Promise<void>,
|
125
|
|
- subscribe: (param:string, callback:Function) => Promise<SubscriptionResponse<{a: string}>>,
|
126
|
|
- unsubscribe: (uuid:string) => Promise<void>
|
127
|
|
- },
|
128
|
|
- Group2: {
|
129
|
|
- echo: (x:string) => Promise<string>
|
|
246
|
+interface IMyImplementation {
|
|
247
|
+ echo: (x: string) => string
|
|
248
|
+ add: (a: number, b: number) => number
|
|
249
|
+ getAsync: () => Promise<{ topic: string, message: string }>
|
|
250
|
+ getCallback: (callback:Function) => string
|
|
251
|
+}
|
|
252
|
+
|
|
253
|
+type MyInterface = {
|
|
254
|
+ MyRPCGroup1: {
|
|
255
|
+ echo: IMyImplementation['echo']
|
|
256
|
+ add: IMyImplementation['add']
|
|
257
|
+ getAsync: IMyImplementation['getAsync']
|
|
258
|
+ getCallback: IMyImplementation['getCallback']
|
130
|
259
|
}
|
131
|
260
|
}
|
132
|
|
-```
|
133
|
|
-Create a client using
|
134
|
|
-```typescript
|
135
|
|
-RPCSocket.makeSocket<MyInterface>(port, host).then((async (client) => {
|
136
|
|
- const r = await client.Group2.echo("hee") //tsc knows about available RPCs
|
137
|
|
-}))
|
138
|
261
|
|
139
|
|
-/* OR */
|
|
262
|
+class MyImplementation implements IMyImplementation, RPCExporter<MyInterface>{
|
|
263
|
+ //"X" as "X" syntax is required to satisfy the type system (as it assumes string to be the true type)
|
|
264
|
+ name = "MyRpcGroup11" as "MyRPCGroup1"
|
140
|
265
|
|
141
|
|
-const client = new RPCSocket(port, host)
|
142
|
|
-client.connect<MyInterface>().then((async (client) => {
|
143
|
|
- const r = await client.Group2.echo("hee") //tsc knows about available RPCs
|
144
|
|
-}))
|
145
|
|
-```
|
|
266
|
+ //List the functions you declared in MyInterface
|
|
267
|
+ exportRPCs = () => [
|
|
268
|
+ this.echo,
|
|
269
|
+ this.add,
|
|
270
|
+ this.getAsync,
|
|
271
|
+ this.getCallback
|
|
272
|
+ ]
|
|
273
|
+
|
|
274
|
+ //Write your implementations as you normally would
|
|
275
|
+ echo = (text: string) => text
|
|
276
|
+
|
|
277
|
+ add = (a: number, b: number) : number => a + b
|
|
278
|
+
|
|
279
|
+ getAsync = async () : Promise<{topic: string, message:string}>=> await new Promise((res, _) => {
|
|
280
|
+ setTimeout(() => {
|
|
281
|
+ res({
|
|
282
|
+ topic: "Hey!!",
|
|
283
|
+ message: "Hello World Async!"
|
|
284
|
+ })
|
|
285
|
+ }, 250)
|
|
286
|
+ })
|
|
287
|
+
|
|
288
|
+ getCallback = (callback: Function) : string => {
|
|
289
|
+ setTimeout(() => {
|
|
290
|
+ try{
|
|
291
|
+ callback({
|
|
292
|
+ topic: "Hey!!",
|
|
293
|
+ message: "Hello World Callback!"
|
|
294
|
+ })
|
|
295
|
+ }catch(e){
|
|
296
|
+ console.log(String(e))
|
|
297
|
+ }
|
|
298
|
+ }, 250)
|
|
299
|
+ return "Please wait for a callback :)"
|
|
300
|
+ }
|
|
301
|
+}
|
|
302
|
+
|
|
303
|
+type ProjectInterface = MyInterface
|
|
304
|
+ //& MyOtherInterface
|
|
305
|
+ //& MyOtherOtherInterface
|
|
306
|
+ // ...
|
|
307
|
+;
|
|
308
|
+
|
|
309
|
+new RPCServer<ProjectInterface>(20004, [new MyImplementation() /*, new MyOtherImplementation(), new MyOtherOtherImplementation() */])
|
|
310
|
+
|
|
311
|
+new RPCSocket<ProjectInterface>(20004, 'localhost').connect().then(async sock => {
|
|
312
|
+ // ...
|
|
313
|
+})
|
146
|
314
|
|
147
|
|
-Create a server using
|
148
|
|
-```typescript
|
149
|
|
-new RPCServer<{a:string}, MyInterface>(port,
|
150
|
|
- [{
|
151
|
|
- //...
|
152
|
|
- },{
|
153
|
|
- name: 'Group2', //Auto completion for names
|
154
|
|
- exportRPCs: () => [{
|
155
|
|
- name: 'echo', //this name too!
|
156
|
|
- call: async (x) => x+"llo World!" //the paramter and return types are known by tsc
|
157
|
|
- }]
|
158
|
|
- }]
|
159
|
|
-)
|
160
|
315
|
```
|
161
|
316
|
|
|
317
|
+
|
162
|
318
|
# [Full documentation](https://gitea.nitowa.xyz/docs/rpclibrary)
|