mqtt-plus 1.4.18 → 1.4.20

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 (34) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dst-stage1/mqtt-plus-api.js +0 -1
  3. package/dst-stage1/mqtt-plus-auth.d.ts +2 -2
  4. package/dst-stage1/mqtt-plus-auth.js +0 -1
  5. package/dst-stage1/mqtt-plus-base.js +0 -1
  6. package/dst-stage1/mqtt-plus-codec.js +0 -1
  7. package/dst-stage1/mqtt-plus-encode.js +0 -1
  8. package/dst-stage1/mqtt-plus-error.js +0 -1
  9. package/dst-stage1/mqtt-plus-event.js +0 -1
  10. package/dst-stage1/mqtt-plus-info.js +0 -1
  11. package/dst-stage1/mqtt-plus-meta.js +0 -1
  12. package/dst-stage1/mqtt-plus-msg.js +0 -1
  13. package/dst-stage1/mqtt-plus-options.d.ts +2 -2
  14. package/dst-stage1/mqtt-plus-options.js +0 -1
  15. package/dst-stage1/mqtt-plus-service.js +0 -1
  16. package/dst-stage1/mqtt-plus-sink.js +26 -10
  17. package/dst-stage1/mqtt-plus-source.js +17 -11
  18. package/dst-stage1/mqtt-plus-subscription.js +0 -1
  19. package/dst-stage1/mqtt-plus-timer.js +0 -1
  20. package/dst-stage1/mqtt-plus-trace.js +0 -1
  21. package/dst-stage1/mqtt-plus-util.js +0 -1
  22. package/dst-stage1/mqtt-plus-version.js +0 -1
  23. package/dst-stage1/mqtt-plus.js +0 -1
  24. package/dst-stage1/tsc.tsbuildinfo +1 -1
  25. package/dst-stage2/mqtt-plus.cjs.cjs +30 -11
  26. package/dst-stage2/mqtt-plus.esm.js +30 -11
  27. package/dst-stage2/mqtt-plus.umd.js +11 -11
  28. package/package.d/{vite+8.0.8.patch → vite+8.0.12.patch} +2 -2
  29. package/package.json +14 -14
  30. package/src/mqtt-plus-auth.ts +2 -2
  31. package/src/mqtt-plus-options.ts +3 -3
  32. package/src/mqtt-plus-sink.ts +26 -9
  33. package/src/mqtt-plus-source.ts +18 -10
  34. /package/package.d/{@typescript-eslint+typescript-estree+8.58.1.patch → @typescript-eslint+typescript-estree+8.59.3.patch} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
  ChangeLog
3
3
  =========
4
4
 
5
+ 1.4.20 (2026-05-13)
6
+ -------------------
7
+
8
+ - UPDATE: upgrade NPM dependencies
9
+
10
+ 1.4.19 (2026-04-18)
11
+ -------------------
12
+
13
+ - IMPROVEMENT: improve error handling in source and sink traits
14
+ - UPDATE: upgrade NPM dependencies
15
+
5
16
  1.4.18 (2026-04-17)
6
17
  -------------------
7
18
 
@@ -22,4 +22,3 @@
22
22
  ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  */
24
24
  export {};
25
- //# sourceMappingURL=mqtt-plus-api.js.map
@@ -1,7 +1,7 @@
1
1
  import type { APISchema } from "./mqtt-plus-api";
2
2
  import { MetaTrait } from "./mqtt-plus-meta";
3
- export type AuthMode = "require" | "optional";
4
- export type AuthRole = string;
3
+ type AuthMode = "require" | "optional";
4
+ type AuthRole = string;
5
5
  export type AuthOption = AuthRole | {
6
6
  mode: AuthMode;
7
7
  roles: AuthRole[];
@@ -127,4 +127,3 @@ export class AuthTrait extends MetaTrait {
127
127
  return authenticated;
128
128
  }
129
129
  }
130
- //# sourceMappingURL=mqtt-plus-auth.js.map
@@ -233,4 +233,3 @@ export class BaseTrait extends TraceTrait {
233
233
  }
234
234
  }
235
235
  }
236
- //# sourceMappingURL=mqtt-plus-base.js.map
@@ -121,4 +121,3 @@ export class CodecTrait extends OptionsTrait {
121
121
  this.codec = new Codec(this.options.codec);
122
122
  }
123
123
  }
124
- //# sourceMappingURL=mqtt-plus-codec.js.map
@@ -60,4 +60,3 @@ export class EncodeTrait extends CodecTrait {
60
60
  return arr;
61
61
  }
62
62
  }
63
- //# sourceMappingURL=mqtt-plus-encode.js.map
@@ -290,4 +290,3 @@ export function run(...args) {
290
290
  return result;
291
291
  }
292
292
  }
293
- //# sourceMappingURL=mqtt-plus-error.js.map
@@ -186,4 +186,3 @@ export class EventTrait extends AuthTrait {
186
186
  return run(`publish event as MQTT message to topic "${topic}"`, () => this.publishToTopic(topic, message, { qos: 2, ...options }));
187
187
  }
188
188
  }
189
- //# sourceMappingURL=mqtt-plus-event.js.map
@@ -22,4 +22,3 @@
22
22
  ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  */
24
24
  export {};
25
- //# sourceMappingURL=mqtt-plus-info.js.map
@@ -53,4 +53,3 @@ export class MetaTrait extends TimerTrait {
53
53
  return { ...Object.fromEntries(this._meta), ...extra };
54
54
  }
55
55
  }
56
- //# sourceMappingURL=mqtt-plus-meta.js.map
@@ -370,4 +370,3 @@ export class MsgTrait extends EncodeTrait {
370
370
  this.msg = new Msg();
371
371
  }
372
372
  }
373
- //# sourceMappingURL=mqtt-plus-msg.js.map
@@ -4,8 +4,8 @@ type TopicMatching = {
4
4
  operation: string;
5
5
  peerId?: string;
6
6
  };
7
- export type TopicMake = (name: string, operation: string, peerId?: string) => string;
8
- export type TopicMatch = (topic: string) => TopicMatching | null;
7
+ type TopicMake = (name: string, operation: string, peerId?: string) => string;
8
+ type TopicMatch = (topic: string) => TopicMatching | null;
9
9
  export interface APIOptions {
10
10
  id: string;
11
11
  codec: "cbor" | "json";
@@ -50,4 +50,3 @@ export class OptionsTrait {
50
50
  };
51
51
  }
52
52
  }
53
- //# sourceMappingURL=mqtt-plus-options.js.map
@@ -303,4 +303,3 @@ export class ServiceTrait extends EventTrait {
303
303
  return promise;
304
304
  }
305
305
  }
306
- //# sourceMappingURL=mqtt-plus-service.js.map
@@ -120,8 +120,6 @@ export class SinkTrait extends SourceTrait {
120
120
  const receiver = request.receiver;
121
121
  /* create a resource spool for request cleanup */
122
122
  const reqSpool = new Spool();
123
- this.pushSpools.set(requestId, reqSpool);
124
- reqSpool.roll(() => { this.pushSpools.delete(requestId); });
125
123
  /* sanity check sender */
126
124
  if (sender === undefined || sender === "") {
127
125
  this.error(new Error("invalid request: missing sender"));
@@ -162,6 +160,8 @@ export class SinkTrait extends SourceTrait {
162
160
  }
163
161
  this.pushRecvControllers.set(requestId, abortController);
164
162
  reqSpool.roll(() => { this.pushRecvControllers.delete(requestId); });
163
+ this.pushSpools.set(requestId, reqSpool);
164
+ reqSpool.roll(() => { this.pushSpools.delete(requestId); });
165
165
  /* check authentication and prepare stream */
166
166
  let dataCompleted = false;
167
167
  let ackSent = false;
@@ -358,10 +358,9 @@ export class SinkTrait extends SourceTrait {
358
358
  await sendResponse(undefined, true);
359
359
  ackSent = true;
360
360
  /* call handler */
361
- await Promise.race([
362
- Promise.resolve(callback(...params, info)),
363
- abortPromise
364
- ]);
361
+ const callbackPromise = Promise.resolve(callback(...params, info));
362
+ callbackPromise.catch(() => { }); /* guard against unhandled rejection if abort wins the race */
363
+ await Promise.race([callbackPromise, abortPromise]);
365
364
  /* ensure stream is consumed or destroyed to prevent hang */
366
365
  if (readable.readableFlowing !== true && !readable.destroyed)
367
366
  readable.resume();
@@ -395,10 +394,14 @@ export class SinkTrait extends SourceTrait {
395
394
  if (stream !== undefined && !stream.destroyed)
396
395
  stream.destroy(error);
397
396
  }
398
- /* send error as nak response or as mid-stream error response */
397
+ /* send error as nak response or as mid-stream error response
398
+ (skip when a terminal signal was already emitted, e.g. the
399
+ pre-emptive credit=0 cancel published by the timeout handler) */
399
400
  this.error(error);
400
- errorResponseSent = true;
401
- await sendResponse(error.message).catch(() => { });
401
+ if (!errorResponseSent) {
402
+ errorResponseSent = true;
403
+ await sendResponse(error.message).catch(() => { });
404
+ }
402
405
  }
403
406
  finally {
404
407
  /* cleanup resources */
@@ -625,6 +628,17 @@ export class SinkTrait extends SourceTrait {
625
628
  creditGate.replenish(response.credit);
626
629
  refreshTimeout();
627
630
  }
631
+ else if (pushAcked && initialCredit === undefined) {
632
+ /* protocol violation: receiver sent credit despite
633
+ not granting initial credit during ack */
634
+ const error = new Error(`push to sink "${name}" received unsolicited credit (credit-flow disabled)`);
635
+ remoteErrorObject = error;
636
+ abortController.abort(error);
637
+ if (!pushFinalized) {
638
+ pushFinalized = true;
639
+ pushFinalizeReject(error);
640
+ }
641
+ }
628
642
  else
629
643
  pendingCredit += response.credit;
630
644
  });
@@ -660,6 +674,9 @@ export class SinkTrait extends SourceTrait {
660
674
  refreshTimeout();
661
675
  pendingCredit = 0;
662
676
  }
677
+ else if (pendingCredit > 0)
678
+ /* protocol violation: receiver sent credit before ack despite not granting initial credit */
679
+ throw new Error(`push to sink "${name}" received unsolicited credit (credit-flow disabled)`);
663
680
  /* register credit gate at instance level */
664
681
  if (creditGate) {
665
682
  this.pushCreditGates.set(requestId, creditGate);
@@ -736,4 +753,3 @@ export class SinkTrait extends SourceTrait {
736
753
  }
737
754
  }
738
755
  }
739
- //# sourceMappingURL=mqtt-plus-sink.js.map
@@ -199,9 +199,8 @@ export class SourceTrait extends ServiceTrait {
199
199
  try {
200
200
  if (topicName !== request.name)
201
201
  throw new Error(`source name mismatch (topic: "${topicName}", payload: "${request.name}")`);
202
- if (auth)
203
- info.authenticated = await this.authenticated(sender, request.auth, auth, `source "${name}"`);
204
- /* register credit/cancel handler (unconditional for cancel support) */
202
+ /* register credit/cancel handler early (before any await) so cancel
203
+ signals arriving during async authentication are not lost */
205
204
  this.onResponse.set(`source-fetch-credit:${requestId}`, (creditParsed) => {
206
205
  if (abortSignal.aborted)
207
206
  return;
@@ -229,10 +228,13 @@ export class SourceTrait extends ServiceTrait {
229
228
  reqSpool.roll(() => {
230
229
  this.onResponse.delete(`source-fetch-credit:${requestId}`);
231
230
  });
232
- await Promise.race([
233
- Promise.resolve(callback(...params, info)),
234
- abortPromise
235
- ]);
231
+ /* check for authentication */
232
+ if (auth)
233
+ info.authenticated = await this.authenticated(sender, request.auth, auth, `source "${name}"`);
234
+ /* finally call the handler callback */
235
+ const callbackPromise = Promise.resolve(callback(...params, info));
236
+ callbackPromise.catch(() => { }); /* guard against unhandled rejection if abort wins the race */
237
+ await Promise.race([callbackPromise, abortPromise]);
236
238
  /* check for valid data source */
237
239
  if (!(info.stream instanceof Readable) && !(info.buffer instanceof Promise) && !(info.buffer instanceof Uint8Array))
238
240
  throw new Error("handler did not provide data via info.stream or info.buffer fields");
@@ -259,9 +261,14 @@ export class SourceTrait extends ServiceTrait {
259
261
  await sendStreamAsChunks(info.stream, this.options.chunkSize, sendChunk, creditGate, abortSignal);
260
262
  else if (info.buffer instanceof Promise || info.buffer instanceof Uint8Array) {
261
263
  /* handle Buffer result */
262
- const buffer = (info.buffer instanceof Promise)
263
- ? await Promise.race([info.buffer, abortPromise])
264
- : info.buffer;
264
+ let buffer;
265
+ if (info.buffer instanceof Promise) {
266
+ const bufferPromise = info.buffer;
267
+ bufferPromise.catch(() => { }); /* guard against unhandled rejection if abort wins the race */
268
+ buffer = await Promise.race([bufferPromise, abortPromise]);
269
+ }
270
+ else
271
+ buffer = info.buffer;
265
272
  /* re-check abort: a late info.buffer resolution could win the race */
266
273
  /* by a microtask margin even after abort fired -- discard silently */
267
274
  abortSignal.throwIfAborted();
@@ -557,4 +564,3 @@ export class SourceTrait extends ServiceTrait {
557
564
  return result;
558
565
  }
559
566
  }
560
- //# sourceMappingURL=mqtt-plus-source.js.map
@@ -180,4 +180,3 @@ export class SubscriptionTrait extends BaseTrait {
180
180
  await super.destroy();
181
181
  }
182
182
  }
183
- //# sourceMappingURL=mqtt-plus-subscription.js.map
@@ -113,4 +113,3 @@ export class TimerTrait extends SubscriptionTrait {
113
113
  });
114
114
  }
115
115
  }
116
- //# sourceMappingURL=mqtt-plus-timer.js.map
@@ -109,4 +109,3 @@ export class TraceTrait extends MsgTrait {
109
109
  }
110
110
  }
111
111
  }
112
- //# sourceMappingURL=mqtt-plus-trace.js.map
@@ -245,4 +245,3 @@ export function makeMutuallyExclusiveFields(obj, f1Name, f2Name, onConsumed) {
245
245
  configurable: true
246
246
  });
247
247
  }
248
- //# sourceMappingURL=mqtt-plus-util.js.map
@@ -38,4 +38,3 @@ export const MIN_VERSION = "0.0";
38
38
  /* package version (numeric format) */
39
39
  export const version = versionToNum(VERSION);
40
40
  export const minVersion = versionToNum(MIN_VERSION);
41
- //# sourceMappingURL=mqtt-plus-version.js.map
@@ -25,4 +25,3 @@ import { SinkTrait } from "./mqtt-plus-sink";
25
25
  /* export the default API class */
26
26
  export default class MQTTp extends SinkTrait {
27
27
  }
28
- //# sourceMappingURL=mqtt-plus.js.map
@@ -1 +1 @@
1
- {"root":["../src/mqtt-plus-api.ts","../src/mqtt-plus-auth.ts","../src/mqtt-plus-base.ts","../src/mqtt-plus-codec.ts","../src/mqtt-plus-encode.ts","../src/mqtt-plus-error.ts","../src/mqtt-plus-event.ts","../src/mqtt-plus-info.ts","../src/mqtt-plus-meta.ts","../src/mqtt-plus-msg.ts","../src/mqtt-plus-options.ts","../src/mqtt-plus-service.ts","../src/mqtt-plus-sink.ts","../src/mqtt-plus-source.ts","../src/mqtt-plus-subscription.ts","../src/mqtt-plus-timer.ts","../src/mqtt-plus-trace.ts","../src/mqtt-plus-util.ts","../src/mqtt-plus-version.ts","../src/mqtt-plus.ts"],"version":"6.0.2"}
1
+ {"root":["../src/mqtt-plus-api.ts","../src/mqtt-plus-auth.ts","../src/mqtt-plus-base.ts","../src/mqtt-plus-codec.ts","../src/mqtt-plus-encode.ts","../src/mqtt-plus-error.ts","../src/mqtt-plus-event.ts","../src/mqtt-plus-info.ts","../src/mqtt-plus-meta.ts","../src/mqtt-plus-msg.ts","../src/mqtt-plus-options.ts","../src/mqtt-plus-service.ts","../src/mqtt-plus-sink.ts","../src/mqtt-plus-source.ts","../src/mqtt-plus-subscription.ts","../src/mqtt-plus-timer.ts","../src/mqtt-plus-trace.ts","../src/mqtt-plus-util.ts","../src/mqtt-plus-version.ts","../src/mqtt-plus.ts"],"version":"6.0.3"}
@@ -1824,7 +1824,6 @@ var SourceTrait = class extends ServiceTrait {
1824
1824
  let cancelledByFetcher = false;
1825
1825
  try {
1826
1826
  if (topicName !== request.name) throw new Error(`source name mismatch (topic: "${topicName}", payload: "${request.name}")`);
1827
- if (auth) info.authenticated = await this.authenticated(sender, request.auth, auth, `source "${name}"`);
1828
1827
  this.onResponse.set(`source-fetch-credit:${requestId}`, (creditParsed) => {
1829
1828
  if (abortSignal.aborted) return;
1830
1829
  if (creditParsed.name !== name) {
@@ -1849,7 +1848,10 @@ var SourceTrait = class extends ServiceTrait {
1849
1848
  reqSpool.roll(() => {
1850
1849
  this.onResponse.delete(`source-fetch-credit:${requestId}`);
1851
1850
  });
1852
- await Promise.race([Promise.resolve(callback(...params, info)), abortPromise]);
1851
+ if (auth) info.authenticated = await this.authenticated(sender, request.auth, auth, `source "${name}"`);
1852
+ const callbackPromise = Promise.resolve(callback(...params, info));
1853
+ callbackPromise.catch(() => {});
1854
+ await Promise.race([callbackPromise, abortPromise]);
1853
1855
  if (!(info.stream instanceof node_stream.Readable) && !(info.buffer instanceof Promise) && !(info.buffer instanceof Uint8Array)) throw new Error("handler did not provide data via info.stream or info.buffer fields");
1854
1856
  if (info.stream instanceof node_stream.Readable && (info.buffer instanceof Promise || info.buffer instanceof Uint8Array)) throw new Error("handler has set both info.stream and info.buffer fields");
1855
1857
  const initialCredit = request.credit;
@@ -1866,7 +1868,12 @@ var SourceTrait = class extends ServiceTrait {
1866
1868
  ackSent = true;
1867
1869
  if (info.stream instanceof node_stream.Readable) await sendStreamAsChunks(info.stream, this.options.chunkSize, sendChunk, creditGate, abortSignal);
1868
1870
  else if (info.buffer instanceof Promise || info.buffer instanceof Uint8Array) {
1869
- const buffer = info.buffer instanceof Promise ? await Promise.race([info.buffer, abortPromise]) : info.buffer;
1871
+ let buffer;
1872
+ if (info.buffer instanceof Promise) {
1873
+ const bufferPromise = info.buffer;
1874
+ bufferPromise.catch(() => {});
1875
+ buffer = await Promise.race([bufferPromise, abortPromise]);
1876
+ } else buffer = info.buffer;
1870
1877
  abortSignal.throwIfAborted();
1871
1878
  await sendBufferAsChunks(buffer, this.options.chunkSize, sendChunk, creditGate, abortSignal);
1872
1879
  }
@@ -2164,10 +2171,6 @@ var SinkTrait = class extends SourceTrait {
2164
2171
  const sender = request.sender;
2165
2172
  const receiver = request.receiver;
2166
2173
  const reqSpool = new Spool();
2167
- this.pushSpools.set(requestId, reqSpool);
2168
- reqSpool.roll(() => {
2169
- this.pushSpools.delete(requestId);
2170
- });
2171
2174
  if (sender === void 0 || sender === "") {
2172
2175
  this.error(/* @__PURE__ */ new Error("invalid request: missing sender"));
2173
2176
  await reqSpool.unroll();
@@ -2208,6 +2211,10 @@ var SinkTrait = class extends SourceTrait {
2208
2211
  reqSpool.roll(() => {
2209
2212
  this.pushRecvControllers.delete(requestId);
2210
2213
  });
2214
+ this.pushSpools.set(requestId, reqSpool);
2215
+ reqSpool.roll(() => {
2216
+ this.pushSpools.delete(requestId);
2217
+ });
2211
2218
  let dataCompleted = false;
2212
2219
  let ackSent = false;
2213
2220
  let errorResponseSent = false;
@@ -2367,7 +2374,9 @@ var SinkTrait = class extends SourceTrait {
2367
2374
  });
2368
2375
  await sendResponse(void 0, true);
2369
2376
  ackSent = true;
2370
- await Promise.race([Promise.resolve(callback(...params, info)), abortPromise]);
2377
+ const callbackPromise = Promise.resolve(callback(...params, info));
2378
+ callbackPromise.catch(() => {});
2379
+ await Promise.race([callbackPromise, abortPromise]);
2371
2380
  if (readable.readableFlowing !== true && !readable.destroyed) readable.resume();
2372
2381
  await streamDone.catch((err) => {
2373
2382
  this.error(ensureError(err), `stream drain after sink "${name}" callback failed`);
@@ -2388,8 +2397,10 @@ var SinkTrait = class extends SourceTrait {
2388
2397
  if (stream !== void 0 && !stream.destroyed) stream.destroy(error);
2389
2398
  }
2390
2399
  this.error(error);
2391
- errorResponseSent = true;
2392
- await sendResponse(error.message).catch(() => {});
2400
+ if (!errorResponseSent) {
2401
+ errorResponseSent = true;
2402
+ await sendResponse(error.message).catch(() => {});
2403
+ }
2393
2404
  } finally {
2394
2405
  const stream = this.pushStreams.get(requestId);
2395
2406
  if (stream !== void 0 && !stream.destroyed && !dataCompleted && !errorResponseSent) stream.destroy(abortSignal.aborted ? ensureError(abortSignal.reason) : /* @__PURE__ */ new Error("sink push aborted without cause"));
@@ -2572,6 +2583,14 @@ var SinkTrait = class extends SourceTrait {
2572
2583
  if (creditGate !== void 0) {
2573
2584
  creditGate.replenish(response.credit);
2574
2585
  refreshTimeout();
2586
+ } else if (pushAcked && initialCredit === void 0) {
2587
+ const error = /* @__PURE__ */ new Error(`push to sink "${name}" received unsolicited credit (credit-flow disabled)`);
2588
+ remoteErrorObject = error;
2589
+ abortController.abort(error);
2590
+ if (!pushFinalized) {
2591
+ pushFinalized = true;
2592
+ pushFinalizeReject(error);
2593
+ }
2575
2594
  } else pendingCredit += response.credit;
2576
2595
  });
2577
2596
  spool.roll(() => {
@@ -2607,7 +2626,7 @@ var SinkTrait = class extends SourceTrait {
2607
2626
  creditGate = new CreditGate(initialCredit + pendingCredit);
2608
2627
  if (pendingCredit > 0) refreshTimeout();
2609
2628
  pendingCredit = 0;
2610
- }
2629
+ } else if (pendingCredit > 0) throw new Error(`push to sink "${name}" received unsolicited credit (credit-flow disabled)`);
2611
2630
  if (creditGate) {
2612
2631
  this.pushCreditGates.set(requestId, creditGate);
2613
2632
  spool.roll(() => {
@@ -1797,7 +1797,6 @@ var SourceTrait = class extends ServiceTrait {
1797
1797
  let cancelledByFetcher = false;
1798
1798
  try {
1799
1799
  if (topicName !== request.name) throw new Error(`source name mismatch (topic: "${topicName}", payload: "${request.name}")`);
1800
- if (auth) info.authenticated = await this.authenticated(sender, request.auth, auth, `source "${name}"`);
1801
1800
  this.onResponse.set(`source-fetch-credit:${requestId}`, (creditParsed) => {
1802
1801
  if (abortSignal.aborted) return;
1803
1802
  if (creditParsed.name !== name) {
@@ -1822,7 +1821,10 @@ var SourceTrait = class extends ServiceTrait {
1822
1821
  reqSpool.roll(() => {
1823
1822
  this.onResponse.delete(`source-fetch-credit:${requestId}`);
1824
1823
  });
1825
- await Promise.race([Promise.resolve(callback(...params, info)), abortPromise]);
1824
+ if (auth) info.authenticated = await this.authenticated(sender, request.auth, auth, `source "${name}"`);
1825
+ const callbackPromise = Promise.resolve(callback(...params, info));
1826
+ callbackPromise.catch(() => {});
1827
+ await Promise.race([callbackPromise, abortPromise]);
1826
1828
  if (!(info.stream instanceof Readable) && !(info.buffer instanceof Promise) && !(info.buffer instanceof Uint8Array)) throw new Error("handler did not provide data via info.stream or info.buffer fields");
1827
1829
  if (info.stream instanceof Readable && (info.buffer instanceof Promise || info.buffer instanceof Uint8Array)) throw new Error("handler has set both info.stream and info.buffer fields");
1828
1830
  const initialCredit = request.credit;
@@ -1839,7 +1841,12 @@ var SourceTrait = class extends ServiceTrait {
1839
1841
  ackSent = true;
1840
1842
  if (info.stream instanceof Readable) await sendStreamAsChunks(info.stream, this.options.chunkSize, sendChunk, creditGate, abortSignal);
1841
1843
  else if (info.buffer instanceof Promise || info.buffer instanceof Uint8Array) {
1842
- const buffer = info.buffer instanceof Promise ? await Promise.race([info.buffer, abortPromise]) : info.buffer;
1844
+ let buffer;
1845
+ if (info.buffer instanceof Promise) {
1846
+ const bufferPromise = info.buffer;
1847
+ bufferPromise.catch(() => {});
1848
+ buffer = await Promise.race([bufferPromise, abortPromise]);
1849
+ } else buffer = info.buffer;
1843
1850
  abortSignal.throwIfAborted();
1844
1851
  await sendBufferAsChunks(buffer, this.options.chunkSize, sendChunk, creditGate, abortSignal);
1845
1852
  }
@@ -2137,10 +2144,6 @@ var SinkTrait = class extends SourceTrait {
2137
2144
  const sender = request.sender;
2138
2145
  const receiver = request.receiver;
2139
2146
  const reqSpool = new Spool();
2140
- this.pushSpools.set(requestId, reqSpool);
2141
- reqSpool.roll(() => {
2142
- this.pushSpools.delete(requestId);
2143
- });
2144
2147
  if (sender === void 0 || sender === "") {
2145
2148
  this.error(/* @__PURE__ */ new Error("invalid request: missing sender"));
2146
2149
  await reqSpool.unroll();
@@ -2181,6 +2184,10 @@ var SinkTrait = class extends SourceTrait {
2181
2184
  reqSpool.roll(() => {
2182
2185
  this.pushRecvControllers.delete(requestId);
2183
2186
  });
2187
+ this.pushSpools.set(requestId, reqSpool);
2188
+ reqSpool.roll(() => {
2189
+ this.pushSpools.delete(requestId);
2190
+ });
2184
2191
  let dataCompleted = false;
2185
2192
  let ackSent = false;
2186
2193
  let errorResponseSent = false;
@@ -2340,7 +2347,9 @@ var SinkTrait = class extends SourceTrait {
2340
2347
  });
2341
2348
  await sendResponse(void 0, true);
2342
2349
  ackSent = true;
2343
- await Promise.race([Promise.resolve(callback(...params, info)), abortPromise]);
2350
+ const callbackPromise = Promise.resolve(callback(...params, info));
2351
+ callbackPromise.catch(() => {});
2352
+ await Promise.race([callbackPromise, abortPromise]);
2344
2353
  if (readable.readableFlowing !== true && !readable.destroyed) readable.resume();
2345
2354
  await streamDone.catch((err) => {
2346
2355
  this.error(ensureError(err), `stream drain after sink "${name}" callback failed`);
@@ -2361,8 +2370,10 @@ var SinkTrait = class extends SourceTrait {
2361
2370
  if (stream !== void 0 && !stream.destroyed) stream.destroy(error);
2362
2371
  }
2363
2372
  this.error(error);
2364
- errorResponseSent = true;
2365
- await sendResponse(error.message).catch(() => {});
2373
+ if (!errorResponseSent) {
2374
+ errorResponseSent = true;
2375
+ await sendResponse(error.message).catch(() => {});
2376
+ }
2366
2377
  } finally {
2367
2378
  const stream = this.pushStreams.get(requestId);
2368
2379
  if (stream !== void 0 && !stream.destroyed && !dataCompleted && !errorResponseSent) stream.destroy(abortSignal.aborted ? ensureError(abortSignal.reason) : /* @__PURE__ */ new Error("sink push aborted without cause"));
@@ -2545,6 +2556,14 @@ var SinkTrait = class extends SourceTrait {
2545
2556
  if (creditGate !== void 0) {
2546
2557
  creditGate.replenish(response.credit);
2547
2558
  refreshTimeout();
2559
+ } else if (pushAcked && initialCredit === void 0) {
2560
+ const error = /* @__PURE__ */ new Error(`push to sink "${name}" received unsolicited credit (credit-flow disabled)`);
2561
+ remoteErrorObject = error;
2562
+ abortController.abort(error);
2563
+ if (!pushFinalized) {
2564
+ pushFinalized = true;
2565
+ pushFinalizeReject(error);
2566
+ }
2548
2567
  } else pendingCredit += response.credit;
2549
2568
  });
2550
2569
  spool.roll(() => {
@@ -2580,7 +2599,7 @@ var SinkTrait = class extends SourceTrait {
2580
2599
  creditGate = new CreditGate(initialCredit + pendingCredit);
2581
2600
  if (pendingCredit > 0) refreshTimeout();
2582
2601
  pendingCredit = 0;
2583
- }
2602
+ } else if (pendingCredit > 0) throw new Error(`push to sink "${name}" received unsolicited credit (credit-flow disabled)`);
2584
2603
  if (creditGate) {
2585
2604
  this.pushCreditGates.set(requestId, creditGate);
2586
2605
  spool.roll(() => {