mqtt-plus 1.4.10 → 1.4.11
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 +7 -3
- package/CHANGELOG.md +15 -0
- package/doc/mqtt-plus-api.md +7 -7
- package/doc/mqtt-plus-internals.md +0 -3
- package/dst-stage1/mqtt-plus-base.d.ts +1 -1
- package/dst-stage1/mqtt-plus-event.d.ts +3 -3
- package/dst-stage1/mqtt-plus-event.js +6 -6
- package/dst-stage1/mqtt-plus-sink.js +20 -6
- package/dst-stage1/mqtt-plus-source.d.ts +1 -0
- package/dst-stage1/mqtt-plus-source.js +20 -4
- package/dst-stage1/mqtt-plus-timer.d.ts +1 -1
- package/dst-stage1/mqtt-plus-timer.js +8 -2
- package/dst-stage1/mqtt-plus-util.js +3 -1
- package/dst-stage1/mqtt-plus.d.ts +1 -1
- package/dst-stage1/mqtt-plus.js +0 -1
- package/dst-stage2/mqtt-plus.cjs.js +55 -23
- package/dst-stage2/mqtt-plus.esm.js +55 -22
- package/dst-stage2/mqtt-plus.umd.js +12 -12
- package/etc/vite.mts +1 -1
- package/package.json +2 -2
- package/src/mqtt-plus-base.ts +1 -1
- package/src/mqtt-plus-event.ts +10 -10
- package/src/mqtt-plus-sink.ts +24 -8
- package/src/mqtt-plus-source.ts +21 -4
- package/src/mqtt-plus-timer.ts +9 -3
- package/src/mqtt-plus-util.ts +3 -1
- package/src/mqtt-plus.ts +1 -1
- package/tst/mqtt-plus-2-event.spec.ts +2 -2
- package/tst/mqtt-plus-6-misc.spec.ts +1 -1
- package/dst-stage1/mqtt-plus-receiver.d.ts +0 -12
- package/dst-stage1/mqtt-plus-receiver.js +0 -42
- package/dst-stage1/mqtt-plus-resource-dn.d.ts +0 -41
- package/dst-stage1/mqtt-plus-resource-dn.js +0 -286
- package/dst-stage1/mqtt-plus-resource-up.d.ts +0 -32
- package/dst-stage1/mqtt-plus-resource-up.js +0 -230
- package/dst-stage1/mqtt-plus-resource.d.ts +0 -49
- package/dst-stage1/mqtt-plus-resource.js +0 -385
- package/dst-stage1/mqtt-plus-stream.d.ts +0 -24
- package/dst-stage1/mqtt-plus-stream.js +0 -191
- package/dst-stage1/mqtt-plus-topic.d.ts +0 -20
- package/dst-stage1/mqtt-plus-topic.js +0 -112
package/AGENTS.md
CHANGED
|
@@ -33,11 +33,13 @@ npm start sample # run sample/sample.ts via `tsx`
|
|
|
33
33
|
|
|
34
34
|
npm start clean # remove dst-stage1/ and dst-stage2/
|
|
35
35
|
npm start distclean # remove node_modules/ and package-lock.json
|
|
36
|
+
npm start publish # publish to npm (restricted to maintainer host)
|
|
36
37
|
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
Tests require
|
|
40
|
-
|
|
40
|
+
Tests require an MQTT broker under run-time; the test suite starts/stops
|
|
41
|
+
one automatically. If Docker is available, a Mosquitto broker is used;
|
|
42
|
+
otherwise, the Aedes in-process broker serves as the fallback.
|
|
41
43
|
|
|
42
44
|
Build Pipeline
|
|
43
45
|
--------------
|
|
@@ -51,7 +53,7 @@ Two-stage build:
|
|
|
51
53
|
`mqtt-plus.esm.js`, `mqtt-plus.cjs.js`, `mqtt-plus.umd.js`.
|
|
52
54
|
UMD build includes Node polyfills (events, stream, buffer).
|
|
53
55
|
|
|
54
|
-
Configuration lives in `etc/`: `tsc.json`, `vite.mts`, `eslint.mts`, `knip.jsonc`, `stx.conf`, `d2.mts`.
|
|
56
|
+
Configuration lives in `etc/`: `tsc.json`, `vite.mts`, `eslint.mts`, `knip.jsonc`, `stx.conf`, `d2.mts`, `d2.theme.d2`, `logo.ai`, `logo.svg`.
|
|
55
57
|
|
|
56
58
|
Architecture
|
|
57
59
|
------------
|
|
@@ -70,6 +72,7 @@ the bottom of this chain:
|
|
|
70
72
|
↓ TraceTrait — EventEmitter + structured logging
|
|
71
73
|
↓ BaseTrait — MQTT client hookup, subscription management, message routing
|
|
72
74
|
↓ SubscriptionTrait — ref-counted MQTT topic subscription management
|
|
75
|
+
↓ TimerTrait — named timer management (refresh/clear)
|
|
73
76
|
↓ MetaTrait — instance/per-request metadata
|
|
74
77
|
↓ AuthTrait — JWT authentication (jose), role-based access
|
|
75
78
|
↓ EventTrait — Event Emission pattern (event/emit)
|
|
@@ -98,6 +101,7 @@ Each trait lives in its own file: `src/mqtt-plus-<trait>.ts`.
|
|
|
98
101
|
| `src/mqtt-plus-trace.ts` | TraceTrait — EventEmitter and structured logging |
|
|
99
102
|
| `src/mqtt-plus-base.ts` | BaseTrait — MQTT client connection, subscription management, message routing |
|
|
100
103
|
| `src/mqtt-plus-subscription.ts` | SubscriptionTrait — ref-counted MQTT topic subscription management |
|
|
104
|
+
| `src/mqtt-plus-timer.ts` | TimerTrait — named timer management (refresh/clear) |
|
|
101
105
|
| `src/mqtt-plus-meta.ts` | MetaTrait — instance and per-request metadata management |
|
|
102
106
|
| `src/mqtt-plus-auth.ts` | AuthTrait — JWT authentication (jose) and role-based access control |
|
|
103
107
|
| `src/mqtt-plus-event.ts` | EventTrait — Event Emission communication pattern (event/emit) |
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
ChangeLog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
1.4.11 (2026-03-05)
|
|
6
|
+
-------------------
|
|
7
|
+
|
|
8
|
+
- IMPROVEMENT: improve error handling and use error ensure function consistently
|
|
9
|
+
- IMPROVEMENT: improve cleanup handling and correctly track resources
|
|
10
|
+
- IMPROVEMENT: improve typing and fix overloads
|
|
11
|
+
- IMPROVEMENT: avoid warning in Vite
|
|
12
|
+
- BUGFIX: fix building for Vite compatibility
|
|
13
|
+
- BUGFIX: avoid unhandled rejection error
|
|
14
|
+
- BUGFIX: fix test
|
|
15
|
+
- UPDATE: update documentation
|
|
16
|
+
- UPDATE: upgrade NPM dependencies
|
|
17
|
+
- CLEANUP: rename emit() parameter "event" to "name" for consistency
|
|
18
|
+
- CLEANUP: cleanup code
|
|
19
|
+
|
|
5
20
|
1.4.10 (2026-03-01)
|
|
6
21
|
-------------------
|
|
7
22
|
|
package/doc/mqtt-plus-api.md
CHANGED
|
@@ -65,7 +65,7 @@ describing the available events, services, sources, and sinks.
|
|
|
65
65
|
Destruction
|
|
66
66
|
-----------
|
|
67
67
|
|
|
68
|
-
destroy(): void
|
|
68
|
+
destroy(): Promise<void>
|
|
69
69
|
|
|
70
70
|
Clean up the MQTT+ instance by removing all event listeners.
|
|
71
71
|
Call this method when the instance is no longer needed.
|
|
@@ -265,18 +265,18 @@ Event Emission
|
|
|
265
265
|
|
|
266
266
|
/* (simplified TypeScript API method signature) */
|
|
267
267
|
emit(
|
|
268
|
-
|
|
268
|
+
name: string,
|
|
269
269
|
...params: any[]
|
|
270
270
|
): void
|
|
271
271
|
emit({
|
|
272
|
-
|
|
272
|
+
name: string,
|
|
273
273
|
params: any[],
|
|
274
274
|
receiver?: string,
|
|
275
275
|
options?: MQTT::IClientPublishOptions,
|
|
276
276
|
meta?: Record<string, any>
|
|
277
277
|
}): void
|
|
278
278
|
emit({
|
|
279
|
-
|
|
279
|
+
name: string,
|
|
280
280
|
params: any[],
|
|
281
281
|
receiver?: string,
|
|
282
282
|
options?: MQTT::IClientPublishOptions,
|
|
@@ -300,8 +300,8 @@ Emit an event to all subscribers or a specific subscriber ("fire and forget").
|
|
|
300
300
|
- The remote `event()` `callback` is called with `params` and its
|
|
301
301
|
return value is silently ignored.
|
|
302
302
|
|
|
303
|
-
- Internally, publishes to the MQTT topic by `topicMake(
|
|
304
|
-
(default: `${
|
|
303
|
+
- Internally, publishes to the MQTT topic by `topicMake(name, "event-emission", peerId)`
|
|
304
|
+
(default: `${name}/event-emission/any` or `${name}/event-emission/${peerId}`).
|
|
305
305
|
|
|
306
306
|
- *Dry-Run Publishing for MQTT Last-Will:*
|
|
307
307
|
When you need to set up an MQTT "last will" message (automatically published
|
|
@@ -315,7 +315,7 @@ Emit an event to all subscribers or a specific subscriber ("fire and forget").
|
|
|
315
315
|
const mqttpDry = new MQTTp<API>(null, { id: "my-client" })
|
|
316
316
|
const will = mqttpDry.emit({
|
|
317
317
|
dry: true,
|
|
318
|
-
|
|
318
|
+
name: "example/connection",
|
|
319
319
|
params: [ "close" ],
|
|
320
320
|
[...]
|
|
321
321
|
})
|
|
@@ -122,8 +122,6 @@ Exactly one of `result` or `error` is present.
|
|
|
122
122
|
|----------|------------------------|----------|-------------------------------|
|
|
123
123
|
| `name` | `string` | yes | Sink endpoint name |
|
|
124
124
|
| `error` | `string` | no | Error message (nak) or absent (ack) |
|
|
125
|
-
| `auth` | `string[]` | no | JWT tokens (max 8) |
|
|
126
|
-
| `meta` | `Record<string, any>` | no | Arbitrary metadata |
|
|
127
125
|
| `credit` | `integer` | no | Initial flow control credit (min 1) |
|
|
128
126
|
|
|
129
127
|
### `sink-push-chunk`
|
|
@@ -158,7 +156,6 @@ Exactly one of `result` or `error` is present.
|
|
|
158
156
|
|----------|------------------------|----------|-------------------------------|
|
|
159
157
|
| `name` | `string` | yes | Source endpoint name |
|
|
160
158
|
| `error` | `string` | no | Error message (nak) or absent (ack) |
|
|
161
|
-
| `auth` | `string[]` | no | JWT tokens (max 8) |
|
|
162
159
|
| `meta` | `Record<string, any>` | no | Arbitrary metadata |
|
|
163
160
|
|
|
164
161
|
### `source-fetch-chunk`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MqttClient, type IClientSubscribeOptions, type IClientPublishOptions } from "mqtt";
|
|
1
|
+
import { type MqttClient, type IClientSubscribeOptions, type IClientPublishOptions } from "mqtt";
|
|
2
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";
|
|
@@ -11,16 +11,16 @@ export declare class EventTrait<T extends APISchema = APISchema> extends AuthTra
|
|
|
11
11
|
share?: string;
|
|
12
12
|
auth?: AuthOption;
|
|
13
13
|
}): Promise<Registration>;
|
|
14
|
-
emit<K extends EventKeys<T> & string>(
|
|
14
|
+
emit<K extends EventKeys<T> & string>(name: K, ...params: Parameters<T[K]>): void;
|
|
15
15
|
emit<K extends EventKeys<T> & string>(config: {
|
|
16
|
-
|
|
16
|
+
name: K;
|
|
17
17
|
params: Parameters<T[K]>;
|
|
18
18
|
receiver?: string;
|
|
19
19
|
options?: IClientPublishOptions;
|
|
20
20
|
meta?: Record<string, any>;
|
|
21
21
|
}): void;
|
|
22
22
|
emit<K extends EventKeys<T> & string>(config: {
|
|
23
|
-
|
|
23
|
+
name: K;
|
|
24
24
|
params: Parameters<T[K]>;
|
|
25
25
|
receiver?: string;
|
|
26
26
|
options?: IClientPublishOptions;
|
|
@@ -92,7 +92,7 @@ export class EventTrait extends AuthTrait {
|
|
|
92
92
|
}
|
|
93
93
|
emit(eventOrConfig, ...args) {
|
|
94
94
|
/* determine actual parameters */
|
|
95
|
-
let
|
|
95
|
+
let name;
|
|
96
96
|
let params;
|
|
97
97
|
let receiver;
|
|
98
98
|
let options = {};
|
|
@@ -100,7 +100,7 @@ export class EventTrait extends AuthTrait {
|
|
|
100
100
|
let dry;
|
|
101
101
|
if (typeof eventOrConfig === "object" && eventOrConfig !== null) {
|
|
102
102
|
/* object-based API */
|
|
103
|
-
|
|
103
|
+
name = eventOrConfig.name;
|
|
104
104
|
params = eventOrConfig.params;
|
|
105
105
|
receiver = eventOrConfig.receiver;
|
|
106
106
|
options = eventOrConfig.options ?? {};
|
|
@@ -109,7 +109,7 @@ export class EventTrait extends AuthTrait {
|
|
|
109
109
|
}
|
|
110
110
|
else {
|
|
111
111
|
/* positional API */
|
|
112
|
-
|
|
112
|
+
name = eventOrConfig;
|
|
113
113
|
params = args;
|
|
114
114
|
}
|
|
115
115
|
/* generate unique request id */
|
|
@@ -117,10 +117,10 @@ export class EventTrait extends AuthTrait {
|
|
|
117
117
|
/* generate encoded message */
|
|
118
118
|
const auth = this.authenticate();
|
|
119
119
|
const metaStore = this.metaStore(meta);
|
|
120
|
-
const request = this.msg.makeEventEmission(requestId,
|
|
120
|
+
const request = this.msg.makeEventEmission(requestId, name, params, this.options.id, receiver, auth, metaStore);
|
|
121
121
|
const message = this.codec.encode(request);
|
|
122
122
|
/* generate corresponding MQTT topic */
|
|
123
|
-
const topic = this.options.topicMake(
|
|
123
|
+
const topic = this.options.topicMake(name, "event-emission", receiver);
|
|
124
124
|
/* produce result */
|
|
125
125
|
if (dry)
|
|
126
126
|
/* return publish information */
|
|
@@ -128,7 +128,7 @@ export class EventTrait extends AuthTrait {
|
|
|
128
128
|
else
|
|
129
129
|
/* publish message to MQTT topic */
|
|
130
130
|
this.publishToTopic(topic, message, { qos: 2, ...options }).catch((err) => {
|
|
131
|
-
this.error(err, `emitting event "${
|
|
131
|
+
this.error(err, `emitting event "${name}" failed`);
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
134
|
}
|
|
@@ -178,6 +178,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
178
178
|
reqSpool.roll(() => { clearPushTimeout(); });
|
|
179
179
|
/* prepare info object */
|
|
180
180
|
const promise = streamToBuffer(readable);
|
|
181
|
+
promise.catch(() => { }); /* avoid unhandled promise rejection */
|
|
181
182
|
const info = {
|
|
182
183
|
sender,
|
|
183
184
|
stream: readable,
|
|
@@ -262,10 +263,19 @@ export class SinkTrait extends SourceTrait {
|
|
|
262
263
|
/* define abort controller and signal */
|
|
263
264
|
const abortController = new AbortController();
|
|
264
265
|
const abortSignal = abortController.signal;
|
|
266
|
+
/* ensure stream gets destroyed on abort */
|
|
267
|
+
if (data instanceof Readable) {
|
|
268
|
+
const stream = data;
|
|
269
|
+
abortSignal.addEventListener("abort", () => {
|
|
270
|
+
if (!stream.destroyed)
|
|
271
|
+
stream.destroy(ensureError(abortSignal.reason));
|
|
272
|
+
}, { once: true });
|
|
273
|
+
}
|
|
265
274
|
/* utility function for timeout refresh */
|
|
266
275
|
const pushTimerId = `sink-push-send:${requestId}`;
|
|
267
276
|
const refreshTimeout = () => this.timerRefresh(pushTimerId, () => {
|
|
268
|
-
|
|
277
|
+
const error = new Error(`push to sink "${name}" timed out`);
|
|
278
|
+
abortController.abort(error);
|
|
269
279
|
spool.unroll();
|
|
270
280
|
});
|
|
271
281
|
spool.roll(() => { this.timerClear(pushTimerId); });
|
|
@@ -274,6 +284,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
274
284
|
/* send request and wait for response before sending chunks */
|
|
275
285
|
let initialCredit;
|
|
276
286
|
let creditGate;
|
|
287
|
+
let remoteError = false;
|
|
277
288
|
try {
|
|
278
289
|
await new Promise((resolve, reject) => {
|
|
279
290
|
/* handle abort signal */
|
|
@@ -304,8 +315,10 @@ export class SinkTrait extends SourceTrait {
|
|
|
304
315
|
});
|
|
305
316
|
/* override handler for mid-stream (error) responses */
|
|
306
317
|
this.onResponse.set(`sink-push-response:${requestId}`, (response) => {
|
|
307
|
-
if (response.error)
|
|
318
|
+
if (response.error) {
|
|
319
|
+
remoteError = true;
|
|
308
320
|
abortController.abort(new Error(response.error));
|
|
321
|
+
}
|
|
309
322
|
});
|
|
310
323
|
/* create credit gate for flow control (if server granted credit) */
|
|
311
324
|
if (initialCredit !== undefined && initialCredit > 0)
|
|
@@ -342,12 +355,13 @@ export class SinkTrait extends SourceTrait {
|
|
|
342
355
|
await sendBufferAsChunks(data, this.options.chunkSize, sendChunk, creditGate, abortSignal);
|
|
343
356
|
}
|
|
344
357
|
catch (err) {
|
|
345
|
-
|
|
358
|
+
const error = ensureError(err);
|
|
359
|
+
abortController.abort(error);
|
|
360
|
+
/* send error chunk only if receiver is known and error did not originate from receiver
|
|
346
361
|
(otherwise the sink already received the error via the nak response) */
|
|
347
|
-
if (receiver !== undefined) {
|
|
348
|
-
const error = ensureError(err).message;
|
|
362
|
+
if (receiver !== undefined && !remoteError) {
|
|
349
363
|
const chunkTopic = this.options.topicMake(name, "sink-push-chunk", receiver);
|
|
350
|
-
const chunkMsg = this.msg.makeSinkPushChunk(requestId, name, undefined, error, true, this.options.id, receiver);
|
|
364
|
+
const chunkMsg = this.msg.makeSinkPushChunk(requestId, name, undefined, error.message, true, this.options.id, receiver);
|
|
351
365
|
const message = this.codec.encode(chunkMsg);
|
|
352
366
|
await this.publishToTopic(chunkTopic, message, { qos: 2, ...options }).catch(() => { });
|
|
353
367
|
}
|
|
@@ -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
|
+
private sourceControllers;
|
|
9
10
|
destroy(): Promise<void>;
|
|
10
11
|
source<K extends SourceKeys<T> & string>(name: K, callback: WithInfo<T[K], InfoSource>): Promise<Registration>;
|
|
11
12
|
source<K extends SourceKeys<T> & string>(config: {
|
|
@@ -34,9 +34,13 @@ export class SourceTrait extends ServiceTrait {
|
|
|
34
34
|
super(...arguments);
|
|
35
35
|
/* source state */
|
|
36
36
|
this.sourceCreditGates = new Map();
|
|
37
|
+
this.sourceControllers = new Map();
|
|
37
38
|
}
|
|
38
39
|
/* destroy source trait */
|
|
39
40
|
async destroy() {
|
|
41
|
+
for (const controller of this.sourceControllers.values())
|
|
42
|
+
controller.abort(new Error("source destroyed"));
|
|
43
|
+
this.sourceControllers.clear();
|
|
40
44
|
for (const gate of this.sourceCreditGates.values())
|
|
41
45
|
gate.abort();
|
|
42
46
|
this.sourceCreditGates.clear();
|
|
@@ -98,14 +102,25 @@ export class SourceTrait extends ServiceTrait {
|
|
|
98
102
|
};
|
|
99
103
|
/* define abort controller and signal */
|
|
100
104
|
const abortController = new AbortController();
|
|
105
|
+
this.sourceControllers.set(requestId, abortController);
|
|
101
106
|
const abortSignal = abortController.signal;
|
|
107
|
+
/* ensure stream gets destroyed on abort */
|
|
108
|
+
abortSignal.addEventListener("abort", () => {
|
|
109
|
+
if (info.stream instanceof Readable && !info.stream.destroyed)
|
|
110
|
+
info.stream.destroy(ensureError(abortSignal.reason));
|
|
111
|
+
}, { once: true });
|
|
102
112
|
/* utility functions for timeout management */
|
|
103
113
|
const sourceTimerId = `source-fetch-send:${requestId}`;
|
|
104
114
|
const refreshSourceTimeout = () => this.timerRefresh(sourceTimerId, () => {
|
|
105
|
-
|
|
115
|
+
const error = new Error(`source fetch "${name}" timed out`);
|
|
116
|
+
abortController.abort(error);
|
|
106
117
|
const gate = this.sourceCreditGates.get(requestId);
|
|
107
|
-
if (gate !== undefined)
|
|
118
|
+
if (gate !== undefined) {
|
|
108
119
|
gate.abort();
|
|
120
|
+
this.sourceCreditGates.delete(requestId);
|
|
121
|
+
}
|
|
122
|
+
this.sourceControllers.delete(requestId);
|
|
123
|
+
this.onResponse.delete(`source-fetch-credit:${requestId}`);
|
|
109
124
|
});
|
|
110
125
|
const clearSourceTimeout = () => this.timerClear(sourceTimerId);
|
|
111
126
|
refreshSourceTimeout();
|
|
@@ -158,8 +173,7 @@ export class SourceTrait extends ServiceTrait {
|
|
|
158
173
|
catch (err) {
|
|
159
174
|
/* cleanup stream resource (if provided by handler) */
|
|
160
175
|
const error = ensureError(err, `handler for source "${name}" failed`);
|
|
161
|
-
|
|
162
|
-
info.stream.destroy(error);
|
|
176
|
+
abortController.abort(error);
|
|
163
177
|
/* send error as nak response or as error chunk */
|
|
164
178
|
this.error(error);
|
|
165
179
|
if (ackSent)
|
|
@@ -174,6 +188,7 @@ export class SourceTrait extends ServiceTrait {
|
|
|
174
188
|
creditGate.abort();
|
|
175
189
|
this.sourceCreditGates.delete(requestId);
|
|
176
190
|
}
|
|
191
|
+
this.sourceControllers.delete(requestId);
|
|
177
192
|
this.onResponse.delete(`source-fetch-credit:${requestId}`);
|
|
178
193
|
}
|
|
179
194
|
});
|
|
@@ -248,6 +263,7 @@ export class SourceTrait extends ServiceTrait {
|
|
|
248
263
|
});
|
|
249
264
|
/* create promise for collecting stream chunks */
|
|
250
265
|
const buffer = streamToBuffer(stream);
|
|
266
|
+
buffer.catch(() => { }); /* avoid unhandled promise rejection */
|
|
251
267
|
/* create promise for meta (resolved on first chunk) */
|
|
252
268
|
let metaResolve;
|
|
253
269
|
const metaP = new Promise((resolve) => {
|
|
@@ -3,6 +3,6 @@ import { SubscriptionTrait } from "./mqtt-plus-subscription";
|
|
|
3
3
|
export declare class TimerTrait<T extends APISchema = APISchema> extends SubscriptionTrait<T> {
|
|
4
4
|
private timers;
|
|
5
5
|
destroy(): Promise<void>;
|
|
6
|
-
protected timerRefresh(id: string, onTimeout: () => void): void;
|
|
6
|
+
protected timerRefresh(id: string, onTimeout: () => void | Promise<void>): void;
|
|
7
7
|
protected timerClear(id: string): void;
|
|
8
8
|
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
23
|
*/
|
|
24
24
|
import { SubscriptionTrait } from "./mqtt-plus-subscription";
|
|
25
|
+
import { ensureError } from "./mqtt-plus-error";
|
|
25
26
|
/* Timer trait with reusable timer management */
|
|
26
27
|
export class TimerTrait extends SubscriptionTrait {
|
|
27
28
|
constructor() {
|
|
@@ -41,9 +42,14 @@ export class TimerTrait extends SubscriptionTrait {
|
|
|
41
42
|
const timer = this.timers.get(id);
|
|
42
43
|
if (timer !== undefined)
|
|
43
44
|
clearTimeout(timer);
|
|
44
|
-
this.timers.set(id, setTimeout(() => {
|
|
45
|
+
this.timers.set(id, setTimeout(async () => {
|
|
45
46
|
this.timers.delete(id);
|
|
46
|
-
|
|
47
|
+
try {
|
|
48
|
+
await onTimeout();
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
this.error(ensureError(err), `timer "${id}" failed`);
|
|
52
|
+
}
|
|
47
53
|
}, this.options.timeout));
|
|
48
54
|
}
|
|
49
55
|
/* clear a named timer */
|
|
@@ -49,7 +49,9 @@ export class CreditGate {
|
|
|
49
49
|
/* wait for credit to be replenished */
|
|
50
50
|
await new Promise((resolve, reject) => {
|
|
51
51
|
const onAbort = () => {
|
|
52
|
-
this.waiters.
|
|
52
|
+
const idx = this.waiters.indexOf(waiter);
|
|
53
|
+
if (idx !== -1)
|
|
54
|
+
this.waiters.splice(idx, 1);
|
|
53
55
|
reject(abortSignal?.reason ?? new Error("aborted"));
|
|
54
56
|
};
|
|
55
57
|
if (abortSignal)
|
|
@@ -2,6 +2,6 @@ import type { APISchema } from "./mqtt-plus-api";
|
|
|
2
2
|
import { SinkTrait } from "./mqtt-plus-sink";
|
|
3
3
|
export type * from "./mqtt-plus-api";
|
|
4
4
|
export type * from "./mqtt-plus-info";
|
|
5
|
-
export * from "./mqtt-plus-version";
|
|
5
|
+
export type * from "./mqtt-plus-version";
|
|
6
6
|
export default class MQTTp<T extends APISchema = APISchema> extends SinkTrait<T> {
|
|
7
7
|
}
|
package/dst-stage1/mqtt-plus.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
3
2
|
const node_stream = require("node:stream");
|
|
4
3
|
const nanoid = require("nanoid");
|
|
5
4
|
const node_buffer = require("node:buffer");
|
|
@@ -50,7 +49,9 @@ class CreditGate {
|
|
|
50
49
|
else
|
|
51
50
|
await new Promise((resolve, reject) => {
|
|
52
51
|
const onAbort = () => {
|
|
53
|
-
this.waiters.
|
|
52
|
+
const idx = this.waiters.indexOf(waiter);
|
|
53
|
+
if (idx !== -1)
|
|
54
|
+
this.waiters.splice(idx, 1);
|
|
54
55
|
reject(abortSignal?.reason ?? new Error("aborted"));
|
|
55
56
|
};
|
|
56
57
|
if (abortSignal)
|
|
@@ -1213,9 +1214,13 @@ class TimerTrait extends SubscriptionTrait {
|
|
|
1213
1214
|
const timer = this.timers.get(id);
|
|
1214
1215
|
if (timer !== void 0)
|
|
1215
1216
|
clearTimeout(timer);
|
|
1216
|
-
this.timers.set(id, setTimeout(() => {
|
|
1217
|
+
this.timers.set(id, setTimeout(async () => {
|
|
1217
1218
|
this.timers.delete(id);
|
|
1218
|
-
|
|
1219
|
+
try {
|
|
1220
|
+
await onTimeout();
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
this.error(ensureError(err), `timer "${id}" failed`);
|
|
1223
|
+
}
|
|
1219
1224
|
}, this.options.timeout));
|
|
1220
1225
|
}
|
|
1221
1226
|
/* clear a named timer */
|
|
@@ -1399,34 +1404,34 @@ class EventTrait extends AuthTrait {
|
|
|
1399
1404
|
return this.makeRegistration(spool, "event", name, `event-emission:${name}`);
|
|
1400
1405
|
}
|
|
1401
1406
|
emit(eventOrConfig, ...args) {
|
|
1402
|
-
let
|
|
1407
|
+
let name;
|
|
1403
1408
|
let params;
|
|
1404
1409
|
let receiver;
|
|
1405
1410
|
let options = {};
|
|
1406
1411
|
let meta;
|
|
1407
1412
|
let dry;
|
|
1408
1413
|
if (typeof eventOrConfig === "object" && eventOrConfig !== null) {
|
|
1409
|
-
|
|
1414
|
+
name = eventOrConfig.name;
|
|
1410
1415
|
params = eventOrConfig.params;
|
|
1411
1416
|
receiver = eventOrConfig.receiver;
|
|
1412
1417
|
options = eventOrConfig.options ?? {};
|
|
1413
1418
|
meta = eventOrConfig.meta;
|
|
1414
1419
|
dry = eventOrConfig.dry;
|
|
1415
1420
|
} else {
|
|
1416
|
-
|
|
1421
|
+
name = eventOrConfig;
|
|
1417
1422
|
params = args;
|
|
1418
1423
|
}
|
|
1419
1424
|
const requestId = nanoid.nanoid();
|
|
1420
1425
|
const auth = this.authenticate();
|
|
1421
1426
|
const metaStore = this.metaStore(meta);
|
|
1422
|
-
const request = this.msg.makeEventEmission(requestId,
|
|
1427
|
+
const request = this.msg.makeEventEmission(requestId, name, params, this.options.id, receiver, auth, metaStore);
|
|
1423
1428
|
const message = this.codec.encode(request);
|
|
1424
|
-
const topic = this.options.topicMake(
|
|
1429
|
+
const topic = this.options.topicMake(name, "event-emission", receiver);
|
|
1425
1430
|
if (dry)
|
|
1426
1431
|
return { topic, payload: message, options: { qos: 2, ...options } };
|
|
1427
1432
|
else
|
|
1428
1433
|
this.publishToTopic(topic, message, { qos: 2, ...options }).catch((err) => {
|
|
1429
|
-
this.error(err, `emitting event "${
|
|
1434
|
+
this.error(err, `emitting event "${name}" failed`);
|
|
1430
1435
|
});
|
|
1431
1436
|
}
|
|
1432
1437
|
}
|
|
@@ -1552,9 +1557,13 @@ class SourceTrait extends ServiceTrait {
|
|
|
1552
1557
|
constructor() {
|
|
1553
1558
|
super(...arguments);
|
|
1554
1559
|
this.sourceCreditGates = /* @__PURE__ */ new Map();
|
|
1560
|
+
this.sourceControllers = /* @__PURE__ */ new Map();
|
|
1555
1561
|
}
|
|
1556
1562
|
/* destroy source trait */
|
|
1557
1563
|
async destroy() {
|
|
1564
|
+
for (const controller of this.sourceControllers.values())
|
|
1565
|
+
controller.abort(new Error("source destroyed"));
|
|
1566
|
+
this.sourceControllers.clear();
|
|
1558
1567
|
for (const gate of this.sourceCreditGates.values())
|
|
1559
1568
|
gate.abort();
|
|
1560
1569
|
this.sourceCreditGates.clear();
|
|
@@ -1604,13 +1613,23 @@ class SourceTrait extends ServiceTrait {
|
|
|
1604
1613
|
await this.publishToTopic(responseTopic, message, { qos: options.qos ?? 2 });
|
|
1605
1614
|
};
|
|
1606
1615
|
const abortController = new AbortController();
|
|
1616
|
+
this.sourceControllers.set(requestId, abortController);
|
|
1607
1617
|
const abortSignal = abortController.signal;
|
|
1618
|
+
abortSignal.addEventListener("abort", () => {
|
|
1619
|
+
if (info.stream instanceof node_stream.Readable && !info.stream.destroyed)
|
|
1620
|
+
info.stream.destroy(ensureError(abortSignal.reason));
|
|
1621
|
+
}, { once: true });
|
|
1608
1622
|
const sourceTimerId = `source-fetch-send:${requestId}`;
|
|
1609
1623
|
const refreshSourceTimeout = () => this.timerRefresh(sourceTimerId, () => {
|
|
1610
|
-
|
|
1624
|
+
const error = new Error(`source fetch "${name}" timed out`);
|
|
1625
|
+
abortController.abort(error);
|
|
1611
1626
|
const gate = this.sourceCreditGates.get(requestId);
|
|
1612
|
-
if (gate !== void 0)
|
|
1627
|
+
if (gate !== void 0) {
|
|
1613
1628
|
gate.abort();
|
|
1629
|
+
this.sourceCreditGates.delete(requestId);
|
|
1630
|
+
}
|
|
1631
|
+
this.sourceControllers.delete(requestId);
|
|
1632
|
+
this.onResponse.delete(`source-fetch-credit:${requestId}`);
|
|
1614
1633
|
});
|
|
1615
1634
|
const clearSourceTimeout = () => this.timerClear(sourceTimerId);
|
|
1616
1635
|
refreshSourceTimeout();
|
|
@@ -1652,8 +1671,7 @@ class SourceTrait extends ServiceTrait {
|
|
|
1652
1671
|
await sendBufferAsChunks(await info.buffer, this.options.chunkSize, sendChunk, creditGate, abortSignal);
|
|
1653
1672
|
} catch (err) {
|
|
1654
1673
|
const error = ensureError(err, `handler for source "${name}" failed`);
|
|
1655
|
-
|
|
1656
|
-
info.stream.destroy(error);
|
|
1674
|
+
abortController.abort(error);
|
|
1657
1675
|
this.error(error);
|
|
1658
1676
|
if (ackSent)
|
|
1659
1677
|
await sendChunk(void 0, error.message, true).catch(() => {
|
|
@@ -1667,6 +1685,7 @@ class SourceTrait extends ServiceTrait {
|
|
|
1667
1685
|
creditGate.abort();
|
|
1668
1686
|
this.sourceCreditGates.delete(requestId);
|
|
1669
1687
|
}
|
|
1688
|
+
this.sourceControllers.delete(requestId);
|
|
1670
1689
|
this.onResponse.delete(`source-fetch-credit:${requestId}`);
|
|
1671
1690
|
}
|
|
1672
1691
|
});
|
|
@@ -1730,6 +1749,8 @@ class SourceTrait extends ServiceTrait {
|
|
|
1730
1749
|
}
|
|
1731
1750
|
});
|
|
1732
1751
|
const buffer = streamToBuffer(stream);
|
|
1752
|
+
buffer.catch(() => {
|
|
1753
|
+
});
|
|
1733
1754
|
let metaResolve;
|
|
1734
1755
|
const metaP = new Promise((resolve) => {
|
|
1735
1756
|
metaResolve = resolve;
|
|
@@ -1944,6 +1965,8 @@ class SinkTrait extends SourceTrait {
|
|
|
1944
1965
|
clearPushTimeout();
|
|
1945
1966
|
});
|
|
1946
1967
|
const promise = streamToBuffer(readable);
|
|
1968
|
+
promise.catch(() => {
|
|
1969
|
+
});
|
|
1947
1970
|
const info = {
|
|
1948
1971
|
sender,
|
|
1949
1972
|
stream: readable,
|
|
@@ -2014,9 +2037,17 @@ class SinkTrait extends SourceTrait {
|
|
|
2014
2037
|
spool.roll(() => this.subscriptions.unsubscribe(responseTopic));
|
|
2015
2038
|
const abortController = new AbortController();
|
|
2016
2039
|
const abortSignal = abortController.signal;
|
|
2040
|
+
if (data instanceof node_stream.Readable) {
|
|
2041
|
+
const stream = data;
|
|
2042
|
+
abortSignal.addEventListener("abort", () => {
|
|
2043
|
+
if (!stream.destroyed)
|
|
2044
|
+
stream.destroy(ensureError(abortSignal.reason));
|
|
2045
|
+
}, { once: true });
|
|
2046
|
+
}
|
|
2017
2047
|
const pushTimerId = `sink-push-send:${requestId}`;
|
|
2018
2048
|
const refreshTimeout = () => this.timerRefresh(pushTimerId, () => {
|
|
2019
|
-
|
|
2049
|
+
const error = new Error(`push to sink "${name}" timed out`);
|
|
2050
|
+
abortController.abort(error);
|
|
2020
2051
|
spool.unroll();
|
|
2021
2052
|
});
|
|
2022
2053
|
spool.roll(() => {
|
|
@@ -2025,6 +2056,7 @@ class SinkTrait extends SourceTrait {
|
|
|
2025
2056
|
refreshTimeout();
|
|
2026
2057
|
let initialCredit;
|
|
2027
2058
|
let creditGate;
|
|
2059
|
+
let remoteError = false;
|
|
2028
2060
|
try {
|
|
2029
2061
|
await new Promise((resolve, reject) => {
|
|
2030
2062
|
const onAbort = () => {
|
|
@@ -2057,8 +2089,10 @@ class SinkTrait extends SourceTrait {
|
|
|
2057
2089
|
});
|
|
2058
2090
|
});
|
|
2059
2091
|
this.onResponse.set(`sink-push-response:${requestId}`, (response) => {
|
|
2060
|
-
if (response.error)
|
|
2092
|
+
if (response.error) {
|
|
2093
|
+
remoteError = true;
|
|
2061
2094
|
abortController.abort(new Error(response.error));
|
|
2095
|
+
}
|
|
2062
2096
|
});
|
|
2063
2097
|
if (initialCredit !== void 0 && initialCredit > 0)
|
|
2064
2098
|
creditGate = new CreditGate(initialCredit);
|
|
@@ -2090,10 +2124,11 @@ class SinkTrait extends SourceTrait {
|
|
|
2090
2124
|
else if (data instanceof Uint8Array)
|
|
2091
2125
|
await sendBufferAsChunks(data, this.options.chunkSize, sendChunk, creditGate, abortSignal);
|
|
2092
2126
|
} catch (err) {
|
|
2093
|
-
|
|
2094
|
-
|
|
2127
|
+
const error = ensureError(err);
|
|
2128
|
+
abortController.abort(error);
|
|
2129
|
+
if (receiver !== void 0 && !remoteError) {
|
|
2095
2130
|
const chunkTopic = this.options.topicMake(name, "sink-push-chunk", receiver);
|
|
2096
|
-
const chunkMsg = this.msg.makeSinkPushChunk(requestId, name, void 0, error, true, this.options.id, receiver);
|
|
2131
|
+
const chunkMsg = this.msg.makeSinkPushChunk(requestId, name, void 0, error.message, true, this.options.id, receiver);
|
|
2097
2132
|
const message = this.codec.encode(chunkMsg);
|
|
2098
2133
|
await this.publishToTopic(chunkTopic, message, { qos: 2, ...options }).catch(() => {
|
|
2099
2134
|
});
|
|
@@ -2106,7 +2141,4 @@ class SinkTrait extends SourceTrait {
|
|
|
2106
2141
|
}
|
|
2107
2142
|
class MQTTp extends SinkTrait {
|
|
2108
2143
|
}
|
|
2109
|
-
exports
|
|
2110
|
-
exports.default = MQTTp;
|
|
2111
|
-
exports.version = version;
|
|
2112
|
-
exports.versionToNum = versionToNum;
|
|
2144
|
+
module.exports = MQTTp;
|