@hile/message-modem 4.0.1 → 4.0.3
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 +40 -25
- package/README.md +13 -6
- package/dist/deadline-scheduler.d.ts +22 -0
- package/dist/deadline-scheduler.js +139 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +30 -46
- package/package.json +2 -2
package/AI.md
CHANGED
|
@@ -32,13 +32,14 @@ Message handler file:
|
|
|
32
32
|
|
|
33
33
|
```ts
|
|
34
34
|
// src/messages/ping.msg.ts
|
|
35
|
-
import {
|
|
35
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
36
36
|
|
|
37
|
-
export default
|
|
37
|
+
export default defineMicroMessage(async ({ data, params, invocation }) => {
|
|
38
38
|
return {
|
|
39
39
|
type: 'pong',
|
|
40
40
|
data,
|
|
41
41
|
params,
|
|
42
|
+
requestId: invocation.context.values.requestId,
|
|
42
43
|
timestamp: Date.now(),
|
|
43
44
|
}
|
|
44
45
|
})
|
|
@@ -71,7 +72,11 @@ export default defineService('micro.app', async (shutdown) => {
|
|
|
71
72
|
Caller:
|
|
72
73
|
|
|
73
74
|
```ts
|
|
74
|
-
|
|
75
|
+
import { randomUUID } from 'node:crypto'
|
|
76
|
+
import { createExecutionContext } from '@hile/context'
|
|
77
|
+
|
|
78
|
+
const context = createExecutionContext({ requestId: randomUUID() })
|
|
79
|
+
const result = await app.call('example.service', '/ping', { hello: 'world' }, { context })
|
|
75
80
|
```
|
|
76
81
|
|
|
77
82
|
## More Examples
|
|
@@ -80,11 +85,11 @@ Streaming handler:
|
|
|
80
85
|
|
|
81
86
|
```ts
|
|
82
87
|
// src/messages/events.msg.ts
|
|
83
|
-
import {
|
|
88
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
84
89
|
|
|
85
|
-
export default
|
|
90
|
+
export default defineMicroMessage(async function* ({ invocation }) {
|
|
86
91
|
for (let i = 0; i < 3; i++) {
|
|
87
|
-
yield { seq: i }
|
|
92
|
+
yield { seq: i, requestId: invocation.context.values.requestId }
|
|
88
93
|
}
|
|
89
94
|
})
|
|
90
95
|
```
|
|
@@ -92,7 +97,7 @@ export default defineMessage(async function* () {
|
|
|
92
97
|
Streaming caller:
|
|
93
98
|
|
|
94
99
|
```ts
|
|
95
|
-
const stream = await app.stream('example.service', '/events', {})
|
|
100
|
+
const stream = await app.stream('example.service', '/events', {}, { context })
|
|
96
101
|
for await (const chunk of stream) {
|
|
97
102
|
console.log(chunk)
|
|
98
103
|
}
|
|
@@ -129,12 +134,13 @@ Use the message packages for request/response messaging over WebSocket, process
|
|
|
129
134
|
|
|
130
135
|
- Do not use `stream()` for normal single-result calls.
|
|
131
136
|
- Do not rely on message IDs for business idempotency. They are transport IDs.
|
|
132
|
-
-
|
|
137
|
+
- Use `defineMicroMessage()` for Micro business handlers; reserve generic `defineMessage()` for transport-neutral loaders.
|
|
138
|
+
- 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
139
|
|
|
134
140
|
## Install
|
|
135
141
|
|
|
136
142
|
```bash
|
|
137
|
-
pnpm add @hile/micro @hile/message-loader @hile/message-ws
|
|
143
|
+
pnpm add @hile/context @hile/micro @hile/message-loader @hile/message-ws
|
|
138
144
|
```
|
|
139
145
|
|
|
140
146
|
Use transport-specific packages only when you need to build custom IPC or worker-thread bridges.
|
|
@@ -143,7 +149,8 @@ Use transport-specific packages only when you need to build custom IPC or worker
|
|
|
143
149
|
|
|
144
150
|
```ts
|
|
145
151
|
import { defineMessage, MessageLoader } from '@hile/message-loader'
|
|
146
|
-
import {
|
|
152
|
+
import { createExecutionContext } from '@hile/context'
|
|
153
|
+
import { Application, defineMicroMessage, Registry, Server } from '@hile/micro'
|
|
147
154
|
import { MessageWs } from '@hile/message-ws'
|
|
148
155
|
import { MessageIpc } from '@hile/message-ipc'
|
|
149
156
|
import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
@@ -151,7 +158,7 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
151
158
|
|
|
152
159
|
## Compose With
|
|
153
160
|
|
|
154
|
-
-
|
|
161
|
+
- Pass `ExecutionContext` explicitly in every business call or stream option; the receiver gets it in `invocation.context`.
|
|
155
162
|
- `@hile/redis-idempotency` protects retryable side effects in message handlers.
|
|
156
163
|
- `@hile/redis-stream-queue` is better for durable background jobs.
|
|
157
164
|
|
|
@@ -160,10 +167,14 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
160
167
|
- `MessageLoader` maps `*.msg.*` files to routes using `@hile/loader`.
|
|
161
168
|
- `MessageLoader.dispatch(path, data, extras?)` invokes the matched handler.
|
|
162
169
|
- `MessageModem._send()` returns a `Promise`.
|
|
170
|
+
- `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
171
|
- `MessageModem._stream()` returns a Node `Readable` in object mode.
|
|
172
|
+
- 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`.
|
|
173
|
+
- 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.
|
|
174
|
+
- `@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
175
|
- A stream request requires `exec()` to return an async iterable.
|
|
165
|
-
- `Application.call(namespace, url, data, options
|
|
166
|
-
- `Application.stream(namespace, url, data, options
|
|
176
|
+
- `Application.call(namespace, url, data, options)` requires `options.context` and returns a promise.
|
|
177
|
+
- `Application.stream(namespace, url, data, options)` requires `options.context` and returns a readable stream.
|
|
167
178
|
- `Application.publish(topic, payload)` returns an object with `update()` and `unpublish()`.
|
|
168
179
|
- `Application.subscribe(topic, callback)` returns an unsubscribe function.
|
|
169
180
|
- `Registry` stores service addresses and retained config/topic state under `~/.registry`.
|
|
@@ -177,9 +188,10 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
177
188
|
|
|
178
189
|
## Verification Checklist
|
|
179
190
|
|
|
180
|
-
-
|
|
181
|
-
- RPC callers use `await app.call(
|
|
191
|
+
- Micro message files default-export `defineMicroMessage(...)` and receive `invocation.context`.
|
|
192
|
+
- RPC callers use `await app.call(..., { context })`.
|
|
182
193
|
- Streaming handlers are async generators.
|
|
194
|
+
- Custom modem timeout values use the documented safe-integer range.
|
|
183
195
|
- Registry is started before application nodes need discovery.
|
|
184
196
|
- Micro apps use stable namespaces and advertise reachable hosts.
|
|
185
197
|
|
|
@@ -197,10 +209,10 @@ Provider handler:
|
|
|
197
209
|
|
|
198
210
|
```ts
|
|
199
211
|
// src/messages/charge.msg.ts
|
|
200
|
-
import {
|
|
212
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
201
213
|
|
|
202
|
-
export default
|
|
203
|
-
return { charged: true, input: data }
|
|
214
|
+
export default defineMicroMessage(async ({ data, invocation }) => {
|
|
215
|
+
return { charged: true, input: data, requestId: invocation.context.values.requestId }
|
|
204
216
|
})
|
|
205
217
|
```
|
|
206
218
|
|
|
@@ -228,10 +240,14 @@ export default defineService('billing.micro', async (shutdown) => {
|
|
|
228
240
|
Consumer:
|
|
229
241
|
|
|
230
242
|
```ts
|
|
243
|
+
import { randomUUID } from 'node:crypto'
|
|
244
|
+
import { createExecutionContext } from '@hile/context'
|
|
245
|
+
|
|
246
|
+
const context = createExecutionContext({ requestId: randomUUID(), tenantId: 't1' })
|
|
231
247
|
const result = await app.call('billing', '/charge', {
|
|
232
248
|
tenantId: 't1',
|
|
233
249
|
amount: 100,
|
|
234
|
-
})
|
|
250
|
+
}, { context })
|
|
235
251
|
```
|
|
236
252
|
|
|
237
253
|
## File Layout
|
|
@@ -251,16 +267,15 @@ Use this recipe when services communicate over Hile registry-backed RPC.
|
|
|
251
267
|
## Packages To Use
|
|
252
268
|
|
|
253
269
|
- `@hile/micro`
|
|
254
|
-
- `@hile/
|
|
255
|
-
- `@hile/context` when context must cross service boundaries
|
|
270
|
+
- `@hile/context` for the required explicit execution context carrier
|
|
256
271
|
- `@hile/redis-idempotency` for retryable side effects
|
|
257
272
|
|
|
258
273
|
## Implementation Steps
|
|
259
274
|
|
|
260
275
|
1. Start a Registry with `hile registry`.
|
|
261
276
|
2. Start providers with stable namespaces.
|
|
262
|
-
3.
|
|
263
|
-
4.
|
|
277
|
+
3. Default-export `defineMicroMessage()` handlers and load them through `app.load()`.
|
|
278
|
+
4. Create context at ingress and call providers with `await app.call(namespace, url, data, { context })`.
|
|
264
279
|
5. Use `app.stream()` only for async-generator handlers.
|
|
265
280
|
|
|
266
281
|
## Failure And Cleanup Behavior
|
|
@@ -273,8 +288,8 @@ Use this recipe when services communicate over Hile registry-backed RPC.
|
|
|
273
288
|
|
|
274
289
|
- Registry is reachable.
|
|
275
290
|
- Provider namespace matches consumer call.
|
|
276
|
-
- Handlers default-export `
|
|
277
|
-
- Consumer code awaits `app.call(
|
|
291
|
+
- Handlers default-export `defineMicroMessage()` and consume explicit invocation context when needed.
|
|
292
|
+
- Consumer code awaits `app.call(..., { context })` directly.
|
|
278
293
|
|
|
279
294
|
|
|
280
295
|
|
package/README.md
CHANGED
|
@@ -22,13 +22,14 @@ Message handler file:
|
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
// src/messages/ping.msg.ts
|
|
25
|
-
import {
|
|
25
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
26
26
|
|
|
27
|
-
export default
|
|
27
|
+
export default defineMicroMessage(async ({ data, params, invocation }) => {
|
|
28
28
|
return {
|
|
29
29
|
type: 'pong',
|
|
30
30
|
data,
|
|
31
31
|
params,
|
|
32
|
+
requestId: invocation.context.values.requestId,
|
|
32
33
|
timestamp: Date.now(),
|
|
33
34
|
}
|
|
34
35
|
})
|
|
@@ -61,14 +62,19 @@ export default defineService('micro.app', async (shutdown) => {
|
|
|
61
62
|
Caller:
|
|
62
63
|
|
|
63
64
|
```ts
|
|
64
|
-
|
|
65
|
+
import { randomUUID } from 'node:crypto'
|
|
66
|
+
import { createExecutionContext } from '@hile/context'
|
|
67
|
+
|
|
68
|
+
const context = createExecutionContext({ requestId: randomUUID() })
|
|
69
|
+
const result = await app.call('example.service', '/ping', { hello: 'world' }, { context })
|
|
65
70
|
```
|
|
66
71
|
|
|
67
72
|
## Boundaries
|
|
68
73
|
|
|
69
74
|
- Do not use `stream()` for normal single-result calls.
|
|
70
75
|
- Do not rely on message IDs for business idempotency. They are transport IDs.
|
|
71
|
-
-
|
|
76
|
+
- Use `defineMicroMessage()` for Micro business handlers; reserve generic `defineMessage()` for transport-neutral loaders.
|
|
77
|
+
- 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
78
|
|
|
73
79
|
- Appending a secondary response getter to `client.request('/x', data)`
|
|
74
80
|
- Returning a plain object from a handler called through `stream()`.
|
|
@@ -77,9 +83,10 @@ const result = await app.call('example.service', '/ping', { hello: 'world' })
|
|
|
77
83
|
|
|
78
84
|
## Verify
|
|
79
85
|
|
|
80
|
-
-
|
|
81
|
-
- RPC callers use `await app.call(
|
|
86
|
+
- Micro message files default-export `defineMicroMessage(...)` and receive `invocation.context`.
|
|
87
|
+
- RPC callers use `await app.call(..., { context })`.
|
|
82
88
|
- Streaming handlers are async generators.
|
|
89
|
+
- Custom modem timeout values use the documented safe-integer range.
|
|
83
90
|
- Registry is started before application nodes need discovery.
|
|
84
91
|
- Micro apps use stable namespaces and advertise reachable hosts.
|
|
85
92
|
|
|
@@ -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
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) {
|
|
@@ -50,6 +51,7 @@ class CreditReadable extends Readable {
|
|
|
50
51
|
}
|
|
51
52
|
export class MessageModem {
|
|
52
53
|
id = 0;
|
|
54
|
+
deadlines = new DeadlineScheduler();
|
|
53
55
|
aborts = new Map();
|
|
54
56
|
stacks = new Map();
|
|
55
57
|
streams = new Map();
|
|
@@ -71,6 +73,7 @@ export class MessageModem {
|
|
|
71
73
|
this.stacks.clear();
|
|
72
74
|
this.streams.clear();
|
|
73
75
|
this.streamProducers.clear();
|
|
76
|
+
this.deadlines.clear();
|
|
74
77
|
}
|
|
75
78
|
/**
|
|
76
79
|
* 创建自增 ID
|
|
@@ -153,10 +156,8 @@ export class MessageModem {
|
|
|
153
156
|
let totalTimer;
|
|
154
157
|
let idleTimer;
|
|
155
158
|
const clearTimers = () => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (idleTimer)
|
|
159
|
-
clearTimeout(idleTimer);
|
|
159
|
+
this.deadlines.cancel(totalTimer);
|
|
160
|
+
this.deadlines.cancel(idleTimer);
|
|
160
161
|
totalTimer = undefined;
|
|
161
162
|
idleTimer = undefined;
|
|
162
163
|
};
|
|
@@ -170,9 +171,9 @@ export class MessageModem {
|
|
|
170
171
|
if (!idleTimeout || consumer.completed || consumer.cancelled)
|
|
171
172
|
return;
|
|
172
173
|
if (idleTimer)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
this.deadlines.reschedule(idleTimer, idleTimeout);
|
|
175
|
+
else
|
|
176
|
+
idleTimer = this.deadlines.schedule(idleTimeout, () => expire('Stream idle timeout'));
|
|
176
177
|
};
|
|
177
178
|
consumer = {
|
|
178
179
|
stream,
|
|
@@ -208,16 +209,13 @@ export class MessageModem {
|
|
|
208
209
|
this.streams.set(state.id, consumer);
|
|
209
210
|
options?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
210
211
|
if (timeout) {
|
|
211
|
-
totalTimer =
|
|
212
|
-
totalTimer.unref?.();
|
|
212
|
+
totalTimer = this.deadlines.schedule(timeout, () => expire('Stream timeout'));
|
|
213
213
|
}
|
|
214
214
|
touch();
|
|
215
215
|
stream.on('close', () => {
|
|
216
216
|
sendAbort();
|
|
217
217
|
clearTimers();
|
|
218
|
-
|
|
219
|
-
this.streams.delete(state.id);
|
|
220
|
-
}
|
|
218
|
+
this.streams.delete(state.id);
|
|
221
219
|
options?.signal?.removeEventListener('abort', onAbort);
|
|
222
220
|
});
|
|
223
221
|
try {
|
|
@@ -237,7 +235,7 @@ export class MessageModem {
|
|
|
237
235
|
* @returns 消息响应
|
|
238
236
|
*/
|
|
239
237
|
_write(data, options) {
|
|
240
|
-
const timeout = options?.timeout ?? 30000;
|
|
238
|
+
const timeout = streamTimeout(options?.timeout ?? 30000, 'Message timeout');
|
|
241
239
|
const twoway = !!options?.twoway;
|
|
242
240
|
const signal = options?.signal;
|
|
243
241
|
// 创建请求消息数据
|
|
@@ -252,19 +250,17 @@ export class MessageModem {
|
|
|
252
250
|
let timer;
|
|
253
251
|
let posted = false;
|
|
254
252
|
const clear = () => {
|
|
255
|
-
|
|
256
|
-
this.stacks.delete(state.id);
|
|
257
|
-
}
|
|
253
|
+
this.stacks.delete(state.id);
|
|
258
254
|
};
|
|
259
255
|
const clean = () => {
|
|
260
|
-
|
|
261
|
-
|
|
256
|
+
this.deadlines.cancel(timer);
|
|
257
|
+
timer = undefined;
|
|
262
258
|
signal?.removeEventListener('abort', onAbort);
|
|
263
259
|
clear();
|
|
264
260
|
};
|
|
265
261
|
const onAbort = () => {
|
|
266
|
-
|
|
267
|
-
|
|
262
|
+
this.deadlines.cancel(timer);
|
|
263
|
+
timer = undefined;
|
|
268
264
|
try {
|
|
269
265
|
if (posted)
|
|
270
266
|
this.post(this.createPostData(MESSAGE_MODEM_TYPE.ABORT, state.id));
|
|
@@ -292,7 +288,7 @@ export class MessageModem {
|
|
|
292
288
|
resolve: _resolve,
|
|
293
289
|
reject: _reject,
|
|
294
290
|
});
|
|
295
|
-
timer =
|
|
291
|
+
timer = this.deadlines.schedule(timeout, () => _reject(new TimeoutException()));
|
|
296
292
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
297
293
|
if (signal?.aborted) {
|
|
298
294
|
onAbort();
|
|
@@ -314,11 +310,6 @@ export class MessageModem {
|
|
|
314
310
|
onRequest(msg) {
|
|
315
311
|
const controller = new AbortController();
|
|
316
312
|
this.aborts.set(msg.id, controller);
|
|
317
|
-
controller.signal.addEventListener('abort', () => {
|
|
318
|
-
if (this.aborts.has(msg.id)) {
|
|
319
|
-
this.aborts.delete(msg.id);
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
313
|
this.exec(msg.data, controller.signal)
|
|
323
314
|
.then(value => {
|
|
324
315
|
if (controller.signal.aborted)
|
|
@@ -355,10 +346,7 @@ export class MessageModem {
|
|
|
355
346
|
}
|
|
356
347
|
})
|
|
357
348
|
.finally(() => {
|
|
358
|
-
|
|
359
|
-
if (this.aborts.has(msg.id)) {
|
|
360
|
-
this.aborts.delete(msg.id);
|
|
361
|
-
}
|
|
349
|
+
this.aborts.delete(msg.id);
|
|
362
350
|
});
|
|
363
351
|
}
|
|
364
352
|
/**
|
|
@@ -368,9 +356,10 @@ export class MessageModem {
|
|
|
368
356
|
onResponse(msg) {
|
|
369
357
|
const id = msg.id;
|
|
370
358
|
const res = msg.data;
|
|
359
|
+
const stack = this.stacks.get(id);
|
|
371
360
|
// 如果栈中存在该消息,则处理响应消息
|
|
372
|
-
if (
|
|
373
|
-
const { resolve, reject } =
|
|
361
|
+
if (stack) {
|
|
362
|
+
const { resolve, reject } = stack;
|
|
374
363
|
// 如果响应状态码不是 200,则拒绝响应
|
|
375
364
|
if (res?.status !== 200) {
|
|
376
365
|
reject(new Exception(res?.status, res?.message));
|
|
@@ -432,12 +421,6 @@ export class MessageModem {
|
|
|
432
421
|
let sequence = 0;
|
|
433
422
|
this.aborts.set(msg.id, controller);
|
|
434
423
|
this.streamProducers.set(msg.id, producer);
|
|
435
|
-
controller.signal.addEventListener('abort', () => {
|
|
436
|
-
producer.wake?.();
|
|
437
|
-
if (this.aborts.has(msg.id)) {
|
|
438
|
-
this.aborts.delete(msg.id);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
424
|
const takeCredit = async () => {
|
|
442
425
|
while (producer.credits === 0 && !controller.signal.aborted) {
|
|
443
426
|
await new Promise((resolve) => {
|
|
@@ -516,17 +499,15 @@ export class MessageModem {
|
|
|
516
499
|
}
|
|
517
500
|
producer.wake?.();
|
|
518
501
|
this.streamProducers.delete(msg.id);
|
|
519
|
-
|
|
520
|
-
this.aborts.delete(msg.id);
|
|
521
|
-
}
|
|
502
|
+
this.aborts.delete(msg.id);
|
|
522
503
|
});
|
|
523
504
|
}
|
|
524
505
|
onStreamResponse(msg) {
|
|
525
506
|
const id = msg.id;
|
|
526
507
|
const res = msg.data;
|
|
508
|
+
const consumer = this.streams.get(id);
|
|
527
509
|
// 如果栈中存在该消息,则处理响应消息
|
|
528
|
-
if (
|
|
529
|
-
const consumer = this.streams.get(id);
|
|
510
|
+
if (consumer) {
|
|
530
511
|
const stream = consumer.stream;
|
|
531
512
|
if (res) {
|
|
532
513
|
consumer.touch();
|
|
@@ -591,15 +572,18 @@ export class MessageModem {
|
|
|
591
572
|
}
|
|
592
573
|
break;
|
|
593
574
|
// 处理终止消息
|
|
594
|
-
case MESSAGE_MODEM_TYPE.ABORT:
|
|
575
|
+
case MESSAGE_MODEM_TYPE.ABORT: {
|
|
595
576
|
const id = msg.data;
|
|
596
|
-
|
|
597
|
-
|
|
577
|
+
const controller = this.aborts.get(id);
|
|
578
|
+
if (controller) {
|
|
579
|
+
this.aborts.delete(id);
|
|
580
|
+
this.streamProducers.get(id)?.wake?.();
|
|
598
581
|
if (!controller.signal.aborted) {
|
|
599
582
|
controller.abort();
|
|
600
583
|
}
|
|
601
584
|
}
|
|
602
585
|
break;
|
|
586
|
+
}
|
|
603
587
|
case MESSAGE_MODEM_TYPE.STREAM_CREDIT: {
|
|
604
588
|
const credit = msg.data;
|
|
605
589
|
if (!credit
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hile/message-modem",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
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": "3ea69973f9373ddb1f7d5d37338966fd7d081d66"
|
|
26
26
|
}
|