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 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) and uses shorter NanoIDs instead of longer UUIDs
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/hello": (a1: string, a2: number) => string
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, clientId?: string }
135
- topicServiceRequestMatch: { name: string, clientId?: string }
136
- topicServiceResponseMatch: { name: string, clientId?: string }
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, clientId) => clientId ? `${name}/event-notice/${clientId}` : `${name}/event-notice` ``)
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, clientId) => clientId ? `${name}/service-request/${clientId}` : `${name}/service-request` ``)
188
+ (default: `` (name, peerId) => peerId ? `${name}/service-request/${peerId}` : `${name}/service-request` ``)
156
189
  - `topicServiceResponseMake`): Custom topic generation for service responses.
157
- (default: `` (name, clientId) => clientId ? `${name}/service-response/${clientId}` : `${name}/service-response` ``)
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], clientId: m[2] } : null } ``)
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], clientId: m[2] } : null } ``)
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], clientId: m[2] } : null } ``)
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: (...params: any[], info: { sender: string, receiver?: string }) => void
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/${clientId}`) are subscribed. Returns a
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: (...params: any[], info: { sender: string, receiver?: string }) => any
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/${clientId}`) are subscribed. Returns a
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, clientId)`
223
- (default: `${event}/event-notice` or `${event}/event-notice/${clientId}`).
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, clientId)`
244
- (default: `${service}/service-response/${clientId}`) is temporarily subscribed
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 from "mosquitto"
386
- import MQTT from "mqtt"
387
- import MQTTp from "mqtt-plus"
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 Info {
35
+ export interface InfoBase {
29
36
  sender: string;
30
37
  receiver?: string;
31
38
  }
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[]) => void) | ((...args: any[]) => any)>;
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 (...args: any[]) => infer R ? [R] extends [void] ? K : never : never;
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 (...args: any[]) => infer R ? [R] extends [void] ? never : K : never;
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
- 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>;
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 {};
@@ -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 Msg message */
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 ?? requestMatch?.peerId ?? responseMatch?.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;