mqtt-plus 1.4.4 → 1.4.6
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/CHANGELOG.md +19 -0
- package/README.md +101 -62
- package/dst-stage1/mqtt-plus-base.d.ts +8 -6
- package/dst-stage1/mqtt-plus-base.js +18 -6
- package/dst-stage1/mqtt-plus-event.js +7 -15
- package/dst-stage1/mqtt-plus-service.js +7 -15
- package/dst-stage1/mqtt-plus-sink.d.ts +1 -0
- package/dst-stage1/mqtt-plus-sink.js +23 -23
- package/dst-stage1/mqtt-plus-source.d.ts +1 -0
- package/dst-stage1/mqtt-plus-source.js +18 -19
- package/dst-stage1/mqtt-plus-subscription.d.ts +1 -0
- package/dst-stage1/mqtt-plus-subscription.js +11 -2
- package/dst-stage1/mqtt-plus-trace.d.ts +2 -2
- package/dst-stage2/mqtt-plus.cjs.js +81 -79
- package/dst-stage2/mqtt-plus.esm.js +81 -79
- package/dst-stage2/mqtt-plus.umd.js +12 -12
- package/etc/knip.jsonc +2 -1
- package/etc/stx.conf +11 -0
- package/package.json +2 -1
- package/src/mqtt-plus-base.ts +28 -14
- package/src/mqtt-plus-event.ts +8 -17
- package/src/mqtt-plus-meta.ts +1 -1
- package/src/mqtt-plus-service.ts +8 -16
- package/src/mqtt-plus-sink.ts +25 -25
- package/src/mqtt-plus-source.ts +20 -21
- package/src/mqtt-plus-subscription.ts +15 -6
- package/src/mqtt-plus-trace.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
ChangeLog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
1.4.6 (2026-02-22)
|
|
6
|
+
------------------
|
|
7
|
+
|
|
8
|
+
- IMPROVEMENT: improve rendering of about information and add chalk for sample code
|
|
9
|
+
- DOCUMENTATION: update sample code
|
|
10
|
+
- DOCUMENTATION: adjust about description
|
|
11
|
+
|
|
12
|
+
1.4.5 (2026-02-21)
|
|
13
|
+
------------------
|
|
14
|
+
|
|
15
|
+
- IMPROVEMENT: add a "npm start publish" target for convenient publishing
|
|
16
|
+
- IMPROVEMENT: allow QoS to be overridden and change default to level 2
|
|
17
|
+
- BUGFIX: properly destroy resources on cleanup
|
|
18
|
+
- CLEANUP: factor out registration code into base trait
|
|
19
|
+
- CLEANUP: use ensureError utility function for consistent error handling
|
|
20
|
+
- CLEANUP: rename and protect internal symbols and reduce unnecessary typing
|
|
21
|
+
- CLEANUP: avoid a race condition in topic unsubscription handling
|
|
22
|
+
- CLEANUP: improve about information
|
|
23
|
+
|
|
5
24
|
1.4.4 (2026-02-21)
|
|
6
25
|
------------------
|
|
7
26
|
|
package/README.md
CHANGED
|
@@ -16,12 +16,25 @@ About
|
|
|
16
16
|
-----
|
|
17
17
|
|
|
18
18
|
**MQTT+** is a companion add-on API for the TypeScript/JavaScript
|
|
19
|
-
API [MQTT.js](https://www.npmjs.com/package/mqtt),
|
|
20
|
-
|
|
21
|
-
[
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
API [MQTT.js](https://www.npmjs.com/package/mqtt), designed to
|
|
20
|
+
extend [MQTT](http://mqtt.org/) with higher-level
|
|
21
|
+
[communication patterns](doc/mqtt-plus-comm.md) while preserving full type-safety.
|
|
22
|
+
It provides four core communication patterns: fire-and-forget *Event
|
|
23
|
+
Emission*, RPC-style *Service Call*, stream-based *Sink Push*, and
|
|
24
|
+
stream-based *Source Fetch*.
|
|
25
|
+
These patterns enable structured,
|
|
26
|
+
bi-directional client/server and server/server communication
|
|
27
|
+
on top of [MQTT](http://mqtt.org/)’s inherently uni-directional publish/subscribe model.
|
|
28
|
+
Internally, the communication is based on the exchange of typed [CBOR](https://www.rfc-editor.org/rfc/rfc8949.html)
|
|
29
|
+
or [JSON](https://ecma-international.org/publications-and-standards/standards/ecma-404/) messages.
|
|
30
|
+
|
|
31
|
+
The result is a more expressive and maintainable messaging layer
|
|
32
|
+
without sacrificing [MQTT](http://mqtt.org/)’s excellent robustness and
|
|
33
|
+
scalability.
|
|
34
|
+
**MQTT+** is particularly well suited for systems built around a
|
|
35
|
+
[*Hub & Spoke*](https://en.wikipedia.org/wiki/Spoke%E2%80%93hub_distribution_paradigm)
|
|
36
|
+
communication architecture, where typed API contracts and controlled interaction flows are
|
|
37
|
+
critical for reliability and long-term maintainability.
|
|
25
38
|
|
|
26
39
|
Installation
|
|
27
40
|
------------
|
|
@@ -33,16 +46,25 @@ $ npm install mqtt mqtt-plus
|
|
|
33
46
|
Usage
|
|
34
47
|
-----
|
|
35
48
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
The following is a simple but self-contained example usage of
|
|
50
|
+
**MQTT+** based on a common API, a server part, a client part,
|
|
51
|
+
and a MQTT infrastructure. It can be found in the file
|
|
52
|
+
[sample.ts](sample/sample.ts) and can be executed from the **MQTT+**
|
|
53
|
+
source tree via `npm start sample` (assuming the prerequisite *Docker* is
|
|
54
|
+
available for the underlying *Mosquitto* broker based infrastructure):
|
|
41
55
|
|
|
42
56
|
```ts
|
|
57
|
+
import { Readable } from "node:stream"
|
|
58
|
+
import chalk from "chalk"
|
|
59
|
+
import Mosquitto from "mosquitto"
|
|
60
|
+
import MQTT from "mqtt"
|
|
61
|
+
import MQTTp from "mqtt-plus"
|
|
43
62
|
import type { Event, Service, Source, Sink } from "mqtt-plus"
|
|
63
|
+
```
|
|
44
64
|
|
|
45
|
-
|
|
65
|
+
```ts
|
|
66
|
+
/* ==== SAMPLE COMMON API ==== */
|
|
67
|
+
type API = {
|
|
46
68
|
"example/sample": Event<(a1: string, a2: number) => void>
|
|
47
69
|
"example/hello": Service<(a1: string, a2: number) => string>
|
|
48
70
|
"example/download": Source<(filename: string) => void>
|
|
@@ -50,67 +72,84 @@ export type API = {
|
|
|
50
72
|
}
|
|
51
73
|
```
|
|
52
74
|
|
|
53
|
-
The marker types ensure that `event()` and `emit()` only accept
|
|
54
|
-
`Event<T>` endpoints, `service()` and `call()` only accept
|
|
55
|
-
`Service<T>` endpoints, `source()` and `fetch()` only
|
|
56
|
-
accept `Source<T>` endpoints, and `sink()` and `push()` only
|
|
57
|
-
accept `Sink<T>` endpoints.
|
|
58
|
-
|
|
59
|
-
### Server:
|
|
60
|
-
|
|
61
75
|
```ts
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const mqtt = MQTT.connect("wss://127.0.0.1:8883", { [...] })
|
|
67
|
-
const mqttp = new MQTTp<API>(mqtt)
|
|
68
|
-
|
|
69
|
-
mqtt.on("connect", async () => {
|
|
70
|
-
await mqttp.event("example/sample", (a1, a2, info) => {
|
|
71
|
-
console.log("example/sample: SERVER:", a1, a2, info.sender)
|
|
76
|
+
/* ==== SAMPLE SERVER ==== */
|
|
77
|
+
const Server = async (api: MQTTp<API>, log: (msg: string, ...args: any[]) => void) => {
|
|
78
|
+
await api.event("example/sample", (a1, a2) => {
|
|
79
|
+
log("example/sample: SERVER:", a1, a2)
|
|
72
80
|
})
|
|
73
|
-
await
|
|
74
|
-
|
|
81
|
+
await api.service("example/hello", (a1, a2) => {
|
|
82
|
+
log("example/hello: SERVER:", a1, a2)
|
|
75
83
|
return `${a1}:${a2}`
|
|
76
84
|
})
|
|
77
|
-
await
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
await api.source("example/download", async (filename, info) => {
|
|
86
|
+
log("example/download: SERVER:", filename)
|
|
87
|
+
const input = new Readable()
|
|
88
|
+
input.push(api.str2buf(`the ${filename} content`))
|
|
89
|
+
input.push(null)
|
|
90
|
+
info.stream = readable
|
|
80
91
|
})
|
|
81
|
-
await
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
92
|
+
await api.sink("example/upload", async (filename, info) => {
|
|
93
|
+
log("example/upload: SERVER:", filename)
|
|
94
|
+
const chunks: Uint8Array[] = []
|
|
95
|
+
info.stream!.on("data", (chunk: Uint8Array) => { chunks.push(chunk) })
|
|
96
|
+
await new Promise<void>((resolve) => { info.stream!.once("end", resolve) })
|
|
97
|
+
const total = chunks.reduce((n, c) => n + c.length, 0)
|
|
98
|
+
log("received", total, "bytes")
|
|
85
99
|
})
|
|
86
|
-
}
|
|
100
|
+
}
|
|
87
101
|
```
|
|
88
102
|
|
|
89
|
-
### Client:
|
|
90
|
-
|
|
91
103
|
```ts
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
104
|
+
/* ==== SAMPLE CLIENT ==== */
|
|
105
|
+
const Client = async (api: MQTTp<API>, log: (msg: string, ...args: any[]) => void) => {
|
|
106
|
+
api.emit("example/sample", "world", 42)
|
|
107
|
+
|
|
108
|
+
const callOutput = await api.call("example/hello", "world", 42)
|
|
109
|
+
log("example/hello: CLIENT:", callOutput)
|
|
110
|
+
|
|
111
|
+
const output = await api.fetch("example/download", "foo")
|
|
112
|
+
const chunks: Uint8Array[] = []
|
|
113
|
+
output.stream.on("data", (chunk: Uint8Array) => { chunks.push(chunk) })
|
|
114
|
+
await new Promise<void>((resolve) => { output.stream.on("end", resolve) })
|
|
115
|
+
const data = api.buf2str(Buffer.concat(chunks))
|
|
116
|
+
log("example/download: CLIENT:", data)
|
|
117
|
+
|
|
118
|
+
const input = new Readable()
|
|
119
|
+
input.push(api.str2buf("uploaded content"))
|
|
120
|
+
input.push(null)
|
|
121
|
+
await api.push("example/upload", input, "myfile.txt")
|
|
122
|
+
}
|
|
123
|
+
```
|
|
98
124
|
|
|
125
|
+
```ts
|
|
126
|
+
/* ==== SAMPLE INFRASTRUCTURE ==== */
|
|
127
|
+
process.on("uncaughtException", (err: Error): void => {
|
|
128
|
+
console.error(chalk.red(`ERROR: ${err.stack ?? err.message}`))
|
|
129
|
+
console.log(chalk.yellow(mosquitto.logs()))
|
|
130
|
+
process.exit(1)
|
|
131
|
+
})
|
|
132
|
+
const mosquitto = new Mosquitto({
|
|
133
|
+
listen: [ { protocol: "mqtt", address: "127.0.0.1", port: 1883 } ]
|
|
134
|
+
})
|
|
135
|
+
await mosquitto.start()
|
|
136
|
+
const mqtt = MQTT.connect("mqtt://127.0.0.1:1883", {
|
|
137
|
+
username: "example", password: "example"
|
|
138
|
+
})
|
|
139
|
+
const api = new MQTTp<API>(mqtt)
|
|
140
|
+
api.on("log", async (entry) => {
|
|
141
|
+
await entry.resolve()
|
|
142
|
+
console.log(chalk.grey(`api: ${entry}`))
|
|
143
|
+
})
|
|
144
|
+
const log = (msg: string, ...args: any[]) => {
|
|
145
|
+
console.log(chalk.bold.blue("app:"), chalk.blue(msg), chalk.red(JSON.stringify(args)))
|
|
146
|
+
}
|
|
99
147
|
mqtt.on("connect", async () => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const fetchOutput = await mqttp.fetch("example/download", "foo")
|
|
106
|
-
const data = mqttp.buf2str(await fetchOutput.buffer)
|
|
107
|
-
console.log("example/download: CLIENT:", data)
|
|
108
|
-
|
|
109
|
-
const pushInput = mqttp.str2buf("uploaded content")
|
|
110
|
-
await mqttp.push("example/upload", pushInput, "myfile.txt")
|
|
111
|
-
|
|
112
|
-
mqttp.destroy()
|
|
113
|
-
mqtt.end()
|
|
148
|
+
await Server(api, log)
|
|
149
|
+
await Client(api, log)
|
|
150
|
+
await api.destroy()
|
|
151
|
+
await mqtt.endAsync()
|
|
152
|
+
await mosquitto.stop()
|
|
114
153
|
})
|
|
115
154
|
```
|
|
116
155
|
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { MqttClient, type IClientSubscribeOptions, type IClientPublishOptions } from "mqtt";
|
|
2
|
-
import type { APISchema } from "./mqtt-plus-api";
|
|
2
|
+
import type { APISchema, Registration } from "./mqtt-plus-api";
|
|
3
3
|
import type { APIOptions } from "./mqtt-plus-options";
|
|
4
4
|
import { TraceTrait } from "./mqtt-plus-trace";
|
|
5
|
+
import type { Spool } from "./mqtt-plus-error";
|
|
5
6
|
export declare class BaseTrait<T extends APISchema = APISchema> extends TraceTrait<T> {
|
|
6
|
-
|
|
7
|
-
private
|
|
7
|
+
private mqtt;
|
|
8
|
+
private messageHandler;
|
|
8
9
|
protected onRequest: Map<string, (message: any, topicName: string) => void>;
|
|
9
10
|
protected onResponse: Map<string, (message: any, topicName: string) => void>;
|
|
10
11
|
constructor(mqtt: MqttClient | null, options?: Partial<APIOptions>);
|
|
11
12
|
destroy(): Promise<void>;
|
|
12
|
-
protected
|
|
13
|
-
protected
|
|
14
|
-
protected
|
|
13
|
+
protected makeRegistration(spool: Spool, kind: string, name: string, key: string): Registration;
|
|
14
|
+
protected subscribeTopic(topic: string, options?: Partial<IClientSubscribeOptions>): Promise<void>;
|
|
15
|
+
protected unsubscribeTopic(topic: string): Promise<void>;
|
|
16
|
+
protected publishToTopic(topic: string, message: string | Uint8Array, options?: IClientPublishOptions): Promise<void>;
|
|
15
17
|
private _onMessage;
|
|
16
18
|
}
|
|
@@ -56,7 +56,7 @@ export class BaseTrait extends TraceTrait {
|
|
|
56
56
|
this.mqtt = mqtt;
|
|
57
57
|
/* hook into the MQTT message processing */
|
|
58
58
|
this.log("info", "hooking into MQTT client");
|
|
59
|
-
this.
|
|
59
|
+
this.messageHandler = (topic, message, packet) => {
|
|
60
60
|
/* convert message to codec-specific input format
|
|
61
61
|
(NOTICE: MQTT.js uses Buffer in its handler signature only,
|
|
62
62
|
but internally supports string or Buffer, while we are
|
|
@@ -72,15 +72,27 @@ export class BaseTrait extends TraceTrait {
|
|
|
72
72
|
throw new Error("invalid codec configured");
|
|
73
73
|
this._onMessage(topic, input, packet);
|
|
74
74
|
};
|
|
75
|
-
this.mqtt.on("message", this.
|
|
75
|
+
this.mqtt.on("message", this.messageHandler);
|
|
76
76
|
}
|
|
77
77
|
/* destroy API class */
|
|
78
78
|
async destroy() {
|
|
79
79
|
this.log("info", "un-hooking from MQTT client");
|
|
80
|
-
this.mqtt.off("message", this.
|
|
80
|
+
this.mqtt.off("message", this.messageHandler);
|
|
81
|
+
}
|
|
82
|
+
/* create a registration for subsequent destruction */
|
|
83
|
+
makeRegistration(spool, kind, name, key) {
|
|
84
|
+
return {
|
|
85
|
+
destroy: async () => {
|
|
86
|
+
if (!this.onRequest.has(key))
|
|
87
|
+
throw new Error(`destroy: ${kind} "${name}" not registered`);
|
|
88
|
+
await spool.unroll(false)?.catch((err) => {
|
|
89
|
+
this.error(err, `destroy: failed to cleanup: ${err.message}`);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
81
93
|
}
|
|
82
94
|
/* subscribe to an MQTT topic (Promise-based) */
|
|
83
|
-
async
|
|
95
|
+
async subscribeTopic(topic, options = {}) {
|
|
84
96
|
this.log("info", `subscribing to MQTT topic "${topic}"`);
|
|
85
97
|
return new Promise((resolve, reject) => {
|
|
86
98
|
this.mqtt.subscribe(topic, { qos: 2, ...options }, (err, _granted) => {
|
|
@@ -94,7 +106,7 @@ export class BaseTrait extends TraceTrait {
|
|
|
94
106
|
});
|
|
95
107
|
}
|
|
96
108
|
/* unsubscribe from an MQTT topic (Promise-based) */
|
|
97
|
-
async
|
|
109
|
+
async unsubscribeTopic(topic) {
|
|
98
110
|
this.log("info", `unsubscribing from MQTT topic "${topic}"`);
|
|
99
111
|
return new Promise((resolve, reject) => {
|
|
100
112
|
this.mqtt.unsubscribe(topic, (err, _packet) => {
|
|
@@ -108,7 +120,7 @@ export class BaseTrait extends TraceTrait {
|
|
|
108
120
|
});
|
|
109
121
|
}
|
|
110
122
|
/* publish to an MQTT topic (Promise-based) */
|
|
111
|
-
async
|
|
123
|
+
async publishToTopic(topic, message, options = {}) {
|
|
112
124
|
/* determine buffer */
|
|
113
125
|
if (typeof message === "string")
|
|
114
126
|
this.log("info", `publishing to MQTT topic "${topic}" (type: string, length: ${message.length} chars)`);
|
|
@@ -82,20 +82,12 @@ export class EventTrait extends AuthTrait {
|
|
|
82
82
|
});
|
|
83
83
|
spool.roll(() => { this.onRequest.delete(`event-emission:${name}`); });
|
|
84
84
|
/* subscribe to MQTT topics */
|
|
85
|
-
await run(`subscribe to MQTT topic "${topicB}"`, spool, () => this.
|
|
86
|
-
spool.roll(() => this.
|
|
87
|
-
await run(`subscribe to MQTT topic "${topicD}"`, spool, () => this.
|
|
88
|
-
spool.roll(() => this.
|
|
85
|
+
await run(`subscribe to MQTT topic "${topicB}"`, spool, () => this.subscribeTopic(topicB, { qos: 2, ...options }));
|
|
86
|
+
spool.roll(() => this.unsubscribeTopic(topicB).catch(() => { }));
|
|
87
|
+
await run(`subscribe to MQTT topic "${topicD}"`, spool, () => this.subscribeTopic(topicD, { qos: 2, ...options }));
|
|
88
|
+
spool.roll(() => this.unsubscribeTopic(topicD).catch(() => { }));
|
|
89
89
|
/* provide a registration for subsequent destruction */
|
|
90
|
-
return {
|
|
91
|
-
destroy: async () => {
|
|
92
|
-
if (!this.onRequest.has(`event-emission:${name}`))
|
|
93
|
-
throw new Error(`destroy: event "${name}" not registered`);
|
|
94
|
-
await spool.unroll(false)?.catch((err) => {
|
|
95
|
-
this.error(err, `destroy: failed to cleanup: ${err.message}`);
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
};
|
|
90
|
+
return this.makeRegistration(spool, "event", name, `event-emission:${name}`);
|
|
99
91
|
}
|
|
100
92
|
emit(eventOrConfig, ...args) {
|
|
101
93
|
/* determine actual parameters */
|
|
@@ -131,10 +123,10 @@ export class EventTrait extends AuthTrait {
|
|
|
131
123
|
/* produce result */
|
|
132
124
|
if (dry)
|
|
133
125
|
/* return publish information */
|
|
134
|
-
return { topic, payload: message, options: { qos:
|
|
126
|
+
return { topic, payload: message, options: { qos: 2, ...options } };
|
|
135
127
|
else
|
|
136
128
|
/* publish message to MQTT topic */
|
|
137
|
-
this.
|
|
129
|
+
this.publishToTopic(topic, message, { qos: 2, ...options }).catch((err) => {
|
|
138
130
|
this.error(err, `emitting event "${event}" failed`);
|
|
139
131
|
});
|
|
140
132
|
}
|
|
@@ -91,27 +91,19 @@ export class ServiceTrait extends EventTrait {
|
|
|
91
91
|
/* send response message */
|
|
92
92
|
const encoded = this.codec.encode(rpcResponse);
|
|
93
93
|
const topic = this.options.topicMake(name, "service-call-response", senderId);
|
|
94
|
-
return this.
|
|
94
|
+
return this.publishToTopic(topic, encoded, { qos: options.qos ?? 2 });
|
|
95
95
|
}).catch((err) => {
|
|
96
96
|
this.error(err, `handler for service "${name}" failed`);
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
spool.roll(() => { this.onRequest.delete(`service-call-request:${name}`); });
|
|
100
100
|
/* subscribe to MQTT topics */
|
|
101
|
-
await run(`subscribe to MQTT topic "${topicB}"`, spool, () => this.
|
|
102
|
-
spool.roll(() => this.
|
|
103
|
-
await run(`subscribe to MQTT topic "${topicD}"`, spool, () => this.
|
|
104
|
-
spool.roll(() => this.
|
|
101
|
+
await run(`subscribe to MQTT topic "${topicB}"`, spool, () => this.subscribeTopic(topicB, { qos: 2, ...options }));
|
|
102
|
+
spool.roll(() => this.unsubscribeTopic(topicB).catch(() => { }));
|
|
103
|
+
await run(`subscribe to MQTT topic "${topicD}"`, spool, () => this.subscribeTopic(topicD, { qos: 2, ...options }));
|
|
104
|
+
spool.roll(() => this.unsubscribeTopic(topicD).catch(() => { }));
|
|
105
105
|
/* provide a registration for subsequent destruction */
|
|
106
|
-
return {
|
|
107
|
-
destroy: async () => {
|
|
108
|
-
if (!this.onRequest.has(`service-call-request:${name}`))
|
|
109
|
-
throw new Error(`destroy: service "${name}" not registered`);
|
|
110
|
-
await spool.unroll(false)?.catch((err) => {
|
|
111
|
-
this.error(err, `destroy: failed to cleanup: ${err.message}`);
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
};
|
|
106
|
+
return this.makeRegistration(spool, "service", name, `service-call-request:${name}`);
|
|
115
107
|
}
|
|
116
108
|
async call(nameOrConfig, ...args) {
|
|
117
109
|
/* determine actual parameters */
|
|
@@ -171,7 +163,7 @@ export class ServiceTrait extends EventTrait {
|
|
|
171
163
|
/* generate corresponding MQTT topic */
|
|
172
164
|
const topic = this.options.topicMake(name, "service-call-request", receiver);
|
|
173
165
|
/* publish message to MQTT topic */
|
|
174
|
-
await run(`publish service request as MQTT message to topic "${topic}"`, spool, () => this.
|
|
166
|
+
await run(`publish service request as MQTT message to topic "${topic}"`, spool, () => this.publishToTopic(topic, message, { qos: 2, ...options }));
|
|
175
167
|
return promise;
|
|
176
168
|
}
|
|
177
169
|
}
|
|
@@ -7,6 +7,7 @@ import type { AuthOption } from "./mqtt-plus-auth";
|
|
|
7
7
|
export declare class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
|
|
8
8
|
private pushStreams;
|
|
9
9
|
private pushSpools;
|
|
10
|
+
destroy(): Promise<void>;
|
|
10
11
|
sink<K extends SinkKeys<T> & string>(name: K, callback: WithInfo<T[K], InfoSink>): Promise<Registration>;
|
|
11
12
|
sink<K extends SinkKeys<T> & string>(config: {
|
|
12
13
|
name: K;
|
|
@@ -26,7 +26,7 @@ import { Readable } from "node:stream";
|
|
|
26
26
|
import { nanoid } from "nanoid";
|
|
27
27
|
/* internal requirements */
|
|
28
28
|
import { CreditGate, streamToBuffer, sendBufferAsChunks, sendStreamAsChunks, makeMutuallyExclusiveFields } from "./mqtt-plus-util";
|
|
29
|
-
import { run, Spool } from "./mqtt-plus-error";
|
|
29
|
+
import { run, Spool, ensureError } from "./mqtt-plus-error";
|
|
30
30
|
import { SourceTrait } from "./mqtt-plus-source";
|
|
31
31
|
/* Sink Push Trait */
|
|
32
32
|
export class SinkTrait extends SourceTrait {
|
|
@@ -36,6 +36,14 @@ export class SinkTrait extends SourceTrait {
|
|
|
36
36
|
this.pushStreams = new Map();
|
|
37
37
|
this.pushSpools = new Map();
|
|
38
38
|
}
|
|
39
|
+
/* destroy sink trait */
|
|
40
|
+
async destroy() {
|
|
41
|
+
for (const stream of this.pushStreams.values())
|
|
42
|
+
stream.destroy();
|
|
43
|
+
this.pushStreams.clear();
|
|
44
|
+
this.pushSpools.clear();
|
|
45
|
+
await super.destroy();
|
|
46
|
+
}
|
|
39
47
|
async sink(nameOrConfig, ...args) {
|
|
40
48
|
/* determine actual parameters */
|
|
41
49
|
let name;
|
|
@@ -90,7 +98,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
90
98
|
const credit = chunkCredit > 0 ? chunkCredit : undefined;
|
|
91
99
|
const response = this.msg.makeSinkPushResponse(requestId, name, error, this.options.id, sender, authToken, metaStore, credit);
|
|
92
100
|
const message = this.codec.encode(response);
|
|
93
|
-
await this.
|
|
101
|
+
await this.publishToTopic(responseTopic, message, { qos: options.qos ?? 2 });
|
|
94
102
|
};
|
|
95
103
|
/* create a resource spool for stream cleanup */
|
|
96
104
|
const reqSpool = new Spool();
|
|
@@ -130,7 +138,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
130
138
|
const creditMsg = this.msg.makeSinkPushCredit(requestId, name, creditToGrant, this.options.id, sender);
|
|
131
139
|
const encoded = this.codec.encode(creditMsg);
|
|
132
140
|
const creditTopic = this.options.topicMake(name, "sink-push-credit", sender);
|
|
133
|
-
this.
|
|
141
|
+
this.publishToTopic(creditTopic, encoded, { qos: options.qos ?? 2 }).catch((err) => {
|
|
134
142
|
this.error(err, `sending credit for push "${name}" failed`);
|
|
135
143
|
});
|
|
136
144
|
refreshPushTimeout();
|
|
@@ -189,22 +197,14 @@ export class SinkTrait extends SourceTrait {
|
|
|
189
197
|
});
|
|
190
198
|
spool.roll(() => { this.onRequest.delete(`sink-push-request:${name}`); });
|
|
191
199
|
/* subscribe to MQTT topics */
|
|
192
|
-
await run(`subscribe to MQTT topic "${topicReqB}"`, spool, () => this.
|
|
193
|
-
spool.roll(() => this.
|
|
194
|
-
await run(`subscribe to MQTT topic "${topicReqD}"`, spool, () => this.
|
|
195
|
-
spool.roll(() => this.
|
|
196
|
-
await run(`subscribe to MQTT topic "${topicChunkD}"`, spool, () => this.
|
|
197
|
-
spool.roll(() => this.
|
|
200
|
+
await run(`subscribe to MQTT topic "${topicReqB}"`, spool, () => this.subscribeTopic(topicReqB, { qos: 2, ...options }));
|
|
201
|
+
spool.roll(() => this.unsubscribeTopic(topicReqB).catch(() => { }));
|
|
202
|
+
await run(`subscribe to MQTT topic "${topicReqD}"`, spool, () => this.subscribeTopic(topicReqD, { qos: 2, ...options }));
|
|
203
|
+
spool.roll(() => this.unsubscribeTopic(topicReqD).catch(() => { }));
|
|
204
|
+
await run(`subscribe to MQTT topic "${topicChunkD}"`, spool, () => this.subscribeTopic(topicChunkD, { qos: 2, ...options }));
|
|
205
|
+
spool.roll(() => this.unsubscribeTopic(topicChunkD).catch(() => { }));
|
|
198
206
|
/* provide a registration for subsequent destruction */
|
|
199
|
-
return {
|
|
200
|
-
destroy: async () => {
|
|
201
|
-
if (!this.onRequest.has(`sink-push-request:${name}`))
|
|
202
|
-
throw new Error(`destroy: sink "${name}" not established`);
|
|
203
|
-
await spool.unroll(false)?.catch((err) => {
|
|
204
|
-
this.error(err, `destroy: failed to cleanup: ${err.message}`);
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
};
|
|
207
|
+
return this.makeRegistration(spool, "sink", name, `sink-push-request:${name}`);
|
|
208
208
|
}
|
|
209
209
|
async push(nameOrConfig, ...args) {
|
|
210
210
|
/* determine actual parameters */
|
|
@@ -290,7 +290,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
290
290
|
const request = this.msg.makeSinkPushRequest(requestId, name, params, this.options.id, receiver, auth, metaStore);
|
|
291
291
|
const message = this.codec.encode(request);
|
|
292
292
|
const requestTopic = this.options.topicMake(name, "sink-push-request", receiver);
|
|
293
|
-
run(`publish push request as MQTT message to topic "${requestTopic}"`, spool, () => this.
|
|
293
|
+
run(`publish push request as MQTT message to topic "${requestTopic}"`, spool, () => this.publishToTopic(requestTopic, message, { qos: 2, ...options })).catch((err) => {
|
|
294
294
|
reject(err);
|
|
295
295
|
});
|
|
296
296
|
});
|
|
@@ -305,7 +305,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
305
305
|
/* subscribe to credit topic if flow control is active */
|
|
306
306
|
if (creditGate) {
|
|
307
307
|
const creditTopic = this.options.topicMake(name, "sink-push-credit", this.options.id);
|
|
308
|
-
await run(`subscribe to MQTT topic "${creditTopic}"`, spool, () => this.subscriptions.subscribe(creditTopic, { qos: 2 }));
|
|
308
|
+
await run(`subscribe to MQTT topic "${creditTopic}"`, spool, () => this.subscriptions.subscribe(creditTopic, { qos: options.qos ?? 2 }));
|
|
309
309
|
spool.roll(() => this.subscriptions.unsubscribe(creditTopic));
|
|
310
310
|
const gate = creditGate;
|
|
311
311
|
spool.roll(() => { gate.abort(); });
|
|
@@ -322,7 +322,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
322
322
|
refreshTimeout();
|
|
323
323
|
const chunkMsg = this.msg.makeSinkPushChunk(requestId, name, chunk, error, final, this.options.id, receiver);
|
|
324
324
|
const message = this.codec.encode(chunkMsg);
|
|
325
|
-
await this.
|
|
325
|
+
await this.publishToTopic(chunkTopic, message, { qos: 2, ...options });
|
|
326
326
|
};
|
|
327
327
|
/* iterate over all chunks of the buffer */
|
|
328
328
|
if (data instanceof Readable)
|
|
@@ -333,11 +333,11 @@ export class SinkTrait extends SourceTrait {
|
|
|
333
333
|
await sendBufferAsChunks(data, this.options.chunkSize, sendChunk, creditGate, abortSignal);
|
|
334
334
|
}
|
|
335
335
|
catch (err) {
|
|
336
|
-
const error = err
|
|
336
|
+
const error = ensureError(err).message;
|
|
337
337
|
const chunkTopic = this.options.topicMake(name, "sink-push-chunk", receiver);
|
|
338
338
|
const chunkMsg = this.msg.makeSinkPushChunk(requestId, name, undefined, error, true, this.options.id, receiver);
|
|
339
339
|
const message = this.codec.encode(chunkMsg);
|
|
340
|
-
await this.
|
|
340
|
+
await this.publishToTopic(chunkTopic, message, { qos: 2, ...options }).catch(() => { });
|
|
341
341
|
throw err;
|
|
342
342
|
}
|
|
343
343
|
finally {
|
|
@@ -6,6 +6,7 @@ import { ServiceTrait } from "./mqtt-plus-service";
|
|
|
6
6
|
import type { AuthOption } from "./mqtt-plus-auth";
|
|
7
7
|
export declare class SourceTrait<T extends APISchema = APISchema> extends ServiceTrait<T> {
|
|
8
8
|
private sourceCreditGates;
|
|
9
|
+
destroy(): Promise<void>;
|
|
9
10
|
source<K extends SourceKeys<T> & string>(name: K, callback: WithInfo<T[K], InfoSource>): Promise<Registration>;
|
|
10
11
|
source<K extends SourceKeys<T> & string>(config: {
|
|
11
12
|
name: K;
|
|
@@ -35,6 +35,13 @@ export class SourceTrait extends ServiceTrait {
|
|
|
35
35
|
/* source state */
|
|
36
36
|
this.sourceCreditGates = new Map();
|
|
37
37
|
}
|
|
38
|
+
/* destroy source trait */
|
|
39
|
+
async destroy() {
|
|
40
|
+
for (const gate of this.sourceCreditGates.values())
|
|
41
|
+
gate.abort();
|
|
42
|
+
this.sourceCreditGates.clear();
|
|
43
|
+
await super.destroy();
|
|
44
|
+
}
|
|
38
45
|
async source(nameOrConfig, ...args) {
|
|
39
46
|
/* determine actual parameters */
|
|
40
47
|
let name;
|
|
@@ -88,7 +95,7 @@ export class SourceTrait extends ServiceTrait {
|
|
|
88
95
|
const metaStore = this.metaStore(info.meta);
|
|
89
96
|
const response = this.msg.makeSourceFetchResponse(requestId, name, error, this.options.id, sender, authToken, metaStore);
|
|
90
97
|
const message = this.codec.encode(response);
|
|
91
|
-
await this.
|
|
98
|
+
await this.publishToTopic(responseTopic, message, { qos: options.qos ?? 2 });
|
|
92
99
|
};
|
|
93
100
|
/* utility functions for timeout management */
|
|
94
101
|
const refreshSourceTimeout = () => this.timerRefresh(requestId, () => {
|
|
@@ -103,7 +110,7 @@ export class SourceTrait extends ServiceTrait {
|
|
|
103
110
|
refreshSourceTimeout();
|
|
104
111
|
const chunkMsg = this.msg.makeSourceFetchChunk(requestId, name, chunk, error, final, this.options.id, sender);
|
|
105
112
|
const message = this.codec.encode(chunkMsg);
|
|
106
|
-
await this.
|
|
113
|
+
await this.publishToTopic(chunkTopic, message, { qos: options.qos ?? 2 });
|
|
107
114
|
};
|
|
108
115
|
/* handle credit-based flow control (if credit provided in request) */
|
|
109
116
|
const initialCredit = request.credit;
|
|
@@ -162,22 +169,14 @@ export class SourceTrait extends ServiceTrait {
|
|
|
162
169
|
});
|
|
163
170
|
spool.roll(() => { this.onRequest.delete(`source-fetch-request:${name}`); });
|
|
164
171
|
/* subscribe to MQTT topics */
|
|
165
|
-
await run(`subscribe to MQTT topic "${topicReqB}"`, spool, () => this.
|
|
166
|
-
spool.roll(() => this.
|
|
167
|
-
await run(`subscribe to MQTT topic "${topicReqD}"`, spool, () => this.
|
|
168
|
-
spool.roll(() => this.
|
|
169
|
-
await run(`subscribe to MQTT topic "${topicCreditD}"`, spool, () => this.
|
|
170
|
-
spool.roll(() => this.
|
|
172
|
+
await run(`subscribe to MQTT topic "${topicReqB}"`, spool, () => this.subscribeTopic(topicReqB, { qos: 2, ...options }));
|
|
173
|
+
spool.roll(() => this.unsubscribeTopic(topicReqB).catch(() => { }));
|
|
174
|
+
await run(`subscribe to MQTT topic "${topicReqD}"`, spool, () => this.subscribeTopic(topicReqD, { qos: 2, ...options }));
|
|
175
|
+
spool.roll(() => this.unsubscribeTopic(topicReqD).catch(() => { }));
|
|
176
|
+
await run(`subscribe to MQTT topic "${topicCreditD}"`, spool, () => this.subscribeTopic(topicCreditD, { qos: 2, ...options }));
|
|
177
|
+
spool.roll(() => this.unsubscribeTopic(topicCreditD).catch(() => { }));
|
|
171
178
|
/* provide a registration for subsequent destruction */
|
|
172
|
-
return {
|
|
173
|
-
destroy: async () => {
|
|
174
|
-
if (!this.onRequest.has(`source-fetch-request:${name}`))
|
|
175
|
-
throw new Error(`destroy: source "${name}" not established`);
|
|
176
|
-
await spool.unroll(false)?.catch((err) => {
|
|
177
|
-
this.error(err, `destroy: failed to cleanup: ${err.message}`);
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
};
|
|
179
|
+
return this.makeRegistration(spool, "source", name, `source-fetch-request:${name}`);
|
|
181
180
|
}
|
|
182
181
|
async fetch(nameOrConfig, ...args) {
|
|
183
182
|
/* determine actual parameters */
|
|
@@ -230,7 +229,7 @@ export class SourceTrait extends ServiceTrait {
|
|
|
230
229
|
const creditMsg = this.msg.makeSourceFetchCredit(requestId, name, creditToGrant, this.options.id, targetId);
|
|
231
230
|
const encoded = this.codec.encode(creditMsg);
|
|
232
231
|
const creditTopic = this.options.topicMake(name, "source-fetch-credit", targetId);
|
|
233
|
-
this.
|
|
232
|
+
this.publishToTopic(creditTopic, encoded, { qos: options.qos ?? 2 }).catch((err) => {
|
|
234
233
|
this.error(err, `sending credit for fetch "${name}" failed`);
|
|
235
234
|
});
|
|
236
235
|
}
|
|
@@ -311,7 +310,7 @@ export class SourceTrait extends ServiceTrait {
|
|
|
311
310
|
/* generate corresponding MQTT topic */
|
|
312
311
|
const topic = this.options.topicMake(name, "source-fetch-request", receiver);
|
|
313
312
|
/* publish message to MQTT topic */
|
|
314
|
-
run(`publish fetch request as MQTT message to topic "${topic}"`, spool, () => this.
|
|
313
|
+
run(`publish fetch request as MQTT message to topic "${topic}"`, spool, () => this.publishToTopic(topic, message, { qos: 2, ...options })).catch((err) => {
|
|
315
314
|
stream.destroy(ensureError(err));
|
|
316
315
|
spool.unroll();
|
|
317
316
|
});
|
|
@@ -8,6 +8,7 @@ declare class RefCountedSubscription {
|
|
|
8
8
|
private counts;
|
|
9
9
|
private pending;
|
|
10
10
|
private lingers;
|
|
11
|
+
private unsubbing;
|
|
11
12
|
constructor(subscribeFn: (topic: string, options: IClientSubscribeOptions) => Promise<void>, unsubscribeFn: (topic: string) => Promise<void>, lingerMs?: number);
|
|
12
13
|
subscribe(topic: string, options?: IClientSubscribeOptions): Promise<void>;
|
|
13
14
|
unsubscribe(topic: string): Promise<void>;
|