mqtt-plus 1.2.0 → 1.2.1
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 +7 -0
- package/dst-stage1/mqtt-plus-auth.js +7 -3
- package/dst-stage1/mqtt-plus-codec.d.ts +2 -2
- package/dst-stage1/mqtt-plus-codec.js +8 -8
- package/dst-stage1/mqtt-plus-encode.js +1 -1
- package/dst-stage1/mqtt-plus-event.js +5 -5
- package/dst-stage1/mqtt-plus-msg.js +18 -24
- package/dst-stage1/mqtt-plus-service.d.ts +0 -2
- package/dst-stage1/mqtt-plus-service.js +11 -45
- package/dst-stage1/mqtt-plus-sink.d.ts +0 -2
- package/dst-stage1/mqtt-plus-sink.js +9 -35
- package/dst-stage1/mqtt-plus-source.d.ts +0 -2
- package/dst-stage1/mqtt-plus-source.js +15 -39
- package/dst-stage1/mqtt-plus-trace.d.ts +2 -2
- package/dst-stage1/mqtt-plus-trace.js +1 -1
- package/dst-stage1/mqtt-plus-util.d.ts +10 -0
- package/dst-stage1/mqtt-plus-util.js +34 -0
- package/dst-stage2/mqtt-plus.cjs.js +100 -149
- package/dst-stage2/mqtt-plus.esm.js +100 -149
- package/dst-stage2/mqtt-plus.umd.js +11 -11
- package/package.json +1 -1
- package/src/mqtt-plus-auth.ts +7 -3
- package/src/mqtt-plus-codec.ts +7 -7
- package/src/mqtt-plus-encode.ts +1 -1
- package/src/mqtt-plus-event.ts +5 -5
- package/src/mqtt-plus-msg.ts +39 -51
- package/src/mqtt-plus-service.ts +15 -50
- package/src/mqtt-plus-sink.ts +14 -37
- package/src/mqtt-plus-source.ts +20 -41
- package/src/mqtt-plus-trace.ts +2 -2
- package/src/mqtt-plus-util.ts +38 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
ChangeLog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
1.2.1 (2026-02-07)
|
|
6
|
+
------------------
|
|
7
|
+
|
|
8
|
+
- REFACTOR: use a reference counting subscription class
|
|
9
|
+
- CLEANUP: improve internal validation logic
|
|
10
|
+
- CLEANUP: various code cleanups
|
|
11
|
+
|
|
5
12
|
1.2.0 (2026-02-06)
|
|
6
13
|
------------------
|
|
7
14
|
|
|
@@ -41,9 +41,9 @@ export class AuthTrait extends MetaTrait {
|
|
|
41
41
|
if (credential.length === 0)
|
|
42
42
|
throw new Error("credential must not be empty");
|
|
43
43
|
/* use a derived key with minimum length of 32 for JWT HS256 */
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
this._credential = pbkdf2.deriveKey(sha256.SHA256,
|
|
44
|
+
const pass = new TextEncoder().encode(credential);
|
|
45
|
+
const salt = new TextEncoder().encode("mqtt-plus");
|
|
46
|
+
this._credential = pbkdf2.deriveKey(sha256.SHA256, pass, salt, 100000, 32);
|
|
47
47
|
}
|
|
48
48
|
/* issue client-side token on server-side */
|
|
49
49
|
async issue(payload) {
|
|
@@ -93,6 +93,10 @@ export class AuthTrait extends MetaTrait {
|
|
|
93
93
|
continue;
|
|
94
94
|
if (payload.id && payload.id !== clientId)
|
|
95
95
|
continue;
|
|
96
|
+
if (!Array.isArray(payload.roles))
|
|
97
|
+
continue;
|
|
98
|
+
if (payload.roles.length > 64)
|
|
99
|
+
continue;
|
|
96
100
|
for (const role of roles) {
|
|
97
101
|
if (payload.roles.includes(role)) {
|
|
98
102
|
authenticated = true;
|
|
@@ -7,10 +7,10 @@ export declare class JSONX {
|
|
|
7
7
|
static parse(json: string): unknown;
|
|
8
8
|
}
|
|
9
9
|
declare class Codec {
|
|
10
|
-
private
|
|
10
|
+
private format;
|
|
11
11
|
private types;
|
|
12
12
|
private tags;
|
|
13
|
-
constructor(
|
|
13
|
+
constructor(format: "cbor" | "json");
|
|
14
14
|
encode(data: unknown): Uint8Array | string;
|
|
15
15
|
decode(data: Uint8Array | string): unknown;
|
|
16
16
|
}
|
|
@@ -46,8 +46,8 @@ export class JSONX {
|
|
|
46
46
|
}
|
|
47
47
|
/* the encoder/decoder abstraction */
|
|
48
48
|
class Codec {
|
|
49
|
-
constructor(
|
|
50
|
-
this.
|
|
49
|
+
constructor(format) {
|
|
50
|
+
this.format = format;
|
|
51
51
|
this.types = new CBOR.TypeEncoderMap();
|
|
52
52
|
this.tags = new Map();
|
|
53
53
|
/* support direct encoding/decoding of Buffer */
|
|
@@ -61,7 +61,7 @@ class Codec {
|
|
|
61
61
|
}
|
|
62
62
|
encode(data) {
|
|
63
63
|
let result;
|
|
64
|
-
if (this.
|
|
64
|
+
if (this.format === "cbor") {
|
|
65
65
|
try {
|
|
66
66
|
result = CBOR.encode(data, { types: this.types });
|
|
67
67
|
}
|
|
@@ -69,7 +69,7 @@ class Codec {
|
|
|
69
69
|
throw new Error("failed to encode CBOR format", { cause: ex });
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
else if (this.
|
|
72
|
+
else if (this.format === "json") {
|
|
73
73
|
try {
|
|
74
74
|
result = JSONX.stringify(data);
|
|
75
75
|
}
|
|
@@ -78,12 +78,12 @@ class Codec {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
else
|
|
81
|
-
throw new Error(`invalid format "${this.
|
|
81
|
+
throw new Error(`invalid format "${this.format}"`);
|
|
82
82
|
return result;
|
|
83
83
|
}
|
|
84
84
|
decode(data) {
|
|
85
85
|
let result;
|
|
86
|
-
if (this.
|
|
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
89
|
try {
|
|
@@ -93,7 +93,7 @@ class Codec {
|
|
|
93
93
|
throw new Error("failed to decode CBOR format", { cause: ex });
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
else if (this.
|
|
96
|
+
else if (this.format === "json") {
|
|
97
97
|
if (typeof data !== "string")
|
|
98
98
|
throw new Error("failed to decode JSON format (data type is not string)");
|
|
99
99
|
try {
|
|
@@ -104,7 +104,7 @@ class Codec {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
else
|
|
107
|
-
throw new Error(`invalid format "${this.
|
|
107
|
+
throw new Error(`invalid format "${this.format}"`);
|
|
108
108
|
return result;
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -45,7 +45,7 @@ export class EncodeTrait extends CodecTrait {
|
|
|
45
45
|
}
|
|
46
46
|
buf2arr(data, cons) {
|
|
47
47
|
let arr;
|
|
48
|
-
if (
|
|
48
|
+
if (cons === Buffer)
|
|
49
49
|
arr = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
50
50
|
else if (cons === Uint8Array)
|
|
51
51
|
arr = data;
|
|
@@ -56,9 +56,9 @@ export class EventTrait extends AuthTrait {
|
|
|
56
56
|
if (this.events.has(name))
|
|
57
57
|
throw new Error(`event: event "${name}" already registered`);
|
|
58
58
|
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
59
|
-
const
|
|
60
|
-
const topicB = this.options.topicMake(
|
|
61
|
-
const topicD = this.options.topicMake(
|
|
59
|
+
const topicS = share ? `$share/${share}/${name}` : name;
|
|
60
|
+
const topicB = this.options.topicMake(topicS, "event-emission");
|
|
61
|
+
const topicD = this.options.topicMake(name, "event-emission", this.options.id);
|
|
62
62
|
/* subscribe to MQTT topics */
|
|
63
63
|
await Promise.all([
|
|
64
64
|
this._subscribeTopic(topicB, { qos: 0, ...options }),
|
|
@@ -142,14 +142,14 @@ export class EventTrait extends AuthTrait {
|
|
|
142
142
|
if (topicMatch.name !== name)
|
|
143
143
|
throw new Error(`event name mismatch between topic "${topicMatch.name}" and payload "${name}"`);
|
|
144
144
|
const handler = this.events.get(name);
|
|
145
|
+
if (handler === undefined)
|
|
146
|
+
throw new Error(`handler for event "${name}" not found`);
|
|
145
147
|
const params = parsed.params ?? [];
|
|
146
148
|
const info = { sender: parsed.sender ?? "" };
|
|
147
149
|
if (parsed.receiver)
|
|
148
150
|
info.receiver = parsed.receiver;
|
|
149
151
|
if (parsed.meta)
|
|
150
152
|
info.meta = parsed.meta;
|
|
151
|
-
if (handler === undefined)
|
|
152
|
-
throw new Error(`handler for event "${name}" not found`);
|
|
153
153
|
if (handler.auth)
|
|
154
154
|
info.authenticated = await this.authenticated(parsed.sender, parsed.auth, handler.auth);
|
|
155
155
|
Promise.resolve().then(() => {
|
|
@@ -26,8 +26,8 @@ import * as v from "valibot";
|
|
|
26
26
|
import { EncodeTrait } from "./mqtt-plus-encode";
|
|
27
27
|
/* meta validation schema (non-array plain object) */
|
|
28
28
|
const MetaSchema = v.pipe(v.record(v.string(), v.unknown()), v.check((data) => !Array.isArray(data)));
|
|
29
|
-
/* reusable
|
|
30
|
-
const
|
|
29
|
+
/* reusable auth validation schema (max 8 tokens, max 8192 chars each) */
|
|
30
|
+
const AuthSchema = v.pipe(v.array(v.pipe(v.string(), v.maxLength(8192))), v.maxLength(8));
|
|
31
31
|
/* base class */
|
|
32
32
|
class Base {
|
|
33
33
|
constructor(type, id, sender, receiver) {
|
|
@@ -57,8 +57,8 @@ const EventEmissionSchema = v.strictObject({
|
|
|
57
57
|
...BaseSchema,
|
|
58
58
|
type: v.literal("event-emission"),
|
|
59
59
|
name: v.string(),
|
|
60
|
-
params: v.optional(v.array(v.unknown())),
|
|
61
|
-
auth: v.optional(
|
|
60
|
+
params: v.optional(v.pipe(v.array(v.unknown()), v.maxLength(64))),
|
|
61
|
+
auth: v.optional(AuthSchema),
|
|
62
62
|
meta: v.optional(MetaSchema)
|
|
63
63
|
});
|
|
64
64
|
/* service request */
|
|
@@ -75,8 +75,8 @@ const ServiceCallRequestSchema = v.strictObject({
|
|
|
75
75
|
...BaseSchema,
|
|
76
76
|
type: v.literal("service-call-request"),
|
|
77
77
|
name: v.string(),
|
|
78
|
-
params: v.optional(v.array(v.unknown())),
|
|
79
|
-
auth: v.optional(
|
|
78
|
+
params: v.optional(v.pipe(v.array(v.unknown()), v.maxLength(64))),
|
|
79
|
+
auth: v.optional(AuthSchema),
|
|
80
80
|
meta: v.optional(MetaSchema)
|
|
81
81
|
});
|
|
82
82
|
/* service response */
|
|
@@ -107,8 +107,8 @@ const SinkPushRequestSchema = v.strictObject({
|
|
|
107
107
|
...BaseSchema,
|
|
108
108
|
type: v.literal("sink-push-request"),
|
|
109
109
|
name: v.string(),
|
|
110
|
-
params: v.optional(v.array(v.unknown())),
|
|
111
|
-
auth: v.optional(
|
|
110
|
+
params: v.optional(v.pipe(v.array(v.unknown()), v.maxLength(64))),
|
|
111
|
+
auth: v.optional(AuthSchema),
|
|
112
112
|
meta: v.optional(MetaSchema)
|
|
113
113
|
});
|
|
114
114
|
/* sink push response (ack/nak) */
|
|
@@ -126,7 +126,7 @@ const SinkPushResponseSchema = v.strictObject({
|
|
|
126
126
|
type: v.literal("sink-push-response"),
|
|
127
127
|
name: v.string(),
|
|
128
128
|
error: v.optional(v.string()),
|
|
129
|
-
auth: v.optional(
|
|
129
|
+
auth: v.optional(AuthSchema),
|
|
130
130
|
meta: v.optional(MetaSchema)
|
|
131
131
|
});
|
|
132
132
|
/* sink push chunk (actual data transfer) */
|
|
@@ -143,7 +143,7 @@ const SinkPushChunkSchema = v.strictObject({
|
|
|
143
143
|
...BaseSchema,
|
|
144
144
|
type: v.literal("sink-push-chunk"),
|
|
145
145
|
name: v.string(),
|
|
146
|
-
chunk: v.optional(
|
|
146
|
+
chunk: v.optional(v.instance(Uint8Array)),
|
|
147
147
|
error: v.optional(v.string()),
|
|
148
148
|
final: v.optional(v.boolean())
|
|
149
149
|
});
|
|
@@ -161,8 +161,8 @@ const SourceFetchRequestSchema = v.strictObject({
|
|
|
161
161
|
...BaseSchema,
|
|
162
162
|
type: v.literal("source-fetch-request"),
|
|
163
163
|
name: v.string(),
|
|
164
|
-
params: v.optional(v.array(v.unknown())),
|
|
165
|
-
auth: v.optional(
|
|
164
|
+
params: v.optional(v.pipe(v.array(v.unknown()), v.maxLength(64))),
|
|
165
|
+
auth: v.optional(AuthSchema),
|
|
166
166
|
meta: v.optional(MetaSchema)
|
|
167
167
|
});
|
|
168
168
|
/* source fetch response (ack/nak) */
|
|
@@ -180,7 +180,7 @@ const SourceFetchResponseSchema = v.strictObject({
|
|
|
180
180
|
type: v.literal("source-fetch-response"),
|
|
181
181
|
name: v.string(),
|
|
182
182
|
error: v.optional(v.string()),
|
|
183
|
-
auth: v.optional(
|
|
183
|
+
auth: v.optional(AuthSchema),
|
|
184
184
|
meta: v.optional(MetaSchema)
|
|
185
185
|
});
|
|
186
186
|
/* source fetch chunk (actual data transfer) */
|
|
@@ -197,55 +197,48 @@ const SourceFetchChunkSchema = v.strictObject({
|
|
|
197
197
|
...BaseSchema,
|
|
198
198
|
type: v.literal("source-fetch-chunk"),
|
|
199
199
|
name: v.string(),
|
|
200
|
-
chunk: v.optional(
|
|
200
|
+
chunk: v.optional(v.instance(Uint8Array)),
|
|
201
201
|
error: v.optional(v.string()),
|
|
202
202
|
final: v.optional(v.boolean())
|
|
203
203
|
});
|
|
204
204
|
/* utility class */
|
|
205
205
|
class Msg {
|
|
206
|
-
/*
|
|
206
|
+
/* factories for creating objects */
|
|
207
207
|
makeEventEmission(id, name, params, sender, receiver, auth, meta) {
|
|
208
208
|
return new EventEmission(id, name, params, sender, receiver, auth, meta);
|
|
209
209
|
}
|
|
210
|
-
/* factory for service request */
|
|
211
210
|
makeServiceCallRequest(id, name, params, sender, receiver, auth, meta) {
|
|
212
211
|
return new ServiceCallRequest(id, name, params, sender, receiver, auth, meta);
|
|
213
212
|
}
|
|
214
|
-
/* factory for service response */
|
|
215
213
|
makeServiceCallResponse(id, result, error, sender, receiver) {
|
|
216
214
|
return new ServiceCallResponse(id, result, error, sender, receiver);
|
|
217
215
|
}
|
|
218
|
-
/* factory for sink push request */
|
|
219
216
|
makeSinkPushRequest(id, name, params, sender, receiver, auth, meta) {
|
|
220
217
|
return new SinkPushRequest(id, name, params, sender, receiver, auth, meta);
|
|
221
218
|
}
|
|
222
|
-
/* factory for sink push response */
|
|
223
219
|
makeSinkPushResponse(id, name, error, sender, receiver, auth, meta) {
|
|
224
220
|
return new SinkPushResponse(id, name, error, sender, receiver, auth, meta);
|
|
225
221
|
}
|
|
226
|
-
/* factory for sink push chunk */
|
|
227
222
|
makeSinkPushChunk(id, name, chunk, error, final, sender, receiver) {
|
|
228
223
|
return new SinkPushChunk(id, name, chunk, error, final, sender, receiver);
|
|
229
224
|
}
|
|
230
|
-
/* factory for source fetch request */
|
|
231
225
|
makeSourceFetchRequest(id, name, params, sender, receiver, auth, meta) {
|
|
232
226
|
return new SourceFetchRequest(id, name, params, sender, receiver, auth, meta);
|
|
233
227
|
}
|
|
234
|
-
/* factory for source fetch response */
|
|
235
228
|
makeSourceFetchResponse(id, name, error, sender, receiver, auth, meta) {
|
|
236
229
|
return new SourceFetchResponse(id, name, error, sender, receiver, auth, meta);
|
|
237
230
|
}
|
|
238
|
-
/* factory for source fetch chunk */
|
|
239
231
|
makeSourceFetchChunk(id, name, chunk, error, final, sender, receiver) {
|
|
240
232
|
return new SourceFetchChunk(id, name, chunk, error, final, sender, receiver);
|
|
241
233
|
}
|
|
242
234
|
/* parse any object into typed object */
|
|
243
235
|
parse(obj) {
|
|
236
|
+
/* sanity check input */
|
|
244
237
|
if (typeof obj !== "object" || obj === null)
|
|
245
238
|
throw new Error("invalid argument: not an object");
|
|
246
239
|
if (typeof obj.type !== "string")
|
|
247
240
|
throw new Error("invalid object: missing or invalid \"type\" field");
|
|
248
|
-
/*
|
|
241
|
+
/* helper function for Valibot-based validation */
|
|
249
242
|
const parseObject = (obj, name, schema) => {
|
|
250
243
|
const res = v.safeParse(schema, obj);
|
|
251
244
|
if (!res.success) {
|
|
@@ -254,6 +247,7 @@ class Msg {
|
|
|
254
247
|
}
|
|
255
248
|
return res.output;
|
|
256
249
|
};
|
|
250
|
+
/* dispatch according to type indication by field */
|
|
257
251
|
if (obj.type === "event-emission") {
|
|
258
252
|
const out = parseObject(obj, "EventEmission", EventEmissionSchema);
|
|
259
253
|
return this.makeEventEmission(out.id, out.name, out.params, out.sender, out.receiver, out.auth, out.meta);
|
|
@@ -23,7 +23,5 @@ export declare class ServiceTrait<T extends APISchema = APISchema> extends Event
|
|
|
23
23
|
options?: IClientPublishOptions;
|
|
24
24
|
meta?: Record<string, any>;
|
|
25
25
|
}): Promise<ReturnType<T[K]>>;
|
|
26
|
-
private callSubscribe;
|
|
27
|
-
private callUnsubscribe;
|
|
28
26
|
protected _dispatchMessage(topic: string, parsed: any): Promise<void>;
|
|
29
27
|
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { nanoid } from "nanoid";
|
|
25
25
|
/* internal requirements */
|
|
26
|
+
import { RefCountedSubscription } from "./mqtt-plus-util";
|
|
26
27
|
import { ServiceCallRequest, ServiceCallResponse } from "./mqtt-plus-msg";
|
|
27
28
|
import { EventTrait } from "./mqtt-plus-event";
|
|
28
29
|
/* Service Communication Trait */
|
|
@@ -32,7 +33,7 @@ export class ServiceTrait extends EventTrait {
|
|
|
32
33
|
/* internal state */
|
|
33
34
|
this.services = new Map();
|
|
34
35
|
this.callCallbacks = new Map();
|
|
35
|
-
this.callSubscriptions = new
|
|
36
|
+
this.callSubscriptions = new RefCountedSubscription((topic, options) => this._subscribeTopic(topic, options), (topic) => this._unsubscribeTopic(topic), (err) => this.error(err));
|
|
36
37
|
}
|
|
37
38
|
async service(nameOrConfig, ...args) {
|
|
38
39
|
/* determine actual parameters */
|
|
@@ -58,9 +59,9 @@ export class ServiceTrait extends EventTrait {
|
|
|
58
59
|
if (this.services.has(name))
|
|
59
60
|
throw new Error(`register: service "${name}" already registered`);
|
|
60
61
|
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
61
|
-
const
|
|
62
|
-
const topicB = this.options.topicMake(
|
|
63
|
-
const topicD = this.options.topicMake(
|
|
62
|
+
const topicS = `$share/${share}/${name}`;
|
|
63
|
+
const topicB = this.options.topicMake(topicS, "service-call-request");
|
|
64
|
+
const topicD = this.options.topicMake(name, "service-call-request", this.options.id);
|
|
64
65
|
/* subscribe to MQTT topics */
|
|
65
66
|
await Promise.all([
|
|
66
67
|
this._subscribeTopic(topicB, { qos: 2, ...options }),
|
|
@@ -114,12 +115,13 @@ export class ServiceTrait extends EventTrait {
|
|
|
114
115
|
/* generate unique request id */
|
|
115
116
|
const requestId = nanoid();
|
|
116
117
|
/* subscribe to MQTT response topic */
|
|
117
|
-
|
|
118
|
+
const responseTopic = this.options.topicMake(name, "service-call-response", this.options.id);
|
|
119
|
+
await this.callSubscriptions.subscribe(responseTopic, { qos: options.qos ?? 2 });
|
|
118
120
|
/* create promise for MQTT response handling */
|
|
119
121
|
const promise = new Promise((resolve, reject) => {
|
|
120
122
|
let timer = setTimeout(() => {
|
|
121
123
|
this.callCallbacks.delete(requestId);
|
|
122
|
-
this.
|
|
124
|
+
this.callSubscriptions.unsubscribe(responseTopic);
|
|
123
125
|
timer = null;
|
|
124
126
|
reject(new Error("communication timeout"));
|
|
125
127
|
}, this.options.timeout);
|
|
@@ -150,49 +152,12 @@ export class ServiceTrait extends EventTrait {
|
|
|
150
152
|
const pendingRequest = this.callCallbacks.get(requestId);
|
|
151
153
|
if (pendingRequest !== undefined) {
|
|
152
154
|
this.callCallbacks.delete(requestId);
|
|
153
|
-
this.
|
|
155
|
+
this.callSubscriptions.unsubscribe(responseTopic);
|
|
154
156
|
pendingRequest.callback(err, undefined);
|
|
155
157
|
}
|
|
156
158
|
});
|
|
157
159
|
return promise;
|
|
158
160
|
}
|
|
159
|
-
/* subscribe to RPC response */
|
|
160
|
-
async callSubscribe(service, options = { qos: 2 }) {
|
|
161
|
-
/* generate corresponding MQTT topic */
|
|
162
|
-
const topic = this.options.topicMake(service, "service-call-response", this.options.id);
|
|
163
|
-
/* subscribe to MQTT topic and remember subscription */
|
|
164
|
-
const count = this.callSubscriptions.get(topic) ?? 0;
|
|
165
|
-
this.callSubscriptions.set(topic, count + 1);
|
|
166
|
-
if (count === 0) {
|
|
167
|
-
await this._subscribeTopic(topic, options).catch((err) => {
|
|
168
|
-
const currentCount = this.callSubscriptions.get(topic) ?? 0;
|
|
169
|
-
if (currentCount > 1)
|
|
170
|
-
this.callSubscriptions.set(topic, currentCount - 1);
|
|
171
|
-
else
|
|
172
|
-
this.callSubscriptions.delete(topic);
|
|
173
|
-
this.error(err);
|
|
174
|
-
throw err;
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
/* unsubscribe from RPC response */
|
|
179
|
-
callUnsubscribe(service) {
|
|
180
|
-
/* generate corresponding MQTT topic */
|
|
181
|
-
const topic = this.options.topicMake(service, "service-call-response", this.options.id);
|
|
182
|
-
/* short-circuit processing if (no longer) subscribed */
|
|
183
|
-
if (!this.callSubscriptions.has(topic))
|
|
184
|
-
return;
|
|
185
|
-
/* unsubscribe from MQTT topic and forget subscription */
|
|
186
|
-
const count = this.callSubscriptions.get(topic) ?? 0;
|
|
187
|
-
if (count > 1)
|
|
188
|
-
this.callSubscriptions.set(topic, count - 1);
|
|
189
|
-
else {
|
|
190
|
-
this.callSubscriptions.delete(topic);
|
|
191
|
-
this._unsubscribeTopic(topic).catch((err) => {
|
|
192
|
-
this.error(err);
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
161
|
/* dispatch message (Service pattern handling) */
|
|
197
162
|
async _dispatchMessage(topic, parsed) {
|
|
198
163
|
await super._dispatchMessage(topic, parsed);
|
|
@@ -263,7 +228,8 @@ export class ServiceTrait extends EventTrait {
|
|
|
263
228
|
request.callback(undefined, parsed.result);
|
|
264
229
|
/* unsubscribe from response */
|
|
265
230
|
this.callCallbacks.delete(requestId);
|
|
266
|
-
this.
|
|
231
|
+
const respTopic = this.options.topicMake(request.name, "service-call-response", this.options.id);
|
|
232
|
+
this.callSubscriptions.unsubscribe(respTopic);
|
|
267
233
|
}
|
|
268
234
|
}
|
|
269
235
|
}
|
|
@@ -27,7 +27,5 @@ export declare class SinkTrait<T extends APISchema = APISchema> extends SourceTr
|
|
|
27
27
|
options?: IClientPublishOptions;
|
|
28
28
|
meta?: Record<string, any>;
|
|
29
29
|
}): Promise<void>;
|
|
30
|
-
private pushSubscribe;
|
|
31
|
-
private pushUnsubscribe;
|
|
32
30
|
protected _dispatchMessage(topic: string, parsed: any): Promise<void>;
|
|
33
31
|
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { Readable } from "node:stream";
|
|
26
26
|
import { nanoid } from "nanoid";
|
|
27
27
|
/* internal requirements */
|
|
28
|
-
import { streamToBuffer, sendBufferAsChunks, sendStreamAsChunks } from "./mqtt-plus-util";
|
|
28
|
+
import { RefCountedSubscription, streamToBuffer, sendBufferAsChunks, sendStreamAsChunks } from "./mqtt-plus-util";
|
|
29
29
|
import { SinkPushRequest, SinkPushResponse, SinkPushChunk } from "./mqtt-plus-msg";
|
|
30
30
|
import { SourceTrait } from "./mqtt-plus-source";
|
|
31
31
|
/* Sink Push Communication Trait */
|
|
@@ -37,7 +37,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
37
37
|
this.pushStreams = new Map();
|
|
38
38
|
this.pushTimers = new Map();
|
|
39
39
|
this.pushCallbacks = new Map();
|
|
40
|
-
this.pushSubscriptions = new
|
|
40
|
+
this.pushSubscriptions = new RefCountedSubscription((topic, options) => this._subscribeTopic(topic, options), (topic) => this._unsubscribeTopic(topic), (err) => this.error(err));
|
|
41
41
|
}
|
|
42
42
|
async sink(nameOrConfig, ...args) {
|
|
43
43
|
/* determine actual parameters */
|
|
@@ -63,10 +63,10 @@ export class SinkTrait extends SourceTrait {
|
|
|
63
63
|
if (this.sinks.has(name))
|
|
64
64
|
throw new Error(`sink: sink "${name}" already established`);
|
|
65
65
|
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
66
|
-
const
|
|
67
|
-
const topicReqB = this.options.topicMake(
|
|
68
|
-
const topicReqD = this.options.topicMake(
|
|
69
|
-
const topicChunkD = this.options.topicMake(
|
|
66
|
+
const topicS = `$share/${share}/${name}`;
|
|
67
|
+
const topicReqB = this.options.topicMake(topicS, "sink-push-request");
|
|
68
|
+
const topicReqD = this.options.topicMake(name, "sink-push-request", this.options.id);
|
|
69
|
+
const topicChunkD = this.options.topicMake(name, "sink-push-chunk", this.options.id);
|
|
70
70
|
/* subscribe to MQTT topics */
|
|
71
71
|
await Promise.all([
|
|
72
72
|
this._subscribeTopic(topicReqB, { qos: 2, ...options }),
|
|
@@ -127,7 +127,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
127
127
|
const requestId = nanoid();
|
|
128
128
|
/* subscribe to response topic (for ack/nak) */
|
|
129
129
|
const responseTopic = this.options.topicMake(name, "sink-push-response", this.options.id);
|
|
130
|
-
await this.
|
|
130
|
+
await this.pushSubscriptions.subscribe(responseTopic, { qos: 2 });
|
|
131
131
|
/* define timer */
|
|
132
132
|
let timer = null;
|
|
133
133
|
/* utility function for cleanup */
|
|
@@ -136,7 +136,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
136
136
|
clearTimeout(timer);
|
|
137
137
|
timer = null;
|
|
138
138
|
}
|
|
139
|
-
this.
|
|
139
|
+
this.pushSubscriptions.unsubscribe(responseTopic);
|
|
140
140
|
this.pushCallbacks.delete(requestId);
|
|
141
141
|
};
|
|
142
142
|
/* send request and wait for response before sending chunks */
|
|
@@ -188,31 +188,6 @@ export class SinkTrait extends SourceTrait {
|
|
|
188
188
|
/* split buffer into chunks and send them */
|
|
189
189
|
await sendBufferAsChunks(data, this.options.chunkSize, sendChunk);
|
|
190
190
|
}
|
|
191
|
-
/* subscribe to sink push response topic with reference counting */
|
|
192
|
-
async pushSubscribe(topic, options = { qos: 2 }) {
|
|
193
|
-
const count = this.pushSubscriptions.get(topic) ?? 0;
|
|
194
|
-
this.pushSubscriptions.set(topic, count + 1);
|
|
195
|
-
if (count === 0) {
|
|
196
|
-
await this._subscribeTopic(topic, options).catch((err) => {
|
|
197
|
-
const currentCount = this.pushSubscriptions.get(topic) ?? 0;
|
|
198
|
-
if (currentCount > 1)
|
|
199
|
-
this.pushSubscriptions.set(topic, currentCount - 1);
|
|
200
|
-
else
|
|
201
|
-
this.pushSubscriptions.delete(topic);
|
|
202
|
-
throw err;
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
/* unsubscribe from sink push response topic with reference counting */
|
|
207
|
-
pushUnsubscribe(topic) {
|
|
208
|
-
const count = this.pushSubscriptions.get(topic) ?? 0;
|
|
209
|
-
if (count <= 1) {
|
|
210
|
-
this.pushSubscriptions.delete(topic);
|
|
211
|
-
this._unsubscribeTopic(topic).catch(() => { });
|
|
212
|
-
}
|
|
213
|
-
else
|
|
214
|
-
this.pushSubscriptions.set(topic, count - 1);
|
|
215
|
-
}
|
|
216
191
|
/* dispatch incoming MQTT message */
|
|
217
192
|
async _dispatchMessage(topic, parsed) {
|
|
218
193
|
/* forward dispatching to other traits */
|
|
@@ -329,8 +304,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
329
304
|
throw new Error(`sink name mismatch between topic "${topicMatch.name}" and payload "${parsed.name}"`);
|
|
330
305
|
const error = parsed.error;
|
|
331
306
|
const final = parsed.final;
|
|
332
|
-
const chunk =
|
|
333
|
-
? Uint8Array.from(parsed.chunk) : parsed.chunk;
|
|
307
|
+
const chunk = parsed.chunk;
|
|
334
308
|
/* handle chunk on push */
|
|
335
309
|
const readable = this.pushStreams.get(requestId);
|
|
336
310
|
if (readable !== undefined) {
|
|
@@ -32,7 +32,5 @@ export declare class SourceTrait<T extends APISchema = APISchema> extends Servic
|
|
|
32
32
|
buffer: Promise<Uint8Array>;
|
|
33
33
|
meta: Promise<Record<string, any> | undefined>;
|
|
34
34
|
}>;
|
|
35
|
-
private fetchSubscribe;
|
|
36
|
-
private fetchUnsubscribe;
|
|
37
35
|
protected _dispatchMessage(topic: string, parsed: any): Promise<void>;
|
|
38
36
|
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { Readable } from "node:stream";
|
|
26
26
|
import { nanoid } from "nanoid";
|
|
27
27
|
/* internal requirements */
|
|
28
|
-
import { streamToBuffer, sendBufferAsChunks, sendStreamAsChunks } from "./mqtt-plus-util";
|
|
28
|
+
import { RefCountedSubscription, streamToBuffer, sendBufferAsChunks, sendStreamAsChunks } from "./mqtt-plus-util";
|
|
29
29
|
import { SourceFetchRequest, SourceFetchResponse, SourceFetchChunk } from "./mqtt-plus-msg";
|
|
30
30
|
import { ServiceTrait } from "./mqtt-plus-service";
|
|
31
31
|
/* Source Fetch Communication Trait */
|
|
@@ -35,7 +35,7 @@ export class SourceTrait extends ServiceTrait {
|
|
|
35
35
|
/* source state */
|
|
36
36
|
this.sources = new Map();
|
|
37
37
|
this.fetchCallbacks = new Map();
|
|
38
|
-
this.fetchSubscriptions = new
|
|
38
|
+
this.fetchSubscriptions = new RefCountedSubscription((topic, options) => this._subscribeTopic(topic, options), (topic) => this._unsubscribeTopic(topic), (err) => this.error(err));
|
|
39
39
|
}
|
|
40
40
|
async source(nameOrConfig, ...args) {
|
|
41
41
|
/* determine actual parameters */
|
|
@@ -61,9 +61,9 @@ export class SourceTrait extends ServiceTrait {
|
|
|
61
61
|
if (this.sources.has(name))
|
|
62
62
|
throw new Error(`source: source "${name}" already established`);
|
|
63
63
|
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
64
|
-
const
|
|
65
|
-
const topicReqB = this.options.topicMake(
|
|
66
|
-
const topicReqD = this.options.topicMake(
|
|
64
|
+
const topicS = `$share/${share}/${name}`;
|
|
65
|
+
const topicReqB = this.options.topicMake(topicS, "source-fetch-request");
|
|
66
|
+
const topicReqD = this.options.topicMake(name, "source-fetch-request", this.options.id);
|
|
67
67
|
/* subscribe to MQTT topics */
|
|
68
68
|
await Promise.all([
|
|
69
69
|
this._subscribeTopic(topicReqB, { qos: 2, ...options }),
|
|
@@ -120,11 +120,11 @@ export class SourceTrait extends ServiceTrait {
|
|
|
120
120
|
const responseTopic = this.options.topicMake(name, "source-fetch-response", this.options.id);
|
|
121
121
|
const chunkTopic = this.options.topicMake(name, "source-fetch-chunk", this.options.id);
|
|
122
122
|
await Promise.all([
|
|
123
|
-
this.
|
|
124
|
-
this.
|
|
123
|
+
this.fetchSubscriptions.subscribe(responseTopic, { qos: 2 }),
|
|
124
|
+
this.fetchSubscriptions.subscribe(chunkTopic, { qos: 2 })
|
|
125
125
|
]).catch((err) => {
|
|
126
|
-
this.
|
|
127
|
-
this.
|
|
126
|
+
this.fetchSubscriptions.unsubscribe(responseTopic);
|
|
127
|
+
this.fetchSubscriptions.unsubscribe(chunkTopic);
|
|
128
128
|
throw err;
|
|
129
129
|
});
|
|
130
130
|
/* establish readable for buffering received chunks */
|
|
@@ -157,8 +157,8 @@ export class SourceTrait extends ServiceTrait {
|
|
|
157
157
|
clearTimeout(timer);
|
|
158
158
|
timer = null;
|
|
159
159
|
}
|
|
160
|
-
this.
|
|
161
|
-
this.
|
|
160
|
+
this.fetchSubscriptions.unsubscribe(responseTopic);
|
|
161
|
+
this.fetchSubscriptions.unsubscribe(chunkTopic);
|
|
162
162
|
this.fetchCallbacks.delete(requestId);
|
|
163
163
|
if (resolveMeta)
|
|
164
164
|
metaResolve?.(undefined);
|
|
@@ -213,31 +213,6 @@ export class SourceTrait extends ServiceTrait {
|
|
|
213
213
|
/* produce result */
|
|
214
214
|
return { stream, buffer, meta: metaP };
|
|
215
215
|
}
|
|
216
|
-
/* subscribe to fetch topics with reference counting */
|
|
217
|
-
async fetchSubscribe(topic, options = { qos: 2 }) {
|
|
218
|
-
const count = this.fetchSubscriptions.get(topic) ?? 0;
|
|
219
|
-
this.fetchSubscriptions.set(topic, count + 1);
|
|
220
|
-
if (count === 0) {
|
|
221
|
-
await this._subscribeTopic(topic, options).catch((err) => {
|
|
222
|
-
const currentCount = this.fetchSubscriptions.get(topic) ?? 0;
|
|
223
|
-
if (currentCount > 1)
|
|
224
|
-
this.fetchSubscriptions.set(topic, currentCount - 1);
|
|
225
|
-
else
|
|
226
|
-
this.fetchSubscriptions.delete(topic);
|
|
227
|
-
throw err;
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
/* unsubscribe from fetch topics with reference counting */
|
|
232
|
-
fetchUnsubscribe(topic) {
|
|
233
|
-
const count = this.fetchSubscriptions.get(topic) ?? 0;
|
|
234
|
-
if (count <= 1) {
|
|
235
|
-
this.fetchSubscriptions.delete(topic);
|
|
236
|
-
this._unsubscribeTopic(topic).catch(() => { });
|
|
237
|
-
}
|
|
238
|
-
else
|
|
239
|
-
this.fetchSubscriptions.set(topic, count - 1);
|
|
240
|
-
}
|
|
241
216
|
/* dispatch message (Source Fetch pattern handling) */
|
|
242
217
|
async _dispatchMessage(topic, parsed) {
|
|
243
218
|
await super._dispatchMessage(topic, parsed);
|
|
@@ -296,7 +271,9 @@ export class SourceTrait extends ServiceTrait {
|
|
|
296
271
|
await sendResponse();
|
|
297
272
|
ackSent = true;
|
|
298
273
|
/* dispatch according to data type */
|
|
299
|
-
if (info.stream instanceof Readable)
|
|
274
|
+
if (info.stream instanceof Readable && info.buffer instanceof Promise)
|
|
275
|
+
throw new Error("handler has set both info.stream and info.buffer");
|
|
276
|
+
else if (info.stream instanceof Readable)
|
|
300
277
|
/* handle Readable stream result */
|
|
301
278
|
await sendStreamAsChunks(info.stream, this.options.chunkSize, sendChunk);
|
|
302
279
|
else if (info.buffer instanceof Promise)
|
|
@@ -342,8 +319,7 @@ export class SourceTrait extends ServiceTrait {
|
|
|
342
319
|
throw new Error(`source name mismatch between topic "${topicMatch.name}" and payload "${parsed.name}"`);
|
|
343
320
|
const error = parsed.error;
|
|
344
321
|
const final = parsed.final;
|
|
345
|
-
const chunk =
|
|
346
|
-
? Uint8Array.from(parsed.chunk) : parsed.chunk;
|
|
322
|
+
const chunk = parsed.chunk;
|
|
347
323
|
/* handle chunk on fetch */
|
|
348
324
|
const handler = this.fetchCallbacks.get(requestId);
|
|
349
325
|
if (handler !== undefined)
|
|
@@ -4,8 +4,8 @@ declare class LogEvent {
|
|
|
4
4
|
timestamp: number;
|
|
5
5
|
level: string;
|
|
6
6
|
msg: string | Promise<string>;
|
|
7
|
-
data?:
|
|
8
|
-
constructor(timestamp: number, level: string, msg: string | Promise<string>, data?:
|
|
7
|
+
data?: Record<string, Promise<any> | any> | undefined;
|
|
8
|
+
constructor(timestamp: number, level: string, msg: string | Promise<string>, data?: Record<string, Promise<any> | any> | undefined);
|
|
9
9
|
resolve(): Promise<void>;
|
|
10
10
|
toString(): string;
|
|
11
11
|
}
|
|
@@ -85,7 +85,7 @@ export class TraceTrait extends MsgTrait {
|
|
|
85
85
|
return this._events.emit(...args);
|
|
86
86
|
}
|
|
87
87
|
catch (_err) {
|
|
88
|
-
/* ignore error (
|
|
88
|
+
/* ignore error (caused by emitting "error" without listeners) */
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
91
|
}
|