mqtt-plus 1.4.6 → 1.4.7

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 (43) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +12 -8
  4. package/doc/mqtt-plus-api.md +17 -6
  5. package/doc/mqtt-plus-broker-setup.md +4 -4
  6. package/dst-stage1/mqtt-plus-base.d.ts +2 -2
  7. package/dst-stage1/mqtt-plus-base.js +5 -12
  8. package/dst-stage1/mqtt-plus-codec.js +8 -8
  9. package/dst-stage1/mqtt-plus-event.js +2 -2
  10. package/dst-stage1/mqtt-plus-info.d.ts +2 -2
  11. package/dst-stage1/mqtt-plus-msg.d.ts +4 -7
  12. package/dst-stage1/mqtt-plus-msg.js +11 -17
  13. package/dst-stage1/mqtt-plus-options.d.ts +1 -0
  14. package/dst-stage1/mqtt-plus-options.js +1 -0
  15. package/dst-stage1/mqtt-plus-service.js +8 -13
  16. package/dst-stage1/mqtt-plus-sink.js +30 -36
  17. package/dst-stage1/mqtt-plus-source.js +9 -18
  18. package/dst-stage1/mqtt-plus-subscription.js +6 -5
  19. package/dst-stage1/mqtt-plus-util.d.ts +3 -1
  20. package/dst-stage1/mqtt-plus-util.js +5 -1
  21. package/dst-stage2/mqtt-plus.cjs.js +82 -101
  22. package/dst-stage2/mqtt-plus.esm.js +82 -101
  23. package/dst-stage2/mqtt-plus.umd.js +12 -12
  24. package/etc/knip.jsonc +4 -3
  25. package/etc/stx.conf +4 -2
  26. package/package.json +2 -1
  27. package/src/mqtt-plus-base.ts +7 -13
  28. package/src/mqtt-plus-codec.ts +4 -4
  29. package/src/mqtt-plus-event.ts +2 -2
  30. package/src/mqtt-plus-info.ts +2 -2
  31. package/src/mqtt-plus-msg.ts +7 -16
  32. package/src/mqtt-plus-options.ts +2 -0
  33. package/src/mqtt-plus-service.ts +8 -13
  34. package/src/mqtt-plus-sink.ts +31 -37
  35. package/src/mqtt-plus-source.ts +9 -19
  36. package/src/mqtt-plus-subscription.ts +7 -5
  37. package/src/mqtt-plus-trace.ts +2 -0
  38. package/src/mqtt-plus-util.ts +13 -7
  39. package/tst/mqtt-plus-0-broker-aedes.ts +120 -0
  40. package/tst/{mqtt-plus-0-mosquitto.ts → mqtt-plus-0-broker-mosquitto.ts} +11 -8
  41. package/tst/mqtt-plus-0-broker.ts +52 -0
  42. package/tst/mqtt-plus-0-fixture.ts +20 -17
  43. package/tst/mqtt-plus-1-api.spec.ts +1 -1
package/AGENTS.md CHANGED
@@ -129,7 +129,9 @@ Test files live in `tst/`:
129
129
  | File | Role |
130
130
  |-----------------------------------|------|
131
131
  | `tst/mqtt-plus-0-fixture.ts` | Shared test fixture setup (broker, MQTTp instances, etc.) |
132
- | `tst/mqtt-plus-0-mosquitto.ts` | Helper for starting/stopping the Mosquitto MQTT broker |
132
+ | `tst/mqtt-plus-0-broker.ts` | Broker dispatch: creates Aedes or Mosquitto broker based on env |
133
+ | `tst/mqtt-plus-0-broker-aedes.ts` | Helper for starting/stopping the Aedes MQTT broker |
134
+ | `tst/mqtt-plus-0-broker-mosquitto.ts` | Helper for starting/stopping the Mosquitto MQTT broker |
133
135
  | `tst/mqtt-plus-1-api.spec.ts` | API type and endpoint definition tests |
134
136
  | `tst/mqtt-plus-2-event.spec.ts` | Event Emission pattern tests |
135
137
  | `tst/mqtt-plus-3-service.spec.ts` | Service Call / RPC pattern tests |
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
  ChangeLog
3
3
  =========
4
4
 
5
+ 1.5.0 (2026-02-22)
6
+ ------------------
7
+
8
+ - IMPROVEMENT: provide a global "share" option
9
+ - IMPROVEMENT: use timer utility code for internal timing
10
+ - IMPROVEMENT: improve error handling
11
+ - IMPROVEMENT: make code more type-safe
12
+ - IMPROVEMENT: make "stream" and "buffer" fields mandatory in sink() "info" object
13
+ - IMPROVEMENT: make test suite more robust and support async/non-async handlers
14
+ - IMPROVEMENT: add Aedes MQTT broker support to not require Docker for the test suite
15
+ - BUGFIX: fix leak in subscription handling
16
+ - BUGFIX: workaround for ESM-only "plazy" module when used from CJS context
17
+ - BUGFIX: fix ACL handling
18
+ - CLEANUP: various code cleanups
19
+ - CLEANUP: remove unused fields from protocol messages
20
+ - CLEANUP: add protection and align code with implementation
21
+ - CLEANUP: improve console output and use Chalk for colored rendering
22
+ - CLEANUP: improve about description and text
23
+ - DOCUMENTATION: switch about information to Markdown and SVG format
24
+ - DOCUMENTATION: add hint to NPM
25
+ - DOCUMENTATION: fix name of peer dependency
26
+
5
27
  1.4.6 (2026-02-22)
6
28
  ------------------
7
29
 
package/README.md CHANGED
@@ -6,8 +6,7 @@ MQTT+
6
6
 
7
7
  [MQTT](http://mqtt.org/) [Communication Patterns](doc/mqtt-plus-comm.md)
8
8
 
9
- <p/>
10
- <img src="https://nodei.co/npm/mqtt-plus.png?downloads=true&stars=true" alt=""/>
9
+ [![NPM](https://nodei.co/npm/mqtt-plus.svg?downloads=true&stars=true)](https://nodei.co/npm/mqtt-plus/)
11
10
 
12
11
  [![github (author stars)](https://img.shields.io/github/stars/rse?logo=github&label=author%20stars&color=%233377aa)](https://github.com/rse)
13
12
  [![github (author followers)](https://img.shields.io/github/followers/rse?label=author%20followers&logo=github&color=%234477aa)](https://github.com/rse)
@@ -18,7 +17,7 @@ About
18
17
  **MQTT+** is a companion add-on API for the TypeScript/JavaScript
19
18
  API [MQTT.js](https://www.npmjs.com/package/mqtt), designed to
20
19
  extend [MQTT](http://mqtt.org/) with higher-level
21
- [communication patterns](doc/mqtt-plus-comm.md) while preserving full type-safety.
20
+ [communication patterns](doc/mqtt-plus-comm.md) while preserving full type safety.
22
21
  It provides four core communication patterns: fire-and-forget *Event
23
22
  Emission*, RPC-style *Service Call*, stream-based *Sink Push*, and
24
23
  stream-based *Source Fetch*.
@@ -31,7 +30,7 @@ or [JSON](https://ecma-international.org/publications-and-standards/standards/ec
31
30
  The result is a more expressive and maintainable messaging layer
32
31
  without sacrificing [MQTT](http://mqtt.org/)’s excellent robustness and
33
32
  scalability.
34
- **MQTT+** is particularly well suited for systems built around a
33
+ **MQTT+** is particularly well-suited for systems built around a
35
34
  [*Hub & Spoke*](https://en.wikipedia.org/wiki/Spoke%E2%80%93hub_distribution_paradigm)
36
35
  communication architecture, where typed API contracts and controlled interaction flows are
37
36
  critical for reliability and long-term maintainability.
@@ -39,6 +38,11 @@ critical for reliability and long-term maintainability.
39
38
  Installation
40
39
  ------------
41
40
 
41
+ **MQTT+** is published as a Node Package Manager (NPM) package named
42
+ [`mqtt-plus`](https://npmjs.com/mqtt-plus).
43
+ Install it, together with its peer dependency MQTT.js, with
44
+ the help of the NPM Command-Line Interface (CLI):
45
+
42
46
  ```shell
43
47
  $ npm install mqtt mqtt-plus
44
48
  ```
@@ -48,7 +52,7 @@ Usage
48
52
 
49
53
  The following is a simple but self-contained example usage of
50
54
  **MQTT+** based on a common API, a server part, a client part,
51
- and a MQTT infrastructure. It can be found in the file
55
+ and an MQTT infrastructure. It can be found in the file
52
56
  [sample.ts](sample/sample.ts) and can be executed from the **MQTT+**
53
57
  source tree via `npm start sample` (assuming the prerequisite *Docker* is
54
58
  available for the underlying *Mosquitto* broker based infrastructure):
@@ -92,8 +96,8 @@ const Server = async (api: MQTTp<API>, log: (msg: string, ...args: any[]) => voi
92
96
  await api.sink("example/upload", async (filename, info) => {
93
97
  log("example/upload: SERVER:", filename)
94
98
  const chunks: Uint8Array[] = []
95
- info.stream!.on("data", (chunk: Uint8Array) => { chunks.push(chunk) })
96
- await new Promise<void>((resolve) => { info.stream!.once("end", resolve) })
99
+ info.stream.on("data", (chunk: Uint8Array) => { chunks.push(chunk) })
100
+ await new Promise<void>((resolve) => { info.stream.once("end", resolve) })
97
101
  const total = chunks.reduce((n, c) => n + c.length, 0)
98
102
  log("received", total, "bytes")
99
103
  })
@@ -171,7 +175,7 @@ Notice
171
175
  ------
172
176
 
173
177
  > [!Note]
174
- > **MQTT+** and its peer dependency **MQTT** provide a powerful
178
+ > **MQTT+** and its peer dependency **MQTT.js** provide a powerful
175
179
  > functionality, but are not small in size. **MQTT+** is 3.500 LoC
176
180
  > and 75 KB in size (ESM and CJS format). When bundled with all its
177
181
  > dependencies, it is 220 KB in size (UMD format). Its peer dependency
@@ -17,6 +17,7 @@ Construction
17
17
  id: string
18
18
  codec: "cbor" | "json"
19
19
  timeout: number
20
+ share: string
20
21
  chunkSize: number
21
22
  chunkCredit: number
22
23
  topicMake: (name: string, operation: string, peerId?: string) => string
@@ -36,6 +37,14 @@ describing the available events, services, sources, and sinks.
36
37
  - `id`: Custom MQTT peer identifier (default: auto-generated NanoID).
37
38
  - `codec`: Encoding format, either `cbor` or `json` (default: `cbor`).
38
39
  - `timeout`: Communication timeout in milliseconds (default: `10000`).
40
+ - `share`: Default
41
+ [MQTT Shared Subscription](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901250)
42
+ group name for `service()`, `source()`, and `sink()` registrations
43
+ (default: `""`). Set to a non-empty string (e.g., `"default"`) to
44
+ enable shared subscriptions globally, which internally prefixes
45
+ topics with `$share/<share>/`. This global default can be overridden
46
+ per-call via the `share` option on `event()`, `service()`,
47
+ `source()`, and `sink()`.
39
48
  - `chunkSize`: Chunk size in bytes for source/sink transfers (default: `16384`).
40
49
  - `chunkCredit`: Number of credit units for flow control in source/sink
41
50
  chunked transfers (default: `4`). Controls how many chunks can be
@@ -235,7 +244,8 @@ Register for an event.
235
244
  [MQTT Shared Subscriptions](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901250)
236
245
  (MQTT 5.0) for load-balancing messages across multiple registrations
237
246
  by specifying a group name. This internally prefixes the event with
238
- `$share/<share>/`.
247
+ `$share/<share>/`. By default no shared subscription is used for
248
+ events (unlike `service()`, `source()`, and `sink()`).
239
249
 
240
250
  - The optional `auth` enables authentication validation on incoming events.
241
251
  When set to a role name string (e.g., `"admin"`), authentication is required
@@ -364,7 +374,8 @@ Register a service.
364
374
  [MQTT Shared Subscriptions](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901250)
365
375
  (MQTT 5.0) for load-balancing service calls across multiple services
366
376
  by specifying a group name. This internally prefixes the service
367
- with `$share/<share>/`. By default a share named `default` is used.
377
+ with `$share/<share>/`. Defaults to the global `share` constructor
378
+ option (default: `""`).
368
379
 
369
380
  - The optional `auth` enables authentication validation on incoming service calls.
370
381
  When set to a role name string (e.g., `"admin"`), authentication is required
@@ -464,8 +475,8 @@ Register a sink for receiving data.
464
475
  [MQTT Shared Subscriptions](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901250)
465
476
  (MQTT 5.0) for load-balancing sink pushes across multiple sink
466
477
  handlers by specifying a group name. This internally prefixes the
467
- sink with `$share/<share>/`. By default a share named `default` is
468
- used.
478
+ sink with `$share/<share>/`. Defaults to the global `share` constructor
479
+ option (default: `""`).
469
480
 
470
481
  - The optional `auth` enables authentication validation on incoming sink pushes.
471
482
  When set to a role name string (e.g., `"admin"`), authentication
@@ -587,8 +598,8 @@ Register a source for sending data.
587
598
  [MQTT Shared Subscriptions](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901250)
588
599
  (MQTT 5.0) for load-balancing source requests across multiple
589
600
  sources by specifying a group name. This internally prefixes the
590
- source with `$share/<share>/`. By default a share named `default` is
591
- used.
601
+ source with `$share/<share>/`. Defaults to the global `share` constructor
602
+ option (default: `""`).
592
603
 
593
604
  - The optional `auth` enables authentication validation on incoming source fetches.
594
605
  When set to a role name string (e.g., `"admin"`), authentication
@@ -137,16 +137,16 @@ pattern read example/client/+/source-fetch-chunk/%c
137
137
  # ---- sink push ----
138
138
 
139
139
  topic read example/server/+/sink-push-request/any
140
- topic read $share/default/example/server/+/sink-push-request/any
140
+ topic read $share/server/example/server/+/sink-push-request/any
141
141
  pattern read example/server/+/sink-push-request/%c
142
- pattern read $share/default/example/server/+/sink-push-request/%c
142
+ pattern read $share/server/example/server/+/sink-push-request/%c
143
143
  topic write example/server/+/sink-push-response/+
144
144
  pattern read example/server/+/sink-push-chunk/%c
145
- pattern read $share/default/example/server/+/sink-push-chunk/%c
146
- topic write example/client/+/sink-push-credit/+
145
+ pattern read $share/server/example/server/+/sink-push-chunk/%c
147
146
 
148
147
  topic write example/client/+/sink-push-request/+
149
148
  pattern read example/client/+/sink-push-response/%c
149
+ topic write example/client/+/sink-push-credit/+
150
150
  topic write example/client/+/sink-push-chunk/+
151
151
  ```
152
152
 
@@ -6,8 +6,8 @@ import type { Spool } from "./mqtt-plus-error";
6
6
  export declare class BaseTrait<T extends APISchema = APISchema> extends TraceTrait<T> {
7
7
  private mqtt;
8
8
  private messageHandler;
9
- protected onRequest: Map<string, (message: any, topicName: string) => void>;
10
- protected onResponse: Map<string, (message: any, topicName: string) => void>;
9
+ protected onRequest: Map<string, (message: any, topicName: string) => void | Promise<void>>;
10
+ protected onResponse: Map<string, (message: any, topicName: string) => void | Promise<void>>;
11
11
  constructor(mqtt: MqttClient | null, options?: Partial<APIOptions>);
12
12
  destroy(): Promise<void>;
13
13
  protected makeRegistration(spool: Spool, kind: string, name: string, key: string): Registration;
@@ -21,10 +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 */
25
- import PLazy from "p-lazy";
26
24
  import { TraceTrait } from "./mqtt-plus-trace";
27
25
  import { ensureError } from "./mqtt-plus-error";
26
+ import { PLazy } from "./mqtt-plus-util";
28
27
  /* MQTTp Base class with shared infrastructure */
29
28
  export class BaseTrait extends TraceTrait {
30
29
  /* construct API class */
@@ -189,24 +188,18 @@ export class BaseTrait extends TraceTrait {
189
188
  /* dispatch request message */
190
189
  const handler = this.onRequest.get(`${topicMatch.operation}:${message.name}`);
191
190
  if (handler !== undefined) {
192
- try {
193
- handler(message, topicMatch.name);
194
- }
195
- catch (err) {
191
+ Promise.resolve(handler(message, topicMatch.name)).catch((err) => {
196
192
  this.error(ensureError(err, `dispatching request message from MQTT topic "${topic}" failed`));
197
- }
193
+ });
198
194
  }
199
195
  }
200
196
  else if (this.msg.isResponse(message)) {
201
197
  /* dispatch response message */
202
198
  const handler = this.onResponse.get(`${topicMatch.operation}:${message.id}`);
203
199
  if (handler !== undefined) {
204
- try {
205
- handler(message, topicMatch.name);
206
- }
207
- catch (err) {
200
+ Promise.resolve(handler(message, topicMatch.name)).catch((err) => {
208
201
  this.error(ensureError(err, `dispatching response message from MQTT topic "${topic}" failed`));
209
- }
202
+ });
210
203
  }
211
204
  }
212
205
  }
@@ -65,16 +65,16 @@ class Codec {
65
65
  try {
66
66
  result = CBOR.encode(data, { types: this.types });
67
67
  }
68
- catch (ex) {
69
- throw new Error("failed to encode CBOR format", { cause: ex });
68
+ catch (err) {
69
+ throw new Error("failed to encode CBOR format", { cause: err });
70
70
  }
71
71
  }
72
72
  else if (this.format === "json") {
73
73
  try {
74
74
  result = JSONX.stringify(data);
75
75
  }
76
- catch (ex) {
77
- throw new Error("failed to encode JSON format", { cause: ex });
76
+ catch (err) {
77
+ throw new Error("failed to encode JSON format", { cause: err });
78
78
  }
79
79
  }
80
80
  else
@@ -89,8 +89,8 @@ class Codec {
89
89
  try {
90
90
  result = CBOR.decode(data, { tags: this.tags });
91
91
  }
92
- catch (ex) {
93
- throw new Error("failed to decode CBOR format", { cause: ex });
92
+ catch (err) {
93
+ throw new Error("failed to decode CBOR format", { cause: err });
94
94
  }
95
95
  }
96
96
  else if (this.format === "json") {
@@ -99,8 +99,8 @@ class Codec {
99
99
  try {
100
100
  result = JSONX.parse(data);
101
101
  }
102
- catch (ex) {
103
- throw new Error("failed to decode JSON format", { cause: ex });
102
+ catch (err) {
103
+ throw new Error("failed to decode JSON format", { cause: err });
104
104
  }
105
105
  }
106
106
  else
@@ -68,8 +68,8 @@ export class EventTrait extends AuthTrait {
68
68
  info.meta = request.meta;
69
69
  /* asynchronously execute handler */
70
70
  Promise.resolve().then(async () => {
71
- if (topicName !== name)
72
- throw new Error(`event name mismatch (topic: "${topicName}", payload: "${name}")`);
71
+ if (topicName !== request.name)
72
+ throw new Error(`event name mismatch (topic: "${topicName}", payload: "${request.name}")`);
73
73
  if (auth)
74
74
  info.authenticated = await this.authenticated(request.sender, request.auth, auth);
75
75
  if (info.authenticated !== undefined && !info.authenticated)
@@ -14,7 +14,7 @@ export interface InfoSource extends InfoBase {
14
14
  buffer?: Promise<Uint8Array>;
15
15
  }
16
16
  export interface InfoSink extends InfoBase {
17
- stream?: Readable;
18
- buffer?: Promise<Uint8Array>;
17
+ stream: Readable;
18
+ buffer: Promise<Uint8Array>;
19
19
  }
20
20
  export type WithInfo<F, I extends InfoBase> = F extends (...args: infer P) => infer R ? (...args: [...P, info: I]) => R : never;
@@ -38,10 +38,8 @@ export declare class SinkPushRequest extends Base {
38
38
  export declare class SinkPushResponse extends Base {
39
39
  name: string;
40
40
  error?: string | undefined;
41
- auth?: string[] | undefined;
42
- meta?: Record<string, any> | undefined;
43
41
  credit?: number | undefined;
44
- constructor(id: string, name: string, error?: string | undefined, sender?: string, receiver?: string, auth?: string[] | undefined, meta?: Record<string, any> | undefined, credit?: number | undefined);
42
+ constructor(id: string, name: string, error?: string | undefined, sender?: string, receiver?: string, credit?: number | undefined);
45
43
  }
46
44
  export declare class SinkPushChunk extends Base {
47
45
  name: string;
@@ -66,9 +64,8 @@ export declare class SourceFetchRequest extends Base {
66
64
  export declare class SourceFetchResponse extends Base {
67
65
  name: string;
68
66
  error?: string | undefined;
69
- auth?: string[] | undefined;
70
67
  meta?: Record<string, any> | undefined;
71
- constructor(id: string, name: string, error?: string | undefined, sender?: string, receiver?: string, auth?: string[] | undefined, meta?: Record<string, any> | undefined);
68
+ constructor(id: string, name: string, error?: string | undefined, sender?: string, receiver?: string, meta?: Record<string, any> | undefined);
72
69
  }
73
70
  export declare class SourceFetchChunk extends Base {
74
71
  name: string;
@@ -87,11 +84,11 @@ declare class Msg {
87
84
  makeServiceCallRequest(id: string, name: string, params?: any[], sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>): ServiceCallRequest;
88
85
  makeServiceCallResponse(id: string, result?: any, error?: string, sender?: string, receiver?: string): ServiceCallResponse;
89
86
  makeSinkPushRequest(id: string, name: string, params?: any[], sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>): SinkPushRequest;
90
- makeSinkPushResponse(id: string, name: string, error?: string, sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>, credit?: number): SinkPushResponse;
87
+ makeSinkPushResponse(id: string, name: string, error?: string, sender?: string, receiver?: string, credit?: number): SinkPushResponse;
91
88
  makeSinkPushChunk(id: string, name: string, chunk?: Uint8Array, error?: string, final?: boolean, sender?: string, receiver?: string): SinkPushChunk;
92
89
  makeSinkPushCredit(id: string, name: string, credit: number, sender?: string, receiver?: string): SinkPushCredit;
93
90
  makeSourceFetchRequest(id: string, name: string, params?: any[], sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>, credit?: number): SourceFetchRequest;
94
- makeSourceFetchResponse(id: string, name: string, error?: string, sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>): SourceFetchResponse;
91
+ makeSourceFetchResponse(id: string, name: string, error?: string, sender?: string, receiver?: string, meta?: Record<string, any>): SourceFetchResponse;
95
92
  makeSourceFetchChunk(id: string, name: string, chunk?: Uint8Array, error?: string, final?: boolean, sender?: string, receiver?: string): SourceFetchChunk;
96
93
  makeSourceFetchCredit(id: string, name: string, credit: number, sender?: string, receiver?: string): SourceFetchCredit;
97
94
  parse(obj: any): EventEmission | ServiceCallRequest | ServiceCallResponse | SinkPushRequest | SinkPushResponse | SinkPushChunk | SinkPushCredit | SourceFetchRequest | SourceFetchResponse | SourceFetchChunk | SourceFetchCredit;
@@ -116,12 +116,10 @@ const SinkPushRequestSchema = v.strictObject({
116
116
  });
117
117
  /* sink push response (ack/nak) */
118
118
  export class SinkPushResponse extends Base {
119
- constructor(id, name, error, sender, receiver, auth, meta, credit) {
119
+ constructor(id, name, error, sender, receiver, credit) {
120
120
  super("sink-push-response", id, sender, receiver);
121
121
  this.name = name;
122
122
  this.error = error;
123
- this.auth = auth;
124
- this.meta = meta;
125
123
  this.credit = credit;
126
124
  }
127
125
  }
@@ -130,8 +128,6 @@ const SinkPushResponseSchema = v.strictObject({
130
128
  type: v.literal("sink-push-response"),
131
129
  name: v.string(),
132
130
  error: v.optional(v.string()),
133
- auth: v.optional(AuthSchema),
134
- meta: v.optional(MetaSchema),
135
131
  credit: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)))
136
132
  });
137
133
  /* sink push chunk (actual data transfer) */
@@ -188,11 +184,10 @@ const SourceFetchRequestSchema = v.strictObject({
188
184
  });
189
185
  /* source fetch response (ack/nak) */
190
186
  export class SourceFetchResponse extends Base {
191
- constructor(id, name, error, sender, receiver, auth, meta) {
187
+ constructor(id, name, error, sender, receiver, meta) {
192
188
  super("source-fetch-response", id, sender, receiver);
193
189
  this.name = name;
194
190
  this.error = error;
195
- this.auth = auth;
196
191
  this.meta = meta;
197
192
  }
198
193
  }
@@ -201,7 +196,6 @@ const SourceFetchResponseSchema = v.strictObject({
201
196
  type: v.literal("source-fetch-response"),
202
197
  name: v.string(),
203
198
  error: v.optional(v.string()),
204
- auth: v.optional(AuthSchema),
205
199
  meta: v.optional(MetaSchema)
206
200
  });
207
201
  /* source fetch chunk (actual data transfer) */
@@ -251,8 +245,8 @@ class Msg {
251
245
  makeSinkPushRequest(id, name, params, sender, receiver, auth, meta) {
252
246
  return new SinkPushRequest(id, name, params, sender, receiver, auth, meta);
253
247
  }
254
- makeSinkPushResponse(id, name, error, sender, receiver, auth, meta, credit) {
255
- return new SinkPushResponse(id, name, error, sender, receiver, auth, meta, credit);
248
+ makeSinkPushResponse(id, name, error, sender, receiver, credit) {
249
+ return new SinkPushResponse(id, name, error, sender, receiver, credit);
256
250
  }
257
251
  makeSinkPushChunk(id, name, chunk, error, final, sender, receiver) {
258
252
  return new SinkPushChunk(id, name, chunk, error, final, sender, receiver);
@@ -263,8 +257,8 @@ class Msg {
263
257
  makeSourceFetchRequest(id, name, params, sender, receiver, auth, meta, credit) {
264
258
  return new SourceFetchRequest(id, name, params, sender, receiver, auth, meta, credit);
265
259
  }
266
- makeSourceFetchResponse(id, name, error, sender, receiver, auth, meta) {
267
- return new SourceFetchResponse(id, name, error, sender, receiver, auth, meta);
260
+ makeSourceFetchResponse(id, name, error, sender, receiver, meta) {
261
+ return new SourceFetchResponse(id, name, error, sender, receiver, meta);
268
262
  }
269
263
  makeSourceFetchChunk(id, name, chunk, error, final, sender, receiver) {
270
264
  return new SourceFetchChunk(id, name, chunk, error, final, sender, receiver);
@@ -280,9 +274,9 @@ class Msg {
280
274
  /* sanity check version */
281
275
  if (typeof obj.version !== "string")
282
276
  throw new Error("invalid object: missing or invalid \"version\" field");
283
- const m = obj.version.match(/^MQTT\+\/(\d+\.\d+)$/);
284
- const V = m !== null ? versionToNum(m[1]) : 0.0;
285
- if (V !== version)
277
+ const match = obj.version.match(/^MQTT\+\/(\d+\.\d+)$/);
278
+ const versionNum = match !== null ? versionToNum(match[1]) : 0;
279
+ if (versionNum !== version)
286
280
  throw new Error(`protocol version mismatch (expected version ${VERSION}, got version ${obj.version})`);
287
281
  /* helper function for Valibot-based validation */
288
282
  const parseObject = (obj, name, schema) => {
@@ -314,7 +308,7 @@ class Msg {
314
308
  }
315
309
  else if (obj.type === "sink-push-response") {
316
310
  const out = parseObject(obj, "SinkPushResponse", SinkPushResponseSchema);
317
- return this.makeSinkPushResponse(out.id, out.name, out.error, out.sender, out.receiver, out.auth, out.meta, out.credit);
311
+ return this.makeSinkPushResponse(out.id, out.name, out.error, out.sender, out.receiver, out.credit);
318
312
  }
319
313
  else if (obj.type === "sink-push-chunk") {
320
314
  const out = parseObject(obj, "SinkPushChunk", SinkPushChunkSchema);
@@ -330,7 +324,7 @@ class Msg {
330
324
  }
331
325
  else if (obj.type === "source-fetch-response") {
332
326
  const out = parseObject(obj, "SourceFetchResponse", SourceFetchResponseSchema);
333
- return this.makeSourceFetchResponse(out.id, out.name, out.error, out.sender, out.receiver, out.auth, out.meta);
327
+ return this.makeSourceFetchResponse(out.id, out.name, out.error, out.sender, out.receiver, out.meta);
334
328
  }
335
329
  else if (obj.type === "source-fetch-chunk") {
336
330
  const out = parseObject(obj, "SourceFetchChunk", SourceFetchChunkSchema);
@@ -10,6 +10,7 @@ export interface APIOptions {
10
10
  id: string;
11
11
  codec: "cbor" | "json";
12
12
  timeout: number;
13
+ share: string;
13
14
  chunkSize: number;
14
15
  chunkCredit: number;
15
16
  topicMake: TopicMake;
@@ -32,6 +32,7 @@ export class OptionsTrait {
32
32
  id: nanoid(),
33
33
  codec: "cbor",
34
34
  timeout: 10 * 1000,
35
+ share: "",
35
36
  chunkSize: 16 * 1024,
36
37
  chunkCredit: 4,
37
38
  topicMake: (name, operation, peerId) => {
@@ -32,14 +32,14 @@ export class ServiceTrait extends EventTrait {
32
32
  let name;
33
33
  let callback;
34
34
  let options = {};
35
- let share = "default";
35
+ let share = this.options.share;
36
36
  let auth;
37
37
  if (typeof nameOrConfig === "object" && nameOrConfig !== null) {
38
38
  /* object-based API */
39
39
  name = nameOrConfig.name;
40
40
  callback = nameOrConfig.callback;
41
41
  options = nameOrConfig.options ?? {};
42
- share = nameOrConfig.share ?? "default";
42
+ share = nameOrConfig.share ?? this.options.share;
43
43
  auth = nameOrConfig.auth;
44
44
  }
45
45
  else {
@@ -53,7 +53,7 @@ export class ServiceTrait extends EventTrait {
53
53
  if (this.onRequest.has(`service-call-request:${name}`))
54
54
  throw new Error(`service: service "${name}" already registered`);
55
55
  /* generate the corresponding MQTT topics for broadcast and direct use */
56
- const topicS = `$share/${share}/${name}`;
56
+ const topicS = share !== "" ? `$share/${share}/${name}` : name;
57
57
  const topicB = this.options.topicMake(topicS, "service-call-request");
58
58
  const topicD = this.options.topicMake(name, "service-call-request", this.options.id);
59
59
  /* remember the registration */
@@ -72,8 +72,8 @@ export class ServiceTrait extends EventTrait {
72
72
  info.meta = request.meta;
73
73
  /* asynchronously execute handler and send response */
74
74
  Promise.resolve().then(async () => {
75
- if (topicName !== name)
76
- throw new Error(`service name mismatch (topic: "${topicName}", payload: "${name}")`);
75
+ if (topicName !== request.name)
76
+ throw new Error(`service name mismatch (topic: "${topicName}", payload: "${request.name}")`);
77
77
  if (auth)
78
78
  info.authenticated = await this.authenticated(senderId, request.auth, auth);
79
79
  if (info.authenticated !== undefined && !info.authenticated)
@@ -134,18 +134,13 @@ export class ServiceTrait extends EventTrait {
134
134
  await run(`subscribe to MQTT topic "${responseTopic}"`, spool, () => this.subscriptions.subscribe(responseTopic, { qos: options.qos ?? 2 }));
135
135
  spool.roll(() => this.subscriptions.unsubscribe(responseTopic));
136
136
  /* create promise for MQTT response handling */
137
+ const timerId = `service-call:${requestId}`;
137
138
  const promise = new Promise((resolve, reject) => {
138
- let timer = setTimeout(async () => {
139
- timer = null;
139
+ this.timerRefresh(timerId, async () => {
140
140
  await spool.unroll();
141
141
  reject(new Error("communication timeout"));
142
- }, this.options.timeout);
143
- spool.roll(() => {
144
- if (timer !== null) {
145
- clearTimeout(timer);
146
- timer = null;
147
- }
148
142
  });
143
+ spool.roll(() => { this.timerClear(timerId); });
149
144
  this.onResponse.set(`service-call-response:${requestId}`, async (response) => {
150
145
  await spool.unroll();
151
146
  if (response.error !== undefined)