@hile/message-modem 4.0.0 → 4.0.2
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/AI.md +6 -0
- package/README.md +2 -0
- package/dist/deadline-scheduler.d.ts +22 -0
- package/dist/deadline-scheduler.js +139 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.js +124 -46
- package/package.json +2 -2
package/AI.md
CHANGED
|
@@ -130,6 +130,7 @@ Use the message packages for request/response messaging over WebSocket, process
|
|
|
130
130
|
- Do not use `stream()` for normal single-result calls.
|
|
131
131
|
- Do not rely on message IDs for business idempotency. They are transport IDs.
|
|
132
132
|
- Do not bypass `defineMessage()` for file-loaded handlers.
|
|
133
|
+
- Do not pass zero, fractional, non-finite, or oversized message timeouts. Explicit timeout values must be safe integers from `1` through `2_147_483_647` milliseconds.
|
|
133
134
|
|
|
134
135
|
## Install
|
|
135
136
|
|
|
@@ -160,7 +161,11 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
160
161
|
- `MessageLoader` maps `*.msg.*` files to routes using `@hile/loader`.
|
|
161
162
|
- `MessageLoader.dispatch(path, data, extras?)` invokes the matched handler.
|
|
162
163
|
- `MessageModem._send()` returns a `Promise`.
|
|
164
|
+
- `MessageModem._send()` and `_push()` use a `30_000` ms timeout when none is provided. An explicit timeout must be a safe integer from `1` through `2_147_483_647`; invalid values throw `TypeError` before a message is sent.
|
|
163
165
|
- `MessageModem._stream()` returns a Node `Readable` in object mode.
|
|
166
|
+
- Stream `timeout` and `idleTimeout` values use the same `1` through `2_147_483_647` ms range. The stream `window` must be a safe integer from `1` through `64` and defaults to `1`.
|
|
167
|
+
- Each modem schedules request, total-stream, and idle-stream deadlines through one internal deadline scheduler. This reduces active Node.js timers without changing timeout, cancellation, ordering, or error semantics.
|
|
168
|
+
- `@hile/message-ws` keeps public `decodeMessageFrame()` payloads isolated from caller-owned input by default. Its owned WebSocket `RawData` path uses a zero-copy binary Flight payload view internally.
|
|
164
169
|
- A stream request requires `exec()` to return an async iterable.
|
|
165
170
|
- `Application.call(namespace, url, data, options?)` returns a promise.
|
|
166
171
|
- `Application.stream(namespace, url, data, options?)` returns a readable stream.
|
|
@@ -180,6 +185,7 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
180
185
|
- Message files default-export `defineMessage(...)`.
|
|
181
186
|
- RPC callers use `await app.call(...)`.
|
|
182
187
|
- Streaming handlers are async generators.
|
|
188
|
+
- Custom modem timeout values use the documented safe-integer range.
|
|
183
189
|
- Registry is started before application nodes need discovery.
|
|
184
190
|
- Micro apps use stable namespaces and advertise reachable hosts.
|
|
185
191
|
|
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ const result = await app.call('example.service', '/ping', { hello: 'world' })
|
|
|
69
69
|
- Do not use `stream()` for normal single-result calls.
|
|
70
70
|
- Do not rely on message IDs for business idempotency. They are transport IDs.
|
|
71
71
|
- Do not bypass `defineMessage()` for file-loaded handlers.
|
|
72
|
+
- Do not pass zero, fractional, non-finite, or oversized message timeouts. Explicit timeout values must be safe integers from `1` through `2_147_483_647` milliseconds.
|
|
72
73
|
|
|
73
74
|
- Appending a secondary response getter to `client.request('/x', data)`
|
|
74
75
|
- Returning a plain object from a handler called through `stream()`.
|
|
@@ -80,6 +81,7 @@ const result = await app.call('example.service', '/ping', { hello: 'world' })
|
|
|
80
81
|
- Message files default-export `defineMessage(...)`.
|
|
81
82
|
- RPC callers use `await app.call(...)`.
|
|
82
83
|
- Streaming handlers are async generators.
|
|
84
|
+
- Custom modem timeout values use the documented safe-integer range.
|
|
83
85
|
- Registry is started before application nodes need discovery.
|
|
84
86
|
- Micro apps use stable namespaces and advertise reachable hosts.
|
|
85
87
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface DeadlineHandle {
|
|
2
|
+
deadline: number;
|
|
3
|
+
callback: () => void;
|
|
4
|
+
index: number;
|
|
5
|
+
active: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Maintains many logical deadlines with a single active Node.js timer. */
|
|
8
|
+
export declare class DeadlineScheduler {
|
|
9
|
+
private readonly heap;
|
|
10
|
+
private timer?;
|
|
11
|
+
private armedDeadline?;
|
|
12
|
+
schedule(delay: number, callback: () => void): DeadlineHandle;
|
|
13
|
+
reschedule(handle: DeadlineHandle, delay: number): void;
|
|
14
|
+
cancel(handle: DeadlineHandle | undefined): void;
|
|
15
|
+
clear(): void;
|
|
16
|
+
private readonly flush;
|
|
17
|
+
private arm;
|
|
18
|
+
private removeAt;
|
|
19
|
+
private siftUp;
|
|
20
|
+
private siftDown;
|
|
21
|
+
private swap;
|
|
22
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const MAX_TIMER_DELAY = 2_147_483_647;
|
|
2
|
+
function normalizeDelay(delay) {
|
|
3
|
+
return Number.isFinite(delay) && delay >= 1 && delay <= MAX_TIMER_DELAY
|
|
4
|
+
? delay
|
|
5
|
+
: 1;
|
|
6
|
+
}
|
|
7
|
+
/** Maintains many logical deadlines with a single active Node.js timer. */
|
|
8
|
+
export class DeadlineScheduler {
|
|
9
|
+
heap = [];
|
|
10
|
+
timer;
|
|
11
|
+
armedDeadline;
|
|
12
|
+
schedule(delay, callback) {
|
|
13
|
+
const handle = {
|
|
14
|
+
deadline: Date.now() + normalizeDelay(delay),
|
|
15
|
+
callback,
|
|
16
|
+
index: this.heap.length,
|
|
17
|
+
active: true,
|
|
18
|
+
};
|
|
19
|
+
this.heap.push(handle);
|
|
20
|
+
this.siftUp(handle.index);
|
|
21
|
+
this.arm();
|
|
22
|
+
return handle;
|
|
23
|
+
}
|
|
24
|
+
reschedule(handle, delay) {
|
|
25
|
+
if (!handle.active)
|
|
26
|
+
return;
|
|
27
|
+
const previous = handle.deadline;
|
|
28
|
+
handle.deadline = Date.now() + normalizeDelay(delay);
|
|
29
|
+
if (handle.deadline < previous)
|
|
30
|
+
this.siftUp(handle.index);
|
|
31
|
+
else
|
|
32
|
+
this.siftDown(handle.index);
|
|
33
|
+
this.arm();
|
|
34
|
+
}
|
|
35
|
+
cancel(handle) {
|
|
36
|
+
if (!handle?.active)
|
|
37
|
+
return;
|
|
38
|
+
handle.active = false;
|
|
39
|
+
this.removeAt(handle.index);
|
|
40
|
+
this.arm();
|
|
41
|
+
}
|
|
42
|
+
clear() {
|
|
43
|
+
if (this.timer)
|
|
44
|
+
clearTimeout(this.timer);
|
|
45
|
+
this.timer = undefined;
|
|
46
|
+
this.armedDeadline = undefined;
|
|
47
|
+
for (const handle of this.heap) {
|
|
48
|
+
handle.active = false;
|
|
49
|
+
handle.index = -1;
|
|
50
|
+
}
|
|
51
|
+
this.heap.length = 0;
|
|
52
|
+
}
|
|
53
|
+
flush = () => {
|
|
54
|
+
this.timer = undefined;
|
|
55
|
+
this.armedDeadline = undefined;
|
|
56
|
+
let now = Date.now();
|
|
57
|
+
while (this.heap[0]?.deadline <= now) {
|
|
58
|
+
const handle = this.removeAt(0);
|
|
59
|
+
handle.active = false;
|
|
60
|
+
try {
|
|
61
|
+
handle.callback();
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
queueMicrotask(() => { throw error; });
|
|
65
|
+
}
|
|
66
|
+
now = Date.now();
|
|
67
|
+
}
|
|
68
|
+
this.arm();
|
|
69
|
+
};
|
|
70
|
+
arm() {
|
|
71
|
+
const next = this.heap[0];
|
|
72
|
+
if (!next) {
|
|
73
|
+
if (this.timer)
|
|
74
|
+
clearTimeout(this.timer);
|
|
75
|
+
this.timer = undefined;
|
|
76
|
+
this.armedDeadline = undefined;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (this.timer && this.armedDeadline === next.deadline)
|
|
80
|
+
return;
|
|
81
|
+
if (this.timer)
|
|
82
|
+
clearTimeout(this.timer);
|
|
83
|
+
this.armedDeadline = next.deadline;
|
|
84
|
+
this.timer = setTimeout(this.flush, Math.max(0, next.deadline - Date.now()));
|
|
85
|
+
this.timer.unref?.();
|
|
86
|
+
}
|
|
87
|
+
removeAt(index) {
|
|
88
|
+
const removed = this.heap[index];
|
|
89
|
+
const last = this.heap.pop();
|
|
90
|
+
removed.index = -1;
|
|
91
|
+
if (index < this.heap.length) {
|
|
92
|
+
this.heap[index] = last;
|
|
93
|
+
last.index = index;
|
|
94
|
+
const parent = index > 0 ? Math.floor((index - 1) / 2) : -1;
|
|
95
|
+
if (parent >= 0 && this.heap[index].deadline < this.heap[parent].deadline) {
|
|
96
|
+
this.siftUp(index);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.siftDown(index);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return removed;
|
|
103
|
+
}
|
|
104
|
+
siftUp(start) {
|
|
105
|
+
let index = start;
|
|
106
|
+
while (index > 0) {
|
|
107
|
+
const parent = Math.floor((index - 1) / 2);
|
|
108
|
+
if (this.heap[parent].deadline <= this.heap[index].deadline)
|
|
109
|
+
break;
|
|
110
|
+
this.swap(index, parent);
|
|
111
|
+
index = parent;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
siftDown(start) {
|
|
115
|
+
let index = start;
|
|
116
|
+
while (true) {
|
|
117
|
+
const left = index * 2 + 1;
|
|
118
|
+
const right = left + 1;
|
|
119
|
+
let smallest = index;
|
|
120
|
+
if (left < this.heap.length && this.heap[left].deadline < this.heap[smallest].deadline) {
|
|
121
|
+
smallest = left;
|
|
122
|
+
}
|
|
123
|
+
if (right < this.heap.length && this.heap[right].deadline < this.heap[smallest].deadline) {
|
|
124
|
+
smallest = right;
|
|
125
|
+
}
|
|
126
|
+
if (smallest === index)
|
|
127
|
+
return;
|
|
128
|
+
this.swap(index, smallest);
|
|
129
|
+
index = smallest;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
swap(left, right) {
|
|
133
|
+
const value = this.heap[left];
|
|
134
|
+
this.heap[left] = this.heap[right];
|
|
135
|
+
this.heap[right] = value;
|
|
136
|
+
this.heap[left].index = left;
|
|
137
|
+
this.heap[right].index = right;
|
|
138
|
+
}
|
|
139
|
+
}
|
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;
|
|
@@ -27,6 +37,7 @@ export interface MessageStreamChunk<T = any> {
|
|
|
27
37
|
}
|
|
28
38
|
export declare abstract class MessageModem {
|
|
29
39
|
private id;
|
|
40
|
+
private readonly deadlines;
|
|
30
41
|
private readonly aborts;
|
|
31
42
|
private readonly stacks;
|
|
32
43
|
private readonly streams;
|
|
@@ -78,9 +89,7 @@ export declare abstract class MessageModem {
|
|
|
78
89
|
timeout?: number;
|
|
79
90
|
signal?: AbortSignal;
|
|
80
91
|
}): void;
|
|
81
|
-
protected _stream(data: any, options?:
|
|
82
|
-
signal?: AbortSignal;
|
|
83
|
-
}): Readable;
|
|
92
|
+
protected _stream(data: any, options?: MessageStreamOptions): Readable;
|
|
84
93
|
/**
|
|
85
94
|
* 写入消息
|
|
86
95
|
* @param data - 消息数据
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AbortException, Exception, TimeoutException } from "./exception.js";
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
|
+
import { DeadlineScheduler } from './deadline-scheduler.js';
|
|
3
4
|
export * from './exception.js';
|
|
4
5
|
export var MESSAGE_MODEM_TYPE;
|
|
5
6
|
(function (MESSAGE_MODEM_TYPE) {
|
|
@@ -8,6 +9,30 @@ export var MESSAGE_MODEM_TYPE;
|
|
|
8
9
|
MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["ABORT"] = 2] = "ABORT";
|
|
9
10
|
MESSAGE_MODEM_TYPE[MESSAGE_MODEM_TYPE["STREAM_CREDIT"] = 3] = "STREAM_CREDIT";
|
|
10
11
|
})(MESSAGE_MODEM_TYPE || (MESSAGE_MODEM_TYPE = {}));
|
|
12
|
+
const MAX_STREAM_WINDOW = 64;
|
|
13
|
+
const MAX_TIMER_DELAY = 2_147_483_647;
|
|
14
|
+
function streamLimit(value, name) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
18
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function streamWindow(value) {
|
|
23
|
+
const normalized = streamLimit(value, 'Stream window') ?? 1;
|
|
24
|
+
if (normalized > MAX_STREAM_WINDOW) {
|
|
25
|
+
throw new TypeError(`Stream window must not exceed ${MAX_STREAM_WINDOW}`);
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
function streamTimeout(value, name) {
|
|
30
|
+
const normalized = streamLimit(value, name);
|
|
31
|
+
if (normalized !== undefined && normalized > MAX_TIMER_DELAY) {
|
|
32
|
+
throw new TypeError(`${name} must not exceed ${MAX_TIMER_DELAY}`);
|
|
33
|
+
}
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
11
36
|
class CreditReadable extends Readable {
|
|
12
37
|
onConsumed;
|
|
13
38
|
constructor(onConsumed) {
|
|
@@ -26,6 +51,7 @@ class CreditReadable extends Readable {
|
|
|
26
51
|
}
|
|
27
52
|
export class MessageModem {
|
|
28
53
|
id = 0;
|
|
54
|
+
deadlines = new DeadlineScheduler();
|
|
29
55
|
aborts = new Map();
|
|
30
56
|
stacks = new Map();
|
|
31
57
|
streams = new Map();
|
|
@@ -47,6 +73,7 @@ export class MessageModem {
|
|
|
47
73
|
this.stacks.clear();
|
|
48
74
|
this.streams.clear();
|
|
49
75
|
this.streamProducers.clear();
|
|
76
|
+
this.deadlines.clear();
|
|
50
77
|
}
|
|
51
78
|
/**
|
|
52
79
|
* 创建自增 ID
|
|
@@ -105,28 +132,59 @@ export class MessageModem {
|
|
|
105
132
|
signal: options?.signal,
|
|
106
133
|
});
|
|
107
134
|
}
|
|
108
|
-
_stream(data, options) {
|
|
135
|
+
_stream(data, options = {}) {
|
|
136
|
+
const window = streamWindow(options.window);
|
|
137
|
+
const timeout = streamTimeout(options.timeout, 'Stream timeout');
|
|
138
|
+
const idleTimeout = streamTimeout(options.idleTimeout, 'Stream idle timeout');
|
|
109
139
|
const state = this.createPostData(MESSAGE_MODEM_TYPE.REQUEST, data, true, true);
|
|
110
140
|
state.streamVersion = 1;
|
|
141
|
+
if (window > 1)
|
|
142
|
+
state.streamWindow = window;
|
|
111
143
|
let consumer;
|
|
112
144
|
const stream = new CreditReadable(() => {
|
|
113
|
-
if (
|
|
145
|
+
if (consumer.creditsOwed === 0 || consumer.completed || consumer.cancelled)
|
|
114
146
|
return;
|
|
115
|
-
consumer.
|
|
147
|
+
consumer.creditsOwed--;
|
|
116
148
|
try {
|
|
117
|
-
this.post(this.createPostData(MESSAGE_MODEM_TYPE.STREAM_CREDIT, { id: state.id, seq: consumer.
|
|
149
|
+
this.post(this.createPostData(MESSAGE_MODEM_TYPE.STREAM_CREDIT, { id: state.id, seq: consumer.nextCreditSeq++ }, false));
|
|
118
150
|
}
|
|
119
151
|
catch (error) {
|
|
120
152
|
consumer.cancelled = true;
|
|
121
153
|
stream.destroy(error);
|
|
122
154
|
}
|
|
123
155
|
});
|
|
156
|
+
let totalTimer;
|
|
157
|
+
let idleTimer;
|
|
158
|
+
const clearTimers = () => {
|
|
159
|
+
this.deadlines.cancel(totalTimer);
|
|
160
|
+
this.deadlines.cancel(idleTimer);
|
|
161
|
+
totalTimer = undefined;
|
|
162
|
+
idleTimer = undefined;
|
|
163
|
+
};
|
|
164
|
+
const expire = (message) => {
|
|
165
|
+
if (consumer.completed || consumer.cancelled)
|
|
166
|
+
return;
|
|
167
|
+
sendAbort();
|
|
168
|
+
stream.destroy(new TimeoutException(message));
|
|
169
|
+
};
|
|
170
|
+
const touch = () => {
|
|
171
|
+
if (!idleTimeout || consumer.completed || consumer.cancelled)
|
|
172
|
+
return;
|
|
173
|
+
if (idleTimer)
|
|
174
|
+
this.deadlines.reschedule(idleTimer, idleTimeout);
|
|
175
|
+
else
|
|
176
|
+
idleTimer = this.deadlines.schedule(idleTimeout, () => expire('Stream idle timeout'));
|
|
177
|
+
};
|
|
124
178
|
consumer = {
|
|
125
179
|
stream,
|
|
126
180
|
completed: false,
|
|
127
181
|
cancelled: false,
|
|
128
|
-
|
|
182
|
+
creditsOwed: 0,
|
|
183
|
+
maxCredits: window,
|
|
129
184
|
nextSeq: 0,
|
|
185
|
+
nextCreditSeq: 0,
|
|
186
|
+
touch,
|
|
187
|
+
clearTimers,
|
|
130
188
|
};
|
|
131
189
|
const sendAbort = () => {
|
|
132
190
|
if (consumer.completed || consumer.cancelled)
|
|
@@ -150,11 +208,14 @@ export class MessageModem {
|
|
|
150
208
|
}
|
|
151
209
|
this.streams.set(state.id, consumer);
|
|
152
210
|
options?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
211
|
+
if (timeout) {
|
|
212
|
+
totalTimer = this.deadlines.schedule(timeout, () => expire('Stream timeout'));
|
|
213
|
+
}
|
|
214
|
+
touch();
|
|
153
215
|
stream.on('close', () => {
|
|
154
216
|
sendAbort();
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
217
|
+
clearTimers();
|
|
218
|
+
this.streams.delete(state.id);
|
|
158
219
|
options?.signal?.removeEventListener('abort', onAbort);
|
|
159
220
|
});
|
|
160
221
|
try {
|
|
@@ -174,7 +235,7 @@ export class MessageModem {
|
|
|
174
235
|
* @returns 消息响应
|
|
175
236
|
*/
|
|
176
237
|
_write(data, options) {
|
|
177
|
-
const timeout = options?.timeout ?? 30000;
|
|
238
|
+
const timeout = streamTimeout(options?.timeout ?? 30000, 'Message timeout');
|
|
178
239
|
const twoway = !!options?.twoway;
|
|
179
240
|
const signal = options?.signal;
|
|
180
241
|
// 创建请求消息数据
|
|
@@ -189,19 +250,17 @@ export class MessageModem {
|
|
|
189
250
|
let timer;
|
|
190
251
|
let posted = false;
|
|
191
252
|
const clear = () => {
|
|
192
|
-
|
|
193
|
-
this.stacks.delete(state.id);
|
|
194
|
-
}
|
|
253
|
+
this.stacks.delete(state.id);
|
|
195
254
|
};
|
|
196
255
|
const clean = () => {
|
|
197
|
-
|
|
198
|
-
|
|
256
|
+
this.deadlines.cancel(timer);
|
|
257
|
+
timer = undefined;
|
|
199
258
|
signal?.removeEventListener('abort', onAbort);
|
|
200
259
|
clear();
|
|
201
260
|
};
|
|
202
261
|
const onAbort = () => {
|
|
203
|
-
|
|
204
|
-
|
|
262
|
+
this.deadlines.cancel(timer);
|
|
263
|
+
timer = undefined;
|
|
205
264
|
try {
|
|
206
265
|
if (posted)
|
|
207
266
|
this.post(this.createPostData(MESSAGE_MODEM_TYPE.ABORT, state.id));
|
|
@@ -229,7 +288,7 @@ export class MessageModem {
|
|
|
229
288
|
resolve: _resolve,
|
|
230
289
|
reject: _reject,
|
|
231
290
|
});
|
|
232
|
-
timer =
|
|
291
|
+
timer = this.deadlines.schedule(timeout, () => _reject(new TimeoutException()));
|
|
233
292
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
234
293
|
if (signal?.aborted) {
|
|
235
294
|
onAbort();
|
|
@@ -251,11 +310,6 @@ export class MessageModem {
|
|
|
251
310
|
onRequest(msg) {
|
|
252
311
|
const controller = new AbortController();
|
|
253
312
|
this.aborts.set(msg.id, controller);
|
|
254
|
-
controller.signal.addEventListener('abort', () => {
|
|
255
|
-
if (this.aborts.has(msg.id)) {
|
|
256
|
-
this.aborts.delete(msg.id);
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
313
|
this.exec(msg.data, controller.signal)
|
|
260
314
|
.then(value => {
|
|
261
315
|
if (controller.signal.aborted)
|
|
@@ -292,10 +346,7 @@ export class MessageModem {
|
|
|
292
346
|
}
|
|
293
347
|
})
|
|
294
348
|
.finally(() => {
|
|
295
|
-
|
|
296
|
-
if (this.aborts.has(msg.id)) {
|
|
297
|
-
this.aborts.delete(msg.id);
|
|
298
|
-
}
|
|
349
|
+
this.aborts.delete(msg.id);
|
|
299
350
|
});
|
|
300
351
|
}
|
|
301
352
|
/**
|
|
@@ -305,9 +356,10 @@ export class MessageModem {
|
|
|
305
356
|
onResponse(msg) {
|
|
306
357
|
const id = msg.id;
|
|
307
358
|
const res = msg.data;
|
|
359
|
+
const stack = this.stacks.get(id);
|
|
308
360
|
// 如果栈中存在该消息,则处理响应消息
|
|
309
|
-
if (
|
|
310
|
-
const { resolve, reject } =
|
|
361
|
+
if (stack) {
|
|
362
|
+
const { resolve, reject } = stack;
|
|
311
363
|
// 如果响应状态码不是 200,则拒绝响应
|
|
312
364
|
if (res?.status !== 200) {
|
|
313
365
|
reject(new Exception(res?.status, res?.message));
|
|
@@ -329,6 +381,26 @@ export class MessageModem {
|
|
|
329
381
|
});
|
|
330
382
|
return;
|
|
331
383
|
}
|
|
384
|
+
let window;
|
|
385
|
+
try {
|
|
386
|
+
window = streamWindow(msg.streamWindow);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
this.post({
|
|
390
|
+
id: msg.id,
|
|
391
|
+
mode: MESSAGE_MODEM_TYPE.RESPONSE,
|
|
392
|
+
stream: true,
|
|
393
|
+
streamVersion: 1,
|
|
394
|
+
data: {
|
|
395
|
+
status: 400,
|
|
396
|
+
seq: 0,
|
|
397
|
+
payload: error instanceof Error ? error.message : 'Invalid stream window',
|
|
398
|
+
final: true,
|
|
399
|
+
},
|
|
400
|
+
twoway: false,
|
|
401
|
+
});
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
332
404
|
if (this.streamProducers.has(msg.id) || this.streamProducers.size >= 128) {
|
|
333
405
|
this.post({
|
|
334
406
|
id: msg.id,
|
|
@@ -342,18 +414,13 @@ export class MessageModem {
|
|
|
342
414
|
}
|
|
343
415
|
const controller = new AbortController();
|
|
344
416
|
const producer = {
|
|
345
|
-
credits:
|
|
417
|
+
credits: window,
|
|
418
|
+
maxCredits: window,
|
|
346
419
|
nextCreditSeq: 0,
|
|
347
420
|
};
|
|
348
421
|
let sequence = 0;
|
|
349
422
|
this.aborts.set(msg.id, controller);
|
|
350
423
|
this.streamProducers.set(msg.id, producer);
|
|
351
|
-
controller.signal.addEventListener('abort', () => {
|
|
352
|
-
producer.wake?.();
|
|
353
|
-
if (this.aborts.has(msg.id)) {
|
|
354
|
-
this.aborts.delete(msg.id);
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
424
|
const takeCredit = async () => {
|
|
358
425
|
while (producer.credits === 0 && !controller.signal.aborted) {
|
|
359
426
|
await new Promise((resolve) => {
|
|
@@ -432,19 +499,18 @@ export class MessageModem {
|
|
|
432
499
|
}
|
|
433
500
|
producer.wake?.();
|
|
434
501
|
this.streamProducers.delete(msg.id);
|
|
435
|
-
|
|
436
|
-
this.aborts.delete(msg.id);
|
|
437
|
-
}
|
|
502
|
+
this.aborts.delete(msg.id);
|
|
438
503
|
});
|
|
439
504
|
}
|
|
440
505
|
onStreamResponse(msg) {
|
|
441
506
|
const id = msg.id;
|
|
442
507
|
const res = msg.data;
|
|
508
|
+
const consumer = this.streams.get(id);
|
|
443
509
|
// 如果栈中存在该消息,则处理响应消息
|
|
444
|
-
if (
|
|
445
|
-
const consumer = this.streams.get(id);
|
|
510
|
+
if (consumer) {
|
|
446
511
|
const stream = consumer.stream;
|
|
447
512
|
if (res) {
|
|
513
|
+
consumer.touch();
|
|
448
514
|
if (!Number.isSafeInteger(res.seq) || res.seq !== consumer.nextSeq) {
|
|
449
515
|
stream.destroy(new Exception(409, `Invalid stream sequence: expected ${consumer.nextSeq}, received ${String(res.seq)}`));
|
|
450
516
|
return;
|
|
@@ -453,21 +519,28 @@ export class MessageModem {
|
|
|
453
519
|
if (res.status === 200) {
|
|
454
520
|
if (res.final) {
|
|
455
521
|
consumer.completed = true;
|
|
522
|
+
consumer.clearTimers();
|
|
456
523
|
stream.push(null);
|
|
457
524
|
}
|
|
458
525
|
else {
|
|
459
|
-
consumer.
|
|
526
|
+
if (consumer.creditsOwed >= consumer.maxCredits) {
|
|
527
|
+
stream.destroy(new Exception(429, 'Stream window exceeded'));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
consumer.creditsOwed++;
|
|
460
531
|
stream.push(res.payload);
|
|
461
532
|
}
|
|
462
533
|
}
|
|
463
534
|
else {
|
|
464
535
|
consumer.completed = true;
|
|
536
|
+
consumer.clearTimers();
|
|
465
537
|
const err = new Exception(res.status, res.payload);
|
|
466
538
|
setImmediate(() => stream.destroy(err));
|
|
467
539
|
}
|
|
468
540
|
}
|
|
469
541
|
else {
|
|
470
542
|
consumer.completed = true;
|
|
543
|
+
consumer.clearTimers();
|
|
471
544
|
const err = new Exception(404, 'Empty chunk data');
|
|
472
545
|
setImmediate(() => stream.destroy(err));
|
|
473
546
|
}
|
|
@@ -499,15 +572,18 @@ export class MessageModem {
|
|
|
499
572
|
}
|
|
500
573
|
break;
|
|
501
574
|
// 处理终止消息
|
|
502
|
-
case MESSAGE_MODEM_TYPE.ABORT:
|
|
575
|
+
case MESSAGE_MODEM_TYPE.ABORT: {
|
|
503
576
|
const id = msg.data;
|
|
504
|
-
|
|
505
|
-
|
|
577
|
+
const controller = this.aborts.get(id);
|
|
578
|
+
if (controller) {
|
|
579
|
+
this.aborts.delete(id);
|
|
580
|
+
this.streamProducers.get(id)?.wake?.();
|
|
506
581
|
if (!controller.signal.aborted) {
|
|
507
582
|
controller.abort();
|
|
508
583
|
}
|
|
509
584
|
}
|
|
510
585
|
break;
|
|
586
|
+
}
|
|
511
587
|
case MESSAGE_MODEM_TYPE.STREAM_CREDIT: {
|
|
512
588
|
const credit = msg.data;
|
|
513
589
|
if (!credit
|
|
@@ -517,8 +593,10 @@ export class MessageModem {
|
|
|
517
593
|
|| credit.seq < 0)
|
|
518
594
|
break;
|
|
519
595
|
const producer = this.streamProducers.get(credit.id);
|
|
520
|
-
if (producer
|
|
521
|
-
producer.credits
|
|
596
|
+
if (producer
|
|
597
|
+
&& producer.credits < producer.maxCredits
|
|
598
|
+
&& credit.seq === producer.nextCreditSeq) {
|
|
599
|
+
producer.credits++;
|
|
522
600
|
producer.nextCreditSeq++;
|
|
523
601
|
producer.wake?.();
|
|
524
602
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hile/message-modem",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
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": "46d7bcfc78a914aa2af8cd96e41b08511f5af38e"
|
|
26
26
|
}
|