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.
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +22 -0
- package/README.md +12 -8
- package/doc/mqtt-plus-api.md +17 -6
- package/doc/mqtt-plus-broker-setup.md +4 -4
- package/dst-stage1/mqtt-plus-base.d.ts +2 -2
- package/dst-stage1/mqtt-plus-base.js +5 -12
- package/dst-stage1/mqtt-plus-codec.js +8 -8
- package/dst-stage1/mqtt-plus-event.js +2 -2
- package/dst-stage1/mqtt-plus-info.d.ts +2 -2
- package/dst-stage1/mqtt-plus-msg.d.ts +4 -7
- package/dst-stage1/mqtt-plus-msg.js +11 -17
- package/dst-stage1/mqtt-plus-options.d.ts +1 -0
- package/dst-stage1/mqtt-plus-options.js +1 -0
- package/dst-stage1/mqtt-plus-service.js +8 -13
- package/dst-stage1/mqtt-plus-sink.js +30 -36
- package/dst-stage1/mqtt-plus-source.js +9 -18
- package/dst-stage1/mqtt-plus-subscription.js +6 -5
- package/dst-stage1/mqtt-plus-util.d.ts +3 -1
- package/dst-stage1/mqtt-plus-util.js +5 -1
- package/dst-stage2/mqtt-plus.cjs.js +82 -101
- package/dst-stage2/mqtt-plus.esm.js +82 -101
- package/dst-stage2/mqtt-plus.umd.js +12 -12
- package/etc/knip.jsonc +4 -3
- package/etc/stx.conf +4 -2
- package/package.json +2 -1
- package/src/mqtt-plus-base.ts +7 -13
- package/src/mqtt-plus-codec.ts +4 -4
- package/src/mqtt-plus-event.ts +2 -2
- package/src/mqtt-plus-info.ts +2 -2
- package/src/mqtt-plus-msg.ts +7 -16
- package/src/mqtt-plus-options.ts +2 -0
- package/src/mqtt-plus-service.ts +8 -13
- package/src/mqtt-plus-sink.ts +31 -37
- package/src/mqtt-plus-source.ts +9 -19
- package/src/mqtt-plus-subscription.ts +7 -5
- package/src/mqtt-plus-trace.ts +2 -0
- package/src/mqtt-plus-util.ts +13 -7
- package/tst/mqtt-plus-0-broker-aedes.ts +120 -0
- package/tst/{mqtt-plus-0-mosquitto.ts → mqtt-plus-0-broker-mosquitto.ts} +11 -8
- package/tst/mqtt-plus-0-broker.ts +52 -0
- package/tst/mqtt-plus-0-fixture.ts +20 -17
- 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-
|
|
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
|
-
|
|
10
|
-
<img src="https://nodei.co/npm/mqtt-plus.png?downloads=true&stars=true" alt=""/>
|
|
9
|
+
[](https://nodei.co/npm/mqtt-plus/)
|
|
11
10
|
|
|
12
11
|
[](https://github.com/rse)
|
|
13
12
|
[](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
|
|
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
|
|
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
|
|
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
|
|
96
|
-
await new Promise<void>((resolve) => { info.stream
|
|
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
|
package/doc/mqtt-plus-api.md
CHANGED
|
@@ -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>/`.
|
|
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>/`.
|
|
468
|
-
|
|
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>/`.
|
|
591
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
69
|
-
throw new Error("failed to encode CBOR format", { cause:
|
|
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 (
|
|
77
|
-
throw new Error("failed to encode JSON format", { cause:
|
|
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 (
|
|
93
|
-
throw new Error("failed to decode CBOR format", { cause:
|
|
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 (
|
|
103
|
-
throw new Error("failed to decode JSON format", { cause:
|
|
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
|
|
18
|
-
buffer
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
255
|
-
return new SinkPushResponse(id, name, error, sender, receiver,
|
|
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,
|
|
267
|
-
return new SourceFetchResponse(id, name, error, sender, receiver,
|
|
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
|
|
284
|
-
const
|
|
285
|
-
if (
|
|
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.
|
|
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.
|
|
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);
|
|
@@ -32,14 +32,14 @@ export class ServiceTrait extends EventTrait {
|
|
|
32
32
|
let name;
|
|
33
33
|
let callback;
|
|
34
34
|
let options = {};
|
|
35
|
-
let share =
|
|
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 ??
|
|
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
|
-
|
|
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)
|