@hile/message-modem 4.0.2 → 4.0.4

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AbortException, Exception, TimeoutException } from "./exception.js";
1
+ import { AbortException, Exception, MessageInputError, TimeoutException } from "./exception.js";
2
2
  import { Readable } from 'node:stream';
3
3
  import { DeadlineScheduler } from './deadline-scheduler.js';
4
4
  export * from './exception.js';
@@ -8,9 +8,44 @@ export var MESSAGE_MODEM_TYPE;
8
8
  MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["RESPONSE"] = 1] = "RESPONSE";
9
9
  MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["ABORT"] = 2] = "ABORT";
10
10
  MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["STREAM_CREDIT"] = 3] = "STREAM_CREDIT";
11
+ MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["STREAM_DATA"] = 4] = "STREAM_DATA";
12
+ MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["STREAM_CANCEL"] = 5] = "STREAM_CANCEL";
11
13
  })(MESSAGE_MODEM_TYPE || (MESSAGE_MODEM_TYPE = {}));
12
14
  const MAX_STREAM_WINDOW = 64;
15
+ const DEFAULT_INPUT_STREAM_WINDOW = 1;
16
+ const MAX_CONCURRENT_STREAMS = 128;
13
17
  const MAX_TIMER_DELAY = 2_147_483_647;
18
+ export function isBinaryInput(value) {
19
+ return value instanceof Uint8Array || value instanceof ArrayBuffer;
20
+ }
21
+ export function isMessageInput(value) {
22
+ return isBinaryInput(value) || isAsyncIterable(value);
23
+ }
24
+ async function* toInputIterable(input) {
25
+ if (input instanceof ArrayBuffer) {
26
+ yield new Uint8Array(input);
27
+ return;
28
+ }
29
+ if (input instanceof Uint8Array) {
30
+ yield input;
31
+ return;
32
+ }
33
+ yield* input;
34
+ }
35
+ function normalizeMessageInput(data, explicitInput) {
36
+ if (explicitInput !== undefined) {
37
+ if (isMessageInput(data)) {
38
+ throw new TypeError('A message accepts only one request input stream');
39
+ }
40
+ if (!isMessageInput(explicitInput)) {
41
+ throw new TypeError('Message input must be an AsyncIterable, Uint8Array, or ArrayBuffer');
42
+ }
43
+ return { data, input: toInputIterable(explicitInput) };
44
+ }
45
+ if (!isMessageInput(data))
46
+ return { data };
47
+ return { data: undefined, input: toInputIterable(data) };
48
+ }
14
49
  function streamLimit(value, name) {
15
50
  if (value === undefined)
16
51
  return undefined;
@@ -33,6 +68,17 @@ function streamTimeout(value, name) {
33
68
  }
34
69
  return normalized;
35
70
  }
71
+ function returnProducerIterator(producer) {
72
+ if (producer.iteratorReturned || !producer.iterator?.return)
73
+ return;
74
+ producer.iteratorReturned = true;
75
+ try {
76
+ void Promise.resolve(producer.iterator.return()).catch(() => { });
77
+ }
78
+ catch {
79
+ // Iterator cleanup must not mask the request's original outcome.
80
+ }
81
+ }
36
82
  class CreditReadable extends Readable {
37
83
  onConsumed;
38
84
  constructor(onConsumed) {
@@ -56,6 +102,8 @@ export class MessageModem {
56
102
  stacks = new Map();
57
103
  streams = new Map();
58
104
  streamProducers = new Map();
105
+ inputStreams = new Map();
106
+ inputStreamProducers = new Map();
59
107
  _dispose() {
60
108
  for (const { reject } of this.stacks.values()) {
61
109
  reject(new AbortException());
@@ -66,13 +114,25 @@ export class MessageModem {
66
114
  for (const { stream } of this.streams.values()) {
67
115
  stream.destroy(new AbortException());
68
116
  }
117
+ for (const { stream } of this.inputStreams.values()) {
118
+ stream.destroy(new AbortException());
119
+ }
69
120
  for (const producer of this.streamProducers.values()) {
121
+ producer.cancelled = true;
70
122
  producer.wake?.();
123
+ returnProducerIterator(producer);
124
+ }
125
+ for (const producer of this.inputStreamProducers.values()) {
126
+ producer.cancelled = true;
127
+ producer.wake?.();
128
+ returnProducerIterator(producer);
71
129
  }
72
130
  this.aborts.clear();
73
131
  this.stacks.clear();
74
132
  this.streams.clear();
75
133
  this.streamProducers.clear();
134
+ this.inputStreams.clear();
135
+ this.inputStreamProducers.clear();
76
136
  this.deadlines.clear();
77
137
  }
78
138
  /**
@@ -93,14 +153,13 @@ export class MessageModem {
93
153
  * @param data - 消息数据
94
154
  * @returns 消息数据
95
155
  */
96
- createPostData(mode, data, twoway = true, stream = false) {
156
+ createPostData(mode, data, twoway = true) {
97
157
  const id = this.createIncrementId();
98
158
  const state = {
99
- id, twoway, data, mode, stream,
159
+ id, twoway, data, mode,
100
160
  };
101
161
  if (mode === MESSAGE_MODEM_TYPE.ABORT) {
102
162
  state.twoway = false;
103
- state.stream = false;
104
163
  }
105
164
  return state;
106
165
  }
@@ -112,10 +171,12 @@ export class MessageModem {
112
171
  * @returns 消息响应
113
172
  */
114
173
  _send(data, options) {
115
- return this._write(data, {
174
+ const normalized = normalizeMessageInput(data, options?.input);
175
+ return this._write(normalized.data, {
116
176
  timeout: options?.timeout ?? 30000,
117
177
  twoway: true,
118
178
  signal: options?.signal,
179
+ input: normalized.input,
119
180
  });
120
181
  }
121
182
  /**
@@ -132,21 +193,119 @@ export class MessageModem {
132
193
  signal: options?.signal,
133
194
  });
134
195
  }
196
+ startInputProducer(id, input, onError, onActivity = () => { }) {
197
+ const producer = {
198
+ credits: 0,
199
+ nextCreditSeq: 0,
200
+ nextSeq: 0,
201
+ cancelled: false,
202
+ iteratorReturned: false,
203
+ fail: onError,
204
+ iterator: input[Symbol.asyncIterator](),
205
+ };
206
+ this.inputStreamProducers.set(id, producer);
207
+ void (async () => {
208
+ let readingInput = false;
209
+ try {
210
+ while (!producer.cancelled) {
211
+ while (producer.credits === 0 && !producer.cancelled) {
212
+ await new Promise((resolve) => {
213
+ producer.wake = resolve;
214
+ });
215
+ producer.wake = undefined;
216
+ }
217
+ if (producer.cancelled)
218
+ return;
219
+ producer.credits--;
220
+ readingInput = true;
221
+ const next = await producer.iterator.next();
222
+ if (!next.done && next.value == null) {
223
+ throw new TypeError('Stream chunk must not be null or undefined');
224
+ }
225
+ readingInput = false;
226
+ if (producer.cancelled)
227
+ return;
228
+ onActivity();
229
+ this.post({
230
+ id,
231
+ mode: MESSAGE_MODEM_TYPE.STREAM_DATA,
232
+ twoway: false,
233
+ data: {
234
+ direction: 'input',
235
+ seq: producer.nextSeq++,
236
+ payload: next.done ? undefined : next.value,
237
+ final: next.done === true,
238
+ },
239
+ });
240
+ if (next.done) {
241
+ this.inputStreamProducers.delete(id);
242
+ return;
243
+ }
244
+ }
245
+ }
246
+ catch (error) {
247
+ if (producer.cancelled)
248
+ return;
249
+ this.failInputProducer(id, readingInput
250
+ ? new MessageInputError(error)
251
+ : error instanceof Error
252
+ ? error
253
+ : new Error('Request input stream failed'));
254
+ }
255
+ })();
256
+ }
257
+ failInputProducer(id, error) {
258
+ const producer = this.inputStreamProducers.get(id);
259
+ if (!producer)
260
+ return;
261
+ this.cancelInputProducer(id);
262
+ try {
263
+ this.post({
264
+ id,
265
+ mode: MESSAGE_MODEM_TYPE.ABORT,
266
+ twoway: false,
267
+ });
268
+ }
269
+ catch {
270
+ // The transport may already be closed.
271
+ }
272
+ producer.fail?.(error);
273
+ }
274
+ cancelInputProducer(id) {
275
+ const producer = this.inputStreamProducers.get(id);
276
+ if (!producer)
277
+ return;
278
+ this.inputStreamProducers.delete(id);
279
+ producer.cancelled = true;
280
+ producer.wake?.();
281
+ returnProducerIterator(producer);
282
+ }
135
283
  _stream(data, options = {}) {
284
+ const normalized = normalizeMessageInput(data, options.input);
136
285
  const window = streamWindow(options.window);
137
286
  const timeout = streamTimeout(options.timeout, 'Stream timeout');
138
287
  const idleTimeout = streamTimeout(options.idleTimeout, 'Stream idle timeout');
139
- const state = this.createPostData(MESSAGE_MODEM_TYPE.REQUEST, data, true, true);
140
- state.streamVersion = 1;
141
- if (window > 1)
142
- state.streamWindow = window;
288
+ const state = this.createPostData(MESSAGE_MODEM_TYPE.REQUEST, normalized.data, true);
289
+ state.streams = {
290
+ ...(normalized.input ? { input: true } : {}),
291
+ output: window > 1 ? { window } : {},
292
+ };
143
293
  let consumer;
144
294
  const stream = new CreditReadable(() => {
145
295
  if (consumer.creditsOwed === 0 || consumer.completed || consumer.cancelled)
146
296
  return;
147
297
  consumer.creditsOwed--;
148
298
  try {
149
- this.post(this.createPostData(MESSAGE_MODEM_TYPE.STREAM_CREDIT, { id: state.id, seq: consumer.nextCreditSeq++ }, false));
299
+ this.post({
300
+ id: state.id,
301
+ mode: MESSAGE_MODEM_TYPE.STREAM_CREDIT,
302
+ twoway: false,
303
+ data: {
304
+ direction: 'output',
305
+ seq: consumer.nextCreditSeq++,
306
+ window: consumer.maxCredits,
307
+ },
308
+ });
150
309
  }
151
310
  catch (error) {
152
311
  consumer.cancelled = true;
@@ -185,13 +344,18 @@ export class MessageModem {
185
344
  nextCreditSeq: 0,
186
345
  touch,
187
346
  clearTimers,
347
+ cleanup: () => { },
188
348
  };
189
349
  const sendAbort = () => {
190
350
  if (consumer.completed || consumer.cancelled)
191
351
  return;
192
352
  consumer.cancelled = true;
193
353
  try {
194
- this.post(this.createPostData(MESSAGE_MODEM_TYPE.ABORT, state.id));
354
+ this.post({
355
+ id: state.id,
356
+ mode: MESSAGE_MODEM_TYPE.ABORT,
357
+ twoway: false,
358
+ });
195
359
  }
196
360
  catch {
197
361
  // The transport may already be closed.
@@ -201,12 +365,24 @@ export class MessageModem {
201
365
  sendAbort();
202
366
  stream.destroy(new AbortException());
203
367
  };
368
+ const cleanup = () => {
369
+ clearTimers();
370
+ this.streams.delete(state.id);
371
+ options?.signal?.removeEventListener('abort', onAbort);
372
+ };
373
+ consumer.cleanup = cleanup;
204
374
  if (options?.signal?.aborted) {
205
375
  consumer.cancelled = true;
206
376
  queueMicrotask(() => stream.destroy(new AbortException()));
207
377
  return stream;
208
378
  }
209
379
  this.streams.set(state.id, consumer);
380
+ if (normalized.input) {
381
+ this.startInputProducer(state.id, normalized.input, (error) => {
382
+ consumer.cancelled = true;
383
+ stream.destroy(error);
384
+ }, consumer.touch);
385
+ }
210
386
  options?.signal?.addEventListener('abort', onAbort, { once: true });
211
387
  if (timeout) {
212
388
  totalTimer = this.deadlines.schedule(timeout, () => expire('Stream timeout'));
@@ -214,16 +390,16 @@ export class MessageModem {
214
390
  touch();
215
391
  stream.on('close', () => {
216
392
  sendAbort();
217
- clearTimers();
218
- this.streams.delete(state.id);
219
- options?.signal?.removeEventListener('abort', onAbort);
393
+ this.cancelInputProducer(state.id);
394
+ cleanup();
220
395
  });
221
396
  try {
222
397
  this.post(state);
223
398
  }
224
399
  catch (error) {
225
400
  consumer.cancelled = true;
226
- this.streams.delete(state.id);
401
+ this.cancelInputProducer(state.id);
402
+ cleanup();
227
403
  queueMicrotask(() => stream.destroy(error));
228
404
  }
229
405
  return stream;
@@ -240,6 +416,8 @@ export class MessageModem {
240
416
  const signal = options?.signal;
241
417
  // 创建请求消息数据
242
418
  const state = this.createPostData(MESSAGE_MODEM_TYPE.REQUEST, data, twoway);
419
+ if (options?.input)
420
+ state.streams = { input: true };
243
421
  // 如果消息是单向的,则直接返回
244
422
  if (!twoway) {
245
423
  if (!signal?.aborted)
@@ -257,13 +435,19 @@ export class MessageModem {
257
435
  timer = undefined;
258
436
  signal?.removeEventListener('abort', onAbort);
259
437
  clear();
438
+ this.cancelInputProducer(state.id);
260
439
  };
261
440
  const onAbort = () => {
262
441
  this.deadlines.cancel(timer);
263
442
  timer = undefined;
264
443
  try {
265
- if (posted)
266
- this.post(this.createPostData(MESSAGE_MODEM_TYPE.ABORT, state.id));
444
+ if (posted) {
445
+ this.post({
446
+ id: state.id,
447
+ mode: MESSAGE_MODEM_TYPE.ABORT,
448
+ twoway: false,
449
+ });
450
+ }
267
451
  }
268
452
  catch {
269
453
  /* 例如 WebSocket 已关闭时 send 可能抛错 */
@@ -271,6 +455,7 @@ export class MessageModem {
271
455
  finally {
272
456
  signal?.removeEventListener('abort', onAbort);
273
457
  clear();
458
+ this.cancelInputProducer(state.id);
274
459
  reject(new AbortException());
275
460
  }
276
461
  };
@@ -288,12 +473,29 @@ export class MessageModem {
288
473
  resolve: _resolve,
289
474
  reject: _reject,
290
475
  });
291
- timer = this.deadlines.schedule(timeout, () => _reject(new TimeoutException()));
476
+ timer = this.deadlines.schedule(timeout, () => {
477
+ try {
478
+ if (posted) {
479
+ this.post({
480
+ id: state.id,
481
+ mode: MESSAGE_MODEM_TYPE.ABORT,
482
+ twoway: false,
483
+ });
484
+ }
485
+ }
486
+ catch {
487
+ // The transport may already be closed.
488
+ }
489
+ _reject(new TimeoutException());
490
+ });
292
491
  signal?.addEventListener('abort', onAbort, { once: true });
293
492
  if (signal?.aborted) {
294
493
  onAbort();
295
494
  return;
296
495
  }
496
+ if (options?.input) {
497
+ this.startInputProducer(state.id, options.input, _reject);
498
+ }
297
499
  try {
298
500
  posted = true;
299
501
  this.post(state);
@@ -303,14 +505,127 @@ export class MessageModem {
303
505
  }
304
506
  });
305
507
  }
508
+ createInputConsumer(id) {
509
+ const window = DEFAULT_INPUT_STREAM_WINDOW;
510
+ let consumer;
511
+ const stream = new CreditReadable(() => {
512
+ if (consumer.creditsOwed === 0 || consumer.completed || consumer.cancelled)
513
+ return;
514
+ consumer.creditsOwed--;
515
+ this.post({
516
+ id,
517
+ mode: MESSAGE_MODEM_TYPE.STREAM_CREDIT,
518
+ twoway: false,
519
+ data: {
520
+ direction: 'input',
521
+ seq: consumer.nextCreditSeq++,
522
+ window,
523
+ },
524
+ });
525
+ });
526
+ consumer = {
527
+ stream,
528
+ completed: false,
529
+ cancelled: false,
530
+ creditsOwed: 0,
531
+ maxCredits: window,
532
+ nextSeq: 0,
533
+ nextCreditSeq: 0,
534
+ touch: () => { },
535
+ clearTimers: () => { },
536
+ cleanup: () => { },
537
+ };
538
+ this.inputStreams.set(id, consumer);
539
+ // A handler may intentionally ignore its body. Keep protocol failures from
540
+ // becoming process-level unhandled EventEmitter errors in that case.
541
+ stream.on('error', () => { });
542
+ stream.once('close', () => {
543
+ if (consumer.completed || consumer.cancelled)
544
+ return;
545
+ this.cancelInputConsumer(id, 'Request input consumer closed');
546
+ });
547
+ this.post({
548
+ id,
549
+ mode: MESSAGE_MODEM_TYPE.STREAM_CREDIT,
550
+ twoway: false,
551
+ data: {
552
+ direction: 'input',
553
+ seq: consumer.nextCreditSeq++,
554
+ window,
555
+ },
556
+ });
557
+ return stream;
558
+ }
559
+ canAcceptInputStream(id) {
560
+ return !this.inputStreams.has(id) && this.inputStreams.size < MAX_CONCURRENT_STREAMS;
561
+ }
562
+ cancelInputConsumer(id, message = 'Request handler completed') {
563
+ const consumer = this.inputStreams.get(id);
564
+ if (!consumer)
565
+ return;
566
+ this.inputStreams.delete(id);
567
+ consumer.cancelled = true;
568
+ try {
569
+ this.post({
570
+ id,
571
+ mode: MESSAGE_MODEM_TYPE.STREAM_CANCEL,
572
+ twoway: false,
573
+ data: { direction: 'input', message },
574
+ });
575
+ }
576
+ catch {
577
+ // The transport may already be closed.
578
+ }
579
+ consumer.stream.destroy();
580
+ }
581
+ failInputConsumer(id, error) {
582
+ const consumer = this.inputStreams.get(id);
583
+ if (!consumer)
584
+ return;
585
+ this.inputStreams.delete(id);
586
+ consumer.cancelled = true;
587
+ try {
588
+ this.post({
589
+ id,
590
+ mode: MESSAGE_MODEM_TYPE.STREAM_CANCEL,
591
+ twoway: false,
592
+ data: { direction: 'input', status: error.status, message: error.message },
593
+ });
594
+ }
595
+ catch {
596
+ // The transport may already be closed.
597
+ }
598
+ consumer.stream.destroy(error);
599
+ this.cancelOutputProducer(id);
600
+ const controller = this.aborts.get(id);
601
+ this.aborts.delete(id);
602
+ if (controller && !controller.signal.aborted)
603
+ controller.abort();
604
+ }
306
605
  /**
307
606
  * 处理请求消息
308
607
  * @param msg - 消息数据
309
608
  */
310
609
  onRequest(msg) {
610
+ if (msg.streams?.input && !this.canAcceptInputStream(msg.id)) {
611
+ if (msg.twoway) {
612
+ this.post({
613
+ id: msg.id,
614
+ mode: MESSAGE_MODEM_TYPE.RESPONSE,
615
+ twoway: false,
616
+ data: {
617
+ status: 429,
618
+ data: null,
619
+ message: 'Request input stream capacity exceeded',
620
+ },
621
+ });
622
+ }
623
+ return;
624
+ }
311
625
  const controller = new AbortController();
312
626
  this.aborts.set(msg.id, controller);
313
- this.exec(msg.data, controller.signal)
627
+ const input = msg.streams?.input ? this.createInputConsumer(msg.id) : undefined;
628
+ Promise.resolve(this.exec(msg.data, controller.signal, input))
314
629
  .then(value => {
315
630
  if (controller.signal.aborted)
316
631
  return;
@@ -346,6 +661,7 @@ export class MessageModem {
346
661
  }
347
662
  })
348
663
  .finally(() => {
664
+ this.cancelInputConsumer(msg.id);
349
665
  this.aborts.delete(msg.id);
350
666
  });
351
667
  }
@@ -360,38 +676,30 @@ export class MessageModem {
360
676
  // 如果栈中存在该消息,则处理响应消息
361
677
  if (stack) {
362
678
  const { resolve, reject } = stack;
679
+ if (!res || (typeof res.status !== 'string' && typeof res.status !== 'number')) {
680
+ reject(new Exception(502, 'Invalid response frame'));
681
+ return;
682
+ }
363
683
  // 如果响应状态码不是 200,则拒绝响应
364
- if (res?.status !== 200) {
365
- reject(new Exception(res?.status, res?.message));
684
+ if (res.status !== 200) {
685
+ reject(new Exception(res.status, res.message ?? 'Request failed'));
366
686
  }
367
687
  else {
368
- resolve(res?.data);
688
+ resolve(res.data);
369
689
  }
370
690
  }
371
691
  }
372
692
  onStreamRequest(msg) {
373
- if (msg.streamVersion !== 1) {
374
- this.post({
375
- id: msg.id,
376
- mode: MESSAGE_MODEM_TYPE.RESPONSE,
377
- stream: true,
378
- streamVersion: 1,
379
- data: { status: 400, seq: 0, payload: 'Unsupported stream protocol', final: true },
380
- twoway: false,
381
- });
382
- return;
383
- }
384
693
  let window;
385
694
  try {
386
- window = streamWindow(msg.streamWindow);
695
+ window = streamWindow(msg.streams?.output?.window);
387
696
  }
388
697
  catch (error) {
389
698
  this.post({
390
699
  id: msg.id,
391
- mode: MESSAGE_MODEM_TYPE.RESPONSE,
392
- stream: true,
393
- streamVersion: 1,
700
+ mode: MESSAGE_MODEM_TYPE.STREAM_DATA,
394
701
  data: {
702
+ direction: 'output',
395
703
  status: 400,
396
704
  seq: 0,
397
705
  payload: error instanceof Error ? error.message : 'Invalid stream window',
@@ -401,13 +709,19 @@ export class MessageModem {
401
709
  });
402
710
  return;
403
711
  }
404
- if (this.streamProducers.has(msg.id) || this.streamProducers.size >= 128) {
712
+ if (this.streamProducers.has(msg.id)
713
+ || this.streamProducers.size >= MAX_CONCURRENT_STREAMS
714
+ || (msg.streams?.input && !this.canAcceptInputStream(msg.id))) {
405
715
  this.post({
406
716
  id: msg.id,
407
- mode: MESSAGE_MODEM_TYPE.RESPONSE,
408
- stream: true,
409
- streamVersion: 1,
410
- data: { status: 429, seq: 0, payload: 'Stream capacity exceeded', final: true },
717
+ mode: MESSAGE_MODEM_TYPE.STREAM_DATA,
718
+ data: {
719
+ direction: 'output',
720
+ status: 429,
721
+ seq: 0,
722
+ payload: 'Stream capacity exceeded',
723
+ final: true,
724
+ },
411
725
  twoway: false,
412
726
  });
413
727
  return;
@@ -417,59 +731,63 @@ export class MessageModem {
417
731
  credits: window,
418
732
  maxCredits: window,
419
733
  nextCreditSeq: 0,
734
+ nextSeq: 0,
735
+ cancelled: false,
736
+ iteratorReturned: false,
420
737
  };
421
- let sequence = 0;
422
738
  this.aborts.set(msg.id, controller);
423
739
  this.streamProducers.set(msg.id, producer);
740
+ const input = msg.streams?.input ? this.createInputConsumer(msg.id) : undefined;
424
741
  const takeCredit = async () => {
425
- while (producer.credits === 0 && !controller.signal.aborted) {
742
+ while (producer.credits === 0 && !controller.signal.aborted && !producer.cancelled) {
426
743
  await new Promise((resolve) => {
427
744
  producer.wake = resolve;
428
745
  });
429
746
  producer.wake = undefined;
430
747
  }
431
- if (controller.signal.aborted)
748
+ if (controller.signal.aborted || producer.cancelled)
432
749
  throw new AbortException();
433
750
  producer.credits--;
434
751
  };
435
- this.exec(msg.data, controller.signal)
752
+ Promise.resolve(this.exec(msg.data, controller.signal, input))
436
753
  .then(async (value) => {
437
754
  if (!isAsyncIterable(value)) {
438
755
  throw new Exception(500, 'Invalid async iterable');
439
756
  }
440
757
  const iterator = value[Symbol.asyncIterator]();
441
758
  producer.iterator = iterator;
442
- while (!controller.signal.aborted) {
759
+ while (!controller.signal.aborted && !producer.cancelled) {
443
760
  await takeCredit();
444
761
  const next = await iterator.next();
445
- if (controller.signal.aborted)
762
+ if (controller.signal.aborted || producer.cancelled)
446
763
  return;
447
764
  if (next.done)
448
765
  break;
766
+ if (next.value == null) {
767
+ throw new Exception(500, 'Stream chunk must not be null or undefined');
768
+ }
449
769
  this.post({
450
770
  id: msg.id,
451
- mode: MESSAGE_MODEM_TYPE.RESPONSE,
452
- stream: true,
453
- streamVersion: msg.streamVersion,
771
+ mode: MESSAGE_MODEM_TYPE.STREAM_DATA,
454
772
  data: {
773
+ direction: 'output',
455
774
  status: 200,
456
- seq: sequence++,
775
+ seq: producer.nextSeq++,
457
776
  payload: next.value,
458
777
  final: false,
459
778
  },
460
779
  twoway: false,
461
780
  });
462
781
  }
463
- if (controller.signal.aborted)
782
+ if (controller.signal.aborted || producer.cancelled)
464
783
  return;
465
784
  this.post({
466
785
  id: msg.id,
467
- mode: MESSAGE_MODEM_TYPE.RESPONSE,
468
- stream: true,
469
- streamVersion: msg.streamVersion,
786
+ mode: MESSAGE_MODEM_TYPE.STREAM_DATA,
470
787
  data: {
788
+ direction: 'output',
471
789
  status: 200,
472
- seq: sequence++,
790
+ seq: producer.nextSeq++,
473
791
  payload: undefined,
474
792
  final: true,
475
793
  },
@@ -477,16 +795,15 @@ export class MessageModem {
477
795
  });
478
796
  })
479
797
  .catch(e => {
480
- if (controller.signal.aborted)
798
+ if (controller.signal.aborted || producer.cancelled)
481
799
  return;
482
800
  this.post({
483
801
  id: msg.id,
484
- mode: MESSAGE_MODEM_TYPE.RESPONSE,
485
- stream: true,
486
- streamVersion: msg.streamVersion,
802
+ mode: MESSAGE_MODEM_TYPE.STREAM_DATA,
487
803
  data: {
804
+ direction: 'output',
488
805
  status: e instanceof Exception ? e.status : 500,
489
- seq: sequence,
806
+ seq: producer.nextSeq,
490
807
  payload: e instanceof Error ? e.message : 'Unknown error',
491
808
  final: true,
492
809
  },
@@ -494,10 +811,11 @@ export class MessageModem {
494
811
  });
495
812
  })
496
813
  .finally(() => {
497
- if (controller.signal.aborted && producer.iterator?.return) {
498
- void Promise.resolve(producer.iterator.return()).catch(() => { });
814
+ if ((controller.signal.aborted || producer.cancelled) && producer.iterator?.return) {
815
+ returnProducerIterator(producer);
499
816
  }
500
817
  producer.wake?.();
818
+ this.cancelInputConsumer(msg.id);
501
819
  this.streamProducers.delete(msg.id);
502
820
  this.aborts.delete(msg.id);
503
821
  });
@@ -506,25 +824,29 @@ export class MessageModem {
506
824
  const id = msg.id;
507
825
  const res = msg.data;
508
826
  const consumer = this.streams.get(id);
509
- // 如果栈中存在该消息,则处理响应消息
510
- if (consumer) {
827
+ if (consumer && !consumer.completed && !consumer.cancelled) {
511
828
  const stream = consumer.stream;
512
- if (res) {
513
- consumer.touch();
514
- if (!Number.isSafeInteger(res.seq) || res.seq !== consumer.nextSeq) {
515
- stream.destroy(new Exception(409, `Invalid stream sequence: expected ${consumer.nextSeq}, received ${String(res.seq)}`));
829
+ if (res?.direction === 'output') {
830
+ if (!Number.isSafeInteger(res.seq)
831
+ || res.seq !== consumer.nextSeq
832
+ || typeof res.final !== 'boolean'
833
+ || (typeof res.status !== 'string' && typeof res.status !== 'number')
834
+ || (!res.final && res.payload == null)) {
835
+ this.failOutputConsumer(id, new Exception(409, `Invalid response stream frame: expected sequence ${consumer.nextSeq}, received ${String(res.seq)}`));
516
836
  return;
517
837
  }
838
+ consumer.touch();
518
839
  consumer.nextSeq++;
519
840
  if (res.status === 200) {
520
841
  if (res.final) {
521
842
  consumer.completed = true;
522
- consumer.clearTimers();
843
+ this.cancelInputProducer(id);
844
+ consumer.cleanup();
523
845
  stream.push(null);
524
846
  }
525
847
  else {
526
848
  if (consumer.creditsOwed >= consumer.maxCredits) {
527
- stream.destroy(new Exception(429, 'Stream window exceeded'));
849
+ this.failOutputConsumer(id, new Exception(429, 'Response stream window exceeded'));
528
850
  return;
529
851
  }
530
852
  consumer.creditsOwed++;
@@ -533,18 +855,165 @@ export class MessageModem {
533
855
  }
534
856
  else {
535
857
  consumer.completed = true;
536
- consumer.clearTimers();
537
- const err = new Exception(res.status, res.payload);
858
+ this.cancelInputProducer(id);
859
+ consumer.cleanup();
860
+ const err = new Exception(res.status ?? 500, res.message ?? String(res.payload ?? 'Stream failed'));
538
861
  setImmediate(() => stream.destroy(err));
539
862
  }
540
863
  }
541
864
  else {
542
- consumer.completed = true;
543
- consumer.clearTimers();
544
- const err = new Exception(404, 'Empty chunk data');
545
- setImmediate(() => stream.destroy(err));
865
+ this.failOutputConsumer(id, res
866
+ ? new Exception(400, 'Invalid response stream direction')
867
+ : new Exception(404, 'Empty chunk data'));
868
+ }
869
+ }
870
+ }
871
+ failOutputConsumer(id, error) {
872
+ const consumer = this.streams.get(id);
873
+ if (!consumer || consumer.completed || consumer.cancelled)
874
+ return;
875
+ try {
876
+ this.post({
877
+ id,
878
+ mode: MESSAGE_MODEM_TYPE.ABORT,
879
+ twoway: false,
880
+ });
881
+ }
882
+ catch {
883
+ // The transport may already be closed.
884
+ }
885
+ consumer.cancelled = true;
886
+ this.cancelInputProducer(id);
887
+ consumer.cleanup();
888
+ consumer.stream.destroy(error);
889
+ }
890
+ onInputStreamData(msg) {
891
+ const consumer = this.inputStreams.get(msg.id);
892
+ if (!consumer)
893
+ return;
894
+ const chunk = msg.data;
895
+ if (!chunk
896
+ || chunk.direction !== 'input'
897
+ || !Number.isSafeInteger(chunk.seq)
898
+ || chunk.seq !== consumer.nextSeq
899
+ || typeof chunk.final !== 'boolean'
900
+ || (!chunk.final && chunk.payload == null)) {
901
+ const received = chunk?.seq;
902
+ this.failInputConsumer(msg.id, new Exception(409, `Invalid request input sequence: expected ${consumer.nextSeq}, received ${String(received)}`));
903
+ return;
904
+ }
905
+ consumer.nextSeq++;
906
+ if (chunk.final) {
907
+ consumer.completed = true;
908
+ this.inputStreams.delete(msg.id);
909
+ consumer.stream.push(null);
910
+ return;
911
+ }
912
+ if (consumer.creditsOwed >= consumer.maxCredits) {
913
+ this.failInputConsumer(msg.id, new Exception(429, 'Request input stream window exceeded'));
914
+ return;
915
+ }
916
+ consumer.creditsOwed++;
917
+ consumer.stream.push(chunk.payload);
918
+ }
919
+ onStreamCredit(msg) {
920
+ const credit = msg.data;
921
+ if (!credit
922
+ || (credit.direction !== 'input' && credit.direction !== 'output'))
923
+ return;
924
+ const producer = credit.direction === 'input'
925
+ ? this.inputStreamProducers.get(msg.id)
926
+ : this.streamProducers.get(msg.id);
927
+ if (!producer || producer.cancelled)
928
+ return;
929
+ if (!Number.isSafeInteger(credit.seq) || credit.seq < 0) {
930
+ if (credit.direction === 'input') {
931
+ this.failInputProducer(msg.id, new Exception(400, 'Invalid request input credit sequence'));
932
+ }
933
+ else {
934
+ this.failOutputProducer(msg.id, new Exception(400, 'Invalid response stream credit sequence'));
935
+ }
936
+ return;
937
+ }
938
+ if (credit.seq < producer.nextCreditSeq)
939
+ return;
940
+ if (credit.seq > producer.nextCreditSeq) {
941
+ if (credit.direction === 'input') {
942
+ this.failInputProducer(msg.id, new Exception(409, 'Request input credit sequence skipped'));
943
+ }
944
+ else {
945
+ this.failOutputProducer(msg.id, new Exception(409, 'Response stream credit sequence skipped'));
946
+ }
947
+ return;
948
+ }
949
+ if (producer.maxCredits === undefined) {
950
+ if (credit.direction !== 'input')
951
+ return;
952
+ try {
953
+ producer.maxCredits = streamWindow(credit.window);
954
+ }
955
+ catch {
956
+ this.failInputProducer(msg.id, new Exception(400, 'Invalid request input stream window'));
957
+ return;
958
+ }
959
+ }
960
+ else if (credit.window !== undefined && credit.window !== producer.maxCredits) {
961
+ if (credit.direction === 'input') {
962
+ this.failInputProducer(msg.id, new Exception(409, 'Request input stream window changed'));
963
+ }
964
+ else {
965
+ this.failOutputProducer(msg.id, new Exception(409, 'Response stream window changed'));
546
966
  }
967
+ return;
547
968
  }
969
+ if (producer.credits >= producer.maxCredits) {
970
+ if (credit.direction === 'input') {
971
+ this.failInputProducer(msg.id, new Exception(429, 'Request input credit window exceeded'));
972
+ }
973
+ else {
974
+ this.failOutputProducer(msg.id, new Exception(429, 'Response stream credit window exceeded'));
975
+ }
976
+ return;
977
+ }
978
+ producer.credits++;
979
+ producer.nextCreditSeq++;
980
+ producer.wake?.();
981
+ }
982
+ failOutputProducer(id, error) {
983
+ const producer = this.streamProducers.get(id);
984
+ if (!producer)
985
+ return;
986
+ try {
987
+ this.post({
988
+ id,
989
+ mode: MESSAGE_MODEM_TYPE.STREAM_DATA,
990
+ twoway: false,
991
+ data: {
992
+ direction: 'output',
993
+ status: error.status,
994
+ seq: producer.nextSeq,
995
+ final: true,
996
+ message: error.message,
997
+ },
998
+ });
999
+ }
1000
+ catch {
1001
+ // The transport may already be closed.
1002
+ }
1003
+ this.cancelOutputProducer(id);
1004
+ const controller = this.aborts.get(id);
1005
+ this.aborts.delete(id);
1006
+ if (controller && !controller.signal.aborted)
1007
+ controller.abort();
1008
+ }
1009
+ cancelOutputProducer(id) {
1010
+ const producer = this.streamProducers.get(id);
1011
+ if (!producer)
1012
+ return;
1013
+ this.streamProducers.delete(id);
1014
+ producer.cancelled = true;
1015
+ producer.wake?.();
1016
+ returnProducerIterator(producer);
548
1017
  }
549
1018
  /**
550
1019
  * 接收消息
@@ -555,7 +1024,7 @@ export class MessageModem {
555
1024
  switch (msg.mode) {
556
1025
  // 处理请求消息
557
1026
  case MESSAGE_MODEM_TYPE.REQUEST:
558
- if (msg.stream) {
1027
+ if (msg.streams?.output) {
559
1028
  this.onStreamRequest(msg);
560
1029
  }
561
1030
  else {
@@ -564,41 +1033,56 @@ export class MessageModem {
564
1033
  break;
565
1034
  // 处理响应消息
566
1035
  case MESSAGE_MODEM_TYPE.RESPONSE:
567
- if (msg.stream) {
1036
+ this.onResponse(msg);
1037
+ break;
1038
+ case MESSAGE_MODEM_TYPE.STREAM_DATA: {
1039
+ const chunk = msg.data;
1040
+ if (chunk?.direction === 'input')
1041
+ this.onInputStreamData(msg);
1042
+ else if (chunk?.direction === 'output')
568
1043
  this.onStreamResponse(msg);
569
- }
570
- else {
571
- this.onResponse(msg);
572
- }
1044
+ else if (this.streams.has(msg.id))
1045
+ this.onStreamResponse(msg);
1046
+ else if (this.inputStreams.has(msg.id))
1047
+ this.onInputStreamData(msg);
573
1048
  break;
1049
+ }
574
1050
  // 处理终止消息
575
1051
  case MESSAGE_MODEM_TYPE.ABORT: {
576
- const id = msg.data;
1052
+ const id = msg.id;
577
1053
  const controller = this.aborts.get(id);
578
1054
  if (controller) {
579
1055
  this.aborts.delete(id);
580
- this.streamProducers.get(id)?.wake?.();
581
1056
  if (!controller.signal.aborted) {
582
1057
  controller.abort();
583
1058
  }
584
1059
  }
1060
+ this.cancelOutputProducer(id);
1061
+ this.cancelInputConsumer(id, 'Request aborted');
585
1062
  break;
586
1063
  }
587
- case MESSAGE_MODEM_TYPE.STREAM_CREDIT: {
588
- const credit = msg.data;
589
- if (!credit
590
- || !Number.isSafeInteger(credit.id)
591
- || credit.id < 0
592
- || !Number.isSafeInteger(credit.seq)
593
- || credit.seq < 0)
594
- break;
595
- const producer = this.streamProducers.get(credit.id);
596
- if (producer
597
- && producer.credits < producer.maxCredits
598
- && credit.seq === producer.nextCreditSeq) {
599
- producer.credits++;
600
- producer.nextCreditSeq++;
601
- producer.wake?.();
1064
+ case MESSAGE_MODEM_TYPE.STREAM_CREDIT:
1065
+ this.onStreamCredit(msg);
1066
+ break;
1067
+ case MESSAGE_MODEM_TYPE.STREAM_CANCEL: {
1068
+ const cancel = msg.data;
1069
+ if (cancel?.direction === 'input') {
1070
+ this.cancelInputProducer(msg.id);
1071
+ if (cancel.status !== undefined) {
1072
+ const error = new Exception(cancel.status, cancel.message ?? 'Request input stream rejected');
1073
+ const stack = this.stacks.get(msg.id);
1074
+ if (stack)
1075
+ stack.reject(error);
1076
+ else
1077
+ this.failOutputConsumer(msg.id, error);
1078
+ }
1079
+ }
1080
+ else if (cancel?.direction === 'output') {
1081
+ this.cancelOutputProducer(msg.id);
1082
+ const controller = this.aborts.get(msg.id);
1083
+ this.aborts.delete(msg.id);
1084
+ if (controller && !controller.signal.aborted)
1085
+ controller.abort();
602
1086
  }
603
1087
  break;
604
1088
  }