@hile/message-modem 4.0.1 → 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 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
@@ -37,6 +37,7 @@ export interface MessageStreamChunk<T = any> {
37
37
  }
38
38
  export declare abstract class MessageModem {
39
39
  private id;
40
+ private readonly deadlines;
40
41
  private readonly aborts;
41
42
  private readonly stacks;
42
43
  private readonly streams;
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
- if (totalTimer)
157
- clearTimeout(totalTimer);
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
- clearTimeout(idleTimer);
174
- idleTimer = setTimeout(() => expire('Stream idle timeout'), idleTimeout);
175
- idleTimer.unref?.();
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 = setTimeout(() => expire('Stream timeout'), timeout);
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
- if (this.streams.has(state.id)) {
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
- if (this.stacks.has(state.id)) {
256
- this.stacks.delete(state.id);
257
- }
253
+ this.stacks.delete(state.id);
258
254
  };
259
255
  const clean = () => {
260
- if (timer)
261
- clearTimeout(timer);
256
+ this.deadlines.cancel(timer);
257
+ timer = undefined;
262
258
  signal?.removeEventListener('abort', onAbort);
263
259
  clear();
264
260
  };
265
261
  const onAbort = () => {
266
- if (timer)
267
- clearTimeout(timer);
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 = setTimeout(() => _reject(new TimeoutException()), timeout).unref();
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
- // 删除 Abort 处理函数
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 (this.stacks.has(id)) {
373
- const { resolve, reject } = this.stacks.get(id);
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
- if (this.aborts.has(msg.id)) {
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 (this.streams.has(id)) {
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
- if (this.aborts.has(id)) {
597
- const controller = this.aborts.get(id);
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.1",
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": "fe98c6f8cca2860ccbb213c575cfda44588cbd06"
25
+ "gitHead": "46d7bcfc78a914aa2af8cd96e41b08511f5af38e"
26
26
  }