mqtt-plus 1.4.25 → 1.4.27

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 (68) hide show
  1. package/.ase/config.yaml +1 -0
  2. package/.ase/service.log +34 -0
  3. package/.ase/service.yaml +1 -1
  4. package/.ase/task/TASK-P18.md +1 -1
  5. package/.ase/task/TASK-P21.md +1 -1
  6. package/.claude/settings.local.json +4 -1
  7. package/AGENTS.md +1 -1
  8. package/CHANGELOG.md +29 -5
  9. package/README.md +2 -2
  10. package/doc/mqtt-plus-broker-setup.md +1 -1
  11. package/doc/mqtt-plus-comm.md +3 -3
  12. package/doc/mqtt-plus-internals.md +6 -0
  13. package/dst-stage1/mqtt-plus-base.d.ts +2 -2
  14. package/dst-stage1/mqtt-plus-base.js +13 -10
  15. package/dst-stage1/mqtt-plus-error.js +54 -28
  16. package/dst-stage1/mqtt-plus-event.js +3 -1
  17. package/dst-stage1/mqtt-plus-options.js +7 -0
  18. package/dst-stage1/mqtt-plus-service.js +9 -6
  19. package/dst-stage1/mqtt-plus-sink.js +27 -54
  20. package/dst-stage1/mqtt-plus-source.js +13 -9
  21. package/dst-stage1/mqtt-plus-util.d.ts +1 -0
  22. package/dst-stage1/mqtt-plus-util.js +25 -4
  23. package/dst-stage1/mqtt-plus.d.ts +5 -1
  24. package/dst-stage1/mqtt-plus.js +10 -1
  25. package/dst-stage2/mqtt-plus.cjs.cjs +169 -123
  26. package/dst-stage2/mqtt-plus.esm.js +168 -122
  27. package/dst-stage2/mqtt-plus.umd.js +11 -11
  28. package/etc/stx.conf +3 -2
  29. package/etc/vite.mts +1 -1
  30. package/package.d/@typescript-eslint+typescript-estree+8.66.0.patch +12 -0
  31. package/package.d/{vite+8.1.4.patch → vite+8.2.1.patch} +2 -2
  32. package/package.json +16 -15
  33. package/src/mqtt-plus-base.ts +15 -12
  34. package/src/mqtt-plus-error.ts +65 -33
  35. package/src/mqtt-plus-event.ts +2 -1
  36. package/src/mqtt-plus-options.ts +8 -0
  37. package/src/mqtt-plus-service.ts +9 -6
  38. package/src/mqtt-plus-sink.ts +28 -54
  39. package/src/mqtt-plus-source.ts +13 -9
  40. package/src/mqtt-plus-util.ts +27 -4
  41. package/src/mqtt-plus.ts +14 -3
  42. package/tst/mqtt-plus-0-fixture.ts +1 -0
  43. package/tst/mqtt-plus-2-event.spec.ts +25 -0
  44. package/tst/mqtt-plus-4-sink.spec.ts +23 -0
  45. package/tst/mqtt-plus-5-source.spec.ts +26 -0
  46. package/tst/mqtt-plus-6-misc.spec.ts +58 -0
  47. package/tst/mqtt-plus-8-run.spec.ts +60 -0
  48. package/dst-stage1/mqtt-plus-api.js.map +0 -1
  49. package/dst-stage1/mqtt-plus-auth.js.map +0 -1
  50. package/dst-stage1/mqtt-plus-base.js.map +0 -1
  51. package/dst-stage1/mqtt-plus-codec.js.map +0 -1
  52. package/dst-stage1/mqtt-plus-encode.js.map +0 -1
  53. package/dst-stage1/mqtt-plus-error.js.map +0 -1
  54. package/dst-stage1/mqtt-plus-event.js.map +0 -1
  55. package/dst-stage1/mqtt-plus-info.js.map +0 -1
  56. package/dst-stage1/mqtt-plus-meta.js.map +0 -1
  57. package/dst-stage1/mqtt-plus-msg.js.map +0 -1
  58. package/dst-stage1/mqtt-plus-options.js.map +0 -1
  59. package/dst-stage1/mqtt-plus-service.js.map +0 -1
  60. package/dst-stage1/mqtt-plus-sink.js.map +0 -1
  61. package/dst-stage1/mqtt-plus-source.js.map +0 -1
  62. package/dst-stage1/mqtt-plus-subscription.js.map +0 -1
  63. package/dst-stage1/mqtt-plus-timer.js.map +0 -1
  64. package/dst-stage1/mqtt-plus-trace.js.map +0 -1
  65. package/dst-stage1/mqtt-plus-util.js.map +0 -1
  66. package/dst-stage1/mqtt-plus-version.js.map +0 -1
  67. package/dst-stage1/mqtt-plus.js.map +0 -1
  68. package/package.d/@typescript-eslint+typescript-estree+8.63.0.patch +0 -12
@@ -25,7 +25,7 @@
25
25
  import { Readable } from "node:stream";
26
26
  import { nanoid } from "nanoid";
27
27
  /* internal requirements */
28
- import { CreditGate, ReadableTee, sendBufferAsChunks, sendStreamAsChunks, makeMutuallyExclusiveFields } from "./mqtt-plus-util";
28
+ import { CreditGate, ReadableTee, sendBufferAsChunks, sendStreamAsChunks, makeMutuallyExclusiveFields, alignHandlerParams } from "./mqtt-plus-util";
29
29
  import { run, Spool, ensureError } from "./mqtt-plus-error";
30
30
  import { SourceTrait } from "./mqtt-plus-source";
31
31
  /* Sink Push Trait */
@@ -115,7 +115,7 @@ export class SinkTrait extends SourceTrait {
115
115
  return;
116
116
  /* determine information */
117
117
  const requestId = request.id;
118
- const params = request.params ?? [];
118
+ const params = alignHandlerParams(request.params ?? [], callback);
119
119
  const sender = request.sender;
120
120
  const receiver = request.receiver;
121
121
  /* create a resource spool for request cleanup */
@@ -483,9 +483,14 @@ export class SinkTrait extends SourceTrait {
483
483
  /* ensure stream gets destroyed on abort */
484
484
  if (data instanceof Readable) {
485
485
  const stream = data;
486
+ const noopError = () => { }; /* prevent unhandled error exception */
486
487
  abortSignal.addEventListener("abort", () => {
487
- if (!stream.destroyed)
488
+ if (!stream.destroyed) {
489
+ /* guard the caller's stream, as it still carries no "error"
490
+ listener when the abort precedes the chunk sending phase */
491
+ stream.on("error", noopError);
488
492
  stream.destroy(ensureError(abortSignal.reason));
493
+ }
489
494
  }, { once: true });
490
495
  }
491
496
  /* utility function for timeout refresh */
@@ -521,20 +526,24 @@ export class SinkTrait extends SourceTrait {
521
526
  pushFinalizeReject = reject;
522
527
  });
523
528
  pushFinalize.catch(() => { }); /* avoid unhandled promise rejection */
529
+ /* abort the push and settle the pending initial/finalize promises */
530
+ const failPush = (error) => {
531
+ abortController.abort(error);
532
+ if (!pushAcked) {
533
+ pushInitialSettled = true;
534
+ pushInitialReject(error);
535
+ }
536
+ else if (!pushFinalized) {
537
+ pushFinalized = true;
538
+ pushFinalizeReject(error);
539
+ }
540
+ };
524
541
  /* lock the responder for this communication */
525
542
  const lockResponder = (kind, sender) => {
526
543
  if (sender === undefined || sender === "") {
527
544
  const error = new Error(`received ${kind} without sender`);
528
545
  remoteErrorObject = error;
529
- abortController.abort(error);
530
- if (!pushAcked) {
531
- pushInitialSettled = true;
532
- pushInitialReject(error);
533
- }
534
- else if (!pushFinalized) {
535
- pushFinalized = true;
536
- pushFinalizeReject(error);
537
- }
546
+ failPush(error);
538
547
  return false;
539
548
  }
540
549
  if (responderId === undefined)
@@ -555,15 +564,7 @@ export class SinkTrait extends SourceTrait {
555
564
  if (response.name !== name) {
556
565
  const error = new Error(`sink response name mismatch (expected "${name}", got "${response.name}")`);
557
566
  remoteErrorObject = error;
558
- abortController.abort(error);
559
- if (!pushAcked) {
560
- pushInitialSettled = true;
561
- pushInitialReject(error);
562
- }
563
- else if (!pushFinalized) {
564
- pushFinalized = true;
565
- pushFinalizeReject(error);
566
- }
567
+ failPush(error);
567
568
  return;
568
569
  }
569
570
  if (!lockResponder("sink response", response.sender))
@@ -571,15 +572,7 @@ export class SinkTrait extends SourceTrait {
571
572
  if (response.error) {
572
573
  const error = new Error(response.error);
573
574
  remoteErrorObject = error;
574
- abortController.abort(error);
575
- if (!pushAcked) {
576
- pushInitialSettled = true;
577
- pushInitialReject(error);
578
- }
579
- else if (!pushFinalized) {
580
- pushFinalized = true;
581
- pushFinalizeReject(error);
582
- }
575
+ failPush(error);
583
576
  }
584
577
  else if (!pushAcked) {
585
578
  initialCredit = response.credit;
@@ -604,15 +597,7 @@ export class SinkTrait extends SourceTrait {
604
597
  if (response.name !== name) {
605
598
  const error = new Error(`sink credit name mismatch (expected "${name}", got "${response.name}")`);
606
599
  remoteErrorObject = error;
607
- abortController.abort(error);
608
- if (!pushAcked) {
609
- pushInitialSettled = true;
610
- pushInitialReject(error);
611
- }
612
- else if (!pushFinalized) {
613
- pushFinalized = true;
614
- pushFinalizeReject(error);
615
- }
600
+ failPush(error);
616
601
  return;
617
602
  }
618
603
  if (!lockResponder("sink credit", response.sender))
@@ -621,15 +606,7 @@ export class SinkTrait extends SourceTrait {
621
606
  /* cancel signal from receiver */
622
607
  cancelledByReceiver = true;
623
608
  const error = new Error(`push to sink "${name}" cancelled by receiver`);
624
- abortController.abort(error);
625
- if (!pushAcked) {
626
- pushInitialSettled = true;
627
- pushInitialReject(error);
628
- }
629
- else if (!pushFinalized) {
630
- pushFinalized = true;
631
- pushFinalizeReject(error);
632
- }
609
+ failPush(error);
633
610
  return;
634
611
  }
635
612
  if (creditGate !== undefined) {
@@ -641,11 +618,7 @@ export class SinkTrait extends SourceTrait {
641
618
  not granting initial credit during ack */
642
619
  const error = new Error(`push to sink "${name}" received unsolicited credit (credit-flow disabled)`);
643
620
  remoteErrorObject = error;
644
- abortController.abort(error);
645
- if (!pushFinalized) {
646
- pushFinalized = true;
647
- pushFinalizeReject(error);
648
- }
621
+ failPush(error);
649
622
  }
650
623
  else
651
624
  pendingCredit += response.credit;
@@ -751,7 +724,7 @@ export class SinkTrait extends SourceTrait {
751
724
  response from the receiver to be processed, making the
752
725
  error outcome deterministic (only when race is possible) */
753
726
  if (pushAcked && !remoteErrorObject)
754
- await new Promise((resolve) => { setImmediate(resolve); });
727
+ await new Promise((resolve) => { setTimeout(resolve, 0); });
755
728
  if (remoteErrorObject)
756
729
  throw remoteErrorObject;
757
730
  throw err;
@@ -25,7 +25,7 @@
25
25
  import { Readable } from "node:stream";
26
26
  import { nanoid } from "nanoid";
27
27
  /* internal requirements */
28
- import { CreditGate, ReadableTee, sendBufferAsChunks, sendStreamAsChunks, makeMutuallyExclusiveFields } from "./mqtt-plus-util";
28
+ import { CreditGate, ReadableTee, sendBufferAsChunks, sendStreamAsChunks, makeMutuallyExclusiveFields, alignHandlerParams } from "./mqtt-plus-util";
29
29
  import { run, Spool, ensureError } from "./mqtt-plus-error";
30
30
  import { ServiceTrait } from "./mqtt-plus-service";
31
31
  /* Source Fetch Trait */
@@ -99,7 +99,7 @@ export class SourceTrait extends ServiceTrait {
99
99
  return;
100
100
  /* determine information */
101
101
  const requestId = request.id;
102
- const params = request.params ?? [];
102
+ const params = alignHandlerParams(request.params ?? [], callback);
103
103
  const sender = request.sender;
104
104
  const receiver = request.receiver;
105
105
  /* create a resource spool for request cleanup */
@@ -156,16 +156,20 @@ export class SourceTrait extends ServiceTrait {
156
156
  }
157
157
  });
158
158
  /* ensure stream gets destroyed on abort */
159
- abortSignal.addEventListener("abort", () => {
160
- if (info.stream instanceof Readable && !info.stream.destroyed)
159
+ const noopError = () => { }; /* prevent unhandled error exception */
160
+ const destroyStream = () => {
161
+ if (info.stream instanceof Readable && !info.stream.destroyed) {
162
+ /* guard the handler's stream, as it still carries no "error"
163
+ listener when the abort precedes the chunk sending phase */
164
+ info.stream.on("error", noopError);
161
165
  info.stream.destroy(ensureError(abortSignal.reason));
162
- }, { once: true });
166
+ }
167
+ };
168
+ abortSignal.addEventListener("abort", destroyStream, { once: true });
163
169
  /* safety net: destroy a stream assigned after the abort event fired */
164
170
  reqSpool.roll(() => {
165
- if (abortSignal.aborted
166
- && info.stream instanceof Readable
167
- && !info.stream.destroyed)
168
- info.stream.destroy(ensureError(abortSignal.reason));
171
+ if (abortSignal.aborted)
172
+ destroyStream();
169
173
  });
170
174
  let abortReject;
171
175
  const abortPromise = new Promise((_resolve, reject) => {
@@ -29,5 +29,6 @@ export declare class ReadableTee extends Readable {
29
29
  type SendChunkCallback = (chunk: Uint8Array | undefined, error: string | undefined, final: boolean) => Promise<void>;
30
30
  export declare function sendBufferAsChunks(buffer: Uint8Array, chunkSize: number, sendChunk: SendChunkCallback, creditGate?: CreditGate, abortSignal?: AbortSignal): Promise<void>;
31
31
  export declare function sendStreamAsChunks(readable: Readable, chunkSize: number, sendChunk: SendChunkCallback, creditGate?: CreditGate, abortSignal?: AbortSignal): Promise<void>;
32
+ export declare function alignHandlerParams(params: any[], handler: unknown): any[];
32
33
  export declare function makeMutuallyExclusiveFields<T extends object>(obj: T, f1Name: keyof T & string, f2Name: keyof T & string, onConsumed?: (field: string) => void): void;
33
34
  export {};
@@ -164,6 +164,8 @@ export class ReadableTee extends Readable {
164
164
  }
165
165
  /* utility function for sending a buffer as chunks */
166
166
  export async function sendBufferAsChunks(buffer, chunkSize, sendChunk, creditGate, abortSignal) {
167
+ if (!(Number.isFinite(chunkSize) && chunkSize > 0))
168
+ throw new Error("invalid chunk size");
167
169
  if (buffer.byteLength === 0)
168
170
  await sendChunk(undefined, undefined, true);
169
171
  else {
@@ -181,6 +183,8 @@ export async function sendBufferAsChunks(buffer, chunkSize, sendChunk, creditGat
181
183
  }
182
184
  /* utility function for sending a Readable stream as chunks */
183
185
  export async function sendStreamAsChunks(readable, chunkSize, sendChunk, creditGate, abortSignal) {
186
+ if (!(Number.isFinite(chunkSize) && chunkSize > 0))
187
+ throw new Error("invalid chunk size");
184
188
  let pending;
185
189
  for await (const raw of readable) {
186
190
  if (abortSignal?.aborted)
@@ -211,13 +215,28 @@ export async function sendStreamAsChunks(readable, chunkSize, sendChunk, creditG
211
215
  else
212
216
  await sendChunk(undefined, undefined, true);
213
217
  }
218
+ /* utility function for aligning received parameters to the parameter arity a
219
+ handler declares, as the handler receives its trailing "info" argument
220
+ positionally and a sender omitting trailing optional parameters would
221
+ otherwise shift that argument into a data parameter slot */
222
+ export function alignHandlerParams(params, handler) {
223
+ if (typeof handler !== "function")
224
+ return params;
225
+ const arity = handler.length - 1;
226
+ if (params.length >= arity)
227
+ return params;
228
+ return [...params, ...Array(arity - params.length).fill(undefined)];
229
+ }
214
230
  /* utility function for making two object fields mutually exclusive */
215
231
  export function makeMutuallyExclusiveFields(obj, f1Name, f2Name, onConsumed) {
216
232
  if (!(typeof obj === "object" && obj !== null))
217
233
  throw new Error("invalid object");
218
234
  let consumed;
219
- const f1Value = obj[f1Name];
220
- const f2Value = obj[f2Name];
235
+ let f1Value = obj[f1Name];
236
+ let f2Value = obj[f2Name];
237
+ /* both fields are intentionally non-enumerable, as any enumeration
238
+ (object spread, Object.values(), JSON.stringify(), etc) would
239
+ otherwise consume the first field and then throw on the second */
221
240
  Object.defineProperty(obj, f1Name, {
222
241
  get: () => {
223
242
  if (consumed === "f2")
@@ -228,7 +247,8 @@ export function makeMutuallyExclusiveFields(obj, f1Name, f2Name, onConsumed) {
228
247
  consumed = "f1";
229
248
  return f1Value;
230
249
  },
231
- enumerable: true,
250
+ set: (value) => { f1Value = value; },
251
+ enumerable: false,
232
252
  configurable: true
233
253
  });
234
254
  Object.defineProperty(obj, f2Name, {
@@ -241,7 +261,8 @@ export function makeMutuallyExclusiveFields(obj, f1Name, f2Name, onConsumed) {
241
261
  consumed = "f2";
242
262
  return f2Value;
243
263
  },
244
- enumerable: true,
264
+ set: (value) => { f2Value = value; },
265
+ enumerable: false,
245
266
  configurable: true
246
267
  });
247
268
  }
@@ -2,6 +2,10 @@ 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 type * from "./mqtt-plus-version";
6
5
  export default class MQTTp<T extends APISchema = APISchema> extends SinkTrait<T> {
6
+ static readonly VERSION = "0.0";
7
+ static readonly MIN_VERSION = "0.0";
8
+ static readonly version: number;
9
+ static readonly minVersion: number;
10
+ static readonly versionToNum: (str: string) => number;
7
11
  }
@@ -22,6 +22,15 @@
22
22
  ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  */
24
24
  import { SinkTrait } from "./mqtt-plus-sink";
25
- /* export the default API class */
25
+ import { VERSION, MIN_VERSION, version, minVersion, versionToNum } from "./mqtt-plus-version";
26
+ /* export the default API class (as the sole export of this module, as an
27
+ additional named run-time export would force CJS/UMD consumers into
28
+ the awkward "MQTTp.default" access path) */
26
29
  export default class MQTTp extends SinkTrait {
30
+ /* expose the version information */
31
+ static { this.VERSION = VERSION; }
32
+ static { this.MIN_VERSION = MIN_VERSION; }
33
+ static { this.version = version; }
34
+ static { this.minVersion = minVersion; }
35
+ static { this.versionToNum = versionToNum; }
27
36
  }