mqtt-plus 1.4.0 → 1.4.2
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/AGENTS.md +55 -44
- package/CHANGELOG.md +14 -0
- package/README.md +4 -3
- package/doc/mqtt-plus-api.md +693 -680
- package/doc/mqtt-plus-architecture.d2 +139 -0
- package/doc/mqtt-plus-architecture.md +10 -0
- package/doc/mqtt-plus-architecture.svg +95 -0
- package/doc/mqtt-plus-broker-setup.md +9 -3
- package/doc/mqtt-plus-comm.md +73 -0
- package/doc/mqtt-plus-internals.md +3 -3
- package/dst-stage1/mqtt-plus-base.d.ts +3 -2
- package/dst-stage1/mqtt-plus-base.js +53 -22
- package/dst-stage1/mqtt-plus-event.d.ts +0 -2
- package/dst-stage1/mqtt-plus-event.js +6 -26
- package/dst-stage1/mqtt-plus-meta.d.ts +2 -2
- package/dst-stage1/mqtt-plus-meta.js +2 -2
- package/dst-stage1/mqtt-plus-msg.d.ts +2 -0
- package/dst-stage1/mqtt-plus-msg.js +17 -0
- package/dst-stage1/mqtt-plus-service.d.ts +0 -5
- package/dst-stage1/mqtt-plus-service.js +12 -48
- package/dst-stage1/mqtt-plus-sink.d.ts +0 -10
- package/dst-stage1/mqtt-plus-sink.js +25 -92
- package/dst-stage1/mqtt-plus-source.d.ts +0 -10
- package/dst-stage1/mqtt-plus-source.js +23 -88
- package/dst-stage1/mqtt-plus-subscription.d.ts +20 -0
- package/dst-stage1/mqtt-plus-subscription.js +126 -0
- package/dst-stage1/mqtt-plus-timer.d.ts +8 -0
- package/dst-stage1/mqtt-plus-timer.js +57 -0
- package/dst-stage1/mqtt-plus-topic.d.ts +20 -0
- package/dst-stage1/mqtt-plus-topic.js +112 -0
- package/dst-stage1/mqtt-plus-trace.js +2 -0
- package/dst-stage1/mqtt-plus-util.d.ts +0 -13
- package/dst-stage1/mqtt-plus-util.js +1 -77
- package/dst-stage1/mqtt-plus-version.d.ts +0 -1
- package/dst-stage1/mqtt-plus-version.js +0 -6
- package/dst-stage1/tsc.tsbuildinfo +1 -1
- package/dst-stage2/mqtt-plus.cjs.js +242 -292
- package/dst-stage2/mqtt-plus.esm.js +240 -290
- package/dst-stage2/mqtt-plus.umd.js +12 -12
- package/etc/knip.jsonc +1 -1
- package/etc/stx.conf +6 -4
- package/package.json +1 -1
- package/src/mqtt-plus-base.ts +56 -26
- package/src/mqtt-plus-event.ts +8 -24
- package/src/mqtt-plus-meta.ts +3 -3
- package/src/mqtt-plus-msg.ts +28 -0
- package/src/mqtt-plus-service.ts +12 -50
- package/src/mqtt-plus-sink.ts +32 -105
- package/src/mqtt-plus-source.ts +29 -99
- package/src/mqtt-plus-subscription.ts +141 -0
- package/src/mqtt-plus-timer.ts +61 -0
- package/src/mqtt-plus-trace.ts +4 -0
- package/src/mqtt-plus-util.ts +1 -81
- package/src/mqtt-plus-version.ts +0 -7
- package/tst/mqtt-plus-0-fixture.ts +2 -2
- package/tst/mqtt-plus-0-mosquitto.ts +5 -0
- package/tst/mqtt-plus-1-api.spec.ts +1 -1
- package/tst/mqtt-plus-2-event.spec.ts +0 -6
- package/tst/mqtt-plus-3-service.spec.ts +3 -7
- package/tst/mqtt-plus-4-sink.spec.ts +14 -9
- package/tst/mqtt-plus-5-source.spec.ts +11 -5
- package/tst/mqtt-plus-6-misc.spec.ts +23 -23
- package/tst/tsc.json +1 -1
- package/doc/mqtt-plus-communication.md +0 -68
- /package/doc/{mqtt-plus-1-event-emission.d2 → mqtt-plus-comm-event-emission.d2} +0 -0
- /package/doc/{mqtt-plus-1-event-emission.svg → mqtt-plus-comm-event-emission.svg} +0 -0
- /package/doc/{mqtt-plus-2-service-call.d2 → mqtt-plus-comm-service-call.d2} +0 -0
- /package/doc/{mqtt-plus-2-service-call.svg → mqtt-plus-comm-service-call.svg} +0 -0
- /package/doc/{mqtt-plus-3-sink-push.d2 → mqtt-plus-comm-sink-push.d2} +0 -0
- /package/doc/{mqtt-plus-3-sink-push.svg → mqtt-plus-comm-sink-push.svg} +0 -0
- /package/doc/{mqtt-plus-4-source-fetch.d2 → mqtt-plus-comm-source-fetch.d2} +0 -0
- /package/doc/{mqtt-plus-4-source-fetch.svg → mqtt-plus-comm-source-fetch.svg} +0 -0
- /package/{doc/theme.d2 → etc/d2.theme.d2} +0 -0
|
@@ -22,17 +22,10 @@
|
|
|
22
22
|
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
23
|
*/
|
|
24
24
|
import { nanoid } from "nanoid";
|
|
25
|
-
/* internal requirements */
|
|
26
|
-
import { EventEmission } from "./mqtt-plus-msg";
|
|
27
25
|
import { AuthTrait } from "./mqtt-plus-auth";
|
|
28
26
|
import { run, Spool, ensureError } from "./mqtt-plus-error";
|
|
29
27
|
/* Event Emission Trait */
|
|
30
28
|
export class EventTrait extends AuthTrait {
|
|
31
|
-
constructor() {
|
|
32
|
-
super(...arguments);
|
|
33
|
-
/* internal state */
|
|
34
|
-
this.events = new Map();
|
|
35
|
-
}
|
|
36
29
|
async event(nameOrConfig, ...args) {
|
|
37
30
|
/* determine actual parameters */
|
|
38
31
|
let name;
|
|
@@ -56,14 +49,14 @@ export class EventTrait extends AuthTrait {
|
|
|
56
49
|
/* create resource spool */
|
|
57
50
|
const spool = new Spool();
|
|
58
51
|
/* sanity check situation */
|
|
59
|
-
if (this.
|
|
52
|
+
if (this.onRequest.has(`event-emission:${name}`))
|
|
60
53
|
throw new Error(`event: event "${name}" already registered`);
|
|
61
54
|
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
62
55
|
const topicS = share ? `$share/${share}/${name}` : name;
|
|
63
56
|
const topicB = this.options.topicMake(topicS, "event-emission");
|
|
64
57
|
const topicD = this.options.topicMake(name, "event-emission", this.options.id);
|
|
65
58
|
/* remember the registration */
|
|
66
|
-
this.
|
|
59
|
+
this.onRequest.set(`event-emission:${name}`, (request, topicName) => {
|
|
67
60
|
/* determine event information */
|
|
68
61
|
const senderId = request.sender;
|
|
69
62
|
const params = request.params ?? [];
|
|
@@ -87,7 +80,7 @@ export class EventTrait extends AuthTrait {
|
|
|
87
80
|
this.error(error, `handler for event "${name}" failed`);
|
|
88
81
|
});
|
|
89
82
|
});
|
|
90
|
-
spool.roll(() => { this.
|
|
83
|
+
spool.roll(() => { this.onRequest.delete(`event-emission:${name}`); });
|
|
91
84
|
/* subscribe to MQTT topics */
|
|
92
85
|
await run(`subscribe to MQTT topic "${topicB}"`, spool, () => this._subscribeTopic(topicB, { qos: 2, ...options }));
|
|
93
86
|
spool.roll(() => this._unsubscribeTopic(topicB).catch(() => { }));
|
|
@@ -96,7 +89,7 @@ export class EventTrait extends AuthTrait {
|
|
|
96
89
|
/* provide a registration for subsequent destruction */
|
|
97
90
|
return {
|
|
98
91
|
destroy: async () => {
|
|
99
|
-
if (!this.
|
|
92
|
+
if (!this.onRequest.has(`event-emission:${name}`))
|
|
100
93
|
throw new Error(`destroy: event "${name}" not registered`);
|
|
101
94
|
await spool.unroll(false)?.catch((err) => {
|
|
102
95
|
this.error(err, `destroy: failed to cleanup: ${err.message}`);
|
|
@@ -110,7 +103,7 @@ export class EventTrait extends AuthTrait {
|
|
|
110
103
|
let params;
|
|
111
104
|
let receiver;
|
|
112
105
|
let options = {};
|
|
113
|
-
let meta
|
|
106
|
+
let meta;
|
|
114
107
|
let dry;
|
|
115
108
|
if (typeof eventOrConfig === "object" && eventOrConfig !== null) {
|
|
116
109
|
/* object-based API */
|
|
@@ -118,7 +111,7 @@ export class EventTrait extends AuthTrait {
|
|
|
118
111
|
params = eventOrConfig.params;
|
|
119
112
|
receiver = eventOrConfig.receiver;
|
|
120
113
|
options = eventOrConfig.options ?? {};
|
|
121
|
-
meta = eventOrConfig.meta
|
|
114
|
+
meta = eventOrConfig.meta;
|
|
122
115
|
dry = eventOrConfig.dry;
|
|
123
116
|
}
|
|
124
117
|
else {
|
|
@@ -145,17 +138,4 @@ export class EventTrait extends AuthTrait {
|
|
|
145
138
|
this.error(err, `emitting event "${event}" failed`);
|
|
146
139
|
});
|
|
147
140
|
}
|
|
148
|
-
/* dispatch message (Event pattern handling) */
|
|
149
|
-
async _dispatchMessage(topic, message) {
|
|
150
|
-
await super._dispatchMessage(topic, message);
|
|
151
|
-
const topicMatch = this.options.topicMatch(topic);
|
|
152
|
-
/* on server-side handle event emission request */
|
|
153
|
-
if (topicMatch !== null
|
|
154
|
-
&& topicMatch.operation === "event-emission"
|
|
155
|
-
&& message instanceof EventEmission) {
|
|
156
|
-
const handler = this.events.get(message.name);
|
|
157
|
-
if (handler !== undefined)
|
|
158
|
-
handler(message, topicMatch.name);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
141
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { APISchema } from "./mqtt-plus-api";
|
|
2
|
-
import {
|
|
3
|
-
export declare class MetaTrait<T extends APISchema = APISchema> extends
|
|
2
|
+
import { TimerTrait } from "./mqtt-plus-timer";
|
|
3
|
+
export declare class MetaTrait<T extends APISchema = APISchema> extends TimerTrait<T> {
|
|
4
4
|
private _meta;
|
|
5
5
|
meta(): Record<string, any>;
|
|
6
6
|
meta(key: string): any;
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
22
|
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
23
|
*/
|
|
24
|
-
import {
|
|
24
|
+
import { TimerTrait } from "./mqtt-plus-timer";
|
|
25
25
|
/* Meta trait with meta information management */
|
|
26
|
-
export class MetaTrait extends
|
|
26
|
+
export class MetaTrait extends TimerTrait {
|
|
27
27
|
constructor() {
|
|
28
28
|
super(...arguments);
|
|
29
29
|
/* internal state */
|
|
@@ -95,6 +95,8 @@ declare class Msg {
|
|
|
95
95
|
makeSourceFetchChunk(id: string, name: string, chunk?: Uint8Array, error?: string, final?: boolean, sender?: string, receiver?: string): SourceFetchChunk;
|
|
96
96
|
makeSourceFetchCredit(id: string, name: string, credit: number, sender?: string, receiver?: string): SourceFetchCredit;
|
|
97
97
|
parse(obj: any): EventEmission | ServiceCallRequest | ServiceCallResponse | SinkPushRequest | SinkPushResponse | SinkPushChunk | SinkPushCredit | SourceFetchRequest | SourceFetchResponse | SourceFetchChunk | SourceFetchCredit;
|
|
98
|
+
isRequest(msg: any): msg is (EventEmission | ServiceCallRequest | SourceFetchRequest | SinkPushRequest);
|
|
99
|
+
isResponse(msg: any): msg is (ServiceCallResponse | SinkPushResponse | SinkPushChunk | SinkPushCredit | SourceFetchResponse | SourceFetchChunk | SourceFetchCredit);
|
|
98
100
|
}
|
|
99
101
|
export declare class MsgTrait<T extends APISchema = APISchema> extends EncodeTrait<T> {
|
|
100
102
|
protected msg: Msg;
|
|
@@ -343,6 +343,23 @@ class Msg {
|
|
|
343
343
|
else
|
|
344
344
|
throw new Error("invalid object: not of any known type");
|
|
345
345
|
}
|
|
346
|
+
/* guard for request messages */
|
|
347
|
+
isRequest(msg) {
|
|
348
|
+
return (msg instanceof EventEmission
|
|
349
|
+
|| msg instanceof ServiceCallRequest
|
|
350
|
+
|| msg instanceof SourceFetchRequest
|
|
351
|
+
|| msg instanceof SinkPushRequest);
|
|
352
|
+
}
|
|
353
|
+
/* guard for response messages */
|
|
354
|
+
isResponse(msg) {
|
|
355
|
+
return (msg instanceof ServiceCallResponse
|
|
356
|
+
|| msg instanceof SinkPushResponse
|
|
357
|
+
|| msg instanceof SinkPushChunk
|
|
358
|
+
|| msg instanceof SinkPushCredit
|
|
359
|
+
|| msg instanceof SourceFetchResponse
|
|
360
|
+
|| msg instanceof SourceFetchChunk
|
|
361
|
+
|| msg instanceof SourceFetchCredit);
|
|
362
|
+
}
|
|
346
363
|
}
|
|
347
364
|
/* message trait */
|
|
348
365
|
export class MsgTrait extends EncodeTrait {
|
|
@@ -4,10 +4,6 @@ import type { WithInfo, InfoService } from "./mqtt-plus-info";
|
|
|
4
4
|
import { EventTrait } from "./mqtt-plus-event";
|
|
5
5
|
import type { AuthOption } from "./mqtt-plus-auth";
|
|
6
6
|
export declare class ServiceTrait<T extends APISchema = APISchema> extends EventTrait<T> {
|
|
7
|
-
private services;
|
|
8
|
-
private callCallbacks;
|
|
9
|
-
private callSubscriptions;
|
|
10
|
-
destroy(): void;
|
|
11
7
|
service<K extends ServiceKeys<T> & string>(name: K, callback: WithInfo<T[K], InfoService>): Promise<Registration>;
|
|
12
8
|
service<K extends ServiceKeys<T> & string>(config: {
|
|
13
9
|
name: K;
|
|
@@ -24,5 +20,4 @@ export declare class ServiceTrait<T extends APISchema = APISchema> extends Event
|
|
|
24
20
|
options?: IClientPublishOptions;
|
|
25
21
|
meta?: Record<string, any>;
|
|
26
22
|
}): Promise<ReturnType<T[K]>>;
|
|
27
|
-
protected _dispatchMessage(topic: string, message: any): Promise<void>;
|
|
28
23
|
}
|
|
@@ -23,24 +23,10 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { nanoid } from "nanoid";
|
|
25
25
|
/* internal requirements */
|
|
26
|
-
import { RefCountedSubscription } from "./mqtt-plus-util";
|
|
27
26
|
import { run, Spool, ensureError } from "./mqtt-plus-error";
|
|
28
|
-
import { ServiceCallRequest, ServiceCallResponse } from "./mqtt-plus-msg";
|
|
29
27
|
import { EventTrait } from "./mqtt-plus-event";
|
|
30
28
|
/* Service Call Trait */
|
|
31
29
|
export class ServiceTrait extends EventTrait {
|
|
32
|
-
constructor() {
|
|
33
|
-
super(...arguments);
|
|
34
|
-
/* internal state */
|
|
35
|
-
this.services = new Map();
|
|
36
|
-
this.callCallbacks = new Map();
|
|
37
|
-
this.callSubscriptions = new RefCountedSubscription((topic, options) => this._subscribeTopic(topic, options), (topic) => this._unsubscribeTopic(topic));
|
|
38
|
-
}
|
|
39
|
-
/* destroy service trait */
|
|
40
|
-
destroy() {
|
|
41
|
-
super.destroy();
|
|
42
|
-
this.callSubscriptions.flush();
|
|
43
|
-
}
|
|
44
30
|
async service(nameOrConfig, ...args) {
|
|
45
31
|
/* determine actual parameters */
|
|
46
32
|
let name;
|
|
@@ -64,14 +50,14 @@ export class ServiceTrait extends EventTrait {
|
|
|
64
50
|
/* create a resource spool */
|
|
65
51
|
const spool = new Spool();
|
|
66
52
|
/* sanity check situation */
|
|
67
|
-
if (this.
|
|
68
|
-
throw new Error(`
|
|
53
|
+
if (this.onRequest.has(`service-call-request:${name}`))
|
|
54
|
+
throw new Error(`service: service "${name}" already registered`);
|
|
69
55
|
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
70
56
|
const topicS = `$share/${share}/${name}`;
|
|
71
57
|
const topicB = this.options.topicMake(topicS, "service-call-request");
|
|
72
58
|
const topicD = this.options.topicMake(name, "service-call-request", this.options.id);
|
|
73
59
|
/* remember the registration */
|
|
74
|
-
this.
|
|
60
|
+
this.onRequest.set(`service-call-request:${name}`, (request, topicName) => {
|
|
75
61
|
/* determine request information */
|
|
76
62
|
const requestId = request.id;
|
|
77
63
|
const senderId = request.sender;
|
|
@@ -110,7 +96,7 @@ export class ServiceTrait extends EventTrait {
|
|
|
110
96
|
this.error(err, `handler for service "${name}" failed`);
|
|
111
97
|
});
|
|
112
98
|
});
|
|
113
|
-
spool.roll(() => { this.
|
|
99
|
+
spool.roll(() => { this.onRequest.delete(`service-call-request:${name}`); });
|
|
114
100
|
/* subscribe to MQTT topics */
|
|
115
101
|
await run(`subscribe to MQTT topic "${topicB}"`, spool, () => this._subscribeTopic(topicB, { qos: 2, ...options }));
|
|
116
102
|
spool.roll(() => this._unsubscribeTopic(topicB).catch(() => { }));
|
|
@@ -119,8 +105,8 @@ export class ServiceTrait extends EventTrait {
|
|
|
119
105
|
/* provide a registration for subsequent destruction */
|
|
120
106
|
return {
|
|
121
107
|
destroy: async () => {
|
|
122
|
-
if (!this.
|
|
123
|
-
throw new Error(`destroy: service "${name}"
|
|
108
|
+
if (!this.onRequest.has(`service-call-request:${name}`))
|
|
109
|
+
throw new Error(`destroy: service "${name}" not registered`);
|
|
124
110
|
await spool.unroll(false)?.catch((err) => {
|
|
125
111
|
this.error(err, `destroy: failed to cleanup: ${err.message}`);
|
|
126
112
|
});
|
|
@@ -133,14 +119,14 @@ export class ServiceTrait extends EventTrait {
|
|
|
133
119
|
let params;
|
|
134
120
|
let receiver;
|
|
135
121
|
let options = {};
|
|
136
|
-
let meta
|
|
122
|
+
let meta;
|
|
137
123
|
if (typeof nameOrConfig === "object" && nameOrConfig !== null) {
|
|
138
124
|
/* object-based API */
|
|
139
125
|
name = nameOrConfig.name;
|
|
140
126
|
params = nameOrConfig.params;
|
|
141
127
|
receiver = nameOrConfig.receiver;
|
|
142
128
|
options = nameOrConfig.options ?? {};
|
|
143
|
-
meta = nameOrConfig.meta
|
|
129
|
+
meta = nameOrConfig.meta;
|
|
144
130
|
}
|
|
145
131
|
else {
|
|
146
132
|
/* positional API */
|
|
@@ -153,8 +139,8 @@ export class ServiceTrait extends EventTrait {
|
|
|
153
139
|
const requestId = nanoid();
|
|
154
140
|
/* subscribe to MQTT response topic */
|
|
155
141
|
const responseTopic = this.options.topicMake(name, "service-call-response", this.options.id);
|
|
156
|
-
await run(`subscribe to MQTT topic "${responseTopic}"`, spool, () => this.
|
|
157
|
-
spool.roll(() => this.
|
|
142
|
+
await run(`subscribe to MQTT topic "${responseTopic}"`, spool, () => this.subscriptions.subscribe(responseTopic, { qos: options.qos ?? 2 }));
|
|
143
|
+
spool.roll(() => this.subscriptions.unsubscribe(responseTopic));
|
|
158
144
|
/* create promise for MQTT response handling */
|
|
159
145
|
const promise = new Promise((resolve, reject) => {
|
|
160
146
|
let timer = setTimeout(async () => {
|
|
@@ -168,14 +154,14 @@ export class ServiceTrait extends EventTrait {
|
|
|
168
154
|
timer = null;
|
|
169
155
|
}
|
|
170
156
|
});
|
|
171
|
-
this.
|
|
157
|
+
this.onResponse.set(`service-call-response:${requestId}`, async (response) => {
|
|
172
158
|
await spool.unroll();
|
|
173
159
|
if (response.error !== undefined)
|
|
174
160
|
reject(new Error(response.error));
|
|
175
161
|
else
|
|
176
162
|
resolve(response.result);
|
|
177
163
|
});
|
|
178
|
-
spool.roll(() => { this.
|
|
164
|
+
spool.roll(() => { this.onResponse.delete(`service-call-response:${requestId}`); });
|
|
179
165
|
});
|
|
180
166
|
/* generate encoded message */
|
|
181
167
|
const auth = this.authenticate();
|
|
@@ -188,26 +174,4 @@ export class ServiceTrait extends EventTrait {
|
|
|
188
174
|
await run(`publish service request as MQTT message to topic "${topic}"`, spool, () => this._publishToTopic(topic, message, { qos: 2, ...options }));
|
|
189
175
|
return promise;
|
|
190
176
|
}
|
|
191
|
-
/* dispatch message (Service pattern handling) */
|
|
192
|
-
async _dispatchMessage(topic, message) {
|
|
193
|
-
await super._dispatchMessage(topic, message);
|
|
194
|
-
const topicMatch = this.options.topicMatch(topic);
|
|
195
|
-
/* on server-side handle service call request */
|
|
196
|
-
if (topicMatch !== null
|
|
197
|
-
&& topicMatch.operation === "service-call-request"
|
|
198
|
-
&& message instanceof ServiceCallRequest) {
|
|
199
|
-
const handler = this.services.get(message.name);
|
|
200
|
-
if (handler !== undefined)
|
|
201
|
-
handler(message, topicMatch.name);
|
|
202
|
-
}
|
|
203
|
-
/* on client-side handle service call response */
|
|
204
|
-
else if (topicMatch !== null
|
|
205
|
-
&& topicMatch.operation === "service-call-response"
|
|
206
|
-
&& topicMatch.peerId === this.options.id
|
|
207
|
-
&& message instanceof ServiceCallResponse) {
|
|
208
|
-
const handler = this.callCallbacks.get(message.id);
|
|
209
|
-
if (handler !== undefined)
|
|
210
|
-
handler(message);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
177
|
}
|
|
@@ -5,17 +5,8 @@ import type { WithInfo, InfoSink } from "./mqtt-plus-info";
|
|
|
5
5
|
import { SourceTrait } from "./mqtt-plus-source";
|
|
6
6
|
import type { AuthOption } from "./mqtt-plus-auth";
|
|
7
7
|
export declare class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
|
|
8
|
-
private sinks;
|
|
9
8
|
private pushStreams;
|
|
10
9
|
private pushSpools;
|
|
11
|
-
private pushTimers;
|
|
12
|
-
private pushChunkCallbacks;
|
|
13
|
-
private pushResponseCallbacks;
|
|
14
|
-
private pushCreditCallbacks;
|
|
15
|
-
private pushSubscriptions;
|
|
16
|
-
destroy(): void;
|
|
17
|
-
private _refreshPushTimer;
|
|
18
|
-
private _clearPushTimer;
|
|
19
10
|
sink<K extends SinkKeys<T> & string>(name: K, callback: WithInfo<T[K], InfoSink>): Promise<Registration>;
|
|
20
11
|
sink<K extends SinkKeys<T> & string>(config: {
|
|
21
12
|
name: K;
|
|
@@ -33,5 +24,4 @@ export declare class SinkTrait<T extends APISchema = APISchema> extends SourceTr
|
|
|
33
24
|
options?: IClientPublishOptions;
|
|
34
25
|
meta?: Record<string, any>;
|
|
35
26
|
}): Promise<void>;
|
|
36
|
-
protected _dispatchMessage(topic: string, message: any): Promise<void>;
|
|
37
27
|
}
|
|
@@ -25,50 +25,16 @@
|
|
|
25
25
|
import { Readable } from "node:stream";
|
|
26
26
|
import { nanoid } from "nanoid";
|
|
27
27
|
/* internal requirements */
|
|
28
|
-
import { CreditGate,
|
|
28
|
+
import { CreditGate, streamToBuffer, sendBufferAsChunks, sendStreamAsChunks, makeMutuallyExclusiveFields } from "./mqtt-plus-util";
|
|
29
29
|
import { run, Spool } from "./mqtt-plus-error";
|
|
30
|
-
import { SinkPushRequest, SinkPushResponse, SinkPushChunk, SinkPushCredit } from "./mqtt-plus-msg";
|
|
31
30
|
import { SourceTrait } from "./mqtt-plus-source";
|
|
32
31
|
/* Sink Push Trait */
|
|
33
32
|
export class SinkTrait extends SourceTrait {
|
|
34
33
|
constructor() {
|
|
35
34
|
super(...arguments);
|
|
36
35
|
/* sink state */
|
|
37
|
-
this.sinks = new Map();
|
|
38
36
|
this.pushStreams = new Map();
|
|
39
37
|
this.pushSpools = new Map();
|
|
40
|
-
this.pushTimers = new Map();
|
|
41
|
-
this.pushChunkCallbacks = new Map();
|
|
42
|
-
this.pushResponseCallbacks = new Map();
|
|
43
|
-
this.pushCreditCallbacks = new Map();
|
|
44
|
-
this.pushSubscriptions = new RefCountedSubscription((topic, options) => this._subscribeTopic(topic, options), (topic) => this._unsubscribeTopic(topic));
|
|
45
|
-
}
|
|
46
|
-
/* destroy sink trait */
|
|
47
|
-
destroy() {
|
|
48
|
-
super.destroy();
|
|
49
|
-
this.pushSubscriptions.flush();
|
|
50
|
-
}
|
|
51
|
-
/* refresh push timer for a specific request */
|
|
52
|
-
_refreshPushTimer(requestId) {
|
|
53
|
-
const timer = this.pushTimers.get(requestId);
|
|
54
|
-
if (timer !== undefined)
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
this.pushTimers.set(requestId, setTimeout(() => {
|
|
57
|
-
this.pushTimers.delete(requestId);
|
|
58
|
-
const stream = this.pushStreams.get(requestId);
|
|
59
|
-
if (stream !== undefined)
|
|
60
|
-
stream.destroy(new Error("push stream timeout"));
|
|
61
|
-
const spool = this.pushSpools.get(requestId);
|
|
62
|
-
spool?.unroll();
|
|
63
|
-
}, this.options.timeout));
|
|
64
|
-
}
|
|
65
|
-
/* clear push timer for a specific request */
|
|
66
|
-
_clearPushTimer(requestId) {
|
|
67
|
-
const timer = this.pushTimers.get(requestId);
|
|
68
|
-
if (timer !== undefined) {
|
|
69
|
-
clearTimeout(timer);
|
|
70
|
-
this.pushTimers.delete(requestId);
|
|
71
|
-
}
|
|
72
38
|
}
|
|
73
39
|
async sink(nameOrConfig, ...args) {
|
|
74
40
|
/* determine actual parameters */
|
|
@@ -93,7 +59,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
93
59
|
/* create a resource spool */
|
|
94
60
|
const spool = new Spool();
|
|
95
61
|
/* sanity check situation */
|
|
96
|
-
if (this.
|
|
62
|
+
if (this.onRequest.has(`sink-push-request:${name}`))
|
|
97
63
|
throw new Error(`sink: sink "${name}" already established`);
|
|
98
64
|
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
99
65
|
const topicS = `$share/${share}/${name}`;
|
|
@@ -101,7 +67,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
101
67
|
const topicReqD = this.options.topicMake(name, "sink-push-request", this.options.id);
|
|
102
68
|
const topicChunkD = this.options.topicMake(name, "sink-push-chunk", this.options.id);
|
|
103
69
|
/* remember the registration */
|
|
104
|
-
this.
|
|
70
|
+
this.onRequest.set(`sink-push-request:${name}`, (request, topicName) => {
|
|
105
71
|
/* determine information */
|
|
106
72
|
const requestId = request.id;
|
|
107
73
|
const params = request.params ?? [];
|
|
@@ -144,8 +110,14 @@ export class SinkTrait extends SourceTrait {
|
|
|
144
110
|
creditGranted: chunkCredit
|
|
145
111
|
} : undefined;
|
|
146
112
|
/* utility functions for timeout management */
|
|
147
|
-
const refreshPushTimeout = () => this.
|
|
148
|
-
|
|
113
|
+
const refreshPushTimeout = () => this.timerRefresh(requestId, () => {
|
|
114
|
+
const stream = this.pushStreams.get(requestId);
|
|
115
|
+
if (stream !== undefined)
|
|
116
|
+
stream.destroy(new Error("push stream timeout"));
|
|
117
|
+
const spool = this.pushSpools.get(requestId);
|
|
118
|
+
spool?.unroll();
|
|
119
|
+
});
|
|
120
|
+
const clearPushTimeout = () => this.timerClear(requestId);
|
|
149
121
|
/* create a readable for buffering received chunks */
|
|
150
122
|
const readable = new Readable({
|
|
151
123
|
highWaterMark: chunkCredit > 0 ? chunkCredit * this.options.chunkSize : 16 * 1024,
|
|
@@ -170,7 +142,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
170
142
|
readable.once("close", () => reqSpool.unroll());
|
|
171
143
|
readable.once("error", () => reqSpool.unroll());
|
|
172
144
|
/* register chunk dispatch callback */
|
|
173
|
-
this.
|
|
145
|
+
this.onResponse.set(`sink-push-chunk:${requestId}`, (chunkParsed, chunkTopicName) => {
|
|
174
146
|
if (chunkTopicName !== chunkParsed.name)
|
|
175
147
|
throw new Error(`sink name mismatch between topic "${chunkTopicName}" ` +
|
|
176
148
|
`and payload "${chunkParsed.name}"`);
|
|
@@ -191,7 +163,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
191
163
|
}
|
|
192
164
|
}
|
|
193
165
|
});
|
|
194
|
-
reqSpool.roll(() => { this.
|
|
166
|
+
reqSpool.roll(() => { this.onResponse.delete(`sink-push-chunk:${requestId}`); });
|
|
195
167
|
/* start timeout for push stream cleanup */
|
|
196
168
|
refreshPushTimeout();
|
|
197
169
|
reqSpool.roll(() => { clearPushTimeout(); });
|
|
@@ -215,7 +187,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
215
187
|
await sendResponse(err.message).catch(() => { });
|
|
216
188
|
});
|
|
217
189
|
});
|
|
218
|
-
spool.roll(() => { this.
|
|
190
|
+
spool.roll(() => { this.onRequest.delete(`sink-push-request:${name}`); });
|
|
219
191
|
/* subscribe to MQTT topics */
|
|
220
192
|
await run(`subscribe to MQTT topic "${topicReqB}"`, spool, () => this._subscribeTopic(topicReqB, { qos: 2, ...options }));
|
|
221
193
|
spool.roll(() => this._unsubscribeTopic(topicReqB).catch(() => { }));
|
|
@@ -226,7 +198,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
226
198
|
/* provide a registration for subsequent destruction */
|
|
227
199
|
return {
|
|
228
200
|
destroy: async () => {
|
|
229
|
-
if (!this.
|
|
201
|
+
if (!this.onRequest.has(`sink-push-request:${name}`))
|
|
230
202
|
throw new Error(`destroy: sink "${name}" not established`);
|
|
231
203
|
await spool.unroll(false)?.catch((err) => {
|
|
232
204
|
this.error(err, `destroy: failed to cleanup: ${err.message}`);
|
|
@@ -263,8 +235,8 @@ export class SinkTrait extends SourceTrait {
|
|
|
263
235
|
const requestId = nanoid();
|
|
264
236
|
/* subscribe to response topic (for ack/nak) */
|
|
265
237
|
const responseTopic = this.options.topicMake(name, "sink-push-response", this.options.id);
|
|
266
|
-
await run(`subscribe to MQTT topic "${responseTopic}"`, spool, () => this.
|
|
267
|
-
spool.roll(() => this.
|
|
238
|
+
await run(`subscribe to MQTT topic "${responseTopic}"`, spool, () => this.subscriptions.subscribe(responseTopic, { qos: options.qos ?? 2 }));
|
|
239
|
+
spool.roll(() => this.subscriptions.unsubscribe(responseTopic));
|
|
268
240
|
/* define abort controller and signal */
|
|
269
241
|
const abortController = new AbortController();
|
|
270
242
|
const abortSignal = abortController.signal;
|
|
@@ -297,7 +269,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
297
269
|
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
298
270
|
spool.roll(() => { abortSignal.removeEventListener("abort", onAbort); });
|
|
299
271
|
/* register handlers for initial response */
|
|
300
|
-
this.
|
|
272
|
+
this.onResponse.set(`sink-push-response:${requestId}`, (response) => {
|
|
301
273
|
if (response.error)
|
|
302
274
|
reject(new Error(response.error));
|
|
303
275
|
else {
|
|
@@ -307,11 +279,11 @@ export class SinkTrait extends SourceTrait {
|
|
|
307
279
|
resolve();
|
|
308
280
|
}
|
|
309
281
|
});
|
|
310
|
-
spool.roll(() => { this.
|
|
311
|
-
this.
|
|
282
|
+
spool.roll(() => { this.onResponse.delete(`sink-push-response:${requestId}`); });
|
|
283
|
+
this.onResponse.set(`sink-push-credit:${requestId}`, (_response) => {
|
|
312
284
|
refreshTimeout();
|
|
313
285
|
});
|
|
314
|
-
spool.roll(() => { this.
|
|
286
|
+
spool.roll(() => { this.onResponse.delete(`sink-push-credit:${requestId}`); });
|
|
315
287
|
/* generate and send request message */
|
|
316
288
|
const auth = this.authenticate();
|
|
317
289
|
const metaStore = this.metaStore(meta);
|
|
@@ -323,7 +295,7 @@ export class SinkTrait extends SourceTrait {
|
|
|
323
295
|
});
|
|
324
296
|
});
|
|
325
297
|
/* override handler for mid-stream (error) responses */
|
|
326
|
-
this.
|
|
298
|
+
this.onResponse.set(`sink-push-response:${requestId}`, (response) => {
|
|
327
299
|
if (response.error)
|
|
328
300
|
abortController.abort(new Error(response.error));
|
|
329
301
|
});
|
|
@@ -333,12 +305,12 @@ export class SinkTrait extends SourceTrait {
|
|
|
333
305
|
/* subscribe to credit topic if flow control is active */
|
|
334
306
|
if (creditGate) {
|
|
335
307
|
const creditTopic = this.options.topicMake(name, "sink-push-credit", this.options.id);
|
|
336
|
-
await run(`subscribe to MQTT topic "${creditTopic}"`, spool, () => this.
|
|
337
|
-
spool.roll(() => this.
|
|
308
|
+
await run(`subscribe to MQTT topic "${creditTopic}"`, spool, () => this.subscriptions.subscribe(creditTopic, { qos: 2 }));
|
|
309
|
+
spool.roll(() => this.subscriptions.unsubscribe(creditTopic));
|
|
338
310
|
const gate = creditGate;
|
|
339
311
|
spool.roll(() => { gate.abort(); });
|
|
340
312
|
/* update credit callback to include gate replenish */
|
|
341
|
-
this.
|
|
313
|
+
this.onResponse.set(`sink-push-credit:${requestId}`, (response) => {
|
|
342
314
|
gate.replenish(response.credit);
|
|
343
315
|
refreshTimeout();
|
|
344
316
|
});
|
|
@@ -372,43 +344,4 @@ export class SinkTrait extends SourceTrait {
|
|
|
372
344
|
await spool.unroll();
|
|
373
345
|
}
|
|
374
346
|
}
|
|
375
|
-
/* dispatch incoming MQTT message */
|
|
376
|
-
async _dispatchMessage(topic, message) {
|
|
377
|
-
/* forward dispatching to other traits */
|
|
378
|
-
await super._dispatchMessage(topic, message);
|
|
379
|
-
/* match the MQTT topic */
|
|
380
|
-
const topicMatch = this.options.topicMatch(topic);
|
|
381
|
-
/* handle sink push request (on server-side) */
|
|
382
|
-
if (topicMatch !== null
|
|
383
|
-
&& topicMatch.operation === "sink-push-request"
|
|
384
|
-
&& message instanceof SinkPushRequest) {
|
|
385
|
-
const handler = this.sinks.get(message.name);
|
|
386
|
-
if (handler !== undefined)
|
|
387
|
-
handler(message, topicMatch.name);
|
|
388
|
-
}
|
|
389
|
-
/* handle sink push response (on client-side) */
|
|
390
|
-
else if (topicMatch !== null
|
|
391
|
-
&& topicMatch.operation === "sink-push-response"
|
|
392
|
-
&& message instanceof SinkPushResponse) {
|
|
393
|
-
const handler = this.pushResponseCallbacks.get(message.id);
|
|
394
|
-
if (handler !== undefined)
|
|
395
|
-
handler(message);
|
|
396
|
-
}
|
|
397
|
-
/* handle sink push chunk (on server-side) */
|
|
398
|
-
else if (topicMatch !== null
|
|
399
|
-
&& topicMatch.operation === "sink-push-chunk"
|
|
400
|
-
&& message instanceof SinkPushChunk) {
|
|
401
|
-
const handler = this.pushChunkCallbacks.get(message.id);
|
|
402
|
-
if (handler !== undefined)
|
|
403
|
-
handler(message, topicMatch.name);
|
|
404
|
-
}
|
|
405
|
-
/* handle sink push credit (on client-side) */
|
|
406
|
-
else if (topicMatch !== null
|
|
407
|
-
&& topicMatch.operation === "sink-push-credit"
|
|
408
|
-
&& message instanceof SinkPushCredit) {
|
|
409
|
-
const handler = this.pushCreditCallbacks.get(message.id);
|
|
410
|
-
if (handler !== undefined)
|
|
411
|
-
handler(message);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
347
|
}
|
|
@@ -5,16 +5,7 @@ import type { WithInfo, InfoSource } from "./mqtt-plus-info";
|
|
|
5
5
|
import { ServiceTrait } from "./mqtt-plus-service";
|
|
6
6
|
import type { AuthOption } from "./mqtt-plus-auth";
|
|
7
7
|
export declare class SourceTrait<T extends APISchema = APISchema> extends ServiceTrait<T> {
|
|
8
|
-
private sources;
|
|
9
|
-
private fetchResponseCallbacks;
|
|
10
|
-
private fetchChunkCallbacks;
|
|
11
|
-
private sourceCreditCallbacks;
|
|
12
8
|
private sourceCreditGates;
|
|
13
|
-
private sourceTimers;
|
|
14
|
-
private fetchSubscriptions;
|
|
15
|
-
private _refreshSourceTimer;
|
|
16
|
-
private _clearSourceTimer;
|
|
17
|
-
destroy(): void;
|
|
18
9
|
source<K extends SourceKeys<T> & string>(name: K, callback: WithInfo<T[K], InfoSource>): Promise<Registration>;
|
|
19
10
|
source<K extends SourceKeys<T> & string>(config: {
|
|
20
11
|
name: K;
|
|
@@ -39,5 +30,4 @@ export declare class SourceTrait<T extends APISchema = APISchema> extends Servic
|
|
|
39
30
|
buffer: Promise<Uint8Array>;
|
|
40
31
|
meta: Promise<Record<string, any> | undefined>;
|
|
41
32
|
}>;
|
|
42
|
-
protected _dispatchMessage(topic: string, message: any): Promise<void>;
|
|
43
33
|
}
|