mqtt-plus 1.4.13 → 1.4.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mqtt-plus",
3
- "version": "1.4.13",
3
+ "version": "1.4.14",
4
4
  "description": "MQTT Communication Patterns",
5
5
  "keywords": [ "mqtt",
6
6
  "event", "emit",
@@ -162,6 +162,8 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
162
162
  /* utility functions for timeout management */
163
163
  const pushTimerId = `sink-push-recv:${requestId}`
164
164
  const refreshPushTimeout = () => this.timerRefresh(pushTimerId, () => {
165
+ if (streamEnded)
166
+ return
165
167
  const stream = this.pushStreams.get(requestId)
166
168
  if (stream !== undefined)
167
169
  stream.destroy(new Error("push stream timeout"))
@@ -201,6 +203,7 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
201
203
  return
202
204
  if (chunkParsed.error !== undefined) {
203
205
  streamEnded = true
206
+ clearPushTimeout()
204
207
  readable.destroy(new Error(chunkParsed.error))
205
208
  }
206
209
  else {
@@ -212,6 +215,7 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
212
215
  }
213
216
  if (chunkParsed.final) {
214
217
  streamEnded = true
218
+ clearPushTimeout()
215
219
  readable.push(null)
216
220
  }
217
221
  }
@@ -382,6 +386,7 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
382
386
  let remoteError = false
383
387
  let pushAcked = false
384
388
  let pushFinalized = false
389
+ let pushDataFinalSent = false
385
390
  let pushFinalizeResolve!: () => void
386
391
  let pushFinalizeReject!: (reason?: any) => void
387
392
  const pushFinalize = new Promise<void>((resolve, reject) => {
@@ -465,6 +470,8 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
465
470
  name, chunk, error, final, this.options.id, receiver)
466
471
  const message = this.codec.encode(chunkMsg)
467
472
  await this.publishToTopic(chunkTopic, message, { qos: 2, ...options })
473
+ if (error === undefined && final)
474
+ pushDataFinalSent = true
468
475
  }
469
476
 
470
477
  /* iterate over all chunks of the buffer */
@@ -491,8 +498,9 @@ export class SinkTrait<T extends APISchema = APISchema> extends SourceTrait<T> {
491
498
  abortController.abort(error)
492
499
 
493
500
  /* send error chunk only if push was acked and error did not originate from receiver
494
- (before ack, the sink has no chunk handler yet and will time out on its own) */
495
- if (pushAcked && !remoteError) {
501
+ (before ack, the sink has no chunk handler yet and will time out on its own;
502
+ after final data chunk, no additional terminal chunk should be sent) */
503
+ if (pushAcked && !remoteError && !pushDataFinalSent) {
496
504
  const chunkTopic = this.options.topicMake(name, "sink-push-request", receiver)
497
505
  const chunkMsg = this.msg.makeSinkPushChunk(requestId,
498
506
  name, undefined, error.message, true, this.options.id, receiver)
@@ -142,6 +142,13 @@ export class SourceTrait<T extends APISchema = APISchema> extends ServiceTrait<T
142
142
  await this.publishToTopic(responseTopic, message, { qos: options.qos ?? 2 })
143
143
  }
144
144
 
145
+ /* create a resource spool for request cleanup */
146
+ const reqSpool = new Spool()
147
+ reqSpool.roll(() => {
148
+ this.onResponse.delete(`source-fetch-credit:${requestId}`)
149
+ this.sourceControllers.delete(requestId)
150
+ })
151
+
145
152
  /* define abort controller and signal */
146
153
  const abortController = new AbortController()
147
154
  this.sourceControllers.set(requestId, abortController)
@@ -163,11 +170,11 @@ export class SourceTrait<T extends APISchema = APISchema> extends ServiceTrait<T
163
170
  gate.abort()
164
171
  this.sourceCreditGates.delete(requestId)
165
172
  }
166
- this.sourceControllers.delete(requestId)
167
- this.onResponse.delete(`source-fetch-credit:${requestId}`)
173
+ reqSpool.unroll()
168
174
  })
169
175
  const clearSourceTimeout = () => this.timerClear(sourceTimerId)
170
176
  refreshSourceTimeout()
177
+ reqSpool.roll(() => { clearSourceTimeout() })
171
178
 
172
179
  /* callback for creating and sending a chunk message */
173
180
  const sendChunk = async (
@@ -185,6 +192,7 @@ export class SourceTrait<T extends APISchema = APISchema> extends ServiceTrait<T
185
192
  /* call the handler callback */
186
193
  let ackSent = false
187
194
  let creditGate: CreditGate | undefined
195
+ let cancelledByFetcher = false
188
196
  try {
189
197
  if (topicName !== request.name)
190
198
  throw new Error(`source name mismatch (topic: "${topicName}", payload: "${request.name}")`)
@@ -197,13 +205,19 @@ export class SourceTrait<T extends APISchema = APISchema> extends ServiceTrait<T
197
205
  const initialCredit = request.credit
198
206
  creditGate = (initialCredit !== undefined && initialCredit > 0)
199
207
  ? new CreditGate(initialCredit) : undefined
200
- if (creditGate)
208
+ if (creditGate) {
201
209
  this.sourceCreditGates.set(requestId, creditGate)
210
+ reqSpool.roll(() => {
211
+ creditGate!.abort()
212
+ this.sourceCreditGates.delete(requestId)
213
+ })
214
+ }
202
215
 
203
216
  /* register credit/cancel handler (unconditional for cancel support) */
204
217
  this.onResponse.set(`source-fetch-credit:${requestId}`, (creditParsed: SourceFetchCredit) => {
205
218
  if (creditParsed.credit === 0) {
206
219
  /* cancel signal from fetcher */
220
+ cancelledByFetcher = true
207
221
  abortController.abort(new Error(`source fetch "${name}" cancelled by fetcher`))
208
222
  return
209
223
  }
@@ -238,22 +252,19 @@ export class SourceTrait<T extends APISchema = APISchema> extends ServiceTrait<T
238
252
  const error = ensureError(err, `handler for source "${name}" failed`)
239
253
  abortController.abort(error)
240
254
 
241
- /* send error as nak response or as error chunk */
242
- this.error(error)
243
- if (ackSent)
244
- await sendChunk(undefined, error.message, true).catch(() => {})
245
- else
246
- await sendResponse(error.message).catch(() => {})
255
+ /* on explicit fetcher cancellation, abort silently without emitting error responses */
256
+ if (!cancelledByFetcher) {
257
+ /* send error as nak response or as error chunk */
258
+ this.error(error)
259
+ if (ackSent)
260
+ await sendChunk(undefined, error.message, true).catch(() => {})
261
+ else
262
+ await sendResponse(error.message).catch(() => {})
263
+ }
247
264
  }
248
265
  finally {
249
266
  /* cleanup resources */
250
- clearSourceTimeout()
251
- if (creditGate) {
252
- creditGate.abort()
253
- this.sourceCreditGates.delete(requestId)
254
- }
255
- this.sourceControllers.delete(requestId)
256
- this.onResponse.delete(`source-fetch-credit:${requestId}`)
267
+ await reqSpool.unroll()
257
268
  }
258
269
  })
259
270
  spool.roll(() => { this.onRequest.delete(`source-fetch-request:${name}`) })