mqtt-plus 0.9.3 → 0.9.4
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 +81 -42
- package/dst-stage1/mqtt-plus-api.d.ts +26 -0
- package/dst-stage1/mqtt-plus-api.js +24 -0
- package/dst-stage1/mqtt-plus-base.d.ts +19 -0
- package/dst-stage1/mqtt-plus-base.js +114 -0
- package/dst-stage1/mqtt-plus-codec.d.ts +6 -0
- package/dst-stage1/mqtt-plus-codec.js +11 -0
- package/dst-stage1/mqtt-plus-event.d.ts +18 -0
- package/dst-stage1/mqtt-plus-event.js +103 -0
- package/dst-stage1/mqtt-plus-info.d.ts +15 -0
- package/dst-stage1/mqtt-plus-info.js +24 -0
- package/dst-stage1/mqtt-plus-msg.d.ts +24 -10
- package/dst-stage1/mqtt-plus-msg.js +69 -31
- package/dst-stage1/mqtt-plus-options.d.ts +20 -0
- package/dst-stage1/mqtt-plus-options.js +46 -0
- package/dst-stage1/mqtt-plus-receiver.d.ts +12 -0
- package/dst-stage1/mqtt-plus-receiver.js +42 -0
- package/dst-stage1/mqtt-plus-resource.d.ts +19 -0
- package/dst-stage1/mqtt-plus-resource.js +194 -0
- package/dst-stage1/mqtt-plus-service.d.ts +22 -0
- package/dst-stage1/mqtt-plus-service.js +220 -0
- package/dst-stage1/mqtt-plus-stream.d.ts +20 -0
- package/dst-stage1/mqtt-plus-stream.js +152 -0
- package/dst-stage1/mqtt-plus.d.ts +5 -104
- package/dst-stage1/mqtt-plus.js +4 -508
- package/dst-stage2/mqtt-plus.cjs.js +444 -247
- package/dst-stage2/mqtt-plus.esm.js +442 -246
- package/dst-stage2/mqtt-plus.umd.js +14 -14
- package/etc/eslint.mts +1 -1
- package/etc/stx.conf +7 -1
- package/etc/tsc.tsbuildinfo +1 -1
- package/package.json +17 -5
- package/src/mqtt-plus-api.ts +66 -0
- package/src/mqtt-plus-base.ts +155 -0
- package/src/mqtt-plus-codec.ts +20 -0
- package/src/mqtt-plus-event.ts +164 -0
- package/src/mqtt-plus-info.ts +43 -0
- package/src/mqtt-plus-msg.ts +99 -41
- package/src/mqtt-plus-options.ts +70 -0
- package/src/mqtt-plus-receiver.ts +52 -0
- package/src/mqtt-plus-resource.ts +277 -0
- package/src/mqtt-plus-service.ts +292 -0
- package/src/mqtt-plus-stream.ts +222 -0
- package/src/mqtt-plus.ts +6 -765
- package/tst/mqtt-plus.spec.ts +238 -0
- package/tst/tsc.json +30 -0
package/README.md
CHANGED
|
@@ -69,6 +69,15 @@ communication patterns with optional type safety:
|
|
|
69
69
|
Procedure Call](https://en.wikipedia.org/wiki/Remote_procedure_call)
|
|
70
70
|
(RPC) style communication.
|
|
71
71
|
|
|
72
|
+
- **Resource Transfer**:
|
|
73
|
+
|
|
74
|
+
Resource Transfer is a *bi-directional* communication pattern.
|
|
75
|
+
A Resource is the combination of a resource name and optionally zero or more arguments.
|
|
76
|
+
You *provision* for a resource transfer.
|
|
77
|
+
When a resource is *fetched* or *pushed*, a single particular provisioner (in case
|
|
78
|
+
of a directed resource transfer) or one arbitrary provisioner is called and
|
|
79
|
+
sends or receives the resource and its arguments.
|
|
80
|
+
|
|
72
81
|
> [!Note]
|
|
73
82
|
> **MQTT+** is similar to and derived from
|
|
74
83
|
> [MQTT-JSON-RPC](https://github.com/rse/mqtt-json-rpc) of the same
|
|
@@ -152,18 +161,12 @@ The **MQTT+** API provides the following methods:
|
|
|
152
161
|
constructor<API>(
|
|
153
162
|
mqtt: MqttClient,
|
|
154
163
|
options?: {
|
|
155
|
-
id:
|
|
156
|
-
codec:
|
|
157
|
-
timeout:
|
|
158
|
-
chunkSize:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
topicServiceRequestMake: (topic: string) => TopicMatching | null
|
|
162
|
-
topicServiceResponseMake: (topic: string) => TopicMatching | null
|
|
163
|
-
topicEventNoticeMatch: { name: string, peerId?: string }
|
|
164
|
-
topicStreamChunkMatch: { name: string, peerId?: string }
|
|
165
|
-
topicServiceRequestMatch: { name: string, peerId?: string }
|
|
166
|
-
topicServiceResponseMatch: { name: string, peerId?: string }
|
|
164
|
+
id: string
|
|
165
|
+
codec: "cbor" | "json"
|
|
166
|
+
timeout: number
|
|
167
|
+
chunkSize: number
|
|
168
|
+
topicMake: (name: string, operation: string, peerId?: string) => string
|
|
169
|
+
topicMatch: (topic: string) => { name: string, operation: string, peerId?: string } | null
|
|
167
170
|
}
|
|
168
171
|
)
|
|
169
172
|
|
|
@@ -180,22 +183,12 @@ The **MQTT+** API provides the following methods:
|
|
|
180
183
|
- `codec`: Encoding format (default: `cbor`).
|
|
181
184
|
- `timeout`: Communication timeout in milliseconds (default: `10000`).
|
|
182
185
|
- `chunkSize`: Chunk size in bytes for stream transfers (default: `16384`).
|
|
183
|
-
- `
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
(default: `` (
|
|
189
|
-
- `topicServiceResponseMake`): Custom topic generation for service responses.
|
|
190
|
-
(default: `` (name, peerId) => peerId ? `${name}/service-response/${peerId}` : `${name}/service-response` ``)
|
|
191
|
-
- `topicEventNoticeMatch`: Custom topic matching for event notices.
|
|
192
|
-
(default: `` (topic) => { const m = topic.match(/^(.+?)\/event-notice(?:\/(.+))?$/); return m ? { name: m[1], peerId: m[2] } : null } ``)
|
|
193
|
-
- `topicStreamChunkMatch`: Custom topic matching for stream chunks.
|
|
194
|
-
(default: `` (topic) => { const m = topic.match(/^(.+?)\/stream-chunk(?:\/(.+))?$/); return m ? { name: m[1], peerId: m[2] } : null } ``)
|
|
195
|
-
- `topicServiceRequestMatch`: Custom topic matching for service requests.
|
|
196
|
-
(default: `` (topic) => { const m = topic.match(/^(.+?)\/service-request(?:\/(.+))?$/); return m ? { name: m[1], peerId: m[2] } : null } ``)
|
|
197
|
-
- `topicServiceResponseMatch`: Custom topic matching for service responses.
|
|
198
|
-
(default: `` (topic) => { const m = topic.match(/^(.+?)\/service-response\/(.+)$/); return m ? { name: m[1], peerId: m[2] } : null } ``)
|
|
186
|
+
- `topicMake`: Custom topic generation function.
|
|
187
|
+
The `operation` parameter is one of: `event-notice`, `stream-chunk`, `service-call`, `resource-transfer`.
|
|
188
|
+
(default: `` (name, operation, peerId) => `${name}/${operation}` + (peerId ? `/${peerId}` : "/any") ``)
|
|
189
|
+
- `topicMatch`: Custom topic matching function.
|
|
190
|
+
Returns `{ name, operation, peerId? }` or `null` if no match. The `peerId` is `undefined` for broadcast topics (ending with `/any`).
|
|
191
|
+
(default: `` (topic) => { const m = topic.match(/^(.+)\/([^/]+)\/([^/]+)$/); return m ? { name: m[1], operation: m[2], peerId: m[3] === "any" ? undefined : m[3] } : null } ``)
|
|
199
192
|
|
|
200
193
|
- **Event Subscription**:<br/>
|
|
201
194
|
|
|
@@ -216,7 +209,7 @@ The **MQTT+** API provides the following methods:
|
|
|
216
209
|
There is no return value of `callback`.
|
|
217
210
|
|
|
218
211
|
Internally, on the MQTT broker, the topics generated by
|
|
219
|
-
`
|
|
212
|
+
`topicMake(event, "event-notice")` (default: `${event}/event-notice/any` and
|
|
220
213
|
`${event}/event-notice/${peerId}`) are subscribed. Returns a
|
|
221
214
|
`Subscription` object with an `unsubscribe()` method.
|
|
222
215
|
|
|
@@ -240,7 +233,7 @@ The **MQTT+** API provides the following methods:
|
|
|
240
233
|
There is no return value of `callback`.
|
|
241
234
|
|
|
242
235
|
Internally, on the MQTT broker, the topics generated by
|
|
243
|
-
`
|
|
236
|
+
`topicMake(stream, "stream-chunk")` (default: `${stream}/stream-chunk/any` and
|
|
244
237
|
`${stream}/stream-chunk/${peerId}`) are subscribed. Returns an
|
|
245
238
|
`Attachment` object with an `unattach()` method.
|
|
246
239
|
|
|
@@ -263,10 +256,33 @@ The **MQTT+** API provides the following methods:
|
|
|
263
256
|
The return value of `callback` will resolve the `Promise` returned by the remote `call()`.
|
|
264
257
|
|
|
265
258
|
Internally, on the MQTT broker, the topics by
|
|
266
|
-
`
|
|
267
|
-
`${service}/service-
|
|
259
|
+
`topicMake(service, "service-call")` (default: `${service}/service-call/any` and
|
|
260
|
+
`${service}/service-call/${peerId}`) are subscribed. Returns a
|
|
268
261
|
`Registration` object with an `unregister()` method.
|
|
269
262
|
|
|
263
|
+
- **Resource Provisioning**:<br/>
|
|
264
|
+
|
|
265
|
+
/* (simplified TypeScript API method signature) */
|
|
266
|
+
provision(
|
|
267
|
+
resource: string,
|
|
268
|
+
options?: MQTT::IClientSubscribeOptions
|
|
269
|
+
callback: (
|
|
270
|
+
...params: any[],
|
|
271
|
+
info: { sender: string, receiver?: string }
|
|
272
|
+
) => any
|
|
273
|
+
): Promise<Provisioning>
|
|
274
|
+
|
|
275
|
+
Provision a resource.
|
|
276
|
+
The `resource` has to be a valid MQTT topic name.
|
|
277
|
+
The optional `options` allows setting MQTT.js `subscribe()` options like `qos`.
|
|
278
|
+
The `callback` is called with the `params` passed to a remote `call()`.
|
|
279
|
+
The return value of `callback` will resolve the `Promise` returned by the remote `fetch()` call.
|
|
280
|
+
|
|
281
|
+
Internally, on the MQTT broker, the topics by
|
|
282
|
+
`topicMake(resource, "resource-transfer")` (default: `${resource}/resource-transfer/any` and
|
|
283
|
+
`${resource}/resource-transfer/${peerId}`) are subscribed. Returns a
|
|
284
|
+
`Provisioning` object with an `unprovision()` method.
|
|
285
|
+
|
|
270
286
|
- **Event Emission**:<br/>
|
|
271
287
|
|
|
272
288
|
/* (simplified TypeScript API method signature) */
|
|
@@ -284,8 +300,8 @@ The **MQTT+** API provides the following methods:
|
|
|
284
300
|
The remote `subscribe()` `callback` is called with `params` and its
|
|
285
301
|
return value is silently ignored.
|
|
286
302
|
|
|
287
|
-
Internally, publishes to the MQTT topic by `
|
|
288
|
-
(default: `${event}/event-notice` or `${event}/event-notice/${peerId}`).
|
|
303
|
+
Internally, publishes to the MQTT topic by `topicMake(event, "event-notice", peerId)`
|
|
304
|
+
(default: `${event}/event-notice/any` or `${event}/event-notice/${peerId}`).
|
|
289
305
|
|
|
290
306
|
- **Stream Transfer**:<br/>
|
|
291
307
|
|
|
@@ -311,8 +327,8 @@ The **MQTT+** API provides the following methods:
|
|
|
311
327
|
The remote `attach()` `callback` is called with `params` and an `info` object
|
|
312
328
|
containing a `stream.Readable` for consuming the transferred data.
|
|
313
329
|
|
|
314
|
-
Internally, publishes to the MQTT topic by `
|
|
315
|
-
(default: `${stream}/stream-chunk` or `${stream}/stream-chunk/${peerId}`).
|
|
330
|
+
Internally, publishes to the MQTT topic by `topicMake(stream, "stream-chunk", peerId)`
|
|
331
|
+
(default: `${stream}/stream-chunk/any` or `${stream}/stream-chunk/${peerId}`).
|
|
316
332
|
|
|
317
333
|
- **Service Call**:<br/>
|
|
318
334
|
|
|
@@ -332,8 +348,31 @@ The **MQTT+** API provides the following methods:
|
|
|
332
348
|
return value resolves the returned `Promise`. If the remote `callback`
|
|
333
349
|
throws an exception, this rejects the returned `Promise`.
|
|
334
350
|
|
|
335
|
-
Internally, on the MQTT broker, the topic by `
|
|
336
|
-
(default: `${service}/service-
|
|
351
|
+
Internally, on the MQTT broker, the topic by `topicMake(service, "service-call", peerId)`
|
|
352
|
+
(default: `${service}/service-call/${peerId}`) is temporarily subscribed
|
|
353
|
+
for receiving the response.
|
|
354
|
+
|
|
355
|
+
- **Resource Transfer**:<br/>
|
|
356
|
+
|
|
357
|
+
/* (simplified TypeScript API method signature) */
|
|
358
|
+
fetch(
|
|
359
|
+
blob: string,
|
|
360
|
+
receiver?: Receiver,
|
|
361
|
+
options?: MQTT::IClientSubscribeOptions,
|
|
362
|
+
...params: any[]
|
|
363
|
+
): Promise<Buffer>
|
|
364
|
+
|
|
365
|
+
Fetches a resource from any resource provisioner or from a specific provisioner.
|
|
366
|
+
The optional `receiver` directs the call to a specific provisioner only.
|
|
367
|
+
The optional `options` allows setting MQTT.js `publish()` options like `qos` or `retain`.
|
|
368
|
+
|
|
369
|
+
The remote `provision()` `callback` is called with `params` and its
|
|
370
|
+
return value resolves the returned `Promise`. If the remote `callback`
|
|
371
|
+
throws an exception, this rejects the returned `Promise`.
|
|
372
|
+
|
|
373
|
+
Internally, on the MQTT broker, the topic by
|
|
374
|
+
`topicMake(resource, "resource-transfer", peerId)` (default:
|
|
375
|
+
`${resource}/resource-transfer/${peerId}`) is temporarily subscribed
|
|
337
376
|
for receiving the response.
|
|
338
377
|
|
|
339
378
|
- **Receiver Wrapping**:<br/>
|
|
@@ -368,7 +407,7 @@ mqttp.call("example/hello", "world", 42).then((result) => {
|
|
|
368
407
|
```
|
|
369
408
|
|
|
370
409
|
...the following message is sent to the permanent MQTT topic
|
|
371
|
-
`example/hello/service-
|
|
410
|
+
`example/hello/service-call/any` (the shown NanoIDs are just pseudo
|
|
372
411
|
ones):
|
|
373
412
|
|
|
374
413
|
```json
|
|
@@ -391,7 +430,7 @@ mqttp.register("example/hello", (a1, a2) => {
|
|
|
391
430
|
...and then its result, in the above `mqttp.call()` example `"world:42"`, is then
|
|
392
431
|
sent back as the following success response
|
|
393
432
|
message to the temporary (client-specific) MQTT topic
|
|
394
|
-
`example/hello/service-
|
|
433
|
+
`example/hello/service-call/2IBMSk0NPnrz1AeTERoea`:
|
|
395
434
|
|
|
396
435
|
```json
|
|
397
436
|
{
|
|
@@ -524,9 +563,9 @@ The output will be:
|
|
|
524
563
|
```
|
|
525
564
|
$ node sample.ts
|
|
526
565
|
CONNECT
|
|
527
|
-
RECEIVED example/hello/service-
|
|
566
|
+
RECEIVED example/hello/service-call/any {"id":"vwLzfQDu2uEeOdOfIlT42","sender":"2IBMSk0NPnrz1AeTERoea","service":"example/hello","params":["world",42]}
|
|
528
567
|
example/hello: request: world 42 undefined
|
|
529
|
-
RECEIVED example/hello/service-
|
|
568
|
+
RECEIVED example/hello/service-call/2IBMSk0NPnrz1AeTERoea {"id":"vwLzfQDu2uEeOdOfIlT42","sender":"2IBMSk0NPnrz1AeTERoea","receiver":"2IBMSk0NPnrz1AeTERoea","result":"world:42"}
|
|
530
569
|
example/hello success: world:42
|
|
531
570
|
CLOSE
|
|
532
571
|
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type Brand<T> = T & {
|
|
2
|
+
readonly __brand: unique symbol;
|
|
3
|
+
};
|
|
4
|
+
export type APIEndpoint = APIEndpointEvent | APIEndpointStream | APIEndpointService | APIEndpointResource;
|
|
5
|
+
export type APIEndpointEvent = (...args: any[]) => void;
|
|
6
|
+
export type APIEndpointStream = (...args: any[]) => any;
|
|
7
|
+
export type APIEndpointService = (...args: any[]) => any;
|
|
8
|
+
export type APIEndpointResource = (...args: any[]) => Promise<Buffer>;
|
|
9
|
+
export type Event<T extends APIEndpointEvent> = Brand<T>;
|
|
10
|
+
export type Stream<T extends APIEndpointStream> = Brand<T>;
|
|
11
|
+
export type Service<T extends APIEndpointService> = Brand<T>;
|
|
12
|
+
export type Resource<T extends APIEndpointResource> = Brand<T>;
|
|
13
|
+
export type APISchema = Record<string, APIEndpoint>;
|
|
14
|
+
export type EventKeys<T> = string extends keyof T ? string : {
|
|
15
|
+
[K in keyof T]: T[K] extends Event<infer _F> ? K : never;
|
|
16
|
+
}[keyof T];
|
|
17
|
+
export type StreamKeys<T> = string extends keyof T ? string : {
|
|
18
|
+
[K in keyof T]: T[K] extends Stream<infer _F> ? K : never;
|
|
19
|
+
}[keyof T];
|
|
20
|
+
export type ServiceKeys<T> = string extends keyof T ? string : {
|
|
21
|
+
[K in keyof T]: T[K] extends Service<infer _F> ? K : never;
|
|
22
|
+
}[keyof T];
|
|
23
|
+
export type ResourceKeys<T> = string extends keyof T ? string : {
|
|
24
|
+
[K in keyof T]: T[K] extends Resource<infer _F> ? K : never;
|
|
25
|
+
}[keyof T];
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** MQTT+ -- MQTT Communication Patterns
|
|
3
|
+
** Copyright (c) 2018-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
**
|
|
5
|
+
** Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
** a copy of this software and associated documentation files (the
|
|
7
|
+
** "Software"), to deal in the Software without restriction, including
|
|
8
|
+
** without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
** distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
** permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
** the following conditions:
|
|
12
|
+
**
|
|
13
|
+
** The above copyright notice and this permission notice shall be included
|
|
14
|
+
** in all copies or substantial portions of the Software.
|
|
15
|
+
**
|
|
16
|
+
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MqttClient, IClientPublishOptions, IClientSubscribeOptions } from "mqtt";
|
|
2
|
+
import { APISchema } from "./mqtt-plus-api";
|
|
3
|
+
import { APIOptions } from "./mqtt-plus-options";
|
|
4
|
+
import { ReceiverTrait } from "./mqtt-plus-receiver";
|
|
5
|
+
export declare class BaseTrait<T extends APISchema = APISchema> extends ReceiverTrait<T> {
|
|
6
|
+
protected mqtt: MqttClient;
|
|
7
|
+
constructor(mqtt: MqttClient, options?: Partial<APIOptions>);
|
|
8
|
+
destroy(): void;
|
|
9
|
+
protected _subscribeTopic(topic: string, options?: Partial<IClientSubscribeOptions>): Promise<void>;
|
|
10
|
+
protected _unsubscribeTopic(topic: string): Promise<void>;
|
|
11
|
+
private _isIClientPublishOptions;
|
|
12
|
+
protected _parseCallArgs<U extends any[]>(args: any[]): {
|
|
13
|
+
receiver?: string;
|
|
14
|
+
options: IClientPublishOptions;
|
|
15
|
+
params: U;
|
|
16
|
+
};
|
|
17
|
+
private _onMessage;
|
|
18
|
+
protected _dispatchMessage(_topic: string, _parsed: any): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** MQTT+ -- MQTT Communication Patterns
|
|
3
|
+
** Copyright (c) 2018-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
**
|
|
5
|
+
** Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
** a copy of this software and associated documentation files (the
|
|
7
|
+
** "Software"), to deal in the Software without restriction, including
|
|
8
|
+
** without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
** distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
** permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
** the following conditions:
|
|
12
|
+
**
|
|
13
|
+
** The above copyright notice and this permission notice shall be included
|
|
14
|
+
** in all copies or substantial portions of the Software.
|
|
15
|
+
**
|
|
16
|
+
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
import { ReceiverTrait } from "./mqtt-plus-receiver";
|
|
25
|
+
/* MQTTp Base class with shared infrastructure */
|
|
26
|
+
export class BaseTrait extends ReceiverTrait {
|
|
27
|
+
/* construct API class */
|
|
28
|
+
constructor(mqtt, options = {}) {
|
|
29
|
+
super(options);
|
|
30
|
+
/* store MQTT client */
|
|
31
|
+
this.mqtt = mqtt;
|
|
32
|
+
/* hook into the MQTT message processing */
|
|
33
|
+
this.mqtt.on("message", (topic, message, packet) => {
|
|
34
|
+
this._onMessage(topic, message, packet);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/* destroy API class */
|
|
38
|
+
destroy() {
|
|
39
|
+
this.mqtt.removeAllListeners();
|
|
40
|
+
}
|
|
41
|
+
/* subscribe to an MQTT topic (Promise-based) */
|
|
42
|
+
async _subscribeTopic(topic, options = {}) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
this.mqtt.subscribe(topic, { qos: 2, ...options }, (err, _granted) => {
|
|
45
|
+
if (err)
|
|
46
|
+
reject(err);
|
|
47
|
+
else
|
|
48
|
+
resolve();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/* unsubscribe from an MQTT topic (Promise-based) */
|
|
53
|
+
async _unsubscribeTopic(topic) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
this.mqtt.unsubscribe(topic, (err, _packet) => {
|
|
56
|
+
if (err)
|
|
57
|
+
reject(err);
|
|
58
|
+
else
|
|
59
|
+
resolve();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/* check whether argument has structure of interface IClientPublishOptions */
|
|
64
|
+
_isIClientPublishOptions(arg) {
|
|
65
|
+
if (typeof arg !== "object")
|
|
66
|
+
return false;
|
|
67
|
+
const keys = ["qos", "retain", "dup", "properties", "cbStorePut"];
|
|
68
|
+
return Object.keys(arg).every((key) => keys.includes(key));
|
|
69
|
+
}
|
|
70
|
+
/* parse optional peerId and options from variadic arguments */
|
|
71
|
+
_parseCallArgs(args) {
|
|
72
|
+
let receiver;
|
|
73
|
+
let options = {};
|
|
74
|
+
let params = args;
|
|
75
|
+
if (args.length >= 2 && this._isReceiver(args[0]) && this._isIClientPublishOptions(args[1])) {
|
|
76
|
+
receiver = this._getReceiver(args[0]);
|
|
77
|
+
options = args[1];
|
|
78
|
+
params = args.slice(2);
|
|
79
|
+
}
|
|
80
|
+
else if (args.length >= 1 && this._isReceiver(args[0])) {
|
|
81
|
+
receiver = this._getReceiver(args[0]);
|
|
82
|
+
params = args.slice(1);
|
|
83
|
+
}
|
|
84
|
+
else if (args.length >= 1 && this._isIClientPublishOptions(args[0])) {
|
|
85
|
+
options = args[0];
|
|
86
|
+
params = args.slice(1);
|
|
87
|
+
}
|
|
88
|
+
return { receiver, options, params };
|
|
89
|
+
}
|
|
90
|
+
/* handle incoming MQTT message */
|
|
91
|
+
_onMessage(topic, message, packet) {
|
|
92
|
+
/* try to parse payload as payload */
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
let input = message;
|
|
96
|
+
if (this.options.codec === "json")
|
|
97
|
+
input = message.toString();
|
|
98
|
+
const payload = this.codec.decode(input);
|
|
99
|
+
parsed = this.msg.parse(payload);
|
|
100
|
+
}
|
|
101
|
+
catch (_err) {
|
|
102
|
+
const err = _err instanceof Error
|
|
103
|
+
? new Error(`failed to parse message: ${_err.message}`)
|
|
104
|
+
: new Error("failed to parse message");
|
|
105
|
+
this.mqtt.emit("error", err);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
/* dispatch to trait handlers */
|
|
109
|
+
this._dispatchMessage(topic, parsed);
|
|
110
|
+
}
|
|
111
|
+
/* dispatch parsed message to appropriate handler
|
|
112
|
+
(base implementation, to be overridden in super-traits) */
|
|
113
|
+
_dispatchMessage(_topic, _parsed) { }
|
|
114
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { APISchema } from "./mqtt-plus-api";
|
|
2
|
+
import { APIOptions, OptionsTrait } from "./mqtt-plus-options";
|
|
1
3
|
export default class Codec {
|
|
2
4
|
private type;
|
|
3
5
|
constructor(type: "cbor" | "json");
|
|
4
6
|
encode(data: unknown): Buffer | string;
|
|
5
7
|
decode(data: Buffer | string): unknown;
|
|
6
8
|
}
|
|
9
|
+
export declare class CodecTrait<T extends APISchema = APISchema> extends OptionsTrait<T> {
|
|
10
|
+
protected codec: Codec;
|
|
11
|
+
constructor(options?: Partial<APIOptions>);
|
|
12
|
+
}
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
22
|
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
23
|
*/
|
|
24
|
+
/* external requirements */
|
|
24
25
|
import CBOR from "cbor";
|
|
26
|
+
import { OptionsTrait } from "./mqtt-plus-options";
|
|
25
27
|
/* the encoder/decoder abstraction */
|
|
26
28
|
export default class Codec {
|
|
27
29
|
constructor(type) {
|
|
@@ -72,3 +74,12 @@ export default class Codec {
|
|
|
72
74
|
return result;
|
|
73
75
|
}
|
|
74
76
|
}
|
|
77
|
+
/* Codec trait */
|
|
78
|
+
export class CodecTrait extends OptionsTrait {
|
|
79
|
+
/* construct API class */
|
|
80
|
+
constructor(options = {}) {
|
|
81
|
+
super(options);
|
|
82
|
+
/* establish a codec */
|
|
83
|
+
this.codec = new Codec(this.options.codec);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { IClientPublishOptions, IClientSubscribeOptions } from "mqtt";
|
|
2
|
+
import { APISchema, EventKeys } from "./mqtt-plus-api";
|
|
3
|
+
import type { WithInfo, InfoEvent } from "./mqtt-plus-info";
|
|
4
|
+
import type { Receiver } from "./mqtt-plus-receiver";
|
|
5
|
+
import { BaseTrait } from "./mqtt-plus-base";
|
|
6
|
+
export interface Subscription {
|
|
7
|
+
unsubscribe(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare class EventTrait<T extends APISchema = APISchema> extends BaseTrait<T> {
|
|
10
|
+
private subscriptions;
|
|
11
|
+
subscribe<K extends EventKeys<T> & string>(event: K, callback: WithInfo<T[K], InfoEvent>): Promise<Subscription>;
|
|
12
|
+
subscribe<K extends EventKeys<T> & string>(event: K, options: Partial<IClientSubscribeOptions>, callback: WithInfo<T[K], InfoEvent>): Promise<Subscription>;
|
|
13
|
+
emit<K extends EventKeys<T> & string>(event: K, ...params: Parameters<T[K]>): void;
|
|
14
|
+
emit<K extends EventKeys<T> & string>(event: K, receiver: Receiver, ...params: Parameters<T[K]>): void;
|
|
15
|
+
emit<K extends EventKeys<T> & string>(event: K, options: IClientPublishOptions, ...params: Parameters<T[K]>): void;
|
|
16
|
+
emit<K extends EventKeys<T> & string>(event: K, receiver: Receiver, options: IClientPublishOptions, ...params: Parameters<T[K]>): void;
|
|
17
|
+
protected _dispatchMessage(topic: string, parsed: any): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** MQTT+ -- MQTT Communication Patterns
|
|
3
|
+
** Copyright (c) 2018-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
**
|
|
5
|
+
** Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
** a copy of this software and associated documentation files (the
|
|
7
|
+
** "Software"), to deal in the Software without restriction, including
|
|
8
|
+
** without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
** distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
** permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
** the following conditions:
|
|
12
|
+
**
|
|
13
|
+
** The above copyright notice and this permission notice shall be included
|
|
14
|
+
** in all copies or substantial portions of the Software.
|
|
15
|
+
**
|
|
16
|
+
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
import { nanoid } from "nanoid";
|
|
25
|
+
/* internal requirements */
|
|
26
|
+
import { EventEmission } from "./mqtt-plus-msg";
|
|
27
|
+
import { BaseTrait } from "./mqtt-plus-base";
|
|
28
|
+
/* Event Communication Trait */
|
|
29
|
+
export class EventTrait extends BaseTrait {
|
|
30
|
+
constructor() {
|
|
31
|
+
super(...arguments);
|
|
32
|
+
/* internal state */
|
|
33
|
+
this.subscriptions = new Map();
|
|
34
|
+
}
|
|
35
|
+
async subscribe(event, ...args) {
|
|
36
|
+
/* determine parameters */
|
|
37
|
+
let options = {};
|
|
38
|
+
let callback = args[0];
|
|
39
|
+
if (args.length === 2 && typeof args[0] === "object") {
|
|
40
|
+
options = args[0];
|
|
41
|
+
callback = args[1];
|
|
42
|
+
}
|
|
43
|
+
/* sanity check situation */
|
|
44
|
+
if (this.subscriptions.has(event))
|
|
45
|
+
throw new Error(`subscribe: event "${event}" already subscribed`);
|
|
46
|
+
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
47
|
+
const topicB = this.options.topicMake(event, "event-emission");
|
|
48
|
+
const topicD = this.options.topicMake(event, "event-emission", this.options.id);
|
|
49
|
+
/* subscribe to MQTT topics */
|
|
50
|
+
await Promise.all([
|
|
51
|
+
this._subscribeTopic(topicB, { qos: 0, ...options }),
|
|
52
|
+
this._subscribeTopic(topicD, { qos: 0, ...options })
|
|
53
|
+
]).catch((err) => {
|
|
54
|
+
this._unsubscribeTopic(topicB).catch(() => { });
|
|
55
|
+
this._unsubscribeTopic(topicD).catch(() => { });
|
|
56
|
+
throw err;
|
|
57
|
+
});
|
|
58
|
+
/* remember the subscription */
|
|
59
|
+
this.subscriptions.set(event, callback);
|
|
60
|
+
/* provide a subscription for subsequent unsubscribing */
|
|
61
|
+
const self = this;
|
|
62
|
+
const subscription = {
|
|
63
|
+
async unsubscribe() {
|
|
64
|
+
if (!self.subscriptions.has(event))
|
|
65
|
+
throw new Error(`unsubscribe: event "${event}" not subscribed`);
|
|
66
|
+
self.subscriptions.delete(event);
|
|
67
|
+
return Promise.all([
|
|
68
|
+
self._unsubscribeTopic(topicB),
|
|
69
|
+
self._unsubscribeTopic(topicD)
|
|
70
|
+
]).then(() => { });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return subscription;
|
|
74
|
+
}
|
|
75
|
+
emit(event, ...args) {
|
|
76
|
+
/* determine actual parameters */
|
|
77
|
+
const { receiver, options, params } = this._parseCallArgs(args);
|
|
78
|
+
/* generate unique request id */
|
|
79
|
+
const rid = nanoid();
|
|
80
|
+
/* generate encoded message */
|
|
81
|
+
const request = this.msg.makeEventEmission(rid, event, params, this.options.id, receiver);
|
|
82
|
+
const message = this.codec.encode(request);
|
|
83
|
+
/* generate corresponding MQTT topic */
|
|
84
|
+
const topic = this.options.topicMake(event, "event-emission", receiver);
|
|
85
|
+
/* publish message to MQTT topic */
|
|
86
|
+
this.mqtt.publish(topic, message, { qos: 2, ...options });
|
|
87
|
+
}
|
|
88
|
+
/* dispatch message (Event pattern handling) */
|
|
89
|
+
_dispatchMessage(topic, parsed) {
|
|
90
|
+
super._dispatchMessage(topic, parsed);
|
|
91
|
+
const topicMatch = this.options.topicMatch(topic);
|
|
92
|
+
if (topicMatch !== null
|
|
93
|
+
&& topicMatch.operation === "event-emission"
|
|
94
|
+
&& parsed instanceof EventEmission) {
|
|
95
|
+
/* just deliver event */
|
|
96
|
+
const name = parsed.event;
|
|
97
|
+
const handler = this.subscriptions.get(name);
|
|
98
|
+
const params = parsed.params ?? [];
|
|
99
|
+
const info = { sender: parsed.sender ?? "", receiver: parsed.receiver };
|
|
100
|
+
handler?.(...params, info);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import stream from "stream";
|
|
2
|
+
export interface InfoBase {
|
|
3
|
+
sender: string;
|
|
4
|
+
receiver?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface InfoEvent extends InfoBase {
|
|
7
|
+
}
|
|
8
|
+
export interface InfoStream extends InfoBase {
|
|
9
|
+
stream: stream.Readable;
|
|
10
|
+
}
|
|
11
|
+
export interface InfoService extends InfoBase {
|
|
12
|
+
}
|
|
13
|
+
export interface InfoResource extends InfoBase {
|
|
14
|
+
}
|
|
15
|
+
export type WithInfo<F, I extends InfoBase> = F extends (...args: infer P) => infer R ? (...args: [...P, info: I]) => R : never;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** MQTT+ -- MQTT Communication Patterns
|
|
3
|
+
** Copyright (c) 2018-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
**
|
|
5
|
+
** Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
** a copy of this software and associated documentation files (the
|
|
7
|
+
** "Software"), to deal in the Software without restriction, including
|
|
8
|
+
** without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
** distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
** permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
** the following conditions:
|
|
12
|
+
**
|
|
13
|
+
** The above copyright notice and this permission notice shall be included
|
|
14
|
+
** in all copies or substantial portions of the Software.
|
|
15
|
+
**
|
|
16
|
+
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
export {};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { APISchema } from "./mqtt-plus-api";
|
|
2
|
+
import { CodecTrait } from "./mqtt-plus-codec";
|
|
1
3
|
export declare class Base {
|
|
4
|
+
type: string;
|
|
2
5
|
id: string;
|
|
3
6
|
sender?: string | undefined;
|
|
4
7
|
receiver?: string | undefined;
|
|
5
|
-
constructor(id: string, sender?: string | undefined, receiver?: string | undefined);
|
|
8
|
+
constructor(type: string, id: string, sender?: string | undefined, receiver?: string | undefined);
|
|
6
9
|
}
|
|
7
10
|
export declare class EventEmission extends Base {
|
|
8
11
|
event: string;
|
|
@@ -20,19 +23,30 @@ export declare class ServiceRequest extends Base {
|
|
|
20
23
|
params?: any[] | undefined;
|
|
21
24
|
constructor(id: string, service: string, params?: any[] | undefined, sender?: string, receiver?: string);
|
|
22
25
|
}
|
|
23
|
-
export declare class
|
|
24
|
-
result
|
|
25
|
-
|
|
26
|
+
export declare class ServiceResponse extends Base {
|
|
27
|
+
result?: any | undefined;
|
|
28
|
+
error?: string | undefined;
|
|
29
|
+
constructor(id: string, result?: any | undefined, error?: string | undefined, sender?: string, receiver?: string);
|
|
26
30
|
}
|
|
27
|
-
export declare class
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
export declare class ResourceRequest extends Base {
|
|
32
|
+
resource: string;
|
|
33
|
+
params?: any[] | undefined;
|
|
34
|
+
constructor(id: string, resource: string, params?: any[] | undefined, sender?: string, receiver?: string);
|
|
35
|
+
}
|
|
36
|
+
export declare class ResourceResponse extends Base {
|
|
37
|
+
chunk: Buffer | null | undefined;
|
|
38
|
+
error: string | undefined;
|
|
39
|
+
constructor(id: string, chunk: Buffer | null | undefined, error: string | undefined, sender?: string, receiver?: string);
|
|
30
40
|
}
|
|
31
41
|
export default class Msg {
|
|
32
42
|
makeEventEmission(id: string, event: string, params?: any[], sender?: string, receiver?: string): EventEmission;
|
|
33
43
|
makeStreamChunk(id: string, stream: string, chunk: Buffer | null, params?: any[], sender?: string, receiver?: string): StreamChunk;
|
|
34
44
|
makeServiceRequest(id: string, service: string, params?: any[], sender?: string, receiver?: string): ServiceRequest;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
makeServiceResponse(id: string, result?: any, error?: string, sender?: string, receiver?: string): ServiceResponse;
|
|
46
|
+
makeResourceRequest(id: string, resource: string, params?: any[], sender?: string, receiver?: string): ResourceRequest;
|
|
47
|
+
makeResourceResponse(id: string, chunk?: Buffer | null, error?: string, sender?: string, receiver?: string): ResourceResponse;
|
|
48
|
+
parse(obj: any): EventEmission | StreamChunk | ServiceRequest | ServiceResponse | ResourceRequest | ResourceResponse;
|
|
49
|
+
}
|
|
50
|
+
export declare class MsgTrait<T extends APISchema = APISchema> extends CodecTrait<T> {
|
|
51
|
+
protected msg: Msg;
|
|
38
52
|
}
|