mqtt-json-rpc 2.1.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,6 @@
1
1
 
2
+ <img src="https://raw.githubusercontent.com/rse/mqtt-json-rpc/master/etc/logo.svg" width="300" align="right" alt=""/>
3
+
2
4
  MQTT-JSON-RPC
3
5
  =============
4
6
 
@@ -7,8 +9,8 @@ MQTT-JSON-RPC
7
9
  <p/>
8
10
  <img src="https://nodei.co/npm/mqtt-json-rpc.png?downloads=true&stars=true" alt=""/>
9
11
 
10
- <p/>
11
- <img src="https://david-dm.org/rse/mqtt-json-rpc.png" alt=""/>
12
+ [![github (author stars)](https://img.shields.io/github/stars/rse?logo=github&label=author%20stars&color=%233377aa)](https://github.com/rse)
13
+ [![github (author followers)](https://img.shields.io/github/followers/rse?label=author%20followers&logo=github&color=%234477aa)](https://github.com/rse)
12
14
 
13
15
  Installation
14
16
  ------------
@@ -20,44 +22,81 @@ $ npm install mqtt mqtt-json-rpc
20
22
  About
21
23
  -----
22
24
 
23
- This is an addon API for the
24
- [MQTT.js](https://www.npmjs.com/package/mqtt) API of
25
- [Node.js](https://nodejs.org/), for
26
- [Remote Procedure Call](https://en.wikipedia.org/wiki/Remote_procedure_call) (RPC)
25
+ This is an addon API for the excellent
26
+ [MQTT.js](https://www.npmjs.com/package/mqtt) JavaScript/TypeScript API
27
+ for [Remote Procedure Call](https://en.wikipedia.org/wiki/Remote_procedure_call) (RPC)
27
28
  communication based on the [JSON-RPC](http://www.jsonrpc.org/)
28
29
  protocol. This allows a bi-directional request/response-style communication over
29
30
  the technically uni-directional message protocol [MQTT](http://mqtt.org).
30
31
 
32
+ Conceptually, this RPC API provides two types of communication patterns:
33
+
34
+ - **Event Emission**:
35
+ Event Emission is a *uni-directional* communication pattern.
36
+ An Event is the combination of an event name and optionally zero or more arguments.
37
+ You *subscribe* to events.
38
+ When an event is *emitted*, either a single particular subscriber (in case of
39
+ a directed event emission) or all subscribers are called and receive the
40
+ arguments as extra information.
41
+
42
+ - **Service Call**:
43
+ Service Call is a *bi-directional* communication pattern.
44
+ A Service is the combination of a service name and optionally zero or more arguments.
45
+ You *register* for a service.
46
+ When a service is *called*, a single particular registrator (in case
47
+ of a directed service call) or one arbitrary registrator is called and
48
+ receives the arguments as the request. The registrator then has to
49
+ provide the service response.
50
+
51
+ Notice: while the provided Event Emission functionality is just a very thing
52
+ wrapper around the regular MQTT message publishing API of MQTT.js, the
53
+ Service Call functionality is the core and heart of this addon API.
54
+
31
55
  Usage
32
56
  -----
33
57
 
34
- #### Server:
58
+ ### API:
35
59
 
36
- ```js
37
- const MQTT = require("mqtt")
38
- const RPC = require("mqtt-json-rpc")
60
+ ```ts
61
+ export type API = {
62
+ "example/sample": (a1: string, a2: boolean) => void
63
+ "example/hello": (a1: string, a2: number) => string
64
+ }
65
+ ```
39
66
 
40
- const mqtt = MQTT.connect("wss://127.0.0.1:8889", { ... })
41
- const rpc = new RPC(mqtt)
67
+ ### Server:
42
68
 
43
- mqtt.on("connect", () => {
69
+ ```ts
70
+ import MQTT from "mqtt"
71
+ import RPC from "mqtt-json-rpc"
72
+ import type { API } from [...]
73
+
74
+ const mqtt = MQTT.connect("wss://127.0.0.1:8883", { ... })
75
+ const rpc = new RPC<API>(mqtt)
76
+
77
+ mqtt.on("connect", async () => {
78
+ rpc.subscribe("example/sample", (a1, a2) => {
79
+ console.log("example/sample: ", a1, a2)
80
+ })
44
81
  rpc.register("example/hello", (a1, a2) => {
45
- console.log("example/hello: request: ", a1, a2)
82
+ console.log("example/hello: ", a1, a2)
46
83
  return `${a1}:${a2}`
47
84
  })
48
85
  })
49
86
  ```
50
87
 
51
- #### Client:
88
+ ### Client:
52
89
 
53
- ```js
54
- const MQTT = require("mqtt")
55
- const RPC = require("mqtt-json-rpc")
90
+ ```ts
91
+ import MQTT from "mqtt"
92
+ import RPC from "mqtt-json-rpc"
93
+ import type { API } from [...]
56
94
 
57
- const mqtt = MQTT.connect("wss://127.0.0.1:8889", { ... })
58
- const rpc = new RPC(mqtt)
95
+ const mqtt = MQTT.connect("wss://127.0.0.1:8883", { ... })
96
+ const rpc = new RPC<API>(mqtt)
59
97
 
60
98
  mqtt.on("connect", () => {
99
+ rpc.emit("example/sample", "foo", true)
61
100
  rpc.call("example/hello", "world", 42).then((response) => {
62
101
  console.log("example/hello response: ", response)
63
102
  mqtt.end()
@@ -68,60 +107,160 @@ mqtt.on("connect", () => {
68
107
  Application Programming Interface
69
108
  ---------------------------------
70
109
 
71
- The MQTT-JSON-RPC API provides the following methods (check out the
72
- corresponding [TypeScript definition](mqtt-json-rpc.d.ts)) file):
73
-
74
- - `constructor(mqtt: MQTT, options?: { encoding?: string, timeout?: number }): MQTT-JSON-RPC`:<br/>
75
- The `mqtt` is the [MQTT.js](https://www.npmjs.com/package/mqtt) instance.
76
- The optional `encoding` option can be either `json` (default), `msgpack` or `cbor`.
77
- The optional `timeout` option is the timeout in seconds.
78
-
79
- - `MQTT-JSON-RPC#registered(method: string): boolean`:<br/>
80
- Check for the previous registration of a method. The `method` has to
81
- be a valid MQTT topic name. The method returns `true` if `method` is
82
- already registered, else it returns `false`.
83
-
84
- - `MQTT-JSON-RPC#register(method: string, callback: (...args: any[]) => any): Promise<boolean>`:<br/>
85
- Register a method. The `method` has to be a valid MQTT topic
86
- name. The `callback` is called with the `params` passed to
87
- the remote `MQTT-JSON-RPC#notify()` or `MQTT-JSON-RPC#call()`. For
88
- a remote `MQTT-JSON-RPC#notify()`, the return value of `callback` will be
89
- ignored. For a remote `MQTT-JSON-RPC#call()`, the return value of `callback`
90
- will resolve the promise returned by the remote `MQTT-JSON-RPC#call()`.
91
- Internally, on the MQTT broker the topic `${method}/request` is
92
- subscribed.
93
-
94
- - `MQTT-JSON-RPC#unregister(method: string): Promise<void>`:<br/>
95
- Unregister a previously registered method.
96
- Internally, on the MQTT broker the topic `${method}/request` is unsubscribed.
97
-
98
- - `MQTT-JSON-RPC#notify(method: string, ...params: any[]): void`:<br/>
99
- Notify a method. The remote `MQTT-JSON-RPC#register()` `callback` is called
100
- with `params` and its return value is silently ignored.
101
-
102
- - `MQTT-JSON-RPC#call(method: string, ...params: any[]): Promise<any>`:<br/>
103
- Call a method. The remote `MQTT-JSON-RPC#register()` `callback` is
104
- called with `params` and its return value resolves the returned
105
- `Promise`. If the remote `callback` throws an exception, this rejects
106
- the returned `Promise`. Internally, on the MQTT broker the topic
107
- `${method}/response/<cid>` is temporarily subscribed for receiving the
108
- response (`<cid>` is a UUID v1 to uniquely identify the MQTT-JSON-RPC
109
- caller instance).
110
+ The RPC API provides the following methods:
111
+
112
+ - **Construction**:<br/>
113
+
114
+ constructor(
115
+ mqtt: MqttClient,
116
+ options?: {
117
+ clientId: string
118
+ codec: "cbor" | "json"
119
+ timeout: number
120
+ topicEventNoticeMake: (topic: string) => TopicMatching | null
121
+ topicServiceRequestMake: (topic: string) => TopicMatching | null
122
+ topicServiceResponseMake: (topic: string) => TopicMatching | null
123
+ topicEventNoticeMatch: { name: string, clientId?: string }
124
+ topicServiceRequestMatch: { name: string, clientId?: string }
125
+ topicServiceResponseMatch: { name: string, clientId?: string }
126
+ }
127
+ )
128
+
129
+ The `mqtt` is the [MQTT.js](https://www.npmjs.com/package/mqtt) instance,
130
+ which has to be establish separately.
131
+ The optional `options` object supports the following fields:
132
+ - `clientId`: Custom client identifier (default: auto-generated UUID v1).
133
+ - `codec`: Encoding format (default: `cbor`).
134
+ - `timeout`: Communication timeout in milliseconds (default: `10000`).
135
+ - `topicEventNoticeMake`: Custom topic generation for event notices.
136
+ (default: `` (name, clientId) => clientId ? `${name}/event-notice/${clientId}` : `${name}/event-notice` ``)
137
+ - `topicServiceRequestMake`: Custom topic generation for service requests.
138
+ (default: `` (name, clientId) => clientId ? `${name}/service-request/${clientId}` : `${name}/service-request` ``)
139
+ - `topicServiceResponseMake`): Custom topic generation for service responses.
140
+ (default: `` (name, clientId) => clientId ? `${name}/service-response/${clientId}` : `${name}/service-response` ``)
141
+ - `topicEventNoticeMatch`: Custom topic matching for event notices.
142
+ (default: `` (topic) => { const m = topic.match(/^(.+?)\/event-notice(?:\/(.+))?$/); return m ? { name: m[1], clientId: m[2] } : null } ``)
143
+ - `topicServiceRequestMatch`: Custom topic matching for service requests.
144
+ (default: `` (topic) => { const m = topic.match(/^(.+?)\/service-request(?:\/(.+))?$/); return m ? { name: m[1], clientId: m[2] } : null } ``)
145
+ - `topicServiceResponseMatch`: Custom topic matching for service responses.
146
+ (default: `` (topic) => { const m = topic.match(/^(.+?)\/service-response\/(.+)$/); return m ? { name: m[1], clientId: m[2] } : null } ``)
147
+
148
+ - **Event Subscription**:<br/>
149
+
150
+ /* (simplified TypeScript API method signature) */
151
+ subscribe(
152
+ event: string,
153
+ options?: MQTT::IClientSubscribeOptions
154
+ callback: (...params: any[]) => void,
155
+ ): Promise<Subscription>
156
+
157
+ Subscribe to an event.
158
+ The `event` has to be a valid MQTT topic name.
159
+ The optional `options` allows setting MQTT.js `subscribe()` options like `qos`.
160
+ The `callback` is called with the `params` passed to a remote `emit()`.
161
+ There is no return value of `callback`.
162
+
163
+ Internally, on the MQTT broker, the topics generated by
164
+ `topicEventNoticeMake()` (default: `${event}/event-notice` and
165
+ `${event}/event-notice/${clientId}`) are subscribed. Returns a
166
+ `Subscription` object with an `unsubscribe()` method.
167
+
168
+ - **Service Registration**:<br/>
169
+
170
+ /* (simplified TypeScript API method signature) */
171
+ register(
172
+ service: string,
173
+ options?: MQTT::IClientSubscribeOptions
174
+ callback: (...params: any[]) => any,
175
+ ): Promise<Registration>
176
+
177
+ Register a service.
178
+ The `service` has to be a valid MQTT topic name.
179
+ The optional `options` allows setting MQTT.js `subscribe()` options like `qos`.
180
+ The `callback` is called with the `params` passed to a remote `call()`.
181
+ The return value of `callback` will resolve the `Promise` returned by the remote `call()`.
182
+
183
+ Internally, on the MQTT broker, the topics by
184
+ `topicServiceRequestMake()` (default: `${service}/service-request` and
185
+ `${service}/service-request/${clientId}`) are subscribed. Returns a
186
+ `Registration` object with an `unregister()` method.
187
+
188
+ - **Event Emission**:<br/>
189
+
190
+ /* (simplified TypeScript API method signature) */
191
+ emit(
192
+ event: string,
193
+ clientId?: ClientId,
194
+ options?: MQTT::IClientSubscribeOptions,
195
+ ...params: any[]
196
+ ): void
197
+
198
+ Emit an event to all subscribers or a specific subscriber ("fire and forget").
199
+ The optional `clientId` directs the event to a specific subscriber only.
200
+ The optional `options` allows setting MQTT.js `publish()` options like `qos` or `retain`.
201
+
202
+ The remote `subscribe()` `callback` is called with `params` and its
203
+ return value is silently ignored.
204
+
205
+ Internally, publishes to the MQTT topic by `topicEventNoticeMake(event, clientId)`
206
+ (default: `${event}/event-notice` or `${event}/event-notice/${clientId}`).
207
+
208
+ - **Service Call**:<br/>
209
+
210
+ /* (simplified TypeScript API method signature) */
211
+ call(
212
+ service: string,
213
+ clientId?: ClientId,
214
+ options?: MQTT::IClientSubscribeOptions,
215
+ ...params: any[]
216
+ ): Promise<any>
217
+
218
+ Call a service on all registrants or on a specific registrant ("request and response").
219
+ The optional `clientId` directs the call to a specific registrant only.
220
+ The optional `options` allows setting MQTT.js `publish()` options like `qos` or `retain`.
221
+
222
+ The remote `register()` `callback` is called with `params` and its
223
+ return value resolves the returned `Promise`. If the remote `callback`
224
+ throws an exception, this rejects the returned `Promise`.
225
+
226
+ Internally, on the MQTT broker, the topic by `topicServiceResponseMake(service, clientId)`
227
+ (default: `${service}/service-response/${clientId}`) is temporarily subscribed
228
+ for receiving the response.
229
+
230
+ - **Client Id Wrapping**:<br/>
231
+
232
+ clientId(
233
+ id: string
234
+ ): ClientId
235
+
236
+ Wrap a client ID string for use with `emit()` or `call()` to direct the
237
+ message to a specific client. Returns a `ClientId` object.
110
238
 
111
239
  Internals
112
240
  ---------
113
241
 
114
- Internally, remote methods are assigned to MQTT topics. When calling a
115
- remote method named `example/hello` with parameters `"world"` and `42` via...
242
+ In the following, assume that an RPC instance is created with:
243
+
244
+ ```ts
245
+ import MQTT from "mqtt"
246
+ import RPC from "mqtt-json-rpc"
247
+
248
+ const mqtt = MQTT.connect("...", { ... })
249
+ const rpc = new RPC(mqtt, { clientId: "d1acc980-0e4e-11e8-98f0-ab5030b47df4", codec: "json" })
250
+ ```
116
251
 
117
- ```js
252
+ Internally, remote services are assigned to MQTT topics. When calling a
253
+ remote service named `example/hello` with parameters `"world"` and `42` via...
254
+
255
+ ```ts
118
256
  rpc.call("example/hello", "world", 42).then((result) => {
119
257
  ...
120
258
  })
121
259
  ```
122
260
 
123
261
  ...the following JSON-RPC 2.0 request message is sent to the permanent MQTT
124
- topic `example/hello/request`:
262
+ topic `example/hello/service-request` (UUID `d1db7aa0-0e4e-11e8-b1d9-5f0ab230c0d9` is
263
+ a random generated one):
125
264
 
126
265
  ```json
127
266
  {
@@ -132,18 +271,18 @@ topic `example/hello/request`:
132
271
  }
133
272
  ```
134
273
 
135
- Beforehand, this `example/hello` method should have been registered with...
274
+ Beforehand, this `example/hello` service should have been registered with...
136
275
 
137
- ```js
276
+ ```ts
138
277
  rpc.register("example/hello", (a1, a2) => {
139
278
  return `${a1}:${a2}`
140
279
  })
141
280
  ```
142
281
 
143
- ...and then its result, in the above `rpc.call` example `"world:42"`, is then
282
+ ...and then its result, in the above `rpc.call()` example `"world:42"`, is then
144
283
  sent back as the following JSON-RPC 2.0 success response
145
284
  message to the temporary (client-specific) MQTT topic
146
- `example/hello/response/d1acc980-0e4e-11e8-98f0-ab5030b47df4`:
285
+ `example/hello/service-response/d1acc980-0e4e-11e8-98f0-ab5030b47df4`:
147
286
 
148
287
  ```json
149
288
  {
@@ -153,18 +292,18 @@ message to the temporary (client-specific) MQTT topic
153
292
  }
154
293
  ```
155
294
 
156
- The JSON-RPC 2.0 `id` field always consists of `<cid>:<rid>`, where
157
- `<cid>` is the UUID v1 of the MQTT-JSON-RPC client instance and `<rid>` is
158
- the UUID v1 of the particular method request. The `<cid>` is used for
295
+ The JSON-RPC 2.0 `id` field always consists of `clientId:requestId`, where
296
+ `clientId` is the UUID v1 of the RPC client instance and `requestId` is
297
+ the UUID v1 of the particular service request. The `clientId` is used for
159
298
  sending back the JSON-RPC 2.0 response message to the requestor only.
160
- The `<rid>` is used for correlating the response to the request only.
299
+ The `requestId` is used for correlating the response to the request only.
161
300
 
162
- Example
163
- -------
301
+ Broker Setup
302
+ ------------
164
303
 
165
304
  For a real test-drive of MQTT-JSON-RPC, install the
166
- [Mosquitto](https://mosquitto.org/) MQTT broker with at least a "MQTT
167
- over Secure-WebSockets" lister in the `mosquitto.conf` file like...
305
+ [Mosquitto](https://mosquitto.org/) MQTT broker and a `mosquitto.conf`
306
+ file like...
168
307
 
169
308
  ```
170
309
  [...]
@@ -174,14 +313,10 @@ acl_file mosquitto-acl.txt
174
313
 
175
314
  [...]
176
315
 
177
- # additional listener (wss: MQTT over WebSockets+SSL/TLS)
178
- listener 8889 127.0.0.1
316
+ # additional listener
317
+ listener 1883 127.0.0.1
179
318
  max_connections -1
180
- protocol websockets
181
- cafile mosquitto-ca.crt.pem
182
- certfile mosquitto-sv.crt.pem
183
- keyfile mosquitto-sv.key.pem
184
- require_certificate false
319
+ protocol mqtt
185
320
 
186
321
  [...]
187
322
  ```
@@ -199,20 +334,38 @@ topic readwrite example/#
199
334
  example:$6$awYNe6oCAi+xlvo5$mWIUqyy4I0O3nJ99lP1mkRVqsDGymF8en5NChQQxf7KrVJLUp1SzrrVDe94wWWJa3JGIbOXD9wfFGZdi948e6A==
200
335
  ```
201
336
 
202
- Then test-drive MQTT-JSON-RPC with a complete [sample](sample/sample.js) to see
203
- MQTT-JSON-RPC in action and tracing its communication:
337
+ Alternatively, you can use the [NPM package mosquitto](https://npmjs.com/mosquitto)
338
+ for an equal setup.
339
+
340
+ Example
341
+ -------
342
+
343
+ You can test-drive MQTT-JSON-RPC with a complete [sample](sample/sample.ts) to see
344
+ MQTT-JSON-RPC in action and tracing its communication (the typing of the `RPC`
345
+ class with `API` is optional, but strongly suggested):
346
+
347
+ ```ts
348
+ import Mosquitto from "mosquitto"
349
+ import MQTT from "mqtt"
350
+ import RPC from "mqtt-json-rpc"
204
351
 
205
- ```js
206
- const MQTT = require("mqtt")
207
- const RPC = require("mqtt-json-rpc")
352
+ const mosquitto = new Mosquitto()
353
+ await mosquitto.start()
354
+ await new Promise((resolve) => { setTimeout(resolve, 500) })
208
355
 
209
- const mqtt = MQTT.connect("wss://127.0.0.1:8889", {
210
- rejectUnauthorized: false,
356
+ const mqtt = MQTT.connect("mqtt://127.0.0.1:1883", {
211
357
  username: "example",
212
358
  password: "example"
213
359
  })
214
360
 
215
- const rpc = new RPC(mqtt)
361
+ type API = {
362
+ "example/sample": (a1: string, a2: number) => void
363
+ "example/hello": (a1: string, a2: number) => string
364
+ }
365
+
366
+ const rpc = new RPC<API>(mqtt, { codec: "json" })
367
+
368
+ type Sample = (a: string, b: number) => string
216
369
 
217
370
  mqtt.on("error", (err) => { console.log("ERROR", err) })
218
371
  mqtt.on("offline", () => { console.log("OFFLINE") })
@@ -227,8 +380,9 @@ mqtt.on("connect", () => {
227
380
  return `${a1}:${a2}`
228
381
  })
229
382
  rpc.call("example/hello", "world", 42).then((result) => {
230
- console.log("example/hello sucess: ", result)
383
+ console.log("example/hello success: ", result)
231
384
  mqtt.end()
385
+ await mosquitto.stop()
232
386
  }).catch((err) => {
233
387
  console.log("example/hello error: ", err)
234
388
  })
@@ -238,12 +392,12 @@ mqtt.on("connect", () => {
238
392
  The output will be:
239
393
 
240
394
  ```
241
- $ node sample.js
395
+ $ node sample.ts
242
396
  CONNECT
243
- RECEIVED example/hello/request {"jsonrpc":"2.0","id":"1099cb50-bd2b-11eb-8198-43568ad728c4:10bf7bc0-bd2b-11eb-bac6-439c565b651a","method":"example/hello","params":["world",42]}
397
+ RECEIVED example/hello/service-request {"jsonrpc":"2.0","id":"b441fe30-e8af-11f0-b361-a30e779baa27:b474f510-e8af-11f0-ace2-97e30fcf7dca","method":"example/hello","params":["world",42]}
244
398
  example/hello: request: world 42
245
- RECEIVED example/hello/response/1099cb50-bd2b-11eb-8198-43568ad728c4 {"jsonrpc":"2.0","id":"1099cb50-bd2b-11eb-8198-43568ad728c4:10bf7bc0-bd2b-11eb-bac6-439c565b651a","result":"world:42"}
246
- example/hello sucess: world:42
399
+ RECEIVED example/hello/service-response/b441fe30-e8af-11f0-b361-a30e779baa27 {"jsonrpc":"2.0","id":"b441fe30-e8af-11f0-b361-a30e779baa27:b474f510-e8af-11f0-ace2-97e30fcf7dca","result":"world:42"}
400
+ example/hello success: world:42
247
401
  CLOSE
248
402
  ```
249
403