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.
Files changed (41) hide show
  1. package/AGENTS.md +7 -3
  2. package/CHANGELOG.md +15 -0
  3. package/doc/mqtt-plus-api.md +7 -7
  4. package/doc/mqtt-plus-internals.md +0 -3
  5. package/dst-stage1/mqtt-plus-base.d.ts +1 -1
  6. package/dst-stage1/mqtt-plus-event.d.ts +3 -3
  7. package/dst-stage1/mqtt-plus-event.js +6 -6
  8. package/dst-stage1/mqtt-plus-sink.js +20 -6
  9. package/dst-stage1/mqtt-plus-source.d.ts +1 -0
  10. package/dst-stage1/mqtt-plus-source.js +20 -4
  11. package/dst-stage1/mqtt-plus-timer.d.ts +1 -1
  12. package/dst-stage1/mqtt-plus-timer.js +8 -2
  13. package/dst-stage1/mqtt-plus-util.js +3 -1
  14. package/dst-stage1/mqtt-plus.d.ts +1 -1
  15. package/dst-stage1/mqtt-plus.js +0 -1
  16. package/dst-stage2/mqtt-plus.cjs.js +55 -23
  17. package/dst-stage2/mqtt-plus.esm.js +55 -22
  18. package/dst-stage2/mqtt-plus.umd.js +12 -12
  19. package/etc/vite.mts +1 -1
  20. package/package.json +2 -2
  21. package/src/mqtt-plus-base.ts +1 -1
  22. package/src/mqtt-plus-event.ts +10 -10
  23. package/src/mqtt-plus-sink.ts +24 -8
  24. package/src/mqtt-plus-source.ts +21 -4
  25. package/src/mqtt-plus-timer.ts +9 -3
  26. package/src/mqtt-plus-util.ts +3 -1
  27. package/src/mqtt-plus.ts +1 -1
  28. package/tst/mqtt-plus-2-event.spec.ts +2 -2
  29. package/tst/mqtt-plus-6-misc.spec.ts +1 -1
  30. package/dst-stage1/mqtt-plus-receiver.d.ts +0 -12
  31. package/dst-stage1/mqtt-plus-receiver.js +0 -42
  32. package/dst-stage1/mqtt-plus-resource-dn.d.ts +0 -41
  33. package/dst-stage1/mqtt-plus-resource-dn.js +0 -286
  34. package/dst-stage1/mqtt-plus-resource-up.d.ts +0 -32
  35. package/dst-stage1/mqtt-plus-resource-up.js +0 -230
  36. package/dst-stage1/mqtt-plus-resource.d.ts +0 -49
  37. package/dst-stage1/mqtt-plus-resource.js +0 -385
  38. package/dst-stage1/mqtt-plus-stream.d.ts +0 -24
  39. package/dst-stage1/mqtt-plus-stream.js +0 -191
  40. package/dst-stage1/mqtt-plus-topic.d.ts +0 -20
  41. 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 a Mosquitto MQTT broker under run-time; the `mosquitto`
40
- npm package provides one that the test suite starts/stops automatically.
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
 
@@ -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
- event: string,
268
+ name: string,
269
269
  ...params: any[]
270
270
  ): void
271
271
  emit({
272
- event: string,
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
- event: string,
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(event, "event-emission", peerId)`
304
- (default: `${event}/event-emission/any` or `${event}/event-emission/${peerId}`).
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
- event: "example/connection",
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>(event: K, ...params: Parameters<T[K]>): void;
14
+ emit<K extends EventKeys<T> & string>(name: K, ...params: Parameters<T[K]>): void;
15
15
  emit<K extends EventKeys<T> & string>(config: {
16
- event: K;
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
- event: K;
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 event;
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
- event = eventOrConfig.event;
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
- event = eventOrConfig;
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, event, params, this.options.id, receiver, auth, metaStore);
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(event, "event-emission", receiver);
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 "${event}" failed`);
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
- abortController.abort(new Error(`push to sink "${name}" timed out`));
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
- /* send error chunk only if receiver is known
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
- abortController.abort(new Error(`source fetch "${name}" timed out`));
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
- if (info.stream instanceof Readable && !info.stream.destroyed)
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
- onTimeout();
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.splice(this.waiters.indexOf(waiter), 1);
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
  }
@@ -22,7 +22,6 @@
22
22
  ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  */
24
24
  import { SinkTrait } from "./mqtt-plus-sink";
25
- export * from "./mqtt-plus-version";
26
25
  /* export the default API class */
27
26
  export default class MQTTp extends SinkTrait {
28
27
  }
@@ -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.splice(this.waiters.indexOf(waiter), 1);
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
- onTimeout();
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 event;
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
- event = eventOrConfig.event;
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
- event = eventOrConfig;
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, event, params, this.options.id, receiver, auth, metaStore);
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(event, "event-emission", receiver);
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 "${event}" failed`);
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
- abortController.abort(new Error(`source fetch "${name}" timed out`));
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
- if (info.stream instanceof node_stream.Readable && !info.stream.destroyed)
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
- abortController.abort(new Error(`push to sink "${name}" timed out`));
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
- if (receiver !== void 0) {
2094
- const error = ensureError(err).message;
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.VERSION = VERSION;
2110
- exports.default = MQTTp;
2111
- exports.version = version;
2112
- exports.versionToNum = versionToNum;
2144
+ module.exports = MQTTp;