mqtt-plus 0.9.0
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/README.md +457 -0
- package/dst-stage1/mqtt-plus-codec.d.ts +6 -0
- package/dst-stage1/mqtt-plus-codec.js +74 -0
- package/dst-stage1/mqtt-plus-msg.d.ts +31 -0
- package/dst-stage1/mqtt-plus-msg.js +128 -0
- package/dst-stage1/mqtt-plus.d.ts +73 -0
- package/dst-stage1/mqtt-plus.js +408 -0
- package/dst-stage2/mqtt-plus.cjs.js +6422 -0
- package/dst-stage2/mqtt-plus.esm.js +6423 -0
- package/dst-stage2/mqtt-plus.umd.js +19 -0
- package/etc/eslint.mts +94 -0
- package/etc/logo.ai +5106 -2
- package/etc/logo.svg +17 -0
- package/etc/stx.conf +47 -0
- package/etc/tsc.json +27 -0
- package/etc/tsc.tsbuildinfo +1 -0
- package/etc/vite.mts +66 -0
- package/package.json +60 -0
- package/src/mqtt-plus-codec.ts +59 -0
- package/src/mqtt-plus-msg.ts +171 -0
- package/src/mqtt-plus.ts +598 -0
package/src/mqtt-plus.ts
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** MQTT+ -- MQTT Communication Patterns
|
|
3
|
+
** Copyright (c) 2018-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
**
|
|
5
|
+
** Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
** a copy of this software and associated documentation files (the
|
|
7
|
+
** "Software"), to deal in the Software without restriction, including
|
|
8
|
+
** without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
** distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
** permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
** the following conditions:
|
|
12
|
+
**
|
|
13
|
+
** The above copyright notice and this permission notice shall be included
|
|
14
|
+
** in all copies or substantial portions of the Software.
|
|
15
|
+
**
|
|
16
|
+
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/* external requirements */
|
|
26
|
+
import { MqttClient, IClientPublishOptions,
|
|
27
|
+
IClientSubscribeOptions } from "mqtt"
|
|
28
|
+
import { nanoid } from "nanoid"
|
|
29
|
+
|
|
30
|
+
/* internal requirements */
|
|
31
|
+
import Codec from "./mqtt-plus-codec"
|
|
32
|
+
import Msg, {
|
|
33
|
+
EventEmission,
|
|
34
|
+
ServiceRequest,
|
|
35
|
+
ServiceResponseSuccess,
|
|
36
|
+
ServiceResponseError } from "./mqtt-plus-msg"
|
|
37
|
+
|
|
38
|
+
/* type of a wrapped receiver id (for method overloading) */
|
|
39
|
+
export type Receiver = { __receiver: string }
|
|
40
|
+
|
|
41
|
+
/* MQTT topic making */
|
|
42
|
+
export type TopicMake = (name: string, peerId?: string) => string
|
|
43
|
+
|
|
44
|
+
/* MQTT topic matching */
|
|
45
|
+
export type TopicMatch = (topic: string) => TopicMatching | null
|
|
46
|
+
export type TopicMatching = { name: string, peerId?: string }
|
|
47
|
+
|
|
48
|
+
/* API option type */
|
|
49
|
+
export interface APIOptions {
|
|
50
|
+
id: string
|
|
51
|
+
codec: "cbor" | "json"
|
|
52
|
+
timeout: number
|
|
53
|
+
topicEventNoticeMake: TopicMake
|
|
54
|
+
topicServiceRequestMake: TopicMake
|
|
55
|
+
topicServiceResponseMake: TopicMake
|
|
56
|
+
topicEventNoticeMatch: TopicMatch
|
|
57
|
+
topicServiceRequestMatch: TopicMatch
|
|
58
|
+
topicServiceResponseMatch: TopicMatch
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Registration, Subscription and Observation result types */
|
|
62
|
+
export interface Registration {
|
|
63
|
+
unregister (): Promise<void>
|
|
64
|
+
}
|
|
65
|
+
export interface Subscription {
|
|
66
|
+
unsubscribe (): Promise<void>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* callback info type */
|
|
70
|
+
export interface Info {
|
|
71
|
+
sender: string,
|
|
72
|
+
receiver?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* type utility: extend function with Info parameter */
|
|
76
|
+
export type WithInfo<F> = F extends (...args: infer P) => infer R
|
|
77
|
+
? (...args: [ ...P, info: Info ]) => R
|
|
78
|
+
: never
|
|
79
|
+
|
|
80
|
+
/* type utilities for generic API */
|
|
81
|
+
export type APISchema = Record<string, (...args: any[]) => any>
|
|
82
|
+
|
|
83
|
+
/* extract event keys where return type IS void (events: subscribe/notify/control) */
|
|
84
|
+
export type EventKeys<T> = string extends keyof T ? string : {
|
|
85
|
+
[ K in keyof T ]: T[K] extends (...args: any[]) => infer R
|
|
86
|
+
/* eslint-disable-next-line @typescript-eslint/no-invalid-void-type */
|
|
87
|
+
? [ R ] extends [ void ] ? K : never
|
|
88
|
+
: never
|
|
89
|
+
}[ keyof T ]
|
|
90
|
+
|
|
91
|
+
/* extract service keys where return type is NOT void (services: register/call) */
|
|
92
|
+
export type ServiceKeys<T> = string extends keyof T ? string : {
|
|
93
|
+
[ K in keyof T ]: T[K] extends (...args: any[]) => infer R
|
|
94
|
+
/* eslint-disable-next-line @typescript-eslint/no-invalid-void-type */
|
|
95
|
+
? [ R ] extends [ void ] ? never : K
|
|
96
|
+
: never
|
|
97
|
+
}[ keyof T ]
|
|
98
|
+
|
|
99
|
+
/* MQTTp API class */
|
|
100
|
+
export default class MQTTp<T extends APISchema = APISchema> {
|
|
101
|
+
private options: APIOptions
|
|
102
|
+
private codec: Codec
|
|
103
|
+
private msg = new Msg()
|
|
104
|
+
private registry = new Map<string, ((...params: any[]) => any) | ((...params: any[]) => void)>()
|
|
105
|
+
private requests = new Map<string, { service: string, callback: (err: any, result: any) => void }>()
|
|
106
|
+
private subscriptions = new Map<string, number>()
|
|
107
|
+
|
|
108
|
+
/* construct API class */
|
|
109
|
+
constructor (
|
|
110
|
+
private mqtt: MqttClient,
|
|
111
|
+
options: Partial<APIOptions> = {}
|
|
112
|
+
) {
|
|
113
|
+
/* determine options and provide defaults */
|
|
114
|
+
this.options = {
|
|
115
|
+
id: nanoid(),
|
|
116
|
+
codec: "cbor",
|
|
117
|
+
timeout: 10 * 1000,
|
|
118
|
+
topicEventNoticeMake: (name, peerId) => {
|
|
119
|
+
return peerId
|
|
120
|
+
? `${name}/event-notice/${peerId}`
|
|
121
|
+
: `${name}/event-notice`
|
|
122
|
+
},
|
|
123
|
+
topicServiceRequestMake: (name, peerId) => {
|
|
124
|
+
return peerId
|
|
125
|
+
? `${name}/service-request/${peerId}`
|
|
126
|
+
: `${name}/service-request`
|
|
127
|
+
},
|
|
128
|
+
topicServiceResponseMake: (name, peerId) => {
|
|
129
|
+
return peerId
|
|
130
|
+
? `${name}/service-response/${peerId}`
|
|
131
|
+
: `${name}/service-response`
|
|
132
|
+
},
|
|
133
|
+
topicEventNoticeMatch: (topic) => {
|
|
134
|
+
const m = topic.match(/^(.+?)\/event-notice(?:\/(.+))?$/)
|
|
135
|
+
return m ? { name: m[1], peerId: m[2] } : null
|
|
136
|
+
},
|
|
137
|
+
topicServiceRequestMatch: (topic) => {
|
|
138
|
+
const m = topic.match(/^(.+?)\/service-request(?:\/(.+))?$/)
|
|
139
|
+
return m ? { name: m[1], peerId: m[2] } : null
|
|
140
|
+
},
|
|
141
|
+
topicServiceResponseMatch: (topic) => {
|
|
142
|
+
const m = topic.match(/^(.+?)\/service-response\/(.+)$/)
|
|
143
|
+
return m ? { name: m[1], peerId: m[2] } : null
|
|
144
|
+
},
|
|
145
|
+
...options
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* establish an encoder */
|
|
149
|
+
this.codec = new Codec(this.options.codec)
|
|
150
|
+
|
|
151
|
+
/* hook into the MQTT message processing */
|
|
152
|
+
this.mqtt.on("message", (topic, message) => {
|
|
153
|
+
this._onMessage(topic, message)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* subscribe to an MQTT topic (Promise-based) */
|
|
158
|
+
private async _subscribeTopic (topic: string, options: Partial<IClientSubscribeOptions> = {}) {
|
|
159
|
+
return new Promise<void>((resolve, reject) => {
|
|
160
|
+
this.mqtt.subscribe(topic, { qos: 2, ...options }, (err: Error | null, _granted: any) => {
|
|
161
|
+
if (err) reject(err)
|
|
162
|
+
else resolve()
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* unsubscribe from an MQTT topic (Promise-based) */
|
|
168
|
+
private async _unsubscribeTopic (topic: string) {
|
|
169
|
+
return new Promise<void>((resolve, reject) => {
|
|
170
|
+
this.mqtt.unsubscribe(topic, (err?: Error, _packet?: any) => {
|
|
171
|
+
if (err) reject(err)
|
|
172
|
+
else resolve()
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* subscribe to an RPC event */
|
|
178
|
+
async subscribe<K extends EventKeys<T> & string> (
|
|
179
|
+
event: K,
|
|
180
|
+
callback: WithInfo<T[K]>
|
|
181
|
+
): Promise<Subscription>
|
|
182
|
+
async subscribe<K extends EventKeys<T> & string> (
|
|
183
|
+
event: K,
|
|
184
|
+
options: Partial<IClientSubscribeOptions>,
|
|
185
|
+
callback: WithInfo<T[K]>
|
|
186
|
+
): Promise<Subscription>
|
|
187
|
+
async subscribe<K extends EventKeys<T> & string> (
|
|
188
|
+
event: K,
|
|
189
|
+
...args: any[]
|
|
190
|
+
): Promise<Subscription> {
|
|
191
|
+
/* determine parameters */
|
|
192
|
+
let options: Partial<IClientSubscribeOptions> = {}
|
|
193
|
+
let callback: T[K] = args[0] as T[K]
|
|
194
|
+
if (args.length === 2 && typeof args[0] === "object") {
|
|
195
|
+
options = args[0]
|
|
196
|
+
callback = args[1]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* sanity check situation */
|
|
200
|
+
if (this.registry.has(event))
|
|
201
|
+
throw new Error(`subscribe: event "${event}" already subscribed`)
|
|
202
|
+
|
|
203
|
+
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
204
|
+
const topicB = this.options.topicEventNoticeMake(event)
|
|
205
|
+
const topicD = this.options.topicEventNoticeMake(event, this.options.id)
|
|
206
|
+
|
|
207
|
+
/* subscribe to MQTT topics */
|
|
208
|
+
await Promise.all([
|
|
209
|
+
this._subscribeTopic(topicB, { qos: 0, ...options }),
|
|
210
|
+
this._subscribeTopic(topicD, { qos: 0, ...options })
|
|
211
|
+
]).catch((err: Error) => {
|
|
212
|
+
this._unsubscribeTopic(topicB).catch(() => {})
|
|
213
|
+
this._unsubscribeTopic(topicD).catch(() => {})
|
|
214
|
+
throw err
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
/* remember the subscription */
|
|
218
|
+
this.registry.set(event, callback)
|
|
219
|
+
|
|
220
|
+
/* provide a subscription for subsequent unsubscribing */
|
|
221
|
+
const self = this
|
|
222
|
+
const subscription: Subscription = {
|
|
223
|
+
async unsubscribe (): Promise<void> {
|
|
224
|
+
if (!self.registry.has(event))
|
|
225
|
+
throw new Error(`unsubscribe: event "${event}" not subscribed`)
|
|
226
|
+
self.registry.delete(event)
|
|
227
|
+
return Promise.all([
|
|
228
|
+
self._unsubscribeTopic(topicB),
|
|
229
|
+
self._unsubscribeTopic(topicD)
|
|
230
|
+
]).then(() => {})
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return subscription
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* register an RPC service */
|
|
237
|
+
async register<K extends ServiceKeys<T> & string> (
|
|
238
|
+
service: K,
|
|
239
|
+
callback: WithInfo<T[K]>
|
|
240
|
+
): Promise<Registration>
|
|
241
|
+
async register<K extends ServiceKeys<T> & string> (
|
|
242
|
+
service: K,
|
|
243
|
+
options: Partial<IClientSubscribeOptions>,
|
|
244
|
+
callback: WithInfo<T[K]>
|
|
245
|
+
): Promise<Registration>
|
|
246
|
+
async register<K extends ServiceKeys<T> & string> (
|
|
247
|
+
service: K,
|
|
248
|
+
...args: any[]
|
|
249
|
+
): Promise<Registration> {
|
|
250
|
+
/* determine parameters */
|
|
251
|
+
let options: Partial<IClientSubscribeOptions> = {}
|
|
252
|
+
let callback: T[K] = args[0] as T[K]
|
|
253
|
+
if (args.length === 2 && typeof args[0] === "object") {
|
|
254
|
+
options = args[0]
|
|
255
|
+
callback = args[1]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* sanity check situation */
|
|
259
|
+
if (this.registry.has(service))
|
|
260
|
+
throw new Error(`register: service "${service}" already registered`)
|
|
261
|
+
|
|
262
|
+
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
263
|
+
const topicB = this.options.topicServiceRequestMake(service)
|
|
264
|
+
const topicD = this.options.topicServiceRequestMake(service, this.options.id)
|
|
265
|
+
|
|
266
|
+
/* subscribe to MQTT topics */
|
|
267
|
+
await Promise.all([
|
|
268
|
+
this._subscribeTopic(topicB, { qos: 2, ...options }),
|
|
269
|
+
this._subscribeTopic(topicD, { qos: 2, ...options })
|
|
270
|
+
]).catch((err: Error) => {
|
|
271
|
+
this._unsubscribeTopic(topicB).catch(() => {})
|
|
272
|
+
this._unsubscribeTopic(topicD).catch(() => {})
|
|
273
|
+
throw err
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
/* remember the registration */
|
|
277
|
+
this.registry.set(service, callback)
|
|
278
|
+
|
|
279
|
+
/* provide a registration for subsequent unregistering */
|
|
280
|
+
const self = this
|
|
281
|
+
const registration: Registration = {
|
|
282
|
+
async unregister (): Promise<void> {
|
|
283
|
+
if (!self.registry.has(service))
|
|
284
|
+
throw new Error(`unregister: service "${service}" not registered`)
|
|
285
|
+
self.registry.delete(service)
|
|
286
|
+
return Promise.all([
|
|
287
|
+
self._unsubscribeTopic(topicB),
|
|
288
|
+
self._unsubscribeTopic(topicD)
|
|
289
|
+
]).then(() => {})
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return registration
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* check whether argument has structure of interface IClientPublishOptions */
|
|
296
|
+
private _isIClientPublishOptions (arg: any) {
|
|
297
|
+
if (typeof arg !== "object")
|
|
298
|
+
return false
|
|
299
|
+
const keys = [ "qos", "retain", "dup", "properties", "cbStorePut" ]
|
|
300
|
+
return Object.keys(arg).every((key) => keys.includes(key))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* wrap receiver id into object (required for type-safe overloading) */
|
|
304
|
+
receiver (id: string) {
|
|
305
|
+
return { __receiver: id }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* return client id from wrapper object */
|
|
309
|
+
private _getReceiver (obj: Receiver) {
|
|
310
|
+
return obj.__receiver
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* detect client id wrapper object */
|
|
314
|
+
private _isReceiver (obj: any): obj is Receiver {
|
|
315
|
+
return (typeof obj === "object"
|
|
316
|
+
&& obj !== null
|
|
317
|
+
&& "__receiver" in obj
|
|
318
|
+
&& typeof obj.__receiver === "string"
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* parse optional peerId and options from variadic arguments */
|
|
323
|
+
private _parseCallArgs<T extends any[]> (args: any[]): { receiver?: string, options: IClientPublishOptions, params: T } {
|
|
324
|
+
let receiver: string | undefined
|
|
325
|
+
let options: IClientPublishOptions = {}
|
|
326
|
+
let params = args as T
|
|
327
|
+
if (args.length >= 2 && this._isReceiver(args[0]) && this._isIClientPublishOptions(args[1])) {
|
|
328
|
+
receiver = this._getReceiver(args[0])
|
|
329
|
+
options = args[1]
|
|
330
|
+
params = args.slice(2) as T
|
|
331
|
+
}
|
|
332
|
+
else if (args.length >= 1 && this._isReceiver(args[0])) {
|
|
333
|
+
receiver = this._getReceiver(args[0])
|
|
334
|
+
params = args.slice(1) as T
|
|
335
|
+
}
|
|
336
|
+
else if (args.length >= 1 && this._isIClientPublishOptions(args[0])) {
|
|
337
|
+
options = args[0]
|
|
338
|
+
params = args.slice(1) as T
|
|
339
|
+
}
|
|
340
|
+
return { receiver, options, params }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* emit event ("fire and forget") */
|
|
344
|
+
emit<K extends EventKeys<T> & string> (
|
|
345
|
+
event: K,
|
|
346
|
+
...params: Parameters<T[K]>
|
|
347
|
+
): void
|
|
348
|
+
emit<K extends EventKeys<T> & string> (
|
|
349
|
+
event: K,
|
|
350
|
+
receiver: Receiver,
|
|
351
|
+
...params: Parameters<T[K]>
|
|
352
|
+
): void
|
|
353
|
+
emit<K extends EventKeys<T> & string> (
|
|
354
|
+
event: K,
|
|
355
|
+
options: IClientPublishOptions,
|
|
356
|
+
...params: Parameters<T[K]>
|
|
357
|
+
): void
|
|
358
|
+
emit<K extends EventKeys<T> & string> (
|
|
359
|
+
event: K,
|
|
360
|
+
receiver: Receiver,
|
|
361
|
+
options: IClientPublishOptions,
|
|
362
|
+
...params: Parameters<T[K]>
|
|
363
|
+
): void
|
|
364
|
+
emit<K extends EventKeys<T> & string> (
|
|
365
|
+
event: K,
|
|
366
|
+
...args: any[]
|
|
367
|
+
): void {
|
|
368
|
+
/* determine actual parameters */
|
|
369
|
+
const { receiver, options, params } = this._parseCallArgs<Parameters<T[K]>>(args)
|
|
370
|
+
|
|
371
|
+
/* generate unique request id */
|
|
372
|
+
const rid = nanoid()
|
|
373
|
+
|
|
374
|
+
/* generate encoded Msg message */
|
|
375
|
+
const request = this.msg.makeEventEmission(rid, event, params, this.options.id, receiver)
|
|
376
|
+
const message = this.codec.encode(request)
|
|
377
|
+
|
|
378
|
+
/* generate corresponding MQTT topic */
|
|
379
|
+
const topic = this.options.topicEventNoticeMake(event, receiver)
|
|
380
|
+
|
|
381
|
+
/* publish message to MQTT topic */
|
|
382
|
+
this.mqtt.publish(topic, message, { qos: 2, ...options })
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* call service ("request and response") */
|
|
386
|
+
call<K extends ServiceKeys<T> & string> (
|
|
387
|
+
service: K,
|
|
388
|
+
...params: Parameters<T[K]>
|
|
389
|
+
): Promise<Awaited<ReturnType<T[K]>>>
|
|
390
|
+
call<K extends ServiceKeys<T> & string> (
|
|
391
|
+
service: K,
|
|
392
|
+
receiver: Receiver,
|
|
393
|
+
...params: Parameters<T[K]>
|
|
394
|
+
): Promise<Awaited<ReturnType<T[K]>>>
|
|
395
|
+
call<K extends ServiceKeys<T> & string> (
|
|
396
|
+
service: K,
|
|
397
|
+
options: IClientPublishOptions,
|
|
398
|
+
...params: Parameters<T[K]>
|
|
399
|
+
): Promise<Awaited<ReturnType<T[K]>>>
|
|
400
|
+
call<K extends ServiceKeys<T> & string> (
|
|
401
|
+
service: K,
|
|
402
|
+
receiver: Receiver,
|
|
403
|
+
options: IClientPublishOptions,
|
|
404
|
+
...params: Parameters<T[K]>
|
|
405
|
+
): Promise<Awaited<ReturnType<T[K]>>>
|
|
406
|
+
call<K extends ServiceKeys<T> & string> (
|
|
407
|
+
service: K,
|
|
408
|
+
...args: any[]
|
|
409
|
+
): Promise<Awaited<ReturnType<T[K]>>> {
|
|
410
|
+
/* determine actual parameters */
|
|
411
|
+
const { receiver, options, params } = this._parseCallArgs<Parameters<T[K]>>(args)
|
|
412
|
+
|
|
413
|
+
/* generate unique request id */
|
|
414
|
+
const rid = nanoid()
|
|
415
|
+
|
|
416
|
+
/* subscribe to MQTT response topic */
|
|
417
|
+
this._responseSubscribe(service, { qos: options.qos ?? 2 })
|
|
418
|
+
|
|
419
|
+
/* create promise for MQTT response handling */
|
|
420
|
+
const promise: Promise<Awaited<ReturnType<T[K]>>> = new Promise((resolve, reject) => {
|
|
421
|
+
let timer: NodeJS.Timeout | null = setTimeout(() => {
|
|
422
|
+
this.requests.delete(rid)
|
|
423
|
+
this._responseUnsubscribe(service)
|
|
424
|
+
timer = null
|
|
425
|
+
reject(new Error("communication timeout"))
|
|
426
|
+
}, this.options.timeout)
|
|
427
|
+
this.requests.set(rid, {
|
|
428
|
+
service,
|
|
429
|
+
callback: (err: any, result: Awaited<ReturnType<T[K]>>) => {
|
|
430
|
+
if (timer !== null) {
|
|
431
|
+
clearTimeout(timer)
|
|
432
|
+
timer = null
|
|
433
|
+
}
|
|
434
|
+
if (err) reject(err)
|
|
435
|
+
else resolve(result)
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
/* generate encoded message */
|
|
441
|
+
const request = this.msg.makeServiceRequest(rid, service, params, this.options.id, receiver)
|
|
442
|
+
const message = this.codec.encode(request)
|
|
443
|
+
|
|
444
|
+
/* generate corresponding MQTT topic */
|
|
445
|
+
const topic = this.options.topicServiceRequestMake(service, receiver)
|
|
446
|
+
|
|
447
|
+
/* publish message to MQTT topic */
|
|
448
|
+
this.mqtt.publish(topic, message, { qos: 2, ...options }, (err?: Error) => {
|
|
449
|
+
/* handle request failure */
|
|
450
|
+
const pendingRequest = this.requests.get(rid)
|
|
451
|
+
if (err && pendingRequest !== undefined) {
|
|
452
|
+
this.requests.delete(rid)
|
|
453
|
+
this._responseUnsubscribe(service)
|
|
454
|
+
pendingRequest.callback(err, undefined)
|
|
455
|
+
}
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
return promise
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* subscribe to RPC response */
|
|
462
|
+
private _responseSubscribe (service: string, options: IClientSubscribeOptions = { qos: 2 }): void {
|
|
463
|
+
/* generate corresponding MQTT topic */
|
|
464
|
+
const topic = this.options.topicServiceResponseMake(service, this.options.id)
|
|
465
|
+
|
|
466
|
+
/* subscribe to MQTT topic and remember subscription */
|
|
467
|
+
if (!this.subscriptions.has(topic)) {
|
|
468
|
+
this.subscriptions.set(topic, 0)
|
|
469
|
+
this.mqtt.subscribe(topic, options, (err: Error | null) => {
|
|
470
|
+
if (err)
|
|
471
|
+
this.mqtt.emit("error", err)
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
this.subscriptions.set(topic, this.subscriptions.get(topic)! + 1)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* unsubscribe from RPC response */
|
|
478
|
+
private _responseUnsubscribe (service: string): void {
|
|
479
|
+
/* generate corresponding MQTT topic */
|
|
480
|
+
const topic = this.options.topicServiceResponseMake(service, this.options.id)
|
|
481
|
+
|
|
482
|
+
/* short-circuit processing if (no longer) subscribed */
|
|
483
|
+
if (!this.subscriptions.has(topic))
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
/* unsubscribe from MQTT topic and forget subscription */
|
|
487
|
+
this.subscriptions.set(topic, this.subscriptions.get(topic)! - 1)
|
|
488
|
+
if (this.subscriptions.get(topic) === 0) {
|
|
489
|
+
this.subscriptions.delete(topic)
|
|
490
|
+
this.mqtt.unsubscribe(topic, (err?: Error) => {
|
|
491
|
+
if (err)
|
|
492
|
+
this.mqtt.emit("error", err)
|
|
493
|
+
})
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/* handle incoming MQTT message */
|
|
498
|
+
private _onMessage (topic: string, message: Buffer): void {
|
|
499
|
+
/* ensure we handle only valid messages */
|
|
500
|
+
let eventMatch: TopicMatching | null = null
|
|
501
|
+
let requestMatch: TopicMatching | null = null
|
|
502
|
+
let responseMatch: TopicMatching | null = null
|
|
503
|
+
if ( (eventMatch = this.options.topicEventNoticeMatch(topic)) === null
|
|
504
|
+
&& (requestMatch = this.options.topicServiceRequestMatch(topic)) === null
|
|
505
|
+
&& (responseMatch = this.options.topicServiceResponseMatch(topic)) === null)
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
/* ensure we really handle only messages for us */
|
|
509
|
+
const peerId = eventMatch?.peerId ?? requestMatch?.peerId ?? responseMatch?.peerId
|
|
510
|
+
if (peerId !== undefined && peerId !== this.options.id)
|
|
511
|
+
return
|
|
512
|
+
|
|
513
|
+
/* try to parse payload as payload */
|
|
514
|
+
let parsed: EventEmission | ServiceRequest | ServiceResponseSuccess | ServiceResponseError
|
|
515
|
+
try {
|
|
516
|
+
let input: Buffer | string = message
|
|
517
|
+
if (this.options.codec === "json")
|
|
518
|
+
input = message.toString()
|
|
519
|
+
const payload = this.codec.decode(input)
|
|
520
|
+
parsed = this.msg.parse(payload)
|
|
521
|
+
}
|
|
522
|
+
catch (_err: unknown) {
|
|
523
|
+
const err = _err instanceof Error
|
|
524
|
+
? new Error(`failed to parse message: ${_err.message}`)
|
|
525
|
+
: new Error("failed to parse message")
|
|
526
|
+
this.mqtt.emit("error", err)
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/* dispatch according to message type */
|
|
531
|
+
if (parsed instanceof EventEmission) {
|
|
532
|
+
/* just deliver event */
|
|
533
|
+
const name = parsed.event
|
|
534
|
+
const handler = this.registry.get(name)
|
|
535
|
+
const params = parsed.params ?? []
|
|
536
|
+
const info: Info = { sender: parsed.sender ?? "", receiver: parsed.receiver }
|
|
537
|
+
handler?.(...params, info)
|
|
538
|
+
}
|
|
539
|
+
else if (parsed instanceof ServiceRequest) {
|
|
540
|
+
/* deliver service request and send response */
|
|
541
|
+
const rid = parsed.id
|
|
542
|
+
const name = parsed.service
|
|
543
|
+
const handler = this.registry.get(name)
|
|
544
|
+
let response: Promise<any>
|
|
545
|
+
if (handler !== undefined) {
|
|
546
|
+
/* execute service handler */
|
|
547
|
+
const params = parsed.params ?? []
|
|
548
|
+
const info: Info = { sender: parsed.sender ?? "", receiver: parsed.receiver }
|
|
549
|
+
response = Promise.resolve().then(() => handler(...params, info))
|
|
550
|
+
}
|
|
551
|
+
else
|
|
552
|
+
response = Promise.reject(new Error(`method not found: ${name}`))
|
|
553
|
+
response.then((result: any) => {
|
|
554
|
+
/* create success response */
|
|
555
|
+
return this.msg.makeServiceResponseSuccess(rid, result, this.options.id, parsed.sender)
|
|
556
|
+
}, (result: any) => {
|
|
557
|
+
/* determine error message and build error response */
|
|
558
|
+
let errorMessage: string
|
|
559
|
+
if (result === undefined || result === null)
|
|
560
|
+
errorMessage = "undefined error"
|
|
561
|
+
else if (typeof result === "string")
|
|
562
|
+
errorMessage = result
|
|
563
|
+
else if (result instanceof Error)
|
|
564
|
+
errorMessage = result.message
|
|
565
|
+
else
|
|
566
|
+
errorMessage = String(result)
|
|
567
|
+
return this.msg.makeServiceResponseError(rid, errorMessage, this.options.id, parsed.sender)
|
|
568
|
+
}).then((rpcResponse) => {
|
|
569
|
+
/* send response message */
|
|
570
|
+
const senderPeerId = parsed.sender
|
|
571
|
+
if (senderPeerId === undefined)
|
|
572
|
+
throw new Error("invalid request: missing sender")
|
|
573
|
+
const encoded = this.codec.encode(rpcResponse)
|
|
574
|
+
const topic = this.options.topicServiceResponseMake(name, senderPeerId)
|
|
575
|
+
this.mqtt.publish(topic, encoded, { qos: 2 })
|
|
576
|
+
}).catch((err: Error) => {
|
|
577
|
+
this.mqtt.emit("error", err)
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
else if (parsed instanceof ServiceResponseSuccess || parsed instanceof ServiceResponseError) {
|
|
581
|
+
/* handle service response */
|
|
582
|
+
const rid = parsed.id
|
|
583
|
+
const request = this.requests.get(rid)
|
|
584
|
+
if (request !== undefined) {
|
|
585
|
+
/* call callback function */
|
|
586
|
+
if (parsed instanceof ServiceResponseSuccess)
|
|
587
|
+
request.callback(undefined, parsed.result)
|
|
588
|
+
else if (parsed instanceof ServiceResponseError)
|
|
589
|
+
request.callback(new Error(parsed.error), undefined)
|
|
590
|
+
|
|
591
|
+
/* unsubscribe from response */
|
|
592
|
+
this.requests.delete(rid)
|
|
593
|
+
this._responseUnsubscribe(request.service)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|