@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 +11 -3
- package/dist/index.js +103 -9
- package/package.json +2 -2
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 (
|
|
142
|
+
if (consumer.creditsOwed === 0 || consumer.completed || consumer.cancelled)
|
|
114
143
|
return;
|
|
115
|
-
consumer.
|
|
144
|
+
consumer.creditsOwed--;
|
|
116
145
|
try {
|
|
117
|
-
this.post(this.createPostData(MESSAGE_MODEM_TYPE.STREAM_CREDIT, { id: state.id, seq: consumer.
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
|
521
|
-
producer.credits
|
|
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.
|
|
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": "
|
|
25
|
+
"gitHead": "fe98c6f8cca2860ccbb213c575cfda44588cbd06"
|
|
26
26
|
}
|