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.
Files changed (46) hide show
  1. package/README.md +81 -42
  2. package/dst-stage1/mqtt-plus-api.d.ts +26 -0
  3. package/dst-stage1/mqtt-plus-api.js +24 -0
  4. package/dst-stage1/mqtt-plus-base.d.ts +19 -0
  5. package/dst-stage1/mqtt-plus-base.js +114 -0
  6. package/dst-stage1/mqtt-plus-codec.d.ts +6 -0
  7. package/dst-stage1/mqtt-plus-codec.js +11 -0
  8. package/dst-stage1/mqtt-plus-event.d.ts +18 -0
  9. package/dst-stage1/mqtt-plus-event.js +103 -0
  10. package/dst-stage1/mqtt-plus-info.d.ts +15 -0
  11. package/dst-stage1/mqtt-plus-info.js +24 -0
  12. package/dst-stage1/mqtt-plus-msg.d.ts +24 -10
  13. package/dst-stage1/mqtt-plus-msg.js +69 -31
  14. package/dst-stage1/mqtt-plus-options.d.ts +20 -0
  15. package/dst-stage1/mqtt-plus-options.js +46 -0
  16. package/dst-stage1/mqtt-plus-receiver.d.ts +12 -0
  17. package/dst-stage1/mqtt-plus-receiver.js +42 -0
  18. package/dst-stage1/mqtt-plus-resource.d.ts +19 -0
  19. package/dst-stage1/mqtt-plus-resource.js +194 -0
  20. package/dst-stage1/mqtt-plus-service.d.ts +22 -0
  21. package/dst-stage1/mqtt-plus-service.js +220 -0
  22. package/dst-stage1/mqtt-plus-stream.d.ts +20 -0
  23. package/dst-stage1/mqtt-plus-stream.js +152 -0
  24. package/dst-stage1/mqtt-plus.d.ts +5 -104
  25. package/dst-stage1/mqtt-plus.js +4 -508
  26. package/dst-stage2/mqtt-plus.cjs.js +444 -247
  27. package/dst-stage2/mqtt-plus.esm.js +442 -246
  28. package/dst-stage2/mqtt-plus.umd.js +14 -14
  29. package/etc/eslint.mts +1 -1
  30. package/etc/stx.conf +7 -1
  31. package/etc/tsc.tsbuildinfo +1 -1
  32. package/package.json +17 -5
  33. package/src/mqtt-plus-api.ts +66 -0
  34. package/src/mqtt-plus-base.ts +155 -0
  35. package/src/mqtt-plus-codec.ts +20 -0
  36. package/src/mqtt-plus-event.ts +164 -0
  37. package/src/mqtt-plus-info.ts +43 -0
  38. package/src/mqtt-plus-msg.ts +99 -41
  39. package/src/mqtt-plus-options.ts +70 -0
  40. package/src/mqtt-plus-receiver.ts +52 -0
  41. package/src/mqtt-plus-resource.ts +277 -0
  42. package/src/mqtt-plus-service.ts +292 -0
  43. package/src/mqtt-plus-stream.ts +222 -0
  44. package/src/mqtt-plus.ts +6 -765
  45. package/tst/mqtt-plus.spec.ts +238 -0
  46. 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: string
156
- codec: "cbor" | "json"
157
- timeout: number
158
- chunkSize: number
159
- topicEventNoticeMake: (topic: string) => TopicMatching | null
160
- topicStreamChunkMake: (topic: string) => TopicMatching | null
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
- - `topicEventNoticeMake`: Custom topic generation for event notices.
184
- (default: `` (name, peerId) => peerId ? `${name}/event-notice/${peerId}` : `${name}/event-notice` ``)
185
- - `topicStreamChunkMake`: Custom topic generation for stream chunks.
186
- (default: `` (name, peerId) => peerId ? `${name}/stream-chunk/${peerId}` : `${name}/stream-chunk` ``)
187
- - `topicServiceRequestMake`: Custom topic generation for service requests.
188
- (default: `` (name, peerId) => peerId ? `${name}/service-request/${peerId}` : `${name}/service-request` ``)
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
- `topicEventNoticeMake()` (default: `${event}/event-notice` and
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
- `topicStreamChunkMake()` (default: `${stream}/stream-chunk` and
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
- `topicServiceRequestMake()` (default: `${service}/service-request` and
267
- `${service}/service-request/${peerId}`) are subscribed. Returns a
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 `topicEventNoticeMake(event, peerId)`
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 `topicStreamChunkMake(stream, peerId)`
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 `topicServiceResponseMake(service, peerId)`
336
- (default: `${service}/service-response/${peerId}`) is temporarily subscribed
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-request` (the shown NanoIDs are just pseudo
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-response/2IBMSk0NPnrz1AeTERoea`:
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-request {"id":"vwLzfQDu2uEeOdOfIlT42","sender":"2IBMSk0NPnrz1AeTERoea","service":"example/hello","params":["world",42]}
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-response/2IBMSk0NPnrz1AeTERoea {"id":"vwLzfQDu2uEeOdOfIlT42","sender":"2IBMSk0NPnrz1AeTERoea","receiver":"2IBMSk0NPnrz1AeTERoea","result":"world:42"}
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 ServiceResponseSuccess extends Base {
24
- result: any;
25
- constructor(id: string, result: any, sender?: string, receiver?: string);
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 ServiceResponseError extends Base {
28
- error: string;
29
- constructor(id: string, error: string, sender?: string, receiver?: string);
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
- makeServiceResponseSuccess(id: string, result: any, sender?: string, receiver?: string): ServiceResponseSuccess;
36
- makeServiceResponseError(id: string, error: string, sender?: string, receiver?: string): ServiceResponseError;
37
- parse(obj: any): EventEmission | StreamChunk | ServiceRequest | ServiceResponseSuccess | ServiceResponseError;
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
  }