mqtt-plus 1.4.7 → 1.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -1
- package/dst-stage1/mqtt-plus-auth.js +4 -0
- package/dst-stage1/mqtt-plus-codec.js +4 -0
- package/dst-stage1/mqtt-plus-encode.d.ts +2 -0
- package/dst-stage1/mqtt-plus-encode.js +5 -2
- package/dst-stage1/mqtt-plus-event.js +3 -3
- package/dst-stage1/mqtt-plus-meta.js +4 -3
- package/dst-stage1/mqtt-plus-sink.js +8 -4
- package/dst-stage1/mqtt-plus-source.js +3 -2
- package/dst-stage1/mqtt-plus-subscription.d.ts +2 -0
- package/dst-stage1/mqtt-plus-subscription.js +62 -37
- package/dst-stage1/mqtt-plus-trace.js +3 -3
- package/dst-stage1/mqtt-plus-util.js +3 -3
- package/dst-stage2/mqtt-plus.cjs.js +94 -56
- package/dst-stage2/mqtt-plus.esm.js +94 -56
- package/dst-stage2/mqtt-plus.umd.js +10 -10
- package/package.json +1 -1
- package/src/mqtt-plus-auth.ts +4 -0
- package/src/mqtt-plus-codec.ts +4 -0
- package/src/mqtt-plus-encode.ts +6 -2
- package/src/mqtt-plus-event.ts +3 -3
- package/src/mqtt-plus-meta.ts +7 -6
- package/src/mqtt-plus-sink.ts +8 -4
- package/src/mqtt-plus-source.ts +3 -2
- package/src/mqtt-plus-subscription.ts +67 -37
- package/src/mqtt-plus-trace.ts +11 -11
- package/src/mqtt-plus-util.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
ChangeLog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
-
1.
|
|
5
|
+
1.4.8 (2026-02-22)
|
|
6
|
+
------------------
|
|
7
|
+
|
|
8
|
+
- PERFORMANCE: cache encoder/decoder in encoding functions
|
|
9
|
+
- BUGFIX: fix memory leak in destroy() for sink
|
|
10
|
+
- BUGFIX: namespace timers of sink() and source() to avoid conflicts
|
|
11
|
+
- BUGFIX: align event() share default with service/source/sink
|
|
12
|
+
- CLEANUP: refactor RefCountedSubscription class to be redundancy-free
|
|
13
|
+
- CLEANUP: various minor code cleanups (formatting, modernization)
|
|
14
|
+
|
|
15
|
+
1.4.7 (2026-02-22)
|
|
6
16
|
------------------
|
|
7
17
|
|
|
8
18
|
- IMPROVEMENT: provide a global "share" option
|
|
@@ -49,6 +49,10 @@ export class AuthTrait extends MetaTrait {
|
|
|
49
49
|
async issue(payload) {
|
|
50
50
|
if (this._credential === null)
|
|
51
51
|
throw new Error("credential has to be provided before issuing tokens");
|
|
52
|
+
if (payload.roles.length === 0)
|
|
53
|
+
throw new Error("payload.roles must be a non-empty array");
|
|
54
|
+
if (payload.roles.length > 64)
|
|
55
|
+
throw new Error("payload.roles must not exceed 64 roles");
|
|
52
56
|
const jwt = new SignJWT(payload);
|
|
53
57
|
jwt.setProtectedHeader({ alg: "HS256", typ: "JWT" });
|
|
54
58
|
const token = await jwt.sign(this._credential);
|
|
@@ -86,6 +86,8 @@ class Codec {
|
|
|
86
86
|
if (this.format === "cbor") {
|
|
87
87
|
if (!(data instanceof Uint8Array))
|
|
88
88
|
throw new Error("failed to decode CBOR format (data type is not Uint8Array)");
|
|
89
|
+
if (data.byteLength === 0)
|
|
90
|
+
throw new Error("failed to decode CBOR format (data is empty)");
|
|
89
91
|
try {
|
|
90
92
|
result = CBOR.decode(data, { tags: this.tags });
|
|
91
93
|
}
|
|
@@ -96,6 +98,8 @@ class Codec {
|
|
|
96
98
|
else if (this.format === "json") {
|
|
97
99
|
if (typeof data !== "string")
|
|
98
100
|
throw new Error("failed to decode JSON format (data type is not string)");
|
|
101
|
+
if (data.length === 0)
|
|
102
|
+
throw new Error("failed to decode JSON format (data is empty)");
|
|
99
103
|
try {
|
|
100
104
|
result = JSONX.parse(data);
|
|
101
105
|
}
|
|
@@ -2,6 +2,8 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import type { APISchema } from "./mqtt-plus-api";
|
|
3
3
|
import { CodecTrait } from "./mqtt-plus-codec";
|
|
4
4
|
export declare class EncodeTrait<T extends APISchema = APISchema> extends CodecTrait<T> {
|
|
5
|
+
private static encoder;
|
|
6
|
+
private static decoder;
|
|
5
7
|
str2buf(data: string): Uint8Array;
|
|
6
8
|
buf2str(data: Uint8Array): string;
|
|
7
9
|
arr2buf(data: Buffer | Uint8Array | Int8Array): Uint8Array;
|
|
@@ -26,13 +26,16 @@ import { Buffer } from "node:buffer";
|
|
|
26
26
|
import { CodecTrait } from "./mqtt-plus-codec";
|
|
27
27
|
/* encoding trait */
|
|
28
28
|
export class EncodeTrait extends CodecTrait {
|
|
29
|
+
/* reusable encoder/decoder instances */
|
|
30
|
+
static { this.encoder = new TextEncoder(); }
|
|
31
|
+
static { this.decoder = new TextDecoder(); }
|
|
29
32
|
/* convert character string to buffer */
|
|
30
33
|
str2buf(data) {
|
|
31
|
-
return
|
|
34
|
+
return EncodeTrait.encoder.encode(data);
|
|
32
35
|
}
|
|
33
36
|
/* convert buffer to character string */
|
|
34
37
|
buf2str(data) {
|
|
35
|
-
return
|
|
38
|
+
return EncodeTrait.decoder.decode(data);
|
|
36
39
|
}
|
|
37
40
|
/* convert byte-based typed array to buffer */
|
|
38
41
|
arr2buf(data) {
|
|
@@ -31,14 +31,14 @@ export class EventTrait extends AuthTrait {
|
|
|
31
31
|
let name;
|
|
32
32
|
let callback;
|
|
33
33
|
let options = {};
|
|
34
|
-
let share;
|
|
34
|
+
let share = this.options.share;
|
|
35
35
|
let auth;
|
|
36
36
|
if (typeof nameOrConfig === "object" && nameOrConfig !== null) {
|
|
37
37
|
/* object-based API */
|
|
38
38
|
name = nameOrConfig.name;
|
|
39
39
|
callback = nameOrConfig.callback;
|
|
40
40
|
options = nameOrConfig.options ?? {};
|
|
41
|
-
share = nameOrConfig.share;
|
|
41
|
+
share = nameOrConfig.share ?? this.options.share;
|
|
42
42
|
auth = nameOrConfig.auth;
|
|
43
43
|
}
|
|
44
44
|
else {
|
|
@@ -52,7 +52,7 @@ export class EventTrait extends AuthTrait {
|
|
|
52
52
|
if (this.onRequest.has(`event-emission:${name}`))
|
|
53
53
|
throw new Error(`event: event "${name}" already registered`);
|
|
54
54
|
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
55
|
-
const topicS = share ? `$share/${share}/${name}` : name;
|
|
55
|
+
const topicS = share !== "" ? `$share/${share}/${name}` : name;
|
|
56
56
|
const topicB = this.options.topicMake(topicS, "event-emission");
|
|
57
57
|
const topicD = this.options.topicMake(name, "event-emission", this.options.id);
|
|
58
58
|
/* remember the registration */
|
|
@@ -29,10 +29,11 @@ export class MetaTrait extends TimerTrait {
|
|
|
29
29
|
/* internal state */
|
|
30
30
|
this._meta = new Map();
|
|
31
31
|
}
|
|
32
|
-
meta(
|
|
33
|
-
|
|
32
|
+
meta(...args) {
|
|
33
|
+
const [key, value] = args;
|
|
34
|
+
if (args.length === 0)
|
|
34
35
|
return Object.fromEntries(this._meta);
|
|
35
|
-
else if (
|
|
36
|
+
else if (args.length === 1)
|
|
36
37
|
return this._meta.get(key);
|
|
37
38
|
else if (value === undefined || value === null)
|
|
38
39
|
this._meta.delete(key);
|
|
@@ -40,6 +40,8 @@ export class SinkTrait extends SourceTrait {
|
|
|
40
40
|
async destroy() {
|
|
41
41
|
for (const stream of this.pushStreams.values())
|
|
42
42
|
stream.destroy();
|
|
43
|
+
for (const spool of this.pushSpools.values())
|
|
44
|
+
await spool.unroll();
|
|
43
45
|
this.pushStreams.clear();
|
|
44
46
|
this.pushSpools.clear();
|
|
45
47
|
await super.destroy();
|
|
@@ -112,14 +114,15 @@ export class SinkTrait extends SourceTrait {
|
|
|
112
114
|
creditGranted: chunkCredit
|
|
113
115
|
} : undefined;
|
|
114
116
|
/* utility functions for timeout management */
|
|
115
|
-
const
|
|
117
|
+
const pushTimerId = `sink-push-recv:${requestId}`;
|
|
118
|
+
const refreshPushTimeout = () => this.timerRefresh(pushTimerId, () => {
|
|
116
119
|
const stream = this.pushStreams.get(requestId);
|
|
117
120
|
if (stream !== undefined)
|
|
118
121
|
stream.destroy(new Error("push stream timeout"));
|
|
119
122
|
const spool = this.pushSpools.get(requestId);
|
|
120
123
|
spool?.unroll();
|
|
121
124
|
});
|
|
122
|
-
const clearPushTimeout = () => this.timerClear(
|
|
125
|
+
const clearPushTimeout = () => this.timerClear(pushTimerId);
|
|
123
126
|
/* create a readable for buffering received chunks */
|
|
124
127
|
const readable = new Readable({
|
|
125
128
|
highWaterMark: chunkCredit > 0 ? chunkCredit * this.options.chunkSize : 16 * 1024,
|
|
@@ -246,11 +249,12 @@ export class SinkTrait extends SourceTrait {
|
|
|
246
249
|
const abortController = new AbortController();
|
|
247
250
|
const abortSignal = abortController.signal;
|
|
248
251
|
/* utility function for timeout refresh */
|
|
249
|
-
const
|
|
252
|
+
const pushTimerId = `sink-push-send:${requestId}`;
|
|
253
|
+
const refreshTimeout = () => this.timerRefresh(pushTimerId, () => {
|
|
250
254
|
abortController.abort(new Error(`push to sink "${name}" timed out`));
|
|
251
255
|
spool.unroll();
|
|
252
256
|
});
|
|
253
|
-
spool.roll(() => { this.timerClear(
|
|
257
|
+
spool.roll(() => { this.timerClear(pushTimerId); });
|
|
254
258
|
/* start timeout handler */
|
|
255
259
|
refreshTimeout();
|
|
256
260
|
/* send request and wait for response before sending chunks */
|
|
@@ -97,12 +97,13 @@ export class SourceTrait extends ServiceTrait {
|
|
|
97
97
|
await this.publishToTopic(responseTopic, message, { qos: options.qos ?? 2 });
|
|
98
98
|
};
|
|
99
99
|
/* utility functions for timeout management */
|
|
100
|
-
const
|
|
100
|
+
const sourceTimerId = `source-fetch-send:${requestId}`;
|
|
101
|
+
const refreshSourceTimeout = () => this.timerRefresh(sourceTimerId, () => {
|
|
101
102
|
const gate = this.sourceCreditGates.get(requestId);
|
|
102
103
|
if (gate !== undefined)
|
|
103
104
|
gate.abort();
|
|
104
105
|
});
|
|
105
|
-
const clearSourceTimeout = () => this.timerClear(
|
|
106
|
+
const clearSourceTimeout = () => this.timerClear(sourceTimerId);
|
|
106
107
|
refreshSourceTimeout();
|
|
107
108
|
/* callback for creating and sending a chunk message */
|
|
108
109
|
const sendChunk = async (chunk, error, final) => {
|
|
@@ -10,6 +10,8 @@ declare class RefCountedSubscription {
|
|
|
10
10
|
private lingers;
|
|
11
11
|
private unsubbing;
|
|
12
12
|
constructor(subscribeFn: (topic: string, options: IClientSubscribeOptions) => Promise<void>, unsubscribeFn: (topic: string) => Promise<void>, lingerMs?: number);
|
|
13
|
+
private incrementCount;
|
|
14
|
+
private decrementCount;
|
|
13
15
|
subscribe(topic: string, options?: IClientSubscribeOptions): Promise<void>;
|
|
14
16
|
unsubscribe(topic: string): Promise<void>;
|
|
15
17
|
flush(): Promise<void>;
|
|
@@ -24,20 +24,42 @@
|
|
|
24
24
|
import { BaseTrait } from "./mqtt-plus-base";
|
|
25
25
|
/* reference-counted subscription helper */
|
|
26
26
|
class RefCountedSubscription {
|
|
27
|
+
/* initial construction with configuration */
|
|
27
28
|
constructor(subscribeFn, unsubscribeFn, lingerMs = 30 * 1000) {
|
|
28
29
|
this.subscribeFn = subscribeFn;
|
|
29
30
|
this.unsubscribeFn = unsubscribeFn;
|
|
30
31
|
this.lingerMs = lingerMs;
|
|
32
|
+
/* internal state */
|
|
31
33
|
this.counts = new Map();
|
|
32
34
|
this.pending = new Map();
|
|
33
35
|
this.lingers = new Map();
|
|
34
36
|
this.unsubbing = new Map();
|
|
35
37
|
}
|
|
38
|
+
/* increment reference count for a topic */
|
|
39
|
+
incrementCount(topic) {
|
|
40
|
+
const count = this.counts.get(topic) ?? 0;
|
|
41
|
+
this.counts.set(topic, count + 1);
|
|
42
|
+
return count;
|
|
43
|
+
}
|
|
44
|
+
/* decrement reference count for a topic */
|
|
45
|
+
decrementCount(topic) {
|
|
46
|
+
const count = this.counts.get(topic);
|
|
47
|
+
if (count) {
|
|
48
|
+
if (count <= 1) {
|
|
49
|
+
this.counts.delete(topic);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
this.counts.set(topic, count - 1);
|
|
54
|
+
return count - 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
36
59
|
/* subscribe to a topic (reference-counted) */
|
|
37
60
|
async subscribe(topic, options = { qos: 2 }) {
|
|
38
61
|
/* increment count first to reserve our interest */
|
|
39
|
-
const count = this.
|
|
40
|
-
this.counts.set(topic, count + 1);
|
|
62
|
+
const count = this.incrementCount(topic);
|
|
41
63
|
/* optionally just cancel a pending linger unsubscription
|
|
42
64
|
(subscription is still kept active on the broker) */
|
|
43
65
|
const linger = this.lingers.get(topic);
|
|
@@ -48,24 +70,31 @@ class RefCountedSubscription {
|
|
|
48
70
|
}
|
|
49
71
|
/* if we are the first, we must perform the actual subscription */
|
|
50
72
|
if (count === 0) {
|
|
73
|
+
/* create a deferred promise and store it in pending immediately,
|
|
74
|
+
so concurrent subscribers arriving during the await below
|
|
75
|
+
will find and await it instead of returning prematurely */
|
|
76
|
+
let resolve;
|
|
77
|
+
let reject;
|
|
78
|
+
const deferred = new Promise((res, rej) => {
|
|
79
|
+
resolve = res;
|
|
80
|
+
reject = rej;
|
|
81
|
+
});
|
|
82
|
+
this.pending.set(topic, deferred);
|
|
51
83
|
/* await any in-flight linger unsubscription to avoid a race
|
|
52
84
|
where the broker processes UNSUBSCRIBE after our SUBSCRIBE */
|
|
53
85
|
const inflight = this.unsubbing.get(topic);
|
|
54
86
|
if (inflight)
|
|
55
87
|
await inflight;
|
|
56
|
-
|
|
88
|
+
/* perform the actual subscription */
|
|
89
|
+
const promise = this.subscribeFn(topic, options).then(() => {
|
|
57
90
|
this.pending.delete(topic);
|
|
91
|
+
resolve();
|
|
58
92
|
}).catch((err) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
this.counts.delete(topic);
|
|
63
|
-
else
|
|
64
|
-
this.counts.set(topic, count - 1);
|
|
65
|
-
}
|
|
93
|
+
this.pending.delete(topic);
|
|
94
|
+
this.decrementCount(topic);
|
|
95
|
+
reject(err);
|
|
66
96
|
throw err;
|
|
67
97
|
});
|
|
68
|
-
this.pending.set(topic, promise);
|
|
69
98
|
return promise;
|
|
70
99
|
}
|
|
71
100
|
else {
|
|
@@ -73,39 +102,35 @@ class RefCountedSubscription {
|
|
|
73
102
|
const pending = this.pending.get(topic);
|
|
74
103
|
if (pending)
|
|
75
104
|
return pending.catch((err) => {
|
|
76
|
-
|
|
77
|
-
if (count) {
|
|
78
|
-
if (count <= 1)
|
|
79
|
-
this.counts.delete(topic);
|
|
80
|
-
else
|
|
81
|
-
this.counts.set(topic, count - 1);
|
|
82
|
-
}
|
|
105
|
+
this.decrementCount(topic);
|
|
83
106
|
throw err;
|
|
84
107
|
});
|
|
85
108
|
}
|
|
86
109
|
}
|
|
87
110
|
/* unsubscribe from a topic (reference-counted) */
|
|
88
111
|
async unsubscribe(topic) {
|
|
89
|
-
const count = this.
|
|
90
|
-
if (count) {
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
this.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
const count = this.decrementCount(topic);
|
|
113
|
+
if (count === 0) {
|
|
114
|
+
if (this.lingerMs > 0) {
|
|
115
|
+
/* defer the actual broker unsubscription */
|
|
116
|
+
const timer = setTimeout(() => {
|
|
117
|
+
this.lingers.delete(topic);
|
|
118
|
+
const promise = this.unsubscribeFn(topic).catch(() => { }).finally(() => {
|
|
119
|
+
this.unsubbing.delete(topic);
|
|
120
|
+
});
|
|
121
|
+
this.unsubbing.set(topic, promise);
|
|
122
|
+
}, this.lingerMs);
|
|
123
|
+
this.lingers.set(topic, timer);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
/* perform the unsubscription immediately, but still store the
|
|
127
|
+
promise in unsubbing so a concurrent subscribe can await it */
|
|
128
|
+
const promise = this.unsubscribeFn(topic).catch(() => { }).finally(() => {
|
|
129
|
+
this.unsubbing.delete(topic);
|
|
130
|
+
});
|
|
131
|
+
this.unsubbing.set(topic, promise);
|
|
132
|
+
await promise;
|
|
106
133
|
}
|
|
107
|
-
else
|
|
108
|
-
this.counts.set(topic, count - 1);
|
|
109
134
|
}
|
|
110
135
|
}
|
|
111
136
|
/* flush all pending linger timers and unsubscribe */
|
|
@@ -38,9 +38,9 @@ class LogEvent {
|
|
|
38
38
|
if (this.msg instanceof Promise)
|
|
39
39
|
this.msg = await this.msg.catch(() => "<resolve-failed>");
|
|
40
40
|
if (this.data)
|
|
41
|
-
for (const
|
|
42
|
-
if (this.data[
|
|
43
|
-
this.data[
|
|
41
|
+
for (const k of Object.keys(this.data))
|
|
42
|
+
if (this.data[k] instanceof Promise)
|
|
43
|
+
this.data[k] = await this.data[k].catch(() => "<resolve-failed>");
|
|
44
44
|
}
|
|
45
45
|
/* render log event as string */
|
|
46
46
|
toString() {
|
|
@@ -85,9 +85,9 @@ function uint8ArrayConcat(arrays) {
|
|
|
85
85
|
const totalLength = arrays.reduce((acc, value) => acc + value.byteLength, 0);
|
|
86
86
|
const result = new Uint8Array(totalLength);
|
|
87
87
|
let offset = 0;
|
|
88
|
-
for (const
|
|
89
|
-
result.set(
|
|
90
|
-
offset +=
|
|
88
|
+
for (const a of arrays) {
|
|
89
|
+
result.set(a, offset);
|
|
90
|
+
offset += a.byteLength;
|
|
91
91
|
}
|
|
92
92
|
return result;
|
|
93
93
|
}
|