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
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
/* base class */
|
|
25
|
+
export class Base {
|
|
26
|
+
constructor(id, sender, receiver) {
|
|
27
|
+
this.id = id;
|
|
28
|
+
this.sender = sender;
|
|
29
|
+
this.receiver = receiver;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/* event emission */
|
|
33
|
+
export class EventEmission extends Base {
|
|
34
|
+
constructor(id, event, params, sender, receiver) {
|
|
35
|
+
super(id, sender, receiver);
|
|
36
|
+
this.event = event;
|
|
37
|
+
this.params = params;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/* service request */
|
|
41
|
+
export class ServiceRequest extends Base {
|
|
42
|
+
constructor(id, service, params, sender, receiver) {
|
|
43
|
+
super(id, sender, receiver);
|
|
44
|
+
this.service = service;
|
|
45
|
+
this.params = params;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/* service response success */
|
|
49
|
+
export class ServiceResponseSuccess extends Base {
|
|
50
|
+
constructor(id, result, sender, receiver) {
|
|
51
|
+
super(id, sender, receiver);
|
|
52
|
+
this.result = result;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/* service response error */
|
|
56
|
+
export class ServiceResponseError extends Base {
|
|
57
|
+
constructor(id, error, sender, receiver) {
|
|
58
|
+
super(id, sender, receiver);
|
|
59
|
+
this.error = error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/* utility class */
|
|
63
|
+
export default class Msg {
|
|
64
|
+
/* factory for event emission */
|
|
65
|
+
makeEventEmission(id, event, params, sender, receiver) {
|
|
66
|
+
return new EventEmission(id, event, params, sender, receiver);
|
|
67
|
+
}
|
|
68
|
+
/* factory for service request */
|
|
69
|
+
makeServiceRequest(id, service, params, sender, receiver) {
|
|
70
|
+
return new ServiceRequest(id, service, params, sender, receiver);
|
|
71
|
+
}
|
|
72
|
+
/* factory for service response success */
|
|
73
|
+
makeServiceResponseSuccess(id, result, sender, receiver) {
|
|
74
|
+
return new ServiceResponseSuccess(id, result, sender, receiver);
|
|
75
|
+
}
|
|
76
|
+
/* factory for service response error */
|
|
77
|
+
makeServiceResponseError(id, error, sender, receiver) {
|
|
78
|
+
return new ServiceResponseError(id, error, sender, receiver);
|
|
79
|
+
}
|
|
80
|
+
/* parse any object into typed object */
|
|
81
|
+
parse(obj) {
|
|
82
|
+
if (typeof obj !== "object" || obj === null)
|
|
83
|
+
throw new Error("invalid argument: not an object");
|
|
84
|
+
/* validate common fields */
|
|
85
|
+
if (!("id" in obj) || typeof obj.id !== "string")
|
|
86
|
+
throw new Error("invalid object: missing or invalid \"id\" field");
|
|
87
|
+
if ("sender" in obj && typeof obj.sender !== "string")
|
|
88
|
+
throw new Error("invalid object: invalid \"sender\" field");
|
|
89
|
+
if ("receiver" in obj && typeof obj.sender !== "string")
|
|
90
|
+
throw new Error("invalid object: invalid \"receiver\" field");
|
|
91
|
+
/* dispatch according to type indication by field */
|
|
92
|
+
const anyFieldsExcept = (obj, allowed) => Object.keys(obj).some((key) => !allowed.includes(key));
|
|
93
|
+
if ("event" in obj) {
|
|
94
|
+
/* detect and parse event emission */
|
|
95
|
+
if (typeof obj.event !== "string")
|
|
96
|
+
throw new Error("invalid EventEmission object: \"event\" field must be a string");
|
|
97
|
+
if (anyFieldsExcept(obj, ["type", "id", "event", "params", "sender", "receiver"]))
|
|
98
|
+
throw new Error("invalid EventEmission object: contains unknown fields");
|
|
99
|
+
if (obj.params !== undefined && (typeof obj.params !== "object" || !Array.isArray(obj.params)))
|
|
100
|
+
throw new Error("invalid EventEmission object: \"params\" field must be an array");
|
|
101
|
+
return this.makeEventEmission(obj.id, obj.event, obj.params, obj.sender, obj.receiver);
|
|
102
|
+
}
|
|
103
|
+
else if ("service" in obj) {
|
|
104
|
+
/* detect and parse service request */
|
|
105
|
+
if (typeof obj.service !== "string")
|
|
106
|
+
throw new Error("invalid ServiceRequest object: \"service\" field must be a string");
|
|
107
|
+
if (anyFieldsExcept(obj, ["type", "id", "service", "params", "sender", "receiver"]))
|
|
108
|
+
throw new Error("invalid ServiceRequest object: contains unknown fields");
|
|
109
|
+
if (obj.params !== undefined && (typeof obj.params !== "object" || !Array.isArray(obj.params)))
|
|
110
|
+
throw new Error("invalid ServiceRequest object: \"params\" field must be an array");
|
|
111
|
+
return this.makeServiceRequest(obj.id, obj.service, obj.params, obj.sender, obj.receiver);
|
|
112
|
+
}
|
|
113
|
+
else if ("result" in obj) {
|
|
114
|
+
/* detect and parse service response success */
|
|
115
|
+
if (anyFieldsExcept(obj, ["type", "id", "result", "sender", "receiver"]))
|
|
116
|
+
throw new Error("invalid ServiceResponseSuccess object: contains unknown fields");
|
|
117
|
+
return this.makeServiceResponseSuccess(obj.id, obj.result, obj.sender, obj.receiver);
|
|
118
|
+
}
|
|
119
|
+
else if ("error" in obj) {
|
|
120
|
+
/* detect and parse service response error */
|
|
121
|
+
if (anyFieldsExcept(obj, ["type", "id", "error", "sender", "receiver"]))
|
|
122
|
+
throw new Error("invalid ServiceResponseError object: contains unknown fields");
|
|
123
|
+
return this.makeServiceResponseError(obj.id, obj.error, obj.sender, obj.receiver);
|
|
124
|
+
}
|
|
125
|
+
else
|
|
126
|
+
throw new Error("invalid object: not of any known type");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { MqttClient, IClientPublishOptions, IClientSubscribeOptions } from "mqtt";
|
|
2
|
+
export type Receiver = {
|
|
3
|
+
__receiver: string;
|
|
4
|
+
};
|
|
5
|
+
export type TopicMake = (name: string, peerId?: string) => string;
|
|
6
|
+
export type TopicMatch = (topic: string) => TopicMatching | null;
|
|
7
|
+
export type TopicMatching = {
|
|
8
|
+
name: string;
|
|
9
|
+
peerId?: string;
|
|
10
|
+
};
|
|
11
|
+
export interface APIOptions {
|
|
12
|
+
id: string;
|
|
13
|
+
codec: "cbor" | "json";
|
|
14
|
+
timeout: number;
|
|
15
|
+
topicEventNoticeMake: TopicMake;
|
|
16
|
+
topicServiceRequestMake: TopicMake;
|
|
17
|
+
topicServiceResponseMake: TopicMake;
|
|
18
|
+
topicEventNoticeMatch: TopicMatch;
|
|
19
|
+
topicServiceRequestMatch: TopicMatch;
|
|
20
|
+
topicServiceResponseMatch: TopicMatch;
|
|
21
|
+
}
|
|
22
|
+
export interface Registration {
|
|
23
|
+
unregister(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export interface Subscription {
|
|
26
|
+
unsubscribe(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export interface Info {
|
|
29
|
+
sender: string;
|
|
30
|
+
receiver?: string;
|
|
31
|
+
}
|
|
32
|
+
export type WithInfo<F> = F extends (...args: infer P) => infer R ? (...args: [...P, info: Info]) => R : never;
|
|
33
|
+
export type APISchema = Record<string, (...args: any[]) => any>;
|
|
34
|
+
export type EventKeys<T> = string extends keyof T ? string : {
|
|
35
|
+
[K in keyof T]: T[K] extends (...args: any[]) => infer R ? [R] extends [void] ? K : never : never;
|
|
36
|
+
}[keyof T];
|
|
37
|
+
export type ServiceKeys<T> = string extends keyof T ? string : {
|
|
38
|
+
[K in keyof T]: T[K] extends (...args: any[]) => infer R ? [R] extends [void] ? never : K : never;
|
|
39
|
+
}[keyof T];
|
|
40
|
+
export default class MQTTp<T extends APISchema = APISchema> {
|
|
41
|
+
private mqtt;
|
|
42
|
+
private options;
|
|
43
|
+
private codec;
|
|
44
|
+
private msg;
|
|
45
|
+
private registry;
|
|
46
|
+
private requests;
|
|
47
|
+
private subscriptions;
|
|
48
|
+
constructor(mqtt: MqttClient, options?: Partial<APIOptions>);
|
|
49
|
+
private _subscribeTopic;
|
|
50
|
+
private _unsubscribeTopic;
|
|
51
|
+
subscribe<K extends EventKeys<T> & string>(event: K, callback: WithInfo<T[K]>): Promise<Subscription>;
|
|
52
|
+
subscribe<K extends EventKeys<T> & string>(event: K, options: Partial<IClientSubscribeOptions>, callback: WithInfo<T[K]>): Promise<Subscription>;
|
|
53
|
+
register<K extends ServiceKeys<T> & string>(service: K, callback: WithInfo<T[K]>): Promise<Registration>;
|
|
54
|
+
register<K extends ServiceKeys<T> & string>(service: K, options: Partial<IClientSubscribeOptions>, callback: WithInfo<T[K]>): Promise<Registration>;
|
|
55
|
+
private _isIClientPublishOptions;
|
|
56
|
+
receiver(id: string): {
|
|
57
|
+
__receiver: string;
|
|
58
|
+
};
|
|
59
|
+
private _getReceiver;
|
|
60
|
+
private _isReceiver;
|
|
61
|
+
private _parseCallArgs;
|
|
62
|
+
emit<K extends EventKeys<T> & string>(event: K, ...params: Parameters<T[K]>): void;
|
|
63
|
+
emit<K extends EventKeys<T> & string>(event: K, receiver: Receiver, ...params: Parameters<T[K]>): void;
|
|
64
|
+
emit<K extends EventKeys<T> & string>(event: K, options: IClientPublishOptions, ...params: Parameters<T[K]>): void;
|
|
65
|
+
emit<K extends EventKeys<T> & string>(event: K, receiver: Receiver, options: IClientPublishOptions, ...params: Parameters<T[K]>): void;
|
|
66
|
+
call<K extends ServiceKeys<T> & string>(service: K, ...params: Parameters<T[K]>): Promise<Awaited<ReturnType<T[K]>>>;
|
|
67
|
+
call<K extends ServiceKeys<T> & string>(service: K, receiver: Receiver, ...params: Parameters<T[K]>): Promise<Awaited<ReturnType<T[K]>>>;
|
|
68
|
+
call<K extends ServiceKeys<T> & string>(service: K, options: IClientPublishOptions, ...params: Parameters<T[K]>): Promise<Awaited<ReturnType<T[K]>>>;
|
|
69
|
+
call<K extends ServiceKeys<T> & string>(service: K, receiver: Receiver, options: IClientPublishOptions, ...params: Parameters<T[K]>): Promise<Awaited<ReturnType<T[K]>>>;
|
|
70
|
+
private _responseSubscribe;
|
|
71
|
+
private _responseUnsubscribe;
|
|
72
|
+
private _onMessage;
|
|
73
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
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
|
+
import { nanoid } from "nanoid";
|
|
25
|
+
/* internal requirements */
|
|
26
|
+
import Codec from "./mqtt-plus-codec";
|
|
27
|
+
import Msg, { EventEmission, ServiceRequest, ServiceResponseSuccess, ServiceResponseError } from "./mqtt-plus-msg";
|
|
28
|
+
/* MQTTp API class */
|
|
29
|
+
export default class MQTTp {
|
|
30
|
+
/* construct API class */
|
|
31
|
+
constructor(mqtt, options = {}) {
|
|
32
|
+
this.mqtt = mqtt;
|
|
33
|
+
this.msg = new Msg();
|
|
34
|
+
this.registry = new Map();
|
|
35
|
+
this.requests = new Map();
|
|
36
|
+
this.subscriptions = new Map();
|
|
37
|
+
/* determine options and provide defaults */
|
|
38
|
+
this.options = {
|
|
39
|
+
id: nanoid(),
|
|
40
|
+
codec: "cbor",
|
|
41
|
+
timeout: 10 * 1000,
|
|
42
|
+
topicEventNoticeMake: (name, peerId) => {
|
|
43
|
+
return peerId
|
|
44
|
+
? `${name}/event-notice/${peerId}`
|
|
45
|
+
: `${name}/event-notice`;
|
|
46
|
+
},
|
|
47
|
+
topicServiceRequestMake: (name, peerId) => {
|
|
48
|
+
return peerId
|
|
49
|
+
? `${name}/service-request/${peerId}`
|
|
50
|
+
: `${name}/service-request`;
|
|
51
|
+
},
|
|
52
|
+
topicServiceResponseMake: (name, peerId) => {
|
|
53
|
+
return peerId
|
|
54
|
+
? `${name}/service-response/${peerId}`
|
|
55
|
+
: `${name}/service-response`;
|
|
56
|
+
},
|
|
57
|
+
topicEventNoticeMatch: (topic) => {
|
|
58
|
+
const m = topic.match(/^(.+?)\/event-notice(?:\/(.+))?$/);
|
|
59
|
+
return m ? { name: m[1], peerId: m[2] } : null;
|
|
60
|
+
},
|
|
61
|
+
topicServiceRequestMatch: (topic) => {
|
|
62
|
+
const m = topic.match(/^(.+?)\/service-request(?:\/(.+))?$/);
|
|
63
|
+
return m ? { name: m[1], peerId: m[2] } : null;
|
|
64
|
+
},
|
|
65
|
+
topicServiceResponseMatch: (topic) => {
|
|
66
|
+
const m = topic.match(/^(.+?)\/service-response\/(.+)$/);
|
|
67
|
+
return m ? { name: m[1], peerId: m[2] } : null;
|
|
68
|
+
},
|
|
69
|
+
...options
|
|
70
|
+
};
|
|
71
|
+
/* establish an encoder */
|
|
72
|
+
this.codec = new Codec(this.options.codec);
|
|
73
|
+
/* hook into the MQTT message processing */
|
|
74
|
+
this.mqtt.on("message", (topic, message) => {
|
|
75
|
+
this._onMessage(topic, message);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/* subscribe to an MQTT topic (Promise-based) */
|
|
79
|
+
async _subscribeTopic(topic, options = {}) {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
this.mqtt.subscribe(topic, { qos: 2, ...options }, (err, _granted) => {
|
|
82
|
+
if (err)
|
|
83
|
+
reject(err);
|
|
84
|
+
else
|
|
85
|
+
resolve();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/* unsubscribe from an MQTT topic (Promise-based) */
|
|
90
|
+
async _unsubscribeTopic(topic) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
this.mqtt.unsubscribe(topic, (err, _packet) => {
|
|
93
|
+
if (err)
|
|
94
|
+
reject(err);
|
|
95
|
+
else
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async subscribe(event, ...args) {
|
|
101
|
+
/* determine parameters */
|
|
102
|
+
let options = {};
|
|
103
|
+
let callback = args[0];
|
|
104
|
+
if (args.length === 2 && typeof args[0] === "object") {
|
|
105
|
+
options = args[0];
|
|
106
|
+
callback = args[1];
|
|
107
|
+
}
|
|
108
|
+
/* sanity check situation */
|
|
109
|
+
if (this.registry.has(event))
|
|
110
|
+
throw new Error(`subscribe: event "${event}" already subscribed`);
|
|
111
|
+
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
112
|
+
const topicB = this.options.topicEventNoticeMake(event);
|
|
113
|
+
const topicD = this.options.topicEventNoticeMake(event, this.options.id);
|
|
114
|
+
/* subscribe to MQTT topics */
|
|
115
|
+
await Promise.all([
|
|
116
|
+
this._subscribeTopic(topicB, { qos: 0, ...options }),
|
|
117
|
+
this._subscribeTopic(topicD, { qos: 0, ...options })
|
|
118
|
+
]).catch((err) => {
|
|
119
|
+
this._unsubscribeTopic(topicB).catch(() => { });
|
|
120
|
+
this._unsubscribeTopic(topicD).catch(() => { });
|
|
121
|
+
throw err;
|
|
122
|
+
});
|
|
123
|
+
/* remember the subscription */
|
|
124
|
+
this.registry.set(event, callback);
|
|
125
|
+
/* provide a subscription for subsequent unsubscribing */
|
|
126
|
+
const self = this;
|
|
127
|
+
const subscription = {
|
|
128
|
+
async unsubscribe() {
|
|
129
|
+
if (!self.registry.has(event))
|
|
130
|
+
throw new Error(`unsubscribe: event "${event}" not subscribed`);
|
|
131
|
+
self.registry.delete(event);
|
|
132
|
+
return Promise.all([
|
|
133
|
+
self._unsubscribeTopic(topicB),
|
|
134
|
+
self._unsubscribeTopic(topicD)
|
|
135
|
+
]).then(() => { });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
return subscription;
|
|
139
|
+
}
|
|
140
|
+
async register(service, ...args) {
|
|
141
|
+
/* determine parameters */
|
|
142
|
+
let options = {};
|
|
143
|
+
let callback = args[0];
|
|
144
|
+
if (args.length === 2 && typeof args[0] === "object") {
|
|
145
|
+
options = args[0];
|
|
146
|
+
callback = args[1];
|
|
147
|
+
}
|
|
148
|
+
/* sanity check situation */
|
|
149
|
+
if (this.registry.has(service))
|
|
150
|
+
throw new Error(`register: service "${service}" already registered`);
|
|
151
|
+
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
152
|
+
const topicB = this.options.topicServiceRequestMake(service);
|
|
153
|
+
const topicD = this.options.topicServiceRequestMake(service, this.options.id);
|
|
154
|
+
/* subscribe to MQTT topics */
|
|
155
|
+
await Promise.all([
|
|
156
|
+
this._subscribeTopic(topicB, { qos: 2, ...options }),
|
|
157
|
+
this._subscribeTopic(topicD, { qos: 2, ...options })
|
|
158
|
+
]).catch((err) => {
|
|
159
|
+
this._unsubscribeTopic(topicB).catch(() => { });
|
|
160
|
+
this._unsubscribeTopic(topicD).catch(() => { });
|
|
161
|
+
throw err;
|
|
162
|
+
});
|
|
163
|
+
/* remember the registration */
|
|
164
|
+
this.registry.set(service, callback);
|
|
165
|
+
/* provide a registration for subsequent unregistering */
|
|
166
|
+
const self = this;
|
|
167
|
+
const registration = {
|
|
168
|
+
async unregister() {
|
|
169
|
+
if (!self.registry.has(service))
|
|
170
|
+
throw new Error(`unregister: service "${service}" not registered`);
|
|
171
|
+
self.registry.delete(service);
|
|
172
|
+
return Promise.all([
|
|
173
|
+
self._unsubscribeTopic(topicB),
|
|
174
|
+
self._unsubscribeTopic(topicD)
|
|
175
|
+
]).then(() => { });
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
return registration;
|
|
179
|
+
}
|
|
180
|
+
/* check whether argument has structure of interface IClientPublishOptions */
|
|
181
|
+
_isIClientPublishOptions(arg) {
|
|
182
|
+
if (typeof arg !== "object")
|
|
183
|
+
return false;
|
|
184
|
+
const keys = ["qos", "retain", "dup", "properties", "cbStorePut"];
|
|
185
|
+
return Object.keys(arg).every((key) => keys.includes(key));
|
|
186
|
+
}
|
|
187
|
+
/* wrap receiver id into object (required for type-safe overloading) */
|
|
188
|
+
receiver(id) {
|
|
189
|
+
return { __receiver: id };
|
|
190
|
+
}
|
|
191
|
+
/* return client id from wrapper object */
|
|
192
|
+
_getReceiver(obj) {
|
|
193
|
+
return obj.__receiver;
|
|
194
|
+
}
|
|
195
|
+
/* detect client id wrapper object */
|
|
196
|
+
_isReceiver(obj) {
|
|
197
|
+
return (typeof obj === "object"
|
|
198
|
+
&& obj !== null
|
|
199
|
+
&& "__receiver" in obj
|
|
200
|
+
&& typeof obj.__receiver === "string");
|
|
201
|
+
}
|
|
202
|
+
/* parse optional peerId and options from variadic arguments */
|
|
203
|
+
_parseCallArgs(args) {
|
|
204
|
+
let receiver;
|
|
205
|
+
let options = {};
|
|
206
|
+
let params = args;
|
|
207
|
+
if (args.length >= 2 && this._isReceiver(args[0]) && this._isIClientPublishOptions(args[1])) {
|
|
208
|
+
receiver = this._getReceiver(args[0]);
|
|
209
|
+
options = args[1];
|
|
210
|
+
params = args.slice(2);
|
|
211
|
+
}
|
|
212
|
+
else if (args.length >= 1 && this._isReceiver(args[0])) {
|
|
213
|
+
receiver = this._getReceiver(args[0]);
|
|
214
|
+
params = args.slice(1);
|
|
215
|
+
}
|
|
216
|
+
else if (args.length >= 1 && this._isIClientPublishOptions(args[0])) {
|
|
217
|
+
options = args[0];
|
|
218
|
+
params = args.slice(1);
|
|
219
|
+
}
|
|
220
|
+
return { receiver, options, params };
|
|
221
|
+
}
|
|
222
|
+
emit(event, ...args) {
|
|
223
|
+
/* determine actual parameters */
|
|
224
|
+
const { receiver, options, params } = this._parseCallArgs(args);
|
|
225
|
+
/* generate unique request id */
|
|
226
|
+
const rid = nanoid();
|
|
227
|
+
/* generate encoded Msg message */
|
|
228
|
+
const request = this.msg.makeEventEmission(rid, event, params, this.options.id, receiver);
|
|
229
|
+
const message = this.codec.encode(request);
|
|
230
|
+
/* generate corresponding MQTT topic */
|
|
231
|
+
const topic = this.options.topicEventNoticeMake(event, receiver);
|
|
232
|
+
/* publish message to MQTT topic */
|
|
233
|
+
this.mqtt.publish(topic, message, { qos: 2, ...options });
|
|
234
|
+
}
|
|
235
|
+
call(service, ...args) {
|
|
236
|
+
/* determine actual parameters */
|
|
237
|
+
const { receiver, options, params } = this._parseCallArgs(args);
|
|
238
|
+
/* generate unique request id */
|
|
239
|
+
const rid = nanoid();
|
|
240
|
+
/* subscribe to MQTT response topic */
|
|
241
|
+
this._responseSubscribe(service, { qos: options.qos ?? 2 });
|
|
242
|
+
/* create promise for MQTT response handling */
|
|
243
|
+
const promise = new Promise((resolve, reject) => {
|
|
244
|
+
let timer = setTimeout(() => {
|
|
245
|
+
this.requests.delete(rid);
|
|
246
|
+
this._responseUnsubscribe(service);
|
|
247
|
+
timer = null;
|
|
248
|
+
reject(new Error("communication timeout"));
|
|
249
|
+
}, this.options.timeout);
|
|
250
|
+
this.requests.set(rid, {
|
|
251
|
+
service,
|
|
252
|
+
callback: (err, result) => {
|
|
253
|
+
if (timer !== null) {
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
timer = null;
|
|
256
|
+
}
|
|
257
|
+
if (err)
|
|
258
|
+
reject(err);
|
|
259
|
+
else
|
|
260
|
+
resolve(result);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
/* generate encoded message */
|
|
265
|
+
const request = this.msg.makeServiceRequest(rid, service, params, this.options.id, receiver);
|
|
266
|
+
const message = this.codec.encode(request);
|
|
267
|
+
/* generate corresponding MQTT topic */
|
|
268
|
+
const topic = this.options.topicServiceRequestMake(service, receiver);
|
|
269
|
+
/* publish message to MQTT topic */
|
|
270
|
+
this.mqtt.publish(topic, message, { qos: 2, ...options }, (err) => {
|
|
271
|
+
/* handle request failure */
|
|
272
|
+
const pendingRequest = this.requests.get(rid);
|
|
273
|
+
if (err && pendingRequest !== undefined) {
|
|
274
|
+
this.requests.delete(rid);
|
|
275
|
+
this._responseUnsubscribe(service);
|
|
276
|
+
pendingRequest.callback(err, undefined);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
return promise;
|
|
280
|
+
}
|
|
281
|
+
/* subscribe to RPC response */
|
|
282
|
+
_responseSubscribe(service, options = { qos: 2 }) {
|
|
283
|
+
/* generate corresponding MQTT topic */
|
|
284
|
+
const topic = this.options.topicServiceResponseMake(service, this.options.id);
|
|
285
|
+
/* subscribe to MQTT topic and remember subscription */
|
|
286
|
+
if (!this.subscriptions.has(topic)) {
|
|
287
|
+
this.subscriptions.set(topic, 0);
|
|
288
|
+
this.mqtt.subscribe(topic, options, (err) => {
|
|
289
|
+
if (err)
|
|
290
|
+
this.mqtt.emit("error", err);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
this.subscriptions.set(topic, this.subscriptions.get(topic) + 1);
|
|
294
|
+
}
|
|
295
|
+
/* unsubscribe from RPC response */
|
|
296
|
+
_responseUnsubscribe(service) {
|
|
297
|
+
/* generate corresponding MQTT topic */
|
|
298
|
+
const topic = this.options.topicServiceResponseMake(service, this.options.id);
|
|
299
|
+
/* short-circuit processing if (no longer) subscribed */
|
|
300
|
+
if (!this.subscriptions.has(topic))
|
|
301
|
+
return;
|
|
302
|
+
/* unsubscribe from MQTT topic and forget subscription */
|
|
303
|
+
this.subscriptions.set(topic, this.subscriptions.get(topic) - 1);
|
|
304
|
+
if (this.subscriptions.get(topic) === 0) {
|
|
305
|
+
this.subscriptions.delete(topic);
|
|
306
|
+
this.mqtt.unsubscribe(topic, (err) => {
|
|
307
|
+
if (err)
|
|
308
|
+
this.mqtt.emit("error", err);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/* handle incoming MQTT message */
|
|
313
|
+
_onMessage(topic, message) {
|
|
314
|
+
/* ensure we handle only valid messages */
|
|
315
|
+
let eventMatch = null;
|
|
316
|
+
let requestMatch = null;
|
|
317
|
+
let responseMatch = null;
|
|
318
|
+
if ((eventMatch = this.options.topicEventNoticeMatch(topic)) === null
|
|
319
|
+
&& (requestMatch = this.options.topicServiceRequestMatch(topic)) === null
|
|
320
|
+
&& (responseMatch = this.options.topicServiceResponseMatch(topic)) === null)
|
|
321
|
+
return;
|
|
322
|
+
/* ensure we really handle only messages for us */
|
|
323
|
+
const peerId = eventMatch?.peerId ?? requestMatch?.peerId ?? responseMatch?.peerId;
|
|
324
|
+
if (peerId !== undefined && peerId !== this.options.id)
|
|
325
|
+
return;
|
|
326
|
+
/* try to parse payload as payload */
|
|
327
|
+
let parsed;
|
|
328
|
+
try {
|
|
329
|
+
let input = message;
|
|
330
|
+
if (this.options.codec === "json")
|
|
331
|
+
input = message.toString();
|
|
332
|
+
const payload = this.codec.decode(input);
|
|
333
|
+
parsed = this.msg.parse(payload);
|
|
334
|
+
}
|
|
335
|
+
catch (_err) {
|
|
336
|
+
const err = _err instanceof Error
|
|
337
|
+
? new Error(`failed to parse message: ${_err.message}`)
|
|
338
|
+
: new Error("failed to parse message");
|
|
339
|
+
this.mqtt.emit("error", err);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
/* dispatch according to message type */
|
|
343
|
+
if (parsed instanceof EventEmission) {
|
|
344
|
+
/* just deliver event */
|
|
345
|
+
const name = parsed.event;
|
|
346
|
+
const handler = this.registry.get(name);
|
|
347
|
+
const params = parsed.params ?? [];
|
|
348
|
+
const info = { sender: parsed.sender ?? "", receiver: parsed.receiver };
|
|
349
|
+
handler?.(...params, info);
|
|
350
|
+
}
|
|
351
|
+
else if (parsed instanceof ServiceRequest) {
|
|
352
|
+
/* deliver service request and send response */
|
|
353
|
+
const rid = parsed.id;
|
|
354
|
+
const name = parsed.service;
|
|
355
|
+
const handler = this.registry.get(name);
|
|
356
|
+
let response;
|
|
357
|
+
if (handler !== undefined) {
|
|
358
|
+
/* execute service handler */
|
|
359
|
+
const params = parsed.params ?? [];
|
|
360
|
+
const info = { sender: parsed.sender ?? "", receiver: parsed.receiver };
|
|
361
|
+
response = Promise.resolve().then(() => handler(...params, info));
|
|
362
|
+
}
|
|
363
|
+
else
|
|
364
|
+
response = Promise.reject(new Error(`method not found: ${name}`));
|
|
365
|
+
response.then((result) => {
|
|
366
|
+
/* create success response */
|
|
367
|
+
return this.msg.makeServiceResponseSuccess(rid, result, this.options.id, parsed.sender);
|
|
368
|
+
}, (result) => {
|
|
369
|
+
/* determine error message and build error response */
|
|
370
|
+
let errorMessage;
|
|
371
|
+
if (result === undefined || result === null)
|
|
372
|
+
errorMessage = "undefined error";
|
|
373
|
+
else if (typeof result === "string")
|
|
374
|
+
errorMessage = result;
|
|
375
|
+
else if (result instanceof Error)
|
|
376
|
+
errorMessage = result.message;
|
|
377
|
+
else
|
|
378
|
+
errorMessage = String(result);
|
|
379
|
+
return this.msg.makeServiceResponseError(rid, errorMessage, this.options.id, parsed.sender);
|
|
380
|
+
}).then((rpcResponse) => {
|
|
381
|
+
/* send response message */
|
|
382
|
+
const senderPeerId = parsed.sender;
|
|
383
|
+
if (senderPeerId === undefined)
|
|
384
|
+
throw new Error("invalid request: missing sender");
|
|
385
|
+
const encoded = this.codec.encode(rpcResponse);
|
|
386
|
+
const topic = this.options.topicServiceResponseMake(name, senderPeerId);
|
|
387
|
+
this.mqtt.publish(topic, encoded, { qos: 2 });
|
|
388
|
+
}).catch((err) => {
|
|
389
|
+
this.mqtt.emit("error", err);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
else if (parsed instanceof ServiceResponseSuccess || parsed instanceof ServiceResponseError) {
|
|
393
|
+
/* handle service response */
|
|
394
|
+
const rid = parsed.id;
|
|
395
|
+
const request = this.requests.get(rid);
|
|
396
|
+
if (request !== undefined) {
|
|
397
|
+
/* call callback function */
|
|
398
|
+
if (parsed instanceof ServiceResponseSuccess)
|
|
399
|
+
request.callback(undefined, parsed.result);
|
|
400
|
+
else if (parsed instanceof ServiceResponseError)
|
|
401
|
+
request.callback(new Error(parsed.error), undefined);
|
|
402
|
+
/* unsubscribe from response */
|
|
403
|
+
this.requests.delete(rid);
|
|
404
|
+
this._responseUnsubscribe(request.service);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|