@hile/message-modem 3.0.0 → 4.0.0

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
@@ -3,13 +3,15 @@ export * from './exception.js';
3
3
  export declare enum MESSAGE_MODEM_TYPE {
4
4
  REQUEST = 0,
5
5
  RESPONSE = 1,
6
- ABORT = 2
6
+ ABORT = 2,
7
+ STREAM_CREDIT = 3
7
8
  }
8
9
  export interface MessageTransferFormat<T = any> {
9
10
  id: number;
10
11
  mode: MESSAGE_MODEM_TYPE;
11
12
  twoway: boolean;
12
13
  stream?: boolean;
14
+ streamVersion?: 1;
13
15
  data?: T;
14
16
  }
15
17
  export interface MessageReturnFormat<T = any> {
@@ -28,6 +30,7 @@ export declare abstract class MessageModem {
28
30
  private readonly aborts;
29
31
  private readonly stacks;
30
32
  private readonly streams;
33
+ private readonly streamProducers;
31
34
  protected _dispose(): void;
32
35
  /**
33
36
  * 创建自增 ID
package/dist/index.js CHANGED
@@ -6,12 +6,30 @@ export var MESSAGE_MODEM_TYPE;
6
6
  MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["REQUEST"] = 0] = "REQUEST";
7
7
  MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["RESPONSE"] = 1] = "RESPONSE";
8
8
  MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["ABORT"] = 2] = "ABORT";
9
+ MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["STREAM_CREDIT"] = 3] = "STREAM_CREDIT";
9
10
  })(MESSAGE_MODEM_TYPE || (MESSAGE_MODEM_TYPE = {}));
11
+ class CreditReadable extends Readable {
12
+ onConsumed;
13
+ constructor(onConsumed) {
14
+ super({ objectMode: true });
15
+ this.onConsumed = onConsumed;
16
+ }
17
+ _read() {
18
+ // Credits are tied to actual read() results, not Node's eager buffer filling.
19
+ }
20
+ read(size) {
21
+ const chunk = super.read(size);
22
+ if (chunk !== null)
23
+ this.onConsumed();
24
+ return chunk;
25
+ }
26
+ }
10
27
  export class MessageModem {
11
28
  id = 0;
12
29
  aborts = new Map();
13
30
  stacks = new Map();
14
31
  streams = new Map();
32
+ streamProducers = new Map();
15
33
  _dispose() {
16
34
  for (const { reject } of this.stacks.values()) {
17
35
  reject(new AbortException());
@@ -19,12 +37,16 @@ export class MessageModem {
19
37
  for (const controller of this.aborts.values()) {
20
38
  controller.abort();
21
39
  }
22
- for (const stream of this.streams.values()) {
40
+ for (const { stream } of this.streams.values()) {
23
41
  stream.destroy(new AbortException());
24
42
  }
43
+ for (const producer of this.streamProducers.values()) {
44
+ producer.wake?.();
45
+ }
25
46
  this.aborts.clear();
26
47
  this.stacks.clear();
27
48
  this.streams.clear();
49
+ this.streamProducers.clear();
28
50
  }
29
51
  /**
30
52
  * 创建自增 ID
@@ -85,23 +107,64 @@ export class MessageModem {
85
107
  }
86
108
  _stream(data, options) {
87
109
  const state = this.createPostData(MESSAGE_MODEM_TYPE.REQUEST, data, true, true);
88
- const stream = new Readable({ objectMode: true, read() { } });
89
- this.streams.set(state.id, stream);
90
- this.post(state);
110
+ state.streamVersion = 1;
111
+ let consumer;
112
+ const stream = new CreditReadable(() => {
113
+ if (!consumer.creditOwed || consumer.completed || consumer.cancelled)
114
+ return;
115
+ consumer.creditOwed = false;
116
+ try {
117
+ this.post(this.createPostData(MESSAGE_MODEM_TYPE.STREAM_CREDIT, { id: state.id, seq: consumer.nextSeq - 1 }, false));
118
+ }
119
+ catch (error) {
120
+ consumer.cancelled = true;
121
+ stream.destroy(error);
122
+ }
123
+ });
124
+ consumer = {
125
+ stream,
126
+ completed: false,
127
+ cancelled: false,
128
+ creditOwed: false,
129
+ nextSeq: 0,
130
+ };
131
+ const sendAbort = () => {
132
+ if (consumer.completed || consumer.cancelled)
133
+ return;
134
+ consumer.cancelled = true;
135
+ try {
136
+ this.post(this.createPostData(MESSAGE_MODEM_TYPE.ABORT, state.id));
137
+ }
138
+ catch {
139
+ // The transport may already be closed.
140
+ }
141
+ };
91
142
  const onAbort = () => {
92
- this.post(this.createPostData(MESSAGE_MODEM_TYPE.ABORT, state.id));
143
+ sendAbort();
93
144
  stream.destroy(new AbortException());
94
- this.streams.delete(state.id);
95
145
  };
96
- if (options?.signal) {
97
- options.signal.addEventListener('abort', onAbort);
146
+ if (options?.signal?.aborted) {
147
+ consumer.cancelled = true;
148
+ queueMicrotask(() => stream.destroy(new AbortException()));
149
+ return stream;
98
150
  }
151
+ this.streams.set(state.id, consumer);
152
+ options?.signal?.addEventListener('abort', onAbort, { once: true });
99
153
  stream.on('close', () => {
154
+ sendAbort();
100
155
  if (this.streams.has(state.id)) {
101
156
  this.streams.delete(state.id);
102
157
  }
103
158
  options?.signal?.removeEventListener('abort', onAbort);
104
159
  });
160
+ try {
161
+ this.post(state);
162
+ }
163
+ catch (error) {
164
+ consumer.cancelled = true;
165
+ this.streams.delete(state.id);
166
+ queueMicrotask(() => stream.destroy(error));
167
+ }
105
168
  return stream;
106
169
  }
107
170
  /**
@@ -116,26 +179,32 @@ export class MessageModem {
116
179
  const signal = options?.signal;
117
180
  // 创建请求消息数据
118
181
  const state = this.createPostData(MESSAGE_MODEM_TYPE.REQUEST, data, twoway);
119
- // 发送消息
120
- this.post(state);
121
182
  // 如果消息是单向的,则直接返回
122
- if (!twoway)
183
+ if (!twoway) {
184
+ if (!signal?.aborted)
185
+ this.post(state);
123
186
  return;
187
+ }
124
188
  return new Promise((resolve, reject) => {
189
+ let timer;
190
+ let posted = false;
125
191
  const clear = () => {
126
192
  if (this.stacks.has(state.id)) {
127
193
  this.stacks.delete(state.id);
128
194
  }
129
195
  };
130
196
  const clean = () => {
131
- clearTimeout(timer);
197
+ if (timer)
198
+ clearTimeout(timer);
132
199
  signal?.removeEventListener('abort', onAbort);
133
200
  clear();
134
201
  };
135
202
  const onAbort = () => {
136
- clearTimeout(timer);
203
+ if (timer)
204
+ clearTimeout(timer);
137
205
  try {
138
- this.post(this.createPostData(MESSAGE_MODEM_TYPE.ABORT, state.id));
206
+ if (posted)
207
+ this.post(this.createPostData(MESSAGE_MODEM_TYPE.ABORT, state.id));
139
208
  }
140
209
  catch {
141
210
  /* 例如 WebSocket 已关闭时 send 可能抛错 */
@@ -156,12 +225,23 @@ export class MessageModem {
156
225
  clean();
157
226
  reject(e);
158
227
  };
159
- const timer = setTimeout(() => _reject(new TimeoutException()), timeout).unref();
160
- signal?.addEventListener('abort', onAbort);
161
228
  this.stacks.set(state.id, {
162
229
  resolve: _resolve,
163
230
  reject: _reject,
164
231
  });
232
+ timer = setTimeout(() => _reject(new TimeoutException()), timeout).unref();
233
+ signal?.addEventListener('abort', onAbort, { once: true });
234
+ if (signal?.aborted) {
235
+ onAbort();
236
+ return;
237
+ }
238
+ try {
239
+ posted = true;
240
+ this.post(state);
241
+ }
242
+ catch (error) {
243
+ _reject(error);
244
+ }
165
245
  });
166
246
  }
167
247
  /**
@@ -238,30 +318,76 @@ export class MessageModem {
238
318
  }
239
319
  }
240
320
  onStreamRequest(msg) {
321
+ if (msg.streamVersion !== 1) {
322
+ this.post({
323
+ id: msg.id,
324
+ mode: MESSAGE_MODEM_TYPE.RESPONSE,
325
+ stream: true,
326
+ streamVersion: 1,
327
+ data: { status: 400, seq: 0, payload: 'Unsupported stream protocol', final: true },
328
+ twoway: false,
329
+ });
330
+ return;
331
+ }
332
+ if (this.streamProducers.has(msg.id) || this.streamProducers.size >= 128) {
333
+ this.post({
334
+ id: msg.id,
335
+ mode: MESSAGE_MODEM_TYPE.RESPONSE,
336
+ stream: true,
337
+ streamVersion: 1,
338
+ data: { status: 429, seq: 0, payload: 'Stream capacity exceeded', final: true },
339
+ twoway: false,
340
+ });
341
+ return;
342
+ }
241
343
  const controller = new AbortController();
344
+ const producer = {
345
+ credits: 1,
346
+ nextCreditSeq: 0,
347
+ };
348
+ let sequence = 0;
242
349
  this.aborts.set(msg.id, controller);
350
+ this.streamProducers.set(msg.id, producer);
243
351
  controller.signal.addEventListener('abort', () => {
352
+ producer.wake?.();
244
353
  if (this.aborts.has(msg.id)) {
245
354
  this.aborts.delete(msg.id);
246
355
  }
247
356
  });
357
+ const takeCredit = async () => {
358
+ while (producer.credits === 0 && !controller.signal.aborted) {
359
+ await new Promise((resolve) => {
360
+ producer.wake = resolve;
361
+ });
362
+ producer.wake = undefined;
363
+ }
364
+ if (controller.signal.aborted)
365
+ throw new AbortException();
366
+ producer.credits--;
367
+ };
248
368
  this.exec(msg.data, controller.signal)
249
369
  .then(async (value) => {
250
370
  if (!isAsyncIterable(value)) {
251
371
  throw new Exception(500, 'Invalid async iterable');
252
372
  }
253
- let i = 0;
254
- for await (const chunk of value) {
373
+ const iterator = value[Symbol.asyncIterator]();
374
+ producer.iterator = iterator;
375
+ while (!controller.signal.aborted) {
376
+ await takeCredit();
377
+ const next = await iterator.next();
255
378
  if (controller.signal.aborted)
256
379
  return;
380
+ if (next.done)
381
+ break;
257
382
  this.post({
258
383
  id: msg.id,
259
384
  mode: MESSAGE_MODEM_TYPE.RESPONSE,
260
385
  stream: true,
386
+ streamVersion: msg.streamVersion,
261
387
  data: {
262
388
  status: 200,
263
- seq: i++,
264
- payload: chunk,
389
+ seq: sequence++,
390
+ payload: next.value,
265
391
  final: false,
266
392
  },
267
393
  twoway: false,
@@ -273,9 +399,10 @@ export class MessageModem {
273
399
  id: msg.id,
274
400
  mode: MESSAGE_MODEM_TYPE.RESPONSE,
275
401
  stream: true,
402
+ streamVersion: msg.streamVersion,
276
403
  data: {
277
404
  status: 200,
278
- seq: i++,
405
+ seq: sequence++,
279
406
  payload: undefined,
280
407
  final: true,
281
408
  },
@@ -289,16 +416,22 @@ export class MessageModem {
289
416
  id: msg.id,
290
417
  mode: MESSAGE_MODEM_TYPE.RESPONSE,
291
418
  stream: true,
419
+ streamVersion: msg.streamVersion,
292
420
  data: {
293
421
  status: e instanceof Exception ? e.status : 500,
294
- seq: 0,
295
- payload: e instanceof Exception ? e.message : 'Unknown error',
422
+ seq: sequence,
423
+ payload: e instanceof Error ? e.message : 'Unknown error',
296
424
  final: true,
297
425
  },
298
426
  twoway: false,
299
427
  });
300
428
  })
301
429
  .finally(() => {
430
+ if (controller.signal.aborted && producer.iterator?.return) {
431
+ void Promise.resolve(producer.iterator.return()).catch(() => { });
432
+ }
433
+ producer.wake?.();
434
+ this.streamProducers.delete(msg.id);
302
435
  if (this.aborts.has(msg.id)) {
303
436
  this.aborts.delete(msg.id);
304
437
  }
@@ -309,22 +442,32 @@ export class MessageModem {
309
442
  const res = msg.data;
310
443
  // 如果栈中存在该消息,则处理响应消息
311
444
  if (this.streams.has(id)) {
312
- const stream = this.streams.get(id);
445
+ const consumer = this.streams.get(id);
446
+ const stream = consumer.stream;
313
447
  if (res) {
448
+ if (!Number.isSafeInteger(res.seq) || res.seq !== consumer.nextSeq) {
449
+ stream.destroy(new Exception(409, `Invalid stream sequence: expected ${consumer.nextSeq}, received ${String(res.seq)}`));
450
+ return;
451
+ }
452
+ consumer.nextSeq++;
314
453
  if (res.status === 200) {
315
454
  if (res.final) {
455
+ consumer.completed = true;
316
456
  stream.push(null);
317
457
  }
318
458
  else {
459
+ consumer.creditOwed = true;
319
460
  stream.push(res.payload);
320
461
  }
321
462
  }
322
463
  else {
464
+ consumer.completed = true;
323
465
  const err = new Exception(res.status, res.payload);
324
466
  setImmediate(() => stream.destroy(err));
325
467
  }
326
468
  }
327
469
  else {
470
+ consumer.completed = true;
328
471
  const err = new Exception(404, 'Empty chunk data');
329
472
  setImmediate(() => stream.destroy(err));
330
473
  }
@@ -365,6 +508,22 @@ export class MessageModem {
365
508
  }
366
509
  }
367
510
  break;
511
+ case MESSAGE_MODEM_TYPE.STREAM_CREDIT: {
512
+ const credit = msg.data;
513
+ if (!credit
514
+ || !Number.isSafeInteger(credit.id)
515
+ || credit.id < 0
516
+ || !Number.isSafeInteger(credit.seq)
517
+ || credit.seq < 0)
518
+ break;
519
+ const producer = this.streamProducers.get(credit.id);
520
+ if (producer && producer.credits === 0 && credit.seq === producer.nextCreditSeq) {
521
+ producer.credits = 1;
522
+ producer.nextCreditSeq++;
523
+ producer.wake?.();
524
+ }
525
+ break;
526
+ }
368
527
  }
369
528
  }
370
529
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hile/message-modem",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -18,8 +18,9 @@
18
18
  "access": "public"
19
19
  },
20
20
  "devDependencies": {
21
+ "@types/node": "^26.2.0",
21
22
  "fix-esm-import-path": "^1.10.3",
22
23
  "vitest": "^4.0.18"
23
24
  },
24
- "gitHead": "0985b6f8abc1f4de0a36324063585fdc3ac1375b"
25
+ "gitHead": "b46cb7f3705a226f58e4d65a2ff985ea54b9a159"
25
26
  }