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.
- package/.ase/config.yaml +1 -0
- package/.ase/service.log +34 -0
- package/.ase/service.yaml +1 -1
- package/.ase/task/TASK-P18.md +1 -1
- package/.ase/task/TASK-P21.md +1 -1
- package/.claude/settings.local.json +4 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +29 -5
- package/README.md +2 -2
- package/doc/mqtt-plus-broker-setup.md +1 -1
- package/doc/mqtt-plus-comm.md +3 -3
- package/doc/mqtt-plus-internals.md +6 -0
- package/dst-stage1/mqtt-plus-base.d.ts +2 -2
- package/dst-stage1/mqtt-plus-base.js +13 -10
- package/dst-stage1/mqtt-plus-error.js +54 -28
- package/dst-stage1/mqtt-plus-event.js +3 -1
- package/dst-stage1/mqtt-plus-options.js +7 -0
- package/dst-stage1/mqtt-plus-service.js +9 -6
- package/dst-stage1/mqtt-plus-sink.js +27 -54
- package/dst-stage1/mqtt-plus-source.js +13 -9
- package/dst-stage1/mqtt-plus-util.d.ts +1 -0
- package/dst-stage1/mqtt-plus-util.js +25 -4
- package/dst-stage1/mqtt-plus.d.ts +5 -1
- package/dst-stage1/mqtt-plus.js +10 -1
- package/dst-stage2/mqtt-plus.cjs.cjs +169 -123
- package/dst-stage2/mqtt-plus.esm.js +168 -122
- package/dst-stage2/mqtt-plus.umd.js +11 -11
- package/etc/stx.conf +3 -2
- package/etc/vite.mts +1 -1
- package/package.d/@typescript-eslint+typescript-estree+8.66.0.patch +12 -0
- package/package.d/{vite+8.1.4.patch → vite+8.2.1.patch} +2 -2
- package/package.json +16 -15
- package/src/mqtt-plus-base.ts +15 -12
- package/src/mqtt-plus-error.ts +65 -33
- package/src/mqtt-plus-event.ts +2 -1
- package/src/mqtt-plus-options.ts +8 -0
- package/src/mqtt-plus-service.ts +9 -6
- package/src/mqtt-plus-sink.ts +28 -54
- package/src/mqtt-plus-source.ts +13 -9
- package/src/mqtt-plus-util.ts +27 -4
- package/src/mqtt-plus.ts +14 -3
- package/tst/mqtt-plus-0-fixture.ts +1 -0
- package/tst/mqtt-plus-2-event.spec.ts +25 -0
- package/tst/mqtt-plus-4-sink.spec.ts +23 -0
- package/tst/mqtt-plus-5-source.spec.ts +26 -0
- package/tst/mqtt-plus-6-misc.spec.ts +58 -0
- package/tst/mqtt-plus-8-run.spec.ts +60 -0
- package/dst-stage1/mqtt-plus-api.js.map +0 -1
- package/dst-stage1/mqtt-plus-auth.js.map +0 -1
- package/dst-stage1/mqtt-plus-base.js.map +0 -1
- package/dst-stage1/mqtt-plus-codec.js.map +0 -1
- package/dst-stage1/mqtt-plus-encode.js.map +0 -1
- package/dst-stage1/mqtt-plus-error.js.map +0 -1
- package/dst-stage1/mqtt-plus-event.js.map +0 -1
- package/dst-stage1/mqtt-plus-info.js.map +0 -1
- package/dst-stage1/mqtt-plus-meta.js.map +0 -1
- package/dst-stage1/mqtt-plus-msg.js.map +0 -1
- package/dst-stage1/mqtt-plus-options.js.map +0 -1
- package/dst-stage1/mqtt-plus-service.js.map +0 -1
- package/dst-stage1/mqtt-plus-sink.js.map +0 -1
- package/dst-stage1/mqtt-plus-source.js.map +0 -1
- package/dst-stage1/mqtt-plus-subscription.js.map +0 -1
- package/dst-stage1/mqtt-plus-timer.js.map +0 -1
- package/dst-stage1/mqtt-plus-trace.js.map +0 -1
- package/dst-stage1/mqtt-plus-util.js.map +0 -1
- package/dst-stage1/mqtt-plus-version.js.map +0 -1
- package/dst-stage1/mqtt-plus.js.map +0 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dst-stage1/mqtt-plus.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|