@hile/message-modem 1.0.7 → 2.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
@@ -1,3 +1,4 @@
1
+ import { Readable } from 'node:stream';
1
2
  export * from './exception.js';
2
3
  export declare enum MESSAGE_MODEM_TYPE {
3
4
  REQUEST = 0,
@@ -8,6 +9,7 @@ export interface MessageTransferFormat<T = any> {
8
9
  id: number;
9
10
  mode: MESSAGE_MODEM_TYPE;
10
11
  twoway: boolean;
12
+ stream?: boolean;
11
13
  data?: T;
12
14
  }
13
15
  export interface MessageReturnFormat<T = any> {
@@ -15,10 +17,17 @@ export interface MessageReturnFormat<T = any> {
15
17
  data: T;
16
18
  message: string;
17
19
  }
20
+ export interface MessageStreamChunk<T = any> {
21
+ status: string | number;
22
+ seq: number;
23
+ payload: T;
24
+ final: boolean;
25
+ }
18
26
  export declare abstract class MessageModem {
19
27
  private id;
20
28
  private readonly aborts;
21
29
  private readonly stacks;
30
+ private readonly streams;
22
31
  protected _dispose(): void;
23
32
  /**
24
33
  * 创建自增 ID
@@ -36,7 +45,7 @@ export declare abstract class MessageModem {
36
45
  * @param data - 消息数据
37
46
  * @returns
38
47
  */
39
- protected abstract exec(data: any): Promise<any>;
48
+ protected abstract exec(data: any, signal?: AbortSignal): Promise<any>;
40
49
  /**
41
50
  * 创建发送消息数据
42
51
  * @param mode - 消息类型
@@ -48,19 +57,27 @@ export declare abstract class MessageModem {
48
57
  * 发送消息
49
58
  * @param data - 消息数据
50
59
  * @param timeout - 超时时间
60
+ * @param signal - 中止信号
51
61
  * @returns 消息响应
52
62
  */
53
- protected _send<T = any>(data: T, timeout?: number): {
54
- abort: () => void;
55
- response: <U = any>() => Promise<U>;
56
- };
63
+ protected _send<T = any>(data: any, options?: {
64
+ timeout?: number;
65
+ signal?: AbortSignal;
66
+ }): Promise<T>;
57
67
  /**
58
68
  * 推送消息
59
69
  * @param data - 消息数据
60
70
  * @param timeout - 超时时间
71
+ * @param signal - 中止信号
61
72
  * @returns 消息响应
62
73
  */
63
- protected _push<T = any>(data: T, timeout?: number): void;
74
+ protected _push<T = any>(data: T, options?: {
75
+ timeout?: number;
76
+ signal?: AbortSignal;
77
+ }): void;
78
+ protected _stream(data: any, options?: {
79
+ signal?: AbortSignal;
80
+ }): Readable;
64
81
  /**
65
82
  * 写入消息
66
83
  * @param data - 消息数据
@@ -78,6 +95,8 @@ export declare abstract class MessageModem {
78
95
  * @param msg - 消息数据
79
96
  */
80
97
  private onResponse;
98
+ private onStreamRequest;
99
+ private onStreamResponse;
81
100
  /**
82
101
  * 接收消息
83
102
  * @param msg - 消息数据
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AbortException, Exception, TimeoutException } from "./exception.js";
2
+ import { Readable } from 'node:stream';
2
3
  export * from './exception.js';
3
4
  export var MESSAGE_MODEM_TYPE;
4
5
  (function (MESSAGE_MODEM_TYPE) {
@@ -10,15 +11,20 @@ export class MessageModem {
10
11
  id = 0;
11
12
  aborts = new Map();
12
13
  stacks = new Map();
14
+ streams = new Map();
13
15
  _dispose() {
14
16
  for (const { reject } of this.stacks.values()) {
15
17
  reject(new AbortException());
16
18
  }
17
- for (const reject of this.aborts.values()) {
18
- reject(new AbortException());
19
+ for (const controller of this.aborts.values()) {
20
+ controller.abort();
21
+ }
22
+ for (const stream of this.streams.values()) {
23
+ stream.destroy(new AbortException());
19
24
  }
20
25
  this.aborts.clear();
21
26
  this.stacks.clear();
27
+ this.streams.clear();
22
28
  }
23
29
  /**
24
30
  * 创建自增 ID
@@ -38,13 +44,14 @@ export class MessageModem {
38
44
  * @param data - 消息数据
39
45
  * @returns 消息数据
40
46
  */
41
- createPostData(mode, data, twoway = true) {
47
+ createPostData(mode, data, twoway = true, stream = false) {
42
48
  const id = this.createIncrementId();
43
49
  const state = {
44
- id, twoway, data, mode,
50
+ id, twoway, data, mode, stream,
45
51
  };
46
52
  if (mode === MESSAGE_MODEM_TYPE.ABORT) {
47
53
  state.twoway = false;
54
+ state.stream = false;
48
55
  }
49
56
  return state;
50
57
  }
@@ -52,19 +59,50 @@ export class MessageModem {
52
59
  * 发送消息
53
60
  * @param data - 消息数据
54
61
  * @param timeout - 超时时间
62
+ * @param signal - 中止信号
55
63
  * @returns 消息响应
56
64
  */
57
- _send(data, timeout = 30000) {
58
- return this._write(data, timeout, true);
65
+ _send(data, options) {
66
+ return this._write(data, {
67
+ timeout: options?.timeout ?? 30000,
68
+ twoway: true,
69
+ signal: options?.signal,
70
+ });
59
71
  }
60
72
  /**
61
73
  * 推送消息
62
74
  * @param data - 消息数据
63
75
  * @param timeout - 超时时间
76
+ * @param signal - 中止信号
64
77
  * @returns 消息响应
65
78
  */
66
- _push(data, timeout = 30000) {
67
- this._write(data, timeout, false);
79
+ _push(data, options) {
80
+ this._write(data, {
81
+ timeout: options?.timeout ?? 30000,
82
+ twoway: false,
83
+ signal: options?.signal,
84
+ });
85
+ }
86
+ _stream(data, options) {
87
+ 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);
91
+ const onAbort = () => {
92
+ this.post(this.createPostData(MESSAGE_MODEM_TYPE.ABORT, state.id));
93
+ stream.destroy(new AbortException());
94
+ this.streams.delete(state.id);
95
+ };
96
+ if (options?.signal) {
97
+ options.signal.addEventListener('abort', onAbort);
98
+ }
99
+ stream.on('close', () => {
100
+ if (this.streams.has(state.id)) {
101
+ this.streams.delete(state.id);
102
+ }
103
+ options?.signal?.removeEventListener('abort', onAbort);
104
+ });
105
+ return stream;
68
106
  }
69
107
  /**
70
108
  * 写入消息
@@ -72,7 +110,10 @@ export class MessageModem {
72
110
  * @param timeout - 超时时间
73
111
  * @returns 消息响应
74
112
  */
75
- _write(data, timeout = 30000, twoway = false) {
113
+ _write(data, options) {
114
+ const timeout = options?.timeout ?? 30000;
115
+ const twoway = !!options?.twoway;
116
+ const signal = options?.signal;
76
117
  // 创建请求消息数据
77
118
  const state = this.createPostData(MESSAGE_MODEM_TYPE.REQUEST, data, twoway);
78
119
  // 发送消息
@@ -80,135 +121,101 @@ export class MessageModem {
80
121
  // 如果消息是单向的,则直接返回
81
122
  if (!twoway)
82
123
  return;
83
- const controller = new AbortController();
84
- return {
85
- // 终止请求
86
- abort: () => controller.abort(),
87
- // 等待响应
88
- response: () => new Promise((resolve, reject) => {
89
- // 清理 stacks
90
- const clear = () => {
91
- if (this.stacks.has(state.id)) {
92
- this.stacks.delete(state.id);
93
- }
94
- };
95
- const clean = () => {
96
- clearTimeout(timer);
97
- controller.signal.removeEventListener('abort', aborthandler);
124
+ return new Promise((resolve, reject) => {
125
+ const clear = () => {
126
+ if (this.stacks.has(state.id)) {
127
+ this.stacks.delete(state.id);
128
+ }
129
+ };
130
+ const clean = () => {
131
+ clearTimeout(timer);
132
+ signal?.removeEventListener('abort', onAbort);
133
+ clear();
134
+ };
135
+ const onAbort = () => {
136
+ clearTimeout(timer);
137
+ try {
138
+ this.post(this.createPostData(MESSAGE_MODEM_TYPE.ABORT, state.id));
139
+ }
140
+ catch {
141
+ /* 例如 WebSocket 已关闭时 send 可能抛错 */
142
+ }
143
+ finally {
144
+ signal?.removeEventListener('abort', onAbort);
98
145
  clear();
99
- };
100
- // Abort 处理函数(post 失败时仍须 reject,否则调用方会一直挂起)
101
- const aborthandler = () => {
102
- clearTimeout(timer);
103
- try {
104
- this.post(this.createPostData(MESSAGE_MODEM_TYPE.ABORT, state.id));
105
- }
106
- catch {
107
- /* 例如 WebSocket 已关闭时 send 可能抛错 */
108
- }
109
- finally {
110
- controller.signal.removeEventListener('abort', aborthandler);
111
- clear();
112
- reject(new AbortException());
113
- }
114
- };
115
- // 成功处理
116
- const _resolve = (data) => {
117
- clean();
118
- resolve(data);
119
- };
120
- // 失败处理
121
- const _reject = (e) => {
122
- clean();
123
- reject(e);
124
- };
125
- // 超时处理
126
- const timer = setTimeout(() => {
127
- if (!controller.signal.aborted) {
128
- controller.abort();
129
- }
130
- else {
131
- _reject(new TimeoutException());
132
- }
133
- }, timeout).unref();
134
- // 添加 Abort 处理函数
135
- controller.signal.addEventListener('abort', aborthandler);
136
- // 添加栈
137
- this.stacks.set(state.id, {
138
- resolve: _resolve,
139
- reject: _reject,
140
- });
141
- })
142
- };
146
+ reject(new AbortException());
147
+ }
148
+ };
149
+ // 成功处理
150
+ const _resolve = (data) => {
151
+ clean();
152
+ resolve(data);
153
+ };
154
+ // 失败处理
155
+ const _reject = (e) => {
156
+ clean();
157
+ reject(e);
158
+ };
159
+ const timer = setTimeout(() => _reject(new TimeoutException()), timeout).unref();
160
+ signal?.addEventListener('abort', onAbort);
161
+ this.stacks.set(state.id, {
162
+ resolve: _resolve,
163
+ reject: _reject,
164
+ });
165
+ });
143
166
  }
144
167
  /**
145
168
  * 处理请求消息
146
169
  * @param msg - 消息数据
147
170
  */
148
171
  onRequest(msg) {
149
- // 执行消息
150
- // 使用 Promise.race 处理消息执行和 Abort 处理
151
- Promise.race([
152
- this.exec(msg.data).catch(e => ({ e })),
153
- new Promise((_, reject) => this.aborts.set(msg.id, reject)),
154
- ]).then(value => {
155
- // 如果消息执行失败
156
- if (value?.e) {
157
- // 如果消息是双向的,则发送响应消息
158
- if (msg.twoway) {
159
- this.post({
160
- id: msg.id,
161
- mode: MESSAGE_MODEM_TYPE.RESPONSE,
162
- twoway: false,
163
- data: {
164
- status: value.e instanceof Exception ? value.e.status : 500,
165
- data: null,
166
- message: value.e.message,
167
- }
168
- });
169
- }
172
+ const controller = new AbortController();
173
+ this.aborts.set(msg.id, controller);
174
+ controller.signal.addEventListener('abort', () => {
175
+ if (this.aborts.has(msg.id)) {
176
+ this.aborts.delete(msg.id);
170
177
  }
171
- else {
172
- // 如果消息是双向的,则发送响应消息
173
- if (msg.twoway) {
174
- this.post({
175
- id: msg.id,
176
- mode: MESSAGE_MODEM_TYPE.RESPONSE,
177
- twoway: false,
178
- data: {
179
- status: 200,
180
- data: value,
181
- }
182
- });
183
- }
178
+ });
179
+ this.exec(msg.data, controller.signal)
180
+ .then(value => {
181
+ if (controller.signal.aborted)
182
+ return;
183
+ if (isAsyncIterable(value)) {
184
+ throw new Exception(500, 'Async iterable is not supported');
185
+ }
186
+ if (msg.twoway) {
187
+ this.post({
188
+ id: msg.id,
189
+ mode: MESSAGE_MODEM_TYPE.RESPONSE,
190
+ twoway: false,
191
+ data: {
192
+ status: 200,
193
+ data: value,
194
+ }
195
+ });
184
196
  }
185
- }).catch(e => {
186
- if (e instanceof AbortException)
197
+ })
198
+ .catch(e => {
199
+ if (controller.signal.aborted)
187
200
  return;
188
- // 如果消息是双向的,则发送响应消息
189
201
  if (msg.twoway) {
190
- // 发送响应消息
191
- const code = e instanceof Exception ? e.status : 500;
192
202
  this.post({
193
203
  id: msg.id,
194
204
  mode: MESSAGE_MODEM_TYPE.RESPONSE,
195
205
  twoway: false,
196
206
  data: {
197
- status: code,
207
+ status: e instanceof Exception ? e.status : 500,
198
208
  data: null,
199
209
  message: e.message,
200
210
  }
201
211
  });
202
212
  }
203
- }).finally(() => {
213
+ })
214
+ .finally(() => {
204
215
  // 删除 Abort 处理函数
205
216
  if (this.aborts.has(msg.id)) {
206
217
  this.aborts.delete(msg.id);
207
218
  }
208
- // 清理栈
209
- if (this.stacks.has(msg.id)) {
210
- this.stacks.delete(msg.id);
211
- }
212
219
  });
213
220
  }
214
221
  /**
@@ -230,6 +237,99 @@ export class MessageModem {
230
237
  }
231
238
  }
232
239
  }
240
+ onStreamRequest(msg) {
241
+ const controller = new AbortController();
242
+ this.aborts.set(msg.id, controller);
243
+ controller.signal.addEventListener('abort', () => {
244
+ if (this.aborts.has(msg.id)) {
245
+ this.aborts.delete(msg.id);
246
+ }
247
+ });
248
+ this.exec(msg.data, controller.signal)
249
+ .then(async (value) => {
250
+ if (!isAsyncIterable(value)) {
251
+ throw new Exception(500, 'Invalid async iterable');
252
+ }
253
+ let i = 0;
254
+ for await (const chunk of value) {
255
+ if (controller.signal.aborted)
256
+ return;
257
+ this.post({
258
+ id: msg.id,
259
+ mode: MESSAGE_MODEM_TYPE.RESPONSE,
260
+ stream: true,
261
+ data: {
262
+ status: 200,
263
+ seq: i++,
264
+ payload: chunk,
265
+ final: false,
266
+ },
267
+ twoway: false,
268
+ });
269
+ }
270
+ if (controller.signal.aborted)
271
+ return;
272
+ this.post({
273
+ id: msg.id,
274
+ mode: MESSAGE_MODEM_TYPE.RESPONSE,
275
+ stream: true,
276
+ data: {
277
+ status: 200,
278
+ seq: i++,
279
+ payload: undefined,
280
+ final: true,
281
+ },
282
+ twoway: false,
283
+ });
284
+ })
285
+ .catch(e => {
286
+ if (controller.signal.aborted)
287
+ return;
288
+ this.post({
289
+ id: msg.id,
290
+ mode: MESSAGE_MODEM_TYPE.RESPONSE,
291
+ stream: true,
292
+ data: {
293
+ status: e instanceof Exception ? e.status : 500,
294
+ seq: 0,
295
+ payload: e instanceof Exception ? e.message : 'Unknown error',
296
+ final: true,
297
+ },
298
+ twoway: false,
299
+ });
300
+ })
301
+ .finally(() => {
302
+ if (this.aborts.has(msg.id)) {
303
+ this.aborts.delete(msg.id);
304
+ }
305
+ });
306
+ }
307
+ onStreamResponse(msg) {
308
+ const id = msg.id;
309
+ const res = msg.data;
310
+ // 如果栈中存在该消息,则处理响应消息
311
+ if (this.streams.has(id)) {
312
+ const stream = this.streams.get(id);
313
+ if (res) {
314
+ if (res.status === 200) {
315
+ if (res.final) {
316
+ stream.push(null);
317
+ }
318
+ else {
319
+ stream.push(res.payload);
320
+ }
321
+ }
322
+ else {
323
+ const err = new Exception(res.status, res.payload);
324
+ setImmediate(() => stream.destroy(err));
325
+ }
326
+ }
327
+ else {
328
+ const err = new Exception(404, 'Empty chunk data');
329
+ setImmediate(() => stream.destroy(err));
330
+ }
331
+ }
332
+ }
233
333
  /**
234
334
  * 接收消息
235
335
  * @param msg - 消息数据
@@ -239,20 +339,35 @@ export class MessageModem {
239
339
  switch (msg.mode) {
240
340
  // 处理请求消息
241
341
  case MESSAGE_MODEM_TYPE.REQUEST:
242
- this.onRequest(msg);
342
+ if (msg.stream) {
343
+ this.onStreamRequest(msg);
344
+ }
345
+ else {
346
+ this.onRequest(msg);
347
+ }
243
348
  break;
244
349
  // 处理响应消息
245
350
  case MESSAGE_MODEM_TYPE.RESPONSE:
246
- this.onResponse(msg);
351
+ if (msg.stream) {
352
+ this.onStreamResponse(msg);
353
+ }
354
+ else {
355
+ this.onResponse(msg);
356
+ }
247
357
  break;
248
358
  // 处理终止消息
249
359
  case MESSAGE_MODEM_TYPE.ABORT:
250
360
  const id = msg.data;
251
361
  if (this.aborts.has(id)) {
252
- const reject = this.aborts.get(id);
253
- reject(new AbortException());
254
- break;
362
+ const controller = this.aborts.get(id);
363
+ if (!controller.signal.aborted) {
364
+ controller.abort();
365
+ }
255
366
  }
367
+ break;
256
368
  }
257
369
  }
258
370
  }
371
+ function isAsyncIterable(value) {
372
+ return value != null && typeof value[Symbol.asyncIterator] === 'function';
373
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hile/message-modem",
3
- "version": "1.0.7",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -21,5 +21,5 @@
21
21
  "fix-esm-import-path": "^1.10.3",
22
22
  "vitest": "^4.0.18"
23
23
  },
24
- "gitHead": "4549afb84d25e8dffdfab2e27ed05e52828c368d"
24
+ "gitHead": "d28b20bc22ee08c45cbf4596672705cb96e8e461"
25
25
  }