@hile/message-modem 4.0.0 → 4.0.1

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.d.ts CHANGED
@@ -12,8 +12,18 @@ export interface MessageTransferFormat<T = any> {
12
12
  twoway: boolean;
13
13
  stream?: boolean;
14
14
  streamVersion?: 1;
15
+ streamWindow?: number;
15
16
  data?: T;
16
17
  }
18
+ export interface MessageStreamOptions {
19
+ signal?: AbortSignal;
20
+ /** Maximum total stream lifetime in milliseconds. */
21
+ timeout?: number;
22
+ /** Maximum time between valid stream responses in milliseconds. */
23
+ idleTimeout?: number;
24
+ /** Maximum number of produced but not yet consumed chunks. */
25
+ window?: number;
26
+ }
17
27
  export interface MessageReturnFormat<T = any> {
18
28
  status: string | number;
19
29
  data: T;
@@ -78,9 +88,7 @@ export declare abstract class MessageModem {
78
88
  timeout?: number;
79
89
  signal?: AbortSignal;
80
90
  }): void;
81
- protected _stream(data: any, options?: {
82
- signal?: AbortSignal;
83
- }): Readable;
91
+ protected _stream(data: any, options?: MessageStreamOptions): Readable;
84
92
  /**
85
93
  * 写入消息
86
94
  * @param data - 消息数据
package/dist/index.js CHANGED
@@ -8,6 +8,30 @@ export var MESSAGE_MODEM_TYPE;
8
8
  MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["ABORT"] = 2] = "ABORT";
9
9
  MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["STREAM_CREDIT"] = 3] = "STREAM_CREDIT";
10
10
  })(MESSAGE_MODEM_TYPE || (MESSAGE_MODEM_TYPE = {}));
11
+ const MAX_STREAM_WINDOW = 64;
12
+ const MAX_TIMER_DELAY = 2_147_483_647;
13
+ function streamLimit(value, name) {
14
+ if (value === undefined)
15
+ return undefined;
16
+ if (!Number.isSafeInteger(value) || value < 1) {
17
+ throw new TypeError(`${name} must be a positive safe integer`);
18
+ }
19
+ return value;
20
+ }
21
+ function streamWindow(value) {
22
+ const normalized = streamLimit(value, 'Stream window') ?? 1;
23
+ if (normalized > MAX_STREAM_WINDOW) {
24
+ throw new TypeError(`Stream window must not exceed ${MAX_STREAM_WINDOW}`);
25
+ }
26
+ return normalized;
27
+ }
28
+ function streamTimeout(value, name) {
29
+ const normalized = streamLimit(value, name);
30
+ if (normalized !== undefined && normalized > MAX_TIMER_DELAY) {
31
+ throw new TypeError(`${name} must not exceed ${MAX_TIMER_DELAY}`);
32
+ }
33
+ return normalized;
34
+ }
11
35
  class CreditReadable extends Readable {
12
36
  onConsumed;
13
37
  constructor(onConsumed) {
@@ -105,28 +129,61 @@ export class MessageModem {
105
129
  signal: options?.signal,
106
130
  });
107
131
  }
108
- _stream(data, options) {
132
+ _stream(data, options = {}) {
133
+ const window = streamWindow(options.window);
134
+ const timeout = streamTimeout(options.timeout, 'Stream timeout');
135
+ const idleTimeout = streamTimeout(options.idleTimeout, 'Stream idle timeout');
109
136
  const state = this.createPostData(MESSAGE_MODEM_TYPE.REQUEST, data, true, true);
110
137
  state.streamVersion = 1;
138
+ if (window > 1)
139
+ state.streamWindow = window;
111
140
  let consumer;
112
141
  const stream = new CreditReadable(() => {
113
- if (!consumer.creditOwed || consumer.completed || consumer.cancelled)
142
+ if (consumer.creditsOwed === 0 || consumer.completed || consumer.cancelled)
114
143
  return;
115
- consumer.creditOwed = false;
144
+ consumer.creditsOwed--;
116
145
  try {
117
- this.post(this.createPostData(MESSAGE_MODEM_TYPE.STREAM_CREDIT, { id: state.id, seq: consumer.nextSeq - 1 }, false));
146
+ this.post(this.createPostData(MESSAGE_MODEM_TYPE.STREAM_CREDIT, { id: state.id, seq: consumer.nextCreditSeq++ }, false));
118
147
  }
119
148
  catch (error) {
120
149
  consumer.cancelled = true;
121
150
  stream.destroy(error);
122
151
  }
123
152
  });
153
+ let totalTimer;
154
+ let idleTimer;
155
+ const clearTimers = () => {
156
+ if (totalTimer)
157
+ clearTimeout(totalTimer);
158
+ if (idleTimer)
159
+ clearTimeout(idleTimer);
160
+ totalTimer = undefined;
161
+ idleTimer = undefined;
162
+ };
163
+ const expire = (message) => {
164
+ if (consumer.completed || consumer.cancelled)
165
+ return;
166
+ sendAbort();
167
+ stream.destroy(new TimeoutException(message));
168
+ };
169
+ const touch = () => {
170
+ if (!idleTimeout || consumer.completed || consumer.cancelled)
171
+ return;
172
+ if (idleTimer)
173
+ clearTimeout(idleTimer);
174
+ idleTimer = setTimeout(() => expire('Stream idle timeout'), idleTimeout);
175
+ idleTimer.unref?.();
176
+ };
124
177
  consumer = {
125
178
  stream,
126
179
  completed: false,
127
180
  cancelled: false,
128
- creditOwed: false,
181
+ creditsOwed: 0,
182
+ maxCredits: window,
129
183
  nextSeq: 0,
184
+ nextCreditSeq: 0,
185
+ touch,
186
+ clearTimers,
130
187
  };
131
188
  const sendAbort = () => {
132
189
  if (consumer.completed || consumer.cancelled)
@@ -150,8 +207,14 @@ export class MessageModem {
150
207
  }
151
208
  this.streams.set(state.id, consumer);
152
209
  options?.signal?.addEventListener('abort', onAbort, { once: true });
210
+ if (timeout) {
211
+ totalTimer = setTimeout(() => expire('Stream timeout'), timeout);
212
+ totalTimer.unref?.();
213
+ }
214
+ touch();
153
215
  stream.on('close', () => {
154
216
  sendAbort();
217
+ clearTimers();
155
218
  if (this.streams.has(state.id)) {
156
219
  this.streams.delete(state.id);
157
220
  }
@@ -329,6 +392,26 @@ export class MessageModem {
329
392
  });
330
393
  return;
331
394
  }
395
+ let window;
396
+ try {
397
+ window = streamWindow(msg.streamWindow);
398
+ }
399
+ catch (error) {
400
+ this.post({
401
+ id: msg.id,
402
+ mode: MESSAGE_MODEM_TYPE.RESPONSE,
403
+ stream: true,
404
+ streamVersion: 1,
405
+ data: {
406
+ status: 400,
407
+ seq: 0,
408
+ payload: error instanceof Error ? error.message : 'Invalid stream window',
409
+ final: true,
410
+ },
411
+ twoway: false,
412
+ });
413
+ return;
414
+ }
332
415
  if (this.streamProducers.has(msg.id) || this.streamProducers.size >= 128) {
333
416
  this.post({
334
417
  id: msg.id,
@@ -342,7 +425,8 @@ export class MessageModem {
342
425
  }
343
426
  const controller = new AbortController();
344
427
  const producer = {
345
- credits: 1,
428
+ credits: window,
429
+ maxCredits: window,
346
430
  nextCreditSeq: 0,
347
431
  };
348
432
  let sequence = 0;
@@ -445,6 +529,7 @@ export class MessageModem {
445
529
  const consumer = this.streams.get(id);
446
530
  const stream = consumer.stream;
447
531
  if (res) {
532
+ consumer.touch();
448
533
  if (!Number.isSafeInteger(res.seq) || res.seq !== consumer.nextSeq) {
449
534
  stream.destroy(new Exception(409, `Invalid stream sequence: expected ${consumer.nextSeq}, received ${String(res.seq)}`));
450
535
  return;
@@ -453,21 +538,28 @@ export class MessageModem {
453
538
  if (res.status === 200) {
454
539
  if (res.final) {
455
540
  consumer.completed = true;
541
+ consumer.clearTimers();
456
542
  stream.push(null);
457
543
  }
458
544
  else {
459
- consumer.creditOwed = true;
545
+ if (consumer.creditsOwed >= consumer.maxCredits) {
546
+ stream.destroy(new Exception(429, 'Stream window exceeded'));
547
+ return;
548
+ }
549
+ consumer.creditsOwed++;
460
550
  stream.push(res.payload);
461
551
  }
462
552
  }
463
553
  else {
464
554
  consumer.completed = true;
555
+ consumer.clearTimers();
465
556
  const err = new Exception(res.status, res.payload);
466
557
  setImmediate(() => stream.destroy(err));
467
558
  }
468
559
  }
469
560
  else {
470
561
  consumer.completed = true;
562
+ consumer.clearTimers();
471
563
  const err = new Exception(404, 'Empty chunk data');
472
564
  setImmediate(() => stream.destroy(err));
473
565
  }
@@ -517,8 +609,10 @@ export class MessageModem {
517
609
  || credit.seq < 0)
518
610
  break;
519
611
  const producer = this.streamProducers.get(credit.id);
520
- if (producer && producer.credits === 0 && credit.seq === producer.nextCreditSeq) {
521
- producer.credits = 1;
612
+ if (producer
613
+ && producer.credits < producer.maxCredits
614
+ && credit.seq === producer.nextCreditSeq) {
615
+ producer.credits++;
522
616
  producer.nextCreditSeq++;
523
617
  producer.wake?.();
524
618
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hile/message-modem",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -22,5 +22,5 @@
22
22
  "fix-esm-import-path": "^1.10.3",
23
23
  "vitest": "^4.0.18"
24
24
  },
25
- "gitHead": "b46cb7f3705a226f58e4d65a2ff985ea54b9a159"
25
+ "gitHead": "fe98c6f8cca2860ccbb213c575cfda44588cbd06"
26
26
  }