mqtt-plus 0.9.2 → 0.9.3
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 +120 -27
- package/dst-stage1/mqtt-plus-msg.d.ts +8 -1
- package/dst-stage1/mqtt-plus-msg.js +25 -0
- package/dst-stage1/mqtt-plus.d.ts +41 -9
- package/dst-stage1/mqtt-plus.js +127 -3
- package/dst-stage2/mqtt-plus.cjs.js +128 -2
- package/dst-stage2/mqtt-plus.esm.js +128 -2
- package/dst-stage2/mqtt-plus.umd.js +12 -12
- package/etc/eslint.mts +2 -1
- package/package.json +1 -1
- package/src/mqtt-plus-msg.ts +37 -1
- package/src/mqtt-plus.ts +222 -32
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ $ npm install mqtt mqtt-plus
|
|
|
22
22
|
About
|
|
23
23
|
-----
|
|
24
24
|
|
|
25
|
-
This is **MQTT+**, an addon API for the excellent
|
|
25
|
+
This is **MQTT+**, an companion addon API for the excellent
|
|
26
26
|
[MQTT](http://mqtt.org/) client TypeScript/JavaScript API
|
|
27
27
|
[MQTT.js](https://www.npmjs.com/package/mqtt), provoding additional
|
|
28
28
|
communication patterns with optional type safety:
|
|
@@ -40,6 +40,20 @@ communication patterns with optional type safety:
|
|
|
40
40
|
pattern allows to direct the event to particular subscribers and
|
|
41
41
|
provides optional information about the sender to subscribers.
|
|
42
42
|
|
|
43
|
+
- **Stream Transfer**:
|
|
44
|
+
|
|
45
|
+
Stream Transfer is a *uni-directional* communication pattern.
|
|
46
|
+
A stream is the combination of a stream name, a `Readable` stream object and optionally zero or more arguments.
|
|
47
|
+
You *attach* to a stream.
|
|
48
|
+
When a stream is *transferred*, either a single particular attacher (in case of
|
|
49
|
+
a directed stream transfer) or all attachers are called and receive the
|
|
50
|
+
arguments as extra information. The `Readable` stream is available via
|
|
51
|
+
`info.stream` in the attacher callback.
|
|
52
|
+
|
|
53
|
+
In contrast to the regular MQTT message publish/subscribe, this
|
|
54
|
+
pattern allows to transfer arbitrary amounts of arbitrary data by
|
|
55
|
+
chunking the data via a stream.
|
|
56
|
+
|
|
43
57
|
- **Service Call**:
|
|
44
58
|
|
|
45
59
|
Service Call is a *bi-directional* communication pattern.
|
|
@@ -60,21 +74,34 @@ communication patterns with optional type safety:
|
|
|
60
74
|
> [MQTT-JSON-RPC](https://github.com/rse/mqtt-json-rpc) of the same
|
|
61
75
|
> author, but instead of just JSON, MQTT+ encodes packets as JSON
|
|
62
76
|
> or CBOR (default), uses an own packet format (allowing sender and
|
|
63
|
-
> receiver information)
|
|
64
|
-
> for identification of sender, receiver and requests
|
|
77
|
+
> receiver information), uses shorter NanoIDs instead of longer UUIDs
|
|
78
|
+
> for identification of sender, receiver and requests, and has
|
|
79
|
+
> no support for stream transfers.
|
|
65
80
|
|
|
66
81
|
Usage
|
|
67
82
|
-----
|
|
68
83
|
|
|
69
84
|
### API:
|
|
70
85
|
|
|
86
|
+
The API type defines the available endpoints. Use the marker types
|
|
87
|
+
`Event<T>`, `Stream<T>`, and `Service<T>` to declare the communication
|
|
88
|
+
pattern of each endpoint:
|
|
89
|
+
|
|
71
90
|
```ts
|
|
91
|
+
import type * as MQTTpt from "mqtt-plus"
|
|
92
|
+
|
|
72
93
|
export type API = {
|
|
73
|
-
"example/sample": (a1: string, a2: boolean) => void
|
|
74
|
-
"example/
|
|
94
|
+
"example/sample": MQTTpt.Event<(a1: string, a2: boolean) => void> /* event */
|
|
95
|
+
"example/upload": MQTTpt.Stream<(a1: string, a2: number) => void> /* stream */
|
|
96
|
+
"example/hello": MQTTpt.Service<(a1: string, a2: number) => string> /* service */
|
|
75
97
|
}
|
|
76
98
|
```
|
|
77
99
|
|
|
100
|
+
The marker types ensure that `subscribe()` and `emit()` only accept
|
|
101
|
+
`Event<T>` endpoints, `attach()` and `transfer()` only accept
|
|
102
|
+
`Stream<T>` endpoints, and `register()` and `call()` only accept
|
|
103
|
+
`Service<T>` endpoints.
|
|
104
|
+
|
|
78
105
|
### Server:
|
|
79
106
|
|
|
80
107
|
```ts
|
|
@@ -128,12 +155,15 @@ The **MQTT+** API provides the following methods:
|
|
|
128
155
|
id: string
|
|
129
156
|
codec: "cbor" | "json"
|
|
130
157
|
timeout: number
|
|
158
|
+
chunkSize: number
|
|
131
159
|
topicEventNoticeMake: (topic: string) => TopicMatching | null
|
|
160
|
+
topicStreamChunkMake: (topic: string) => TopicMatching | null
|
|
132
161
|
topicServiceRequestMake: (topic: string) => TopicMatching | null
|
|
133
162
|
topicServiceResponseMake: (topic: string) => TopicMatching | null
|
|
134
|
-
topicEventNoticeMatch: { name: string,
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
topicEventNoticeMatch: { name: string, peerId?: string }
|
|
164
|
+
topicStreamChunkMatch: { name: string, peerId?: string }
|
|
165
|
+
topicServiceRequestMatch: { name: string, peerId?: string }
|
|
166
|
+
topicServiceResponseMatch: { name: string, peerId?: string }
|
|
137
167
|
}
|
|
138
168
|
)
|
|
139
169
|
|
|
@@ -149,18 +179,23 @@ The **MQTT+** API provides the following methods:
|
|
|
149
179
|
- `id`: Custom MQTT peer identifier (default: auto-generated NanoID).
|
|
150
180
|
- `codec`: Encoding format (default: `cbor`).
|
|
151
181
|
- `timeout`: Communication timeout in milliseconds (default: `10000`).
|
|
182
|
+
- `chunkSize`: Chunk size in bytes for stream transfers (default: `16384`).
|
|
152
183
|
- `topicEventNoticeMake`: Custom topic generation for event notices.
|
|
153
|
-
(default: `` (name,
|
|
184
|
+
(default: `` (name, peerId) => peerId ? `${name}/event-notice/${peerId}` : `${name}/event-notice` ``)
|
|
185
|
+
- `topicStreamChunkMake`: Custom topic generation for stream chunks.
|
|
186
|
+
(default: `` (name, peerId) => peerId ? `${name}/stream-chunk/${peerId}` : `${name}/stream-chunk` ``)
|
|
154
187
|
- `topicServiceRequestMake`: Custom topic generation for service requests.
|
|
155
|
-
(default: `` (name,
|
|
188
|
+
(default: `` (name, peerId) => peerId ? `${name}/service-request/${peerId}` : `${name}/service-request` ``)
|
|
156
189
|
- `topicServiceResponseMake`): Custom topic generation for service responses.
|
|
157
|
-
(default: `` (name,
|
|
190
|
+
(default: `` (name, peerId) => peerId ? `${name}/service-response/${peerId}` : `${name}/service-response` ``)
|
|
158
191
|
- `topicEventNoticeMatch`: Custom topic matching for event notices.
|
|
159
|
-
(default: `` (topic) => { const m = topic.match(/^(.+?)\/event-notice(?:\/(.+))?$/); return m ? { name: m[1],
|
|
192
|
+
(default: `` (topic) => { const m = topic.match(/^(.+?)\/event-notice(?:\/(.+))?$/); return m ? { name: m[1], peerId: m[2] } : null } ``)
|
|
193
|
+
- `topicStreamChunkMatch`: Custom topic matching for stream chunks.
|
|
194
|
+
(default: `` (topic) => { const m = topic.match(/^(.+?)\/stream-chunk(?:\/(.+))?$/); return m ? { name: m[1], peerId: m[2] } : null } ``)
|
|
160
195
|
- `topicServiceRequestMatch`: Custom topic matching for service requests.
|
|
161
|
-
(default: `` (topic) => { const m = topic.match(/^(.+?)\/service-request(?:\/(.+))?$/); return m ? { name: m[1],
|
|
196
|
+
(default: `` (topic) => { const m = topic.match(/^(.+?)\/service-request(?:\/(.+))?$/); return m ? { name: m[1], peerId: m[2] } : null } ``)
|
|
162
197
|
- `topicServiceResponseMatch`: Custom topic matching for service responses.
|
|
163
|
-
(default: `` (topic) => { const m = topic.match(/^(.+?)\/service-response\/(.+)$/); return m ? { name: m[1],
|
|
198
|
+
(default: `` (topic) => { const m = topic.match(/^(.+?)\/service-response\/(.+)$/); return m ? { name: m[1], peerId: m[2] } : null } ``)
|
|
164
199
|
|
|
165
200
|
- **Event Subscription**:<br/>
|
|
166
201
|
|
|
@@ -168,7 +203,10 @@ The **MQTT+** API provides the following methods:
|
|
|
168
203
|
subscribe(
|
|
169
204
|
event: string,
|
|
170
205
|
options?: MQTT::IClientSubscribeOptions
|
|
171
|
-
callback: (
|
|
206
|
+
callback: (
|
|
207
|
+
...params: any[],
|
|
208
|
+
info: { sender: string, receiver?: string }
|
|
209
|
+
) => void
|
|
172
210
|
): Promise<Subscription>
|
|
173
211
|
|
|
174
212
|
Subscribe to an event.
|
|
@@ -179,16 +217,43 @@ The **MQTT+** API provides the following methods:
|
|
|
179
217
|
|
|
180
218
|
Internally, on the MQTT broker, the topics generated by
|
|
181
219
|
`topicEventNoticeMake()` (default: `${event}/event-notice` and
|
|
182
|
-
`${event}/event-notice/${
|
|
220
|
+
`${event}/event-notice/${peerId}`) are subscribed. Returns a
|
|
183
221
|
`Subscription` object with an `unsubscribe()` method.
|
|
184
222
|
|
|
223
|
+
- **Stream Attachment**:<br/>
|
|
224
|
+
|
|
225
|
+
/* (simplified TypeScript API method signature) */
|
|
226
|
+
attach(
|
|
227
|
+
stream: string,
|
|
228
|
+
options?: MQTT::IClientSubscribeOptions
|
|
229
|
+
callback: (
|
|
230
|
+
...params: any[],
|
|
231
|
+
info: { sender: string, receiver?: string, stream: stream.Readable }
|
|
232
|
+
) => void
|
|
233
|
+
): Promise<Attachment>
|
|
234
|
+
|
|
235
|
+
Attach to (observe) a stream.
|
|
236
|
+
The `stream` has to be a valid MQTT topic name.
|
|
237
|
+
The optional `options` allows setting MQTT.js `subscribe()` options like `qos`.
|
|
238
|
+
The `callback` is called with the `params` passed to a remote `transfer()`.
|
|
239
|
+
The `info.stream` provides a Node.js `Readable` stream for consuming the transferred data.
|
|
240
|
+
There is no return value of `callback`.
|
|
241
|
+
|
|
242
|
+
Internally, on the MQTT broker, the topics generated by
|
|
243
|
+
`topicStreamChunkMake()` (default: `${stream}/stream-chunk` and
|
|
244
|
+
`${stream}/stream-chunk/${peerId}`) are subscribed. Returns an
|
|
245
|
+
`Attachment` object with an `unattach()` method.
|
|
246
|
+
|
|
185
247
|
- **Service Registration**:<br/>
|
|
186
248
|
|
|
187
249
|
/* (simplified TypeScript API method signature) */
|
|
188
250
|
register(
|
|
189
251
|
service: string,
|
|
190
252
|
options?: MQTT::IClientSubscribeOptions
|
|
191
|
-
callback: (
|
|
253
|
+
callback: (
|
|
254
|
+
...params: any[],
|
|
255
|
+
info: { sender: string, receiver?: string }
|
|
256
|
+
) => any
|
|
192
257
|
): Promise<Registration>
|
|
193
258
|
|
|
194
259
|
Register a service.
|
|
@@ -199,7 +264,7 @@ The **MQTT+** API provides the following methods:
|
|
|
199
264
|
|
|
200
265
|
Internally, on the MQTT broker, the topics by
|
|
201
266
|
`topicServiceRequestMake()` (default: `${service}/service-request` and
|
|
202
|
-
`${service}/service-request/${
|
|
267
|
+
`${service}/service-request/${peerId}`) are subscribed. Returns a
|
|
203
268
|
`Registration` object with an `unregister()` method.
|
|
204
269
|
|
|
205
270
|
- **Event Emission**:<br/>
|
|
@@ -219,8 +284,35 @@ The **MQTT+** API provides the following methods:
|
|
|
219
284
|
The remote `subscribe()` `callback` is called with `params` and its
|
|
220
285
|
return value is silently ignored.
|
|
221
286
|
|
|
222
|
-
Internally, publishes to the MQTT topic by `topicEventNoticeMake(event,
|
|
223
|
-
(default: `${event}/event-notice` or `${event}/event-notice/${
|
|
287
|
+
Internally, publishes to the MQTT topic by `topicEventNoticeMake(event, peerId)`
|
|
288
|
+
(default: `${event}/event-notice` or `${event}/event-notice/${peerId}`).
|
|
289
|
+
|
|
290
|
+
- **Stream Transfer**:<br/>
|
|
291
|
+
|
|
292
|
+
/* (simplified TypeScript API method signature) */
|
|
293
|
+
transfer(
|
|
294
|
+
stream: string,
|
|
295
|
+
readable: stream.Readable,
|
|
296
|
+
receiver?: Receiver,
|
|
297
|
+
options?: MQTT::IClientPublishOptions,
|
|
298
|
+
...params: any[]
|
|
299
|
+
): Promise<void>
|
|
300
|
+
|
|
301
|
+
Transfer a stream to all attachers or a specific attacher.
|
|
302
|
+
The `readable` is a Node.js `Readable` stream providing the data to transfer.
|
|
303
|
+
The optional `receiver` directs the transfer to a specific attacher only.
|
|
304
|
+
The optional `options` allows setting MQTT.js `publish()` options like `qos` or `retain`.
|
|
305
|
+
|
|
306
|
+
The data is read from `readable` in chunks (default: 16KB,
|
|
307
|
+
configurable via `chunkSize` option) and sent over MQTT until the
|
|
308
|
+
stream is closed. The returned `Promise` resolves when the entire
|
|
309
|
+
stream has been transferred.
|
|
310
|
+
|
|
311
|
+
The remote `attach()` `callback` is called with `params` and an `info` object
|
|
312
|
+
containing a `stream.Readable` for consuming the transferred data.
|
|
313
|
+
|
|
314
|
+
Internally, publishes to the MQTT topic by `topicStreamChunkMake(stream, peerId)`
|
|
315
|
+
(default: `${stream}/stream-chunk` or `${stream}/stream-chunk/${peerId}`).
|
|
224
316
|
|
|
225
317
|
- **Service Call**:<br/>
|
|
226
318
|
|
|
@@ -240,8 +332,8 @@ The **MQTT+** API provides the following methods:
|
|
|
240
332
|
return value resolves the returned `Promise`. If the remote `callback`
|
|
241
333
|
throws an exception, this rejects the returned `Promise`.
|
|
242
334
|
|
|
243
|
-
Internally, on the MQTT broker, the topic by `topicServiceResponseMake(service,
|
|
244
|
-
(default: `${service}/service-response/${
|
|
335
|
+
Internally, on the MQTT broker, the topic by `topicServiceResponseMake(service, peerId)`
|
|
336
|
+
(default: `${service}/service-response/${peerId}`) is temporarily subscribed
|
|
245
337
|
for receiving the response.
|
|
246
338
|
|
|
247
339
|
- **Receiver Wrapping**:<br/>
|
|
@@ -382,9 +474,10 @@ it in action and tracing its communication (the typing of the `MQTTp`
|
|
|
382
474
|
class with `API` is optional, but strongly suggested):
|
|
383
475
|
|
|
384
476
|
```ts
|
|
385
|
-
import Mosquitto
|
|
386
|
-
import MQTT
|
|
387
|
-
import MQTTp
|
|
477
|
+
import Mosquitto from "mosquitto"
|
|
478
|
+
import MQTT from "mqtt"
|
|
479
|
+
import MQTTp from "mqtt-plus"
|
|
480
|
+
import type * as MQTTpt from "mqtt-plus"
|
|
388
481
|
|
|
389
482
|
const mosquitto = new Mosquitto()
|
|
390
483
|
await mosquitto.start()
|
|
@@ -396,8 +489,8 @@ const mqtt = MQTT.connect("mqtt://127.0.0.1:1883", {
|
|
|
396
489
|
})
|
|
397
490
|
|
|
398
491
|
type API = {
|
|
399
|
-
"example/sample": (a1: string, a2: number) => void
|
|
400
|
-
"example/hello": (a1: string, a2: number) => string
|
|
492
|
+
"example/sample": MQTTpt.Event<(a1: string, a2: number) => void>
|
|
493
|
+
"example/hello": MQTTpt.Service<(a1: string, a2: number) => string>
|
|
401
494
|
}
|
|
402
495
|
|
|
403
496
|
const mqttp = new MQTTp<API>(mqtt, { codec: "json" })
|
|
@@ -9,6 +9,12 @@ export declare class EventEmission extends Base {
|
|
|
9
9
|
params?: any[] | undefined;
|
|
10
10
|
constructor(id: string, event: string, params?: any[] | undefined, sender?: string, receiver?: string);
|
|
11
11
|
}
|
|
12
|
+
export declare class StreamChunk extends Base {
|
|
13
|
+
stream: string;
|
|
14
|
+
chunk: Buffer | null;
|
|
15
|
+
params?: any[] | undefined;
|
|
16
|
+
constructor(id: string, stream: string, chunk: Buffer | null, params?: any[] | undefined, sender?: string, receiver?: string);
|
|
17
|
+
}
|
|
12
18
|
export declare class ServiceRequest extends Base {
|
|
13
19
|
service: string;
|
|
14
20
|
params?: any[] | undefined;
|
|
@@ -24,8 +30,9 @@ export declare class ServiceResponseError extends Base {
|
|
|
24
30
|
}
|
|
25
31
|
export default class Msg {
|
|
26
32
|
makeEventEmission(id: string, event: string, params?: any[], sender?: string, receiver?: string): EventEmission;
|
|
33
|
+
makeStreamChunk(id: string, stream: string, chunk: Buffer | null, params?: any[], sender?: string, receiver?: string): StreamChunk;
|
|
27
34
|
makeServiceRequest(id: string, service: string, params?: any[], sender?: string, receiver?: string): ServiceRequest;
|
|
28
35
|
makeServiceResponseSuccess(id: string, result: any, sender?: string, receiver?: string): ServiceResponseSuccess;
|
|
29
36
|
makeServiceResponseError(id: string, error: string, sender?: string, receiver?: string): ServiceResponseError;
|
|
30
|
-
parse(obj: any): EventEmission | ServiceRequest | ServiceResponseSuccess | ServiceResponseError;
|
|
37
|
+
parse(obj: any): EventEmission | StreamChunk | ServiceRequest | ServiceResponseSuccess | ServiceResponseError;
|
|
31
38
|
}
|
|
@@ -37,6 +37,15 @@ export class EventEmission extends Base {
|
|
|
37
37
|
this.params = params;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
/* stream chunk */
|
|
41
|
+
export class StreamChunk extends Base {
|
|
42
|
+
constructor(id, stream, chunk, params, sender, receiver) {
|
|
43
|
+
super(id, sender, receiver);
|
|
44
|
+
this.stream = stream;
|
|
45
|
+
this.chunk = chunk;
|
|
46
|
+
this.params = params;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
40
49
|
/* service request */
|
|
41
50
|
export class ServiceRequest extends Base {
|
|
42
51
|
constructor(id, service, params, sender, receiver) {
|
|
@@ -65,6 +74,10 @@ export default class Msg {
|
|
|
65
74
|
makeEventEmission(id, event, params, sender, receiver) {
|
|
66
75
|
return new EventEmission(id, event, params, sender, receiver);
|
|
67
76
|
}
|
|
77
|
+
/* factory for stream chunk */
|
|
78
|
+
makeStreamChunk(id, stream, chunk, params, sender, receiver) {
|
|
79
|
+
return new StreamChunk(id, stream, chunk, params, sender, receiver);
|
|
80
|
+
}
|
|
68
81
|
/* factory for service request */
|
|
69
82
|
makeServiceRequest(id, service, params, sender, receiver) {
|
|
70
83
|
return new ServiceRequest(id, service, params, sender, receiver);
|
|
@@ -100,6 +113,18 @@ export default class Msg {
|
|
|
100
113
|
throw new Error("invalid EventEmission object: \"params\" field must be an array");
|
|
101
114
|
return this.makeEventEmission(obj.id, obj.event, obj.params, obj.sender, obj.receiver);
|
|
102
115
|
}
|
|
116
|
+
if ("stream" in obj) {
|
|
117
|
+
/* detect and parse stream chunk */
|
|
118
|
+
if (typeof obj.stream !== "string")
|
|
119
|
+
throw new Error("invalid StreamChunk object: \"stream\" field must be a string");
|
|
120
|
+
if (anyFieldsExcept(obj, ["type", "id", "stream", "chunk", "params", "sender", "receiver"]))
|
|
121
|
+
throw new Error("invalid StreamChunk object: contains unknown fields");
|
|
122
|
+
if (obj.chunk !== undefined && typeof obj.chunk !== "object")
|
|
123
|
+
throw new Error("invalid StreamChunk object: \"chunk\" field must be an object or null");
|
|
124
|
+
if (obj.params !== undefined && (typeof obj.params !== "object" || !Array.isArray(obj.params)))
|
|
125
|
+
throw new Error("invalid StreamChunk object: \"params\" field must be an array");
|
|
126
|
+
return this.makeStreamChunk(obj.id, obj.stream, obj.chunk, obj.params, obj.sender, obj.receiver);
|
|
127
|
+
}
|
|
103
128
|
else if ("service" in obj) {
|
|
104
129
|
/* detect and parse service request */
|
|
105
130
|
if (typeof obj.service !== "string")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import stream from "stream";
|
|
1
2
|
import { MqttClient, IClientPublishOptions, IClientSubscribeOptions } from "mqtt";
|
|
2
3
|
export type Receiver = {
|
|
3
4
|
__receiver: string;
|
|
@@ -12,30 +13,53 @@ export interface APIOptions {
|
|
|
12
13
|
id: string;
|
|
13
14
|
codec: "cbor" | "json";
|
|
14
15
|
timeout: number;
|
|
16
|
+
chunkSize: number;
|
|
15
17
|
topicEventNoticeMake: TopicMake;
|
|
18
|
+
topicStreamChunkMake: TopicMake;
|
|
16
19
|
topicServiceRequestMake: TopicMake;
|
|
17
20
|
topicServiceResponseMake: TopicMake;
|
|
18
21
|
topicEventNoticeMatch: TopicMatch;
|
|
22
|
+
topicStreamChunkMatch: TopicMatch;
|
|
19
23
|
topicServiceRequestMatch: TopicMatch;
|
|
20
24
|
topicServiceResponseMatch: TopicMatch;
|
|
21
25
|
}
|
|
22
26
|
export interface Registration {
|
|
23
27
|
unregister(): Promise<void>;
|
|
24
28
|
}
|
|
29
|
+
export interface Attachment {
|
|
30
|
+
unattach(): Promise<void>;
|
|
31
|
+
}
|
|
25
32
|
export interface Subscription {
|
|
26
33
|
unsubscribe(): Promise<void>;
|
|
27
34
|
}
|
|
28
|
-
export interface
|
|
35
|
+
export interface InfoBase {
|
|
29
36
|
sender: string;
|
|
30
37
|
receiver?: string;
|
|
31
38
|
}
|
|
32
|
-
export
|
|
33
|
-
|
|
39
|
+
export interface InfoEvent extends InfoBase {
|
|
40
|
+
}
|
|
41
|
+
export interface InfoStream extends InfoBase {
|
|
42
|
+
stream: stream.Readable;
|
|
43
|
+
}
|
|
44
|
+
export interface InfoService extends InfoBase {
|
|
45
|
+
}
|
|
46
|
+
export type WithInfo<F, I extends InfoBase> = F extends (...args: infer P) => infer R ? (...args: [...P, info: I]) => R : never;
|
|
47
|
+
type Brand<T> = T & {
|
|
48
|
+
readonly __brand: unique symbol;
|
|
49
|
+
};
|
|
50
|
+
export type APIEndpoint = ((...args: any[]) => void) | ((...args: any[]) => any);
|
|
51
|
+
export type Event<T extends APIEndpoint> = Brand<T>;
|
|
52
|
+
export type Stream<T extends APIEndpoint> = Brand<T>;
|
|
53
|
+
export type Service<T extends APIEndpoint> = Brand<T>;
|
|
54
|
+
export type APISchema = Record<string, APIEndpoint>;
|
|
34
55
|
export type EventKeys<T> = string extends keyof T ? string : {
|
|
35
|
-
[K in keyof T]: T[K] extends
|
|
56
|
+
[K in keyof T]: T[K] extends Event<infer _F> ? K : never;
|
|
57
|
+
}[keyof T];
|
|
58
|
+
export type StreamKeys<T> = string extends keyof T ? string : {
|
|
59
|
+
[K in keyof T]: T[K] extends Stream<infer _F> ? K : never;
|
|
36
60
|
}[keyof T];
|
|
37
61
|
export type ServiceKeys<T> = string extends keyof T ? string : {
|
|
38
|
-
[K in keyof T]: T[K] extends
|
|
62
|
+
[K in keyof T]: T[K] extends Service<infer _F> ? K : never;
|
|
39
63
|
}[keyof T];
|
|
40
64
|
export default class MQTTp<T extends APISchema = APISchema> {
|
|
41
65
|
private mqtt;
|
|
@@ -45,13 +69,16 @@ export default class MQTTp<T extends APISchema = APISchema> {
|
|
|
45
69
|
private registry;
|
|
46
70
|
private requests;
|
|
47
71
|
private subscriptions;
|
|
72
|
+
private streams;
|
|
48
73
|
constructor(mqtt: MqttClient, options?: Partial<APIOptions>);
|
|
49
74
|
private _subscribeTopic;
|
|
50
75
|
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
|
-
|
|
54
|
-
|
|
76
|
+
subscribe<K extends EventKeys<T> & string>(event: K, callback: WithInfo<T[K], InfoEvent>): Promise<Subscription>;
|
|
77
|
+
subscribe<K extends EventKeys<T> & string>(event: K, options: Partial<IClientSubscribeOptions>, callback: WithInfo<T[K], InfoEvent>): Promise<Subscription>;
|
|
78
|
+
attach<K extends StreamKeys<T> & string>(stream: K, callback: WithInfo<T[K], InfoStream>): Promise<Attachment>;
|
|
79
|
+
attach<K extends StreamKeys<T> & string>(stream: K, options: Partial<IClientSubscribeOptions>, callback: WithInfo<T[K], InfoStream>): Promise<Attachment>;
|
|
80
|
+
register<K extends ServiceKeys<T> & string>(service: K, callback: WithInfo<T[K], InfoService>): Promise<Registration>;
|
|
81
|
+
register<K extends ServiceKeys<T> & string>(service: K, options: Partial<IClientSubscribeOptions>, callback: WithInfo<T[K], InfoService>): Promise<Registration>;
|
|
55
82
|
private _isIClientPublishOptions;
|
|
56
83
|
receiver(id: string): {
|
|
57
84
|
__receiver: string;
|
|
@@ -63,6 +90,10 @@ export default class MQTTp<T extends APISchema = APISchema> {
|
|
|
63
90
|
emit<K extends EventKeys<T> & string>(event: K, receiver: Receiver, ...params: Parameters<T[K]>): void;
|
|
64
91
|
emit<K extends EventKeys<T> & string>(event: K, options: IClientPublishOptions, ...params: Parameters<T[K]>): void;
|
|
65
92
|
emit<K extends EventKeys<T> & string>(event: K, receiver: Receiver, options: IClientPublishOptions, ...params: Parameters<T[K]>): void;
|
|
93
|
+
transfer<K extends StreamKeys<T> & string>(stream: K, readable: stream.Readable, ...params: Parameters<T[K]>): Promise<void>;
|
|
94
|
+
transfer<K extends StreamKeys<T> & string>(stream: K, readable: stream.Readable, receiver: Receiver, ...params: Parameters<T[K]>): Promise<void>;
|
|
95
|
+
transfer<K extends StreamKeys<T> & string>(stream: K, readable: stream.Readable, options: IClientPublishOptions, ...params: Parameters<T[K]>): Promise<void>;
|
|
96
|
+
transfer<K extends StreamKeys<T> & string>(stream: K, readable: stream.Readable, receiver: Receiver, options: IClientPublishOptions, ...params: Parameters<T[K]>): Promise<void>;
|
|
66
97
|
call<K extends ServiceKeys<T> & string>(service: K, ...params: Parameters<T[K]>): Promise<Awaited<ReturnType<T[K]>>>;
|
|
67
98
|
call<K extends ServiceKeys<T> & string>(service: K, receiver: Receiver, ...params: Parameters<T[K]>): Promise<Awaited<ReturnType<T[K]>>>;
|
|
68
99
|
call<K extends ServiceKeys<T> & string>(service: K, options: IClientPublishOptions, ...params: Parameters<T[K]>): Promise<Awaited<ReturnType<T[K]>>>;
|
|
@@ -71,3 +102,4 @@ export default class MQTTp<T extends APISchema = APISchema> {
|
|
|
71
102
|
private _responseUnsubscribe;
|
|
72
103
|
private _onMessage;
|
|
73
104
|
}
|
|
105
|
+
export {};
|
package/dst-stage1/mqtt-plus.js
CHANGED
|
@@ -21,10 +21,12 @@
|
|
|
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
|
+
/* external requirements */
|
|
25
|
+
import stream from "stream";
|
|
24
26
|
import { nanoid } from "nanoid";
|
|
25
27
|
/* internal requirements */
|
|
26
28
|
import Codec from "./mqtt-plus-codec";
|
|
27
|
-
import Msg, { EventEmission, ServiceRequest, ServiceResponseSuccess, ServiceResponseError } from "./mqtt-plus-msg";
|
|
29
|
+
import Msg, { EventEmission, StreamChunk, ServiceRequest, ServiceResponseSuccess, ServiceResponseError } from "./mqtt-plus-msg";
|
|
28
30
|
/* MQTTp API class */
|
|
29
31
|
export default class MQTTp {
|
|
30
32
|
/* construct API class */
|
|
@@ -34,16 +36,23 @@ export default class MQTTp {
|
|
|
34
36
|
this.registry = new Map();
|
|
35
37
|
this.requests = new Map();
|
|
36
38
|
this.subscriptions = new Map();
|
|
39
|
+
this.streams = new Map();
|
|
37
40
|
/* determine options and provide defaults */
|
|
38
41
|
this.options = {
|
|
39
42
|
id: nanoid(),
|
|
40
43
|
codec: "cbor",
|
|
41
44
|
timeout: 10 * 1000,
|
|
45
|
+
chunkSize: 16 * 1024,
|
|
42
46
|
topicEventNoticeMake: (name, peerId) => {
|
|
43
47
|
return peerId
|
|
44
48
|
? `${name}/event-notice/${peerId}`
|
|
45
49
|
: `${name}/event-notice`;
|
|
46
50
|
},
|
|
51
|
+
topicStreamChunkMake: (name, peerId) => {
|
|
52
|
+
return peerId
|
|
53
|
+
? `${name}/stream-chunk/${peerId}`
|
|
54
|
+
: `${name}/stream-chunk`;
|
|
55
|
+
},
|
|
47
56
|
topicServiceRequestMake: (name, peerId) => {
|
|
48
57
|
return peerId
|
|
49
58
|
? `${name}/service-request/${peerId}`
|
|
@@ -58,6 +67,10 @@ export default class MQTTp {
|
|
|
58
67
|
const m = topic.match(/^(.+?)\/event-notice(?:\/(.+))?$/);
|
|
59
68
|
return m ? { name: m[1], peerId: m[2] } : null;
|
|
60
69
|
},
|
|
70
|
+
topicStreamChunkMatch: (topic) => {
|
|
71
|
+
const m = topic.match(/^(.+?)\/stream-chunk(?:\/(.+))?$/);
|
|
72
|
+
return m ? { name: m[1], peerId: m[2] } : null;
|
|
73
|
+
},
|
|
61
74
|
topicServiceRequestMatch: (topic) => {
|
|
62
75
|
const m = topic.match(/^(.+?)\/service-request(?:\/(.+))?$/);
|
|
63
76
|
return m ? { name: m[1], peerId: m[2] } : null;
|
|
@@ -137,6 +150,46 @@ export default class MQTTp {
|
|
|
137
150
|
};
|
|
138
151
|
return subscription;
|
|
139
152
|
}
|
|
153
|
+
async attach(stream, ...args) {
|
|
154
|
+
/* determine parameters */
|
|
155
|
+
let options = {};
|
|
156
|
+
let callback = args[0];
|
|
157
|
+
if (args.length === 2 && typeof args[0] === "object") {
|
|
158
|
+
options = args[0];
|
|
159
|
+
callback = args[1];
|
|
160
|
+
}
|
|
161
|
+
/* sanity check situation */
|
|
162
|
+
if (this.registry.has(stream))
|
|
163
|
+
throw new Error(`attach: stream "${stream}" already attached`);
|
|
164
|
+
/* generate the corresponding MQTT topics for broadcast and direct use */
|
|
165
|
+
const topicB = this.options.topicStreamChunkMake(stream);
|
|
166
|
+
const topicD = this.options.topicStreamChunkMake(stream, this.options.id);
|
|
167
|
+
/* subscribe to MQTT topics */
|
|
168
|
+
await Promise.all([
|
|
169
|
+
this._subscribeTopic(topicB, { qos: 0, ...options }),
|
|
170
|
+
this._subscribeTopic(topicD, { qos: 0, ...options })
|
|
171
|
+
]).catch((err) => {
|
|
172
|
+
this._unsubscribeTopic(topicB).catch(() => { });
|
|
173
|
+
this._unsubscribeTopic(topicD).catch(() => { });
|
|
174
|
+
throw err;
|
|
175
|
+
});
|
|
176
|
+
/* remember the subscription */
|
|
177
|
+
this.registry.set(stream, callback);
|
|
178
|
+
/* provide an attachment for subsequent unattaching */
|
|
179
|
+
const self = this;
|
|
180
|
+
const attachment = {
|
|
181
|
+
async unattach() {
|
|
182
|
+
if (!self.registry.has(stream))
|
|
183
|
+
throw new Error(`unattach: stream "${stream}" not attached`);
|
|
184
|
+
self.registry.delete(stream);
|
|
185
|
+
return Promise.all([
|
|
186
|
+
self._unsubscribeTopic(topicB),
|
|
187
|
+
self._unsubscribeTopic(topicD)
|
|
188
|
+
]).then(() => { });
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
return attachment;
|
|
192
|
+
}
|
|
140
193
|
async register(service, ...args) {
|
|
141
194
|
/* determine parameters */
|
|
142
195
|
let options = {};
|
|
@@ -224,7 +277,7 @@ export default class MQTTp {
|
|
|
224
277
|
const { receiver, options, params } = this._parseCallArgs(args);
|
|
225
278
|
/* generate unique request id */
|
|
226
279
|
const rid = nanoid();
|
|
227
|
-
/* generate encoded
|
|
280
|
+
/* generate encoded message */
|
|
228
281
|
const request = this.msg.makeEventEmission(rid, event, params, this.options.id, receiver);
|
|
229
282
|
const message = this.codec.encode(request);
|
|
230
283
|
/* generate corresponding MQTT topic */
|
|
@@ -232,6 +285,52 @@ export default class MQTTp {
|
|
|
232
285
|
/* publish message to MQTT topic */
|
|
233
286
|
this.mqtt.publish(topic, message, { qos: 2, ...options });
|
|
234
287
|
}
|
|
288
|
+
transfer(stream, readable, ...args) {
|
|
289
|
+
/* determine actual parameters */
|
|
290
|
+
const { receiver, options, params } = this._parseCallArgs(args);
|
|
291
|
+
/* generate unique request id */
|
|
292
|
+
const rid = nanoid();
|
|
293
|
+
/* generate corresponding MQTT topic */
|
|
294
|
+
const topic = this.options.topicStreamChunkMake(stream, receiver);
|
|
295
|
+
/* utility function for converting a chunk to a Buffer */
|
|
296
|
+
const chunkToBuffer = (chunk) => {
|
|
297
|
+
let buffer;
|
|
298
|
+
if (Buffer.isBuffer(chunk))
|
|
299
|
+
buffer = chunk;
|
|
300
|
+
else if (typeof chunk === "string")
|
|
301
|
+
buffer = Buffer.from(chunk);
|
|
302
|
+
else if (chunk instanceof Uint8Array)
|
|
303
|
+
buffer = Buffer.from(chunk);
|
|
304
|
+
else
|
|
305
|
+
buffer = Buffer.from(String(chunk));
|
|
306
|
+
return buffer;
|
|
307
|
+
};
|
|
308
|
+
/* iterate over all chunks of the buffer */
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
readable.on("readable", () => {
|
|
311
|
+
let chunk;
|
|
312
|
+
while ((chunk = readable.read(this.options.chunkSize)) !== null) {
|
|
313
|
+
/* ensure data is a Buffer */
|
|
314
|
+
const buffer = chunkToBuffer(chunk);
|
|
315
|
+
/* generate encoded message */
|
|
316
|
+
const request = this.msg.makeStreamChunk(rid, stream, buffer, params, this.options.id, receiver);
|
|
317
|
+
const message = this.codec.encode(request);
|
|
318
|
+
/* publish message to MQTT topic */
|
|
319
|
+
this.mqtt.publish(topic, message, { qos: 2, ...options });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
readable.on("end", () => {
|
|
323
|
+
/* send "null" chunk to signal end of stream */
|
|
324
|
+
const request = this.msg.makeStreamChunk(rid, stream, null, params, this.options.id, receiver);
|
|
325
|
+
const message = this.codec.encode(request);
|
|
326
|
+
this.mqtt.publish(topic, message, { qos: 2, ...options });
|
|
327
|
+
resolve();
|
|
328
|
+
});
|
|
329
|
+
readable.on("error", () => {
|
|
330
|
+
reject(new Error("readable stream error"));
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
}
|
|
235
334
|
call(service, ...args) {
|
|
236
335
|
/* determine actual parameters */
|
|
237
336
|
const { receiver, options, params } = this._parseCallArgs(args);
|
|
@@ -313,14 +412,19 @@ export default class MQTTp {
|
|
|
313
412
|
_onMessage(topic, message) {
|
|
314
413
|
/* ensure we handle only valid messages */
|
|
315
414
|
let eventMatch = null;
|
|
415
|
+
let streamMatch = null;
|
|
316
416
|
let requestMatch = null;
|
|
317
417
|
let responseMatch = null;
|
|
318
418
|
if ((eventMatch = this.options.topicEventNoticeMatch(topic)) === null
|
|
419
|
+
&& (streamMatch = this.options.topicStreamChunkMatch(topic)) === null
|
|
319
420
|
&& (requestMatch = this.options.topicServiceRequestMatch(topic)) === null
|
|
320
421
|
&& (responseMatch = this.options.topicServiceResponseMatch(topic)) === null)
|
|
321
422
|
return;
|
|
322
423
|
/* ensure we really handle only messages for us */
|
|
323
|
-
const peerId = eventMatch?.peerId ??
|
|
424
|
+
const peerId = eventMatch?.peerId ??
|
|
425
|
+
streamMatch?.peerId ??
|
|
426
|
+
requestMatch?.peerId ??
|
|
427
|
+
responseMatch?.peerId;
|
|
324
428
|
if (peerId !== undefined && peerId !== this.options.id)
|
|
325
429
|
return;
|
|
326
430
|
/* try to parse payload as payload */
|
|
@@ -348,6 +452,26 @@ export default class MQTTp {
|
|
|
348
452
|
const info = { sender: parsed.sender ?? "", receiver: parsed.receiver };
|
|
349
453
|
handler?.(...params, info);
|
|
350
454
|
}
|
|
455
|
+
else if (parsed instanceof StreamChunk) {
|
|
456
|
+
/* just handle stream chunk */
|
|
457
|
+
const id = parsed.id;
|
|
458
|
+
let chunk = parsed.chunk;
|
|
459
|
+
let readable = this.streams.get(id);
|
|
460
|
+
if (readable === undefined) {
|
|
461
|
+
const name = parsed.stream;
|
|
462
|
+
const params = parsed.params ?? [];
|
|
463
|
+
readable = new stream.Readable({ read(_size) { } });
|
|
464
|
+
this.streams.set(id, readable);
|
|
465
|
+
const info = { sender: parsed.sender ?? "", receiver: parsed.receiver, stream: readable };
|
|
466
|
+
const handler = this.registry.get(name);
|
|
467
|
+
handler?.(...params, info);
|
|
468
|
+
}
|
|
469
|
+
if (chunk !== null && !Buffer.isBuffer(chunk))
|
|
470
|
+
chunk = Buffer.from(chunk);
|
|
471
|
+
readable.push(chunk);
|
|
472
|
+
if (chunk === null)
|
|
473
|
+
this.streams.delete(id);
|
|
474
|
+
}
|
|
351
475
|
else if (parsed instanceof ServiceRequest) {
|
|
352
476
|
/* deliver service request and send response */
|
|
353
477
|
const rid = parsed.id;
|