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.
@@ -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
+