mqtt-json-rpc 2.1.1 → 3.0.1

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,18 @@ 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)
14
+
15
+ > [!Note]
16
+ > This package is no longer actively developed, as it
17
+ > already evolved into a more sophisticated solution named
18
+ > [**MQTT+**](https://github.com/rse/mqtt-plus). **MQTT+**
19
+ > encodes packets as CBOR by default, uses an own packet format
20
+ > (allowing sender and receiver information), uses shorter NanoIDs
21
+ > instead of longer UUIDs for identification of sender, receiver and
22
+ > requests, and additionally provides bi-directional resource transfer
23
+ > via chunk streaming.
12
24
 
13
25
  Installation
14
26
  ------------
@@ -20,44 +32,81 @@ $ npm install mqtt mqtt-json-rpc
20
32
  About
21
33
  -----
22
34
 
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)
35
+ This is an addon API for the excellent
36
+ [MQTT.js](https://www.npmjs.com/package/mqtt) JavaScript/TypeScript API
37
+ for [Remote Procedure Call](https://en.wikipedia.org/wiki/Remote_procedure_call) (RPC)
27
38
  communication based on the [JSON-RPC](http://www.jsonrpc.org/)
28
39
  protocol. This allows a bi-directional request/response-style communication over
29
40
  the technically uni-directional message protocol [MQTT](http://mqtt.org).
30
41
 
42
+ Conceptually, this RPC API provides two types of communication patterns:
43
+
44
+ - **Event Emission**:
45
+ Event Emission is a *uni-directional* communication pattern.
46
+ An Event is the combination of an event name and optionally zero or more arguments.
47
+ You *subscribe* to events.
48
+ When an event is *emitted*, either a single particular subscriber (in case of
49
+ a directed event emission) or all subscribers are called and receive the
50
+ arguments as extra information.
51
+
52
+ - **Service Call**:
53
+ Service Call is a *bi-directional* communication pattern.
54
+ A Service is the combination of a service name and optionally zero or more arguments.
55
+ You *register* for a service.
56
+ When a service is *called*, a single particular registrator (in case
57
+ of a directed service call) or one arbitrary registrator is called and
58
+ receives the arguments as the request. The registrator then has to
59
+ provide the service response.
60
+
61
+ Notice: while the provided Event Emission functionality is just a very thing
62
+ wrapper around the regular MQTT message publishing API of MQTT.js, the
63
+ Service Call functionality is the core and heart of this addon API.
64
+
31
65
  Usage
32
66
  -----
33
67
 
34
- #### Server:
68
+ ### API:
35
69
 
36
- ```js
37
- const MQTT = require("mqtt")
38
- const RPC = require("mqtt-json-rpc")
70
+ ```ts
71
+ export type API = {
72
+ "example/sample": (a1: string, a2: boolean) => void
73
+ "example/hello": (a1: string, a2: number) => string
74
+ }
75
+ ```
39
76
 
40
- const mqtt = MQTT.connect("wss://127.0.0.1:8889", { ... })
41
- const rpc = new RPC(mqtt)
77
+ ### Server:
42
78
 
43
- mqtt.on("connect", () => {
79
+ ```ts
80
+ import MQTT from "mqtt"
81
+ import RPC from "mqtt-json-rpc"
82
+ import type { API } from [...]
83
+
84
+ const mqtt = MQTT.connect("wss://127.0.0.1:8883", { ... })
85
+ const rpc = new RPC<API>(mqtt)
86
+
87
+ mqtt.on("connect", async () => {
88
+ rpc.subscribe("example/sample", (a1, a2) => {
89
+ console.log("example/sample: ", a1, a2)
90
+ })
44
91
  rpc.register("example/hello", (a1, a2) => {
45
- console.log("example/hello: request: ", a1, a2)
92
+ console.log("example/hello: ", a1, a2)
46
93
  return `${a1}:${a2}`
47
94
  })
48
95
  })
49
96
  ```
50
97
 
51
- #### Client:
98
+ ### Client:
52
99
 
53
- ```js
54
- const MQTT = require("mqtt")
55
- const RPC = require("mqtt-json-rpc")
100
+ ```ts
101
+ import MQTT from "mqtt"
102
+ import RPC from "mqtt-json-rpc"
103
+ import type { API } from [...]
56
104
 
57
- const mqtt = MQTT.connect("wss://127.0.0.1:8889", { ... })
58
- const rpc = new RPC(mqtt)
105
+ const mqtt = MQTT.connect("wss://127.0.0.1:8883", { ... })
106
+ const rpc = new RPC<API>(mqtt)
59
107
 
60
108
  mqtt.on("connect", () => {
109
+ rpc.emit("example/sample", "foo", true)
61
110
  rpc.call("example/hello", "world", 42).then((response) => {
62
111
  console.log("example/hello response: ", response)
63
112
  mqtt.end()
@@ -68,60 +117,160 @@ mqtt.on("connect", () => {
68
117
  Application Programming Interface
69
118
  ---------------------------------
70
119
 
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).
120
+ The RPC API provides the following methods:
121
+
122
+ - **Construction**:<br/>
123
+
124
+ constructor(
125
+ mqtt: MqttClient,
126
+ options?: {
127
+ clientId: string
128
+ codec: "cbor" | "json"
129
+ timeout: number
130
+ topicEventNoticeMake: (topic: string) => TopicMatching | null
131
+ topicServiceRequestMake: (topic: string) => TopicMatching | null
132
+ topicServiceResponseMake: (topic: string) => TopicMatching | null
133
+ topicEventNoticeMatch: { name: string, clientId?: string }
134
+ topicServiceRequestMatch: { name: string, clientId?: string }
135
+ topicServiceResponseMatch: { name: string, clientId?: string }
136
+ }
137
+ )
138
+
139
+ The `mqtt` is the [MQTT.js](https://www.npmjs.com/package/mqtt) instance,
140
+ which has to be establish separately.
141
+ The optional `options` object supports the following fields:
142
+ - `clientId`: Custom client identifier (default: auto-generated UUID v1).
143
+ - `codec`: Encoding format (default: `cbor`).
144
+ - `timeout`: Communication timeout in milliseconds (default: `10000`).
145
+ - `topicEventNoticeMake`: Custom topic generation for event notices.
146
+ (default: `` (name, clientId) => clientId ? `${name}/event-notice/${clientId}` : `${name}/event-notice` ``)
147
+ - `topicServiceRequestMake`: Custom topic generation for service requests.
148
+ (default: `` (name, clientId) => clientId ? `${name}/service-request/${clientId}` : `${name}/service-request` ``)
149
+ - `topicServiceResponseMake`): Custom topic generation for service responses.
150
+ (default: `` (name, clientId) => clientId ? `${name}/service-response/${clientId}` : `${name}/service-response` ``)
151
+ - `topicEventNoticeMatch`: Custom topic matching for event notices.
152
+ (default: `` (topic) => { const m = topic.match(/^(.+?)\/event-notice(?:\/(.+))?$/); return m ? { name: m[1], clientId: m[2] } : null } ``)
153
+ - `topicServiceRequestMatch`: Custom topic matching for service requests.
154
+ (default: `` (topic) => { const m = topic.match(/^(.+?)\/service-request(?:\/(.+))?$/); return m ? { name: m[1], clientId: m[2] } : null } ``)
155
+ - `topicServiceResponseMatch`: Custom topic matching for service responses.
156
+ (default: `` (topic) => { const m = topic.match(/^(.+?)\/service-response\/(.+)$/); return m ? { name: m[1], clientId: m[2] } : null } ``)
157
+
158
+ - **Event Subscription**:<br/>
159
+
160
+ /* (simplified TypeScript API method signature) */
161
+ subscribe(
162
+ event: string,
163
+ options?: MQTT::IClientSubscribeOptions
164
+ callback: (...params: any[]) => void,
165
+ ): Promise<Subscription>
166
+
167
+ Subscribe to an event.
168
+ The `event` has to be a valid MQTT topic name.
169
+ The optional `options` allows setting MQTT.js `subscribe()` options like `qos`.
170
+ The `callback` is called with the `params` passed to a remote `emit()`.
171
+ There is no return value of `callback`.
172
+
173
+ Internally, on the MQTT broker, the topics generated by
174
+ `topicEventNoticeMake()` (default: `${event}/event-notice` and
175
+ `${event}/event-notice/${clientId}`) are subscribed. Returns a
176
+ `Subscription` object with an `unsubscribe()` method.
177
+
178
+ - **Service Registration**:<br/>
179
+
180
+ /* (simplified TypeScript API method signature) */
181
+ register(
182
+ service: string,
183
+ options?: MQTT::IClientSubscribeOptions
184
+ callback: (...params: any[]) => any,
185
+ ): Promise<Registration>
186
+
187
+ Register a service.
188
+ The `service` has to be a valid MQTT topic name.
189
+ The optional `options` allows setting MQTT.js `subscribe()` options like `qos`.
190
+ The `callback` is called with the `params` passed to a remote `call()`.
191
+ The return value of `callback` will resolve the `Promise` returned by the remote `call()`.
192
+
193
+ Internally, on the MQTT broker, the topics by
194
+ `topicServiceRequestMake()` (default: `${service}/service-request` and
195
+ `${service}/service-request/${clientId}`) are subscribed. Returns a
196
+ `Registration` object with an `unregister()` method.
197
+
198
+ - **Event Emission**:<br/>
199
+
200
+ /* (simplified TypeScript API method signature) */
201
+ emit(
202
+ event: string,
203
+ clientId?: ClientId,
204
+ options?: MQTT::IClientSubscribeOptions,
205
+ ...params: any[]
206
+ ): void
207
+
208
+ Emit an event to all subscribers or a specific subscriber ("fire and forget").
209
+ The optional `clientId` directs the event to a specific subscriber only.
210
+ The optional `options` allows setting MQTT.js `publish()` options like `qos` or `retain`.
211
+
212
+ The remote `subscribe()` `callback` is called with `params` and its
213
+ return value is silently ignored.
214
+
215
+ Internally, publishes to the MQTT topic by `topicEventNoticeMake(event, clientId)`
216
+ (default: `${event}/event-notice` or `${event}/event-notice/${clientId}`).
217
+
218
+ - **Service Call**:<br/>
219
+
220
+ /* (simplified TypeScript API method signature) */
221
+ call(
222
+ service: string,
223
+ clientId?: ClientId,
224
+ options?: MQTT::IClientSubscribeOptions,
225
+ ...params: any[]
226
+ ): Promise<any>
227
+
228
+ Call a service on all registrants or on a specific registrant ("request and response").
229
+ The optional `clientId` directs the call to a specific registrant only.
230
+ The optional `options` allows setting MQTT.js `publish()` options like `qos` or `retain`.
231
+
232
+ The remote `register()` `callback` is called with `params` and its
233
+ return value resolves the returned `Promise`. If the remote `callback`
234
+ throws an exception, this rejects the returned `Promise`.
235
+
236
+ Internally, on the MQTT broker, the topic by `topicServiceResponseMake(service, clientId)`
237
+ (default: `${service}/service-response/${clientId}`) is temporarily subscribed
238
+ for receiving the response.
239
+
240
+ - **Client Id Wrapping**:<br/>
241
+
242
+ clientId(
243
+ id: string
244
+ ): ClientId
245
+
246
+ Wrap a client ID string for use with `emit()` or `call()` to direct the
247
+ message to a specific client. Returns a `ClientId` object.
110
248
 
111
249
  Internals
112
250
  ---------
113
251
 
114
- Internally, remote methods are assigned to MQTT topics. When calling a
115
- remote method named `example/hello` with parameters `"world"` and `42` via...
252
+ In the following, assume that an RPC instance is created with:
253
+
254
+ ```ts
255
+ import MQTT from "mqtt"
256
+ import RPC from "mqtt-json-rpc"
257
+
258
+ const mqtt = MQTT.connect("...", { ... })
259
+ const rpc = new RPC(mqtt, { clientId: "d1acc980-0e4e-11e8-98f0-ab5030b47df4", codec: "json" })
260
+ ```
261
+
262
+ Internally, remote services are assigned to MQTT topics. When calling a
263
+ remote service named `example/hello` with parameters `"world"` and `42` via...
116
264
 
117
- ```js
265
+ ```ts
118
266
  rpc.call("example/hello", "world", 42).then((result) => {
119
267
  ...
120
268
  })
121
269
  ```
122
270
 
123
271
  ...the following JSON-RPC 2.0 request message is sent to the permanent MQTT
124
- topic `example/hello/request`:
272
+ topic `example/hello/service-request` (UUID `d1db7aa0-0e4e-11e8-b1d9-5f0ab230c0d9` is
273
+ a random generated one):
125
274
 
126
275
  ```json
127
276
  {
@@ -132,18 +281,18 @@ topic `example/hello/request`:
132
281
  }
133
282
  ```
134
283
 
135
- Beforehand, this `example/hello` method should have been registered with...
284
+ Beforehand, this `example/hello` service should have been registered with...
136
285
 
137
- ```js
286
+ ```ts
138
287
  rpc.register("example/hello", (a1, a2) => {
139
288
  return `${a1}:${a2}`
140
289
  })
141
290
  ```
142
291
 
143
- ...and then its result, in the above `rpc.call` example `"world:42"`, is then
292
+ ...and then its result, in the above `rpc.call()` example `"world:42"`, is then
144
293
  sent back as the following JSON-RPC 2.0 success response
145
294
  message to the temporary (client-specific) MQTT topic
146
- `example/hello/response/d1acc980-0e4e-11e8-98f0-ab5030b47df4`:
295
+ `example/hello/service-response/d1acc980-0e4e-11e8-98f0-ab5030b47df4`:
147
296
 
148
297
  ```json
149
298
  {
@@ -153,18 +302,18 @@ message to the temporary (client-specific) MQTT topic
153
302
  }
154
303
  ```
155
304
 
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
305
+ The JSON-RPC 2.0 `id` field always consists of `clientId:requestId`, where
306
+ `clientId` is the UUID v1 of the RPC client instance and `requestId` is
307
+ the UUID v1 of the particular service request. The `clientId` is used for
159
308
  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.
309
+ The `requestId` is used for correlating the response to the request only.
161
310
 
162
- Example
163
- -------
311
+ Broker Setup
312
+ ------------
164
313
 
165
314
  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...
315
+ [Mosquitto](https://mosquitto.org/) MQTT broker and a `mosquitto.conf`
316
+ file like...
168
317
 
169
318
  ```
170
319
  [...]
@@ -174,14 +323,10 @@ acl_file mosquitto-acl.txt
174
323
 
175
324
  [...]
176
325
 
177
- # additional listener (wss: MQTT over WebSockets+SSL/TLS)
178
- listener 8889 127.0.0.1
326
+ # additional listener
327
+ listener 1883 127.0.0.1
179
328
  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
329
+ protocol mqtt
185
330
 
186
331
  [...]
187
332
  ```
@@ -199,20 +344,38 @@ topic readwrite example/#
199
344
  example:$6$awYNe6oCAi+xlvo5$mWIUqyy4I0O3nJ99lP1mkRVqsDGymF8en5NChQQxf7KrVJLUp1SzrrVDe94wWWJa3JGIbOXD9wfFGZdi948e6A==
200
345
  ```
201
346
 
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:
347
+ Alternatively, you can use the [NPM package mosquitto](https://npmjs.com/mosquitto)
348
+ for an equal setup.
204
349
 
205
- ```js
206
- const MQTT = require("mqtt")
207
- const RPC = require("mqtt-json-rpc")
350
+ Example
351
+ -------
208
352
 
209
- const mqtt = MQTT.connect("wss://127.0.0.1:8889", {
210
- rejectUnauthorized: false,
353
+ You can test-drive MQTT-JSON-RPC with a complete [sample](sample/sample.ts) to see
354
+ MQTT-JSON-RPC in action and tracing its communication (the typing of the `RPC`
355
+ class with `API` is optional, but strongly suggested):
356
+
357
+ ```ts
358
+ import Mosquitto from "mosquitto"
359
+ import MQTT from "mqtt"
360
+ import RPC from "mqtt-json-rpc"
361
+
362
+ const mosquitto = new Mosquitto()
363
+ await mosquitto.start()
364
+ await new Promise((resolve) => { setTimeout(resolve, 500) })
365
+
366
+ const mqtt = MQTT.connect("mqtt://127.0.0.1:1883", {
211
367
  username: "example",
212
368
  password: "example"
213
369
  })
214
370
 
215
- const rpc = new RPC(mqtt)
371
+ type API = {
372
+ "example/sample": (a1: string, a2: number) => void
373
+ "example/hello": (a1: string, a2: number) => string
374
+ }
375
+
376
+ const rpc = new RPC<API>(mqtt, { codec: "json" })
377
+
378
+ type Sample = (a: string, b: number) => string
216
379
 
217
380
  mqtt.on("error", (err) => { console.log("ERROR", err) })
218
381
  mqtt.on("offline", () => { console.log("OFFLINE") })
@@ -227,8 +390,9 @@ mqtt.on("connect", () => {
227
390
  return `${a1}:${a2}`
228
391
  })
229
392
  rpc.call("example/hello", "world", 42).then((result) => {
230
- console.log("example/hello sucess: ", result)
393
+ console.log("example/hello success: ", result)
231
394
  mqtt.end()
395
+ await mosquitto.stop()
232
396
  }).catch((err) => {
233
397
  console.log("example/hello error: ", err)
234
398
  })
@@ -238,12 +402,12 @@ mqtt.on("connect", () => {
238
402
  The output will be:
239
403
 
240
404
  ```
241
- $ node sample.js
405
+ $ node sample.ts
242
406
  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]}
407
+ 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
408
  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
409
+ 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"}
410
+ example/hello success: world:42
247
411
  CLOSE
248
412
  ```
249
413