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.
Files changed (73) hide show
  1. package/AGENTS.md +55 -44
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +4 -3
  4. package/doc/mqtt-plus-api.md +693 -680
  5. package/doc/mqtt-plus-architecture.d2 +139 -0
  6. package/doc/mqtt-plus-architecture.md +10 -0
  7. package/doc/mqtt-plus-architecture.svg +95 -0
  8. package/doc/mqtt-plus-broker-setup.md +9 -3
  9. package/doc/mqtt-plus-comm.md +73 -0
  10. package/doc/mqtt-plus-internals.md +3 -3
  11. package/dst-stage1/mqtt-plus-base.d.ts +3 -2
  12. package/dst-stage1/mqtt-plus-base.js +53 -22
  13. package/dst-stage1/mqtt-plus-event.d.ts +0 -2
  14. package/dst-stage1/mqtt-plus-event.js +6 -26
  15. package/dst-stage1/mqtt-plus-meta.d.ts +2 -2
  16. package/dst-stage1/mqtt-plus-meta.js +2 -2
  17. package/dst-stage1/mqtt-plus-msg.d.ts +2 -0
  18. package/dst-stage1/mqtt-plus-msg.js +17 -0
  19. package/dst-stage1/mqtt-plus-service.d.ts +0 -5
  20. package/dst-stage1/mqtt-plus-service.js +12 -48
  21. package/dst-stage1/mqtt-plus-sink.d.ts +0 -10
  22. package/dst-stage1/mqtt-plus-sink.js +25 -92
  23. package/dst-stage1/mqtt-plus-source.d.ts +0 -10
  24. package/dst-stage1/mqtt-plus-source.js +23 -88
  25. package/dst-stage1/mqtt-plus-subscription.d.ts +20 -0
  26. package/dst-stage1/mqtt-plus-subscription.js +126 -0
  27. package/dst-stage1/mqtt-plus-timer.d.ts +8 -0
  28. package/dst-stage1/mqtt-plus-timer.js +57 -0
  29. package/dst-stage1/mqtt-plus-topic.d.ts +20 -0
  30. package/dst-stage1/mqtt-plus-topic.js +112 -0
  31. package/dst-stage1/mqtt-plus-trace.js +2 -0
  32. package/dst-stage1/mqtt-plus-util.d.ts +0 -13
  33. package/dst-stage1/mqtt-plus-util.js +1 -77
  34. package/dst-stage1/mqtt-plus-version.d.ts +0 -1
  35. package/dst-stage1/mqtt-plus-version.js +0 -6
  36. package/dst-stage1/tsc.tsbuildinfo +1 -1
  37. package/dst-stage2/mqtt-plus.cjs.js +242 -292
  38. package/dst-stage2/mqtt-plus.esm.js +240 -290
  39. package/dst-stage2/mqtt-plus.umd.js +12 -12
  40. package/etc/knip.jsonc +1 -1
  41. package/etc/stx.conf +6 -4
  42. package/package.json +1 -1
  43. package/src/mqtt-plus-base.ts +56 -26
  44. package/src/mqtt-plus-event.ts +8 -24
  45. package/src/mqtt-plus-meta.ts +3 -3
  46. package/src/mqtt-plus-msg.ts +28 -0
  47. package/src/mqtt-plus-service.ts +12 -50
  48. package/src/mqtt-plus-sink.ts +32 -105
  49. package/src/mqtt-plus-source.ts +29 -99
  50. package/src/mqtt-plus-subscription.ts +141 -0
  51. package/src/mqtt-plus-timer.ts +61 -0
  52. package/src/mqtt-plus-trace.ts +4 -0
  53. package/src/mqtt-plus-util.ts +1 -81
  54. package/src/mqtt-plus-version.ts +0 -7
  55. package/tst/mqtt-plus-0-fixture.ts +2 -2
  56. package/tst/mqtt-plus-0-mosquitto.ts +5 -0
  57. package/tst/mqtt-plus-1-api.spec.ts +1 -1
  58. package/tst/mqtt-plus-2-event.spec.ts +0 -6
  59. package/tst/mqtt-plus-3-service.spec.ts +3 -7
  60. package/tst/mqtt-plus-4-sink.spec.ts +14 -9
  61. package/tst/mqtt-plus-5-source.spec.ts +11 -5
  62. package/tst/mqtt-plus-6-misc.spec.ts +23 -23
  63. package/tst/tsc.json +1 -1
  64. package/doc/mqtt-plus-communication.md +0 -68
  65. /package/doc/{mqtt-plus-1-event-emission.d2 → mqtt-plus-comm-event-emission.d2} +0 -0
  66. /package/doc/{mqtt-plus-1-event-emission.svg → mqtt-plus-comm-event-emission.svg} +0 -0
  67. /package/doc/{mqtt-plus-2-service-call.d2 → mqtt-plus-comm-service-call.d2} +0 -0
  68. /package/doc/{mqtt-plus-2-service-call.svg → mqtt-plus-comm-service-call.svg} +0 -0
  69. /package/doc/{mqtt-plus-3-sink-push.d2 → mqtt-plus-comm-sink-push.d2} +0 -0
  70. /package/doc/{mqtt-plus-3-sink-push.svg → mqtt-plus-comm-sink-push.svg} +0 -0
  71. /package/doc/{mqtt-plus-4-source-fetch.d2 → mqtt-plus-comm-source-fetch.d2} +0 -0
  72. /package/doc/{mqtt-plus-4-source-fetch.svg → mqtt-plus-comm-source-fetch.svg} +0 -0
  73. /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.events.has(name))
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.events.set(name, (request, topicName) => {
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.events.delete(name); });
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.events.has(name))
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 { BaseTrait } from "./mqtt-plus-base";
3
- export declare class MetaTrait<T extends APISchema = APISchema> extends BaseTrait<T> {
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 { BaseTrait } from "./mqtt-plus-base";
24
+ import { TimerTrait } from "./mqtt-plus-timer";
25
25
  /* Meta trait with meta information management */
26
- export class MetaTrait extends BaseTrait {
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.services.has(name))
68
- throw new Error(`register: service "${name}" already registered`);
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.services.set(name, (request, topicName) => {
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.services.delete(name); });
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.services.has(name))
123
- throw new Error(`destroy: service "${name}" no longer registered`);
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.callSubscriptions.subscribe(responseTopic, { qos: options.qos ?? 2 }));
157
- spool.roll(() => this.callSubscriptions.unsubscribe(responseTopic));
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.callCallbacks.set(requestId, async (response) => {
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.callCallbacks.delete(requestId); });
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, RefCountedSubscription, streamToBuffer, sendBufferAsChunks, sendStreamAsChunks, makeMutuallyExclusiveFields } from "./mqtt-plus-util";
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.sinks.has(name))
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.sinks.set(name, (request, topicName) => {
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._refreshPushTimer(requestId);
148
- const clearPushTimeout = () => this._clearPushTimer(requestId);
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.pushChunkCallbacks.set(requestId, (chunkParsed, chunkTopicName) => {
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.pushChunkCallbacks.delete(requestId); });
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.sinks.delete(name); });
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.sinks.has(name))
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.pushSubscriptions.subscribe(responseTopic, { qos: options.qos ?? 2 }));
267
- spool.roll(() => this.pushSubscriptions.unsubscribe(responseTopic));
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.pushResponseCallbacks.set(requestId, (response) => {
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.pushResponseCallbacks.delete(requestId); });
311
- this.pushCreditCallbacks.set(requestId, (_response) => {
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.pushCreditCallbacks.delete(requestId); });
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.pushResponseCallbacks.set(requestId, (response) => {
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.pushSubscriptions.subscribe(creditTopic, { qos: 2 }));
337
- spool.roll(() => this.pushSubscriptions.unsubscribe(creditTopic));
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.pushCreditCallbacks.set(requestId, (response) => {
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
  }