@kevisual/router 0.0.66 → 0.0.67

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/ws.d.ts ADDED
@@ -0,0 +1,716 @@
1
+ import { EventEmitter } from 'eventemitter3';
2
+ import * as http from 'node:http';
3
+ import { IncomingMessage, ServerResponse } from 'node:http';
4
+ import { IncomingMessage as IncomingMessage$1, ServerResponse as ServerResponse$1 } from 'http';
5
+
6
+ type ReconnectConfig = {
7
+ /**
8
+ * 重连配置选项, 最大重试次数,默认无限
9
+ */
10
+ maxRetries?: number;
11
+ /**
12
+ * 重连配置选项, 重试延迟(ms),默认1000
13
+ */
14
+ retryDelay?: number;
15
+ /**
16
+ * 重连配置选项, 最大延迟(ms),默认30000
17
+ */
18
+ maxDelay?: number;
19
+ /**
20
+ * 重连配置选项, 退避倍数,默认2
21
+ */
22
+ backoffMultiplier?: number;
23
+ };
24
+ /**
25
+ * 一个支持自动重连的 WebSocket 客户端。
26
+ * 在连接断开时会根据配置进行重连尝试,支持指数退避。
27
+ */
28
+ declare class ReconnectingWebSocket {
29
+ private ws;
30
+ private url;
31
+ private config;
32
+ private retryCount;
33
+ private reconnectTimer;
34
+ private isManualClose;
35
+ private messageHandlers;
36
+ private openHandlers;
37
+ private closeHandlers;
38
+ private errorHandlers;
39
+ constructor(url: string, config?: ReconnectConfig);
40
+ log(...args: any[]): void;
41
+ error(...args: any[]): void;
42
+ connect(): void;
43
+ private scheduleReconnect;
44
+ send(data: any): boolean;
45
+ onMessage(handler: (data: any) => void): void;
46
+ onOpen(handler: () => void): void;
47
+ onClose(handler: (code: number, reason: Buffer) => void): void;
48
+ onError(handler: (error: Error) => void): void;
49
+ close(): void;
50
+ getReadyState(): number;
51
+ getRetryCount(): number;
52
+ }
53
+
54
+ type RouterContextT = {
55
+ code?: number;
56
+ [key: string]: any;
57
+ };
58
+ type RouteContext<T = {
59
+ code?: number;
60
+ }, S = any> = {
61
+ /**
62
+ * 本地自己调用的时候使用,可以标识为当前自调用,那么 auth 就不许重复的校验
63
+ * 或者不需要登录的,直接调用
64
+ */
65
+ appId?: string;
66
+ query?: {
67
+ [key: string]: any;
68
+ };
69
+ /** return body */
70
+ body?: number | string | Object;
71
+ forward?: (response: {
72
+ code: number;
73
+ data?: any;
74
+ message?: any;
75
+ }) => void;
76
+ /** return code */
77
+ code?: number;
78
+ /** return msg */
79
+ message?: string;
80
+ state?: S;
81
+ /**
82
+ * 当前路径
83
+ */
84
+ currentPath?: string;
85
+ /**
86
+ * 当前key
87
+ */
88
+ currentKey?: string;
89
+ /**
90
+ * 当前route
91
+ */
92
+ currentRoute?: Route;
93
+ /**
94
+ * 进度
95
+ */
96
+ progress?: [string, string][];
97
+ nextQuery?: {
98
+ [key: string]: any;
99
+ };
100
+ end?: boolean;
101
+ app?: QueryRouter;
102
+ error?: any;
103
+ /** 请求 route的返回结果,不解析body为data */
104
+ call?: (message: {
105
+ path: string;
106
+ key?: string;
107
+ payload?: any;
108
+ [key: string]: any;
109
+ } | {
110
+ id: string;
111
+ apyload?: any;
112
+ [key: string]: any;
113
+ }, ctx?: RouteContext & {
114
+ [key: string]: any;
115
+ }) => Promise<any>;
116
+ /** 请求 route的返回结果,解析了body为data,就类同于 query.post获取的数据*/
117
+ run?: (message: {
118
+ path: string;
119
+ key?: string;
120
+ payload?: any;
121
+ }, ctx?: RouteContext & {
122
+ [key: string]: any;
123
+ }) => Promise<any>;
124
+ index?: number;
125
+ throw?: (code?: number | string, message?: string, tips?: string) => void;
126
+ /** 是否需要序列化, 使用JSON.stringify和JSON.parse */
127
+ needSerialize?: boolean;
128
+ } & T;
129
+ type SimpleObject = Record<string, any>;
130
+ type Run<T extends SimpleObject = {}> = (ctx: Required<RouteContext<T>>) => Promise<typeof ctx | null | void>;
131
+ type NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
132
+ type RouteMiddleware = {
133
+ path: string;
134
+ key?: string;
135
+ id?: string;
136
+ } | string;
137
+ type RouteOpts<U = {}, T = SimpleObject> = {
138
+ path?: string;
139
+ key?: string;
140
+ id?: string;
141
+ run?: Run<U>;
142
+ nextRoute?: NextRoute;
143
+ description?: string;
144
+ metadata?: T;
145
+ middleware?: RouteMiddleware[];
146
+ type?: 'route' | 'middleware';
147
+ /**
148
+ * $#$ will be used to split path and key
149
+ */
150
+ idUsePath?: boolean;
151
+ /**
152
+ * id 合并的分隔符,默认为 $#$
153
+ */
154
+ delimiter?: string;
155
+ isDebug?: boolean;
156
+ };
157
+ type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'nextRoute'>;
158
+ declare const pickValue: readonly ["path", "key", "id", "description", "type", "middleware", "metadata"];
159
+ type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
160
+ declare class Route<U = {
161
+ [key: string]: any;
162
+ }, T extends SimpleObject = SimpleObject> {
163
+ /**
164
+ * 一级路径
165
+ */
166
+ path?: string;
167
+ /**
168
+ * 二级路径
169
+ */
170
+ key?: string;
171
+ id?: string;
172
+ run?: Run;
173
+ nextRoute?: NextRoute;
174
+ description?: string;
175
+ metadata?: T;
176
+ middleware?: RouteMiddleware[];
177
+ type?: string;
178
+ data?: any;
179
+ /**
180
+ * 是否开启debug,开启后会打印错误信息
181
+ */
182
+ isDebug?: boolean;
183
+ constructor(path?: string, key?: string, opts?: RouteOpts);
184
+ prompt(description: string): this;
185
+ prompt(description: Function): this;
186
+ define<T extends {
187
+ [key: string]: any;
188
+ } = RouterContextT>(opts: DefineRouteOpts): this;
189
+ define<T extends {
190
+ [key: string]: any;
191
+ } = RouterContextT>(fn: Run<T & U>): this;
192
+ define<T extends {
193
+ [key: string]: any;
194
+ } = RouterContextT>(key: string, fn: Run<T & U>): this;
195
+ define<T extends {
196
+ [key: string]: any;
197
+ } = RouterContextT>(path: string, key: string, fn: Run<T & U>): this;
198
+ update(opts: DefineRouteOpts, checkList?: string[]): this;
199
+ addTo(router: QueryRouter | {
200
+ add: (route: Route) => void;
201
+ [key: string]: any;
202
+ }, opts?: AddOpts): void;
203
+ setData(data: any): this;
204
+ throw(code?: number | string, message?: string, tips?: string): void;
205
+ }
206
+ /**
207
+ * @parmas override 是否覆盖已存在的route,默认true
208
+ */
209
+ type AddOpts = {
210
+ override?: boolean;
211
+ };
212
+ declare class QueryRouter {
213
+ appId: string;
214
+ routes: Route[];
215
+ maxNextRoute: number;
216
+ context?: RouteContext;
217
+ constructor();
218
+ /**
219
+ * add route
220
+ * @param route
221
+ * @param opts
222
+ */
223
+ add(route: Route, opts?: AddOpts): void;
224
+ /**
225
+ * remove route by path and key
226
+ * @param route
227
+ */
228
+ remove(route: Route | {
229
+ path: string;
230
+ key?: string;
231
+ }): void;
232
+ /**
233
+ * remove route by id
234
+ * @param uniqueId
235
+ */
236
+ removeById(unique: string): void;
237
+ /**
238
+ * 执行route
239
+ * @param path
240
+ * @param key
241
+ * @param ctx
242
+ * @returns
243
+ */
244
+ runRoute(path: string, key: string, ctx?: RouteContext): any;
245
+ /**
246
+ * 第一次执行
247
+ * @param message
248
+ * @param ctx
249
+ * @returns
250
+ */
251
+ parse(message: {
252
+ path: string;
253
+ key?: string;
254
+ payload?: any;
255
+ }, ctx?: RouteContext & {
256
+ [key: string]: any;
257
+ }): Promise<any>;
258
+ /**
259
+ * 返回的数据包含所有的context的请求返回的内容,可做其他处理
260
+ * @param message
261
+ * @param ctx
262
+ * @returns
263
+ */
264
+ call(message: {
265
+ id?: string;
266
+ path?: string;
267
+ key?: string;
268
+ payload?: any;
269
+ }, ctx?: RouteContext & {
270
+ [key: string]: any;
271
+ }): Promise<any>;
272
+ /**
273
+ * 请求 result 的数据
274
+ * @param message
275
+ * @param ctx
276
+ * @deprecated use run or call instead
277
+ * @returns
278
+ */
279
+ queryRoute(message: {
280
+ id?: string;
281
+ path: string;
282
+ key?: string;
283
+ payload?: any;
284
+ }, ctx?: RouteContext & {
285
+ [key: string]: any;
286
+ }): Promise<{
287
+ code: any;
288
+ data: any;
289
+ message: any;
290
+ }>;
291
+ /**
292
+ * Router Run获取数据
293
+ * @param message
294
+ * @param ctx
295
+ * @returns
296
+ */
297
+ run(message: {
298
+ id?: string;
299
+ path?: string;
300
+ key?: string;
301
+ payload?: any;
302
+ }, ctx?: RouteContext & {
303
+ [key: string]: any;
304
+ }): Promise<{
305
+ code: any;
306
+ data: any;
307
+ message: any;
308
+ }>;
309
+ /**
310
+ * 设置上下文
311
+ * @description 这里的上下文是为了在handle函数中使用
312
+ * @param ctx
313
+ */
314
+ setContext(ctx: RouteContext): void;
315
+ getList(filter?: (route: Route) => boolean): RouteInfo[];
316
+ /**
317
+ * 获取handle函数, 这里会去执行parse函数
318
+ */
319
+ getHandle<T = any>(router: QueryRouter, wrapperFn?: HandleFn<T>, ctx?: RouteContext): (msg: {
320
+ id?: string;
321
+ path?: string;
322
+ key?: string;
323
+ [key: string]: any;
324
+ }, handleContext?: RouteContext) => Promise<{
325
+ [key: string]: any;
326
+ code: string;
327
+ data?: any;
328
+ message?: string;
329
+ } | {
330
+ code: any;
331
+ data: any;
332
+ message: any;
333
+ } | {
334
+ code: number;
335
+ message: any;
336
+ data?: undefined;
337
+ }>;
338
+ exportRoutes(): Route<{
339
+ [key: string]: any;
340
+ }, SimpleObject>[];
341
+ importRoutes(routes: Route[]): void;
342
+ importRouter(router: QueryRouter): void;
343
+ throw(code?: number | string, message?: string, tips?: string): void;
344
+ hasRoute(path: string, key?: string): Route<{
345
+ [key: string]: any;
346
+ }, SimpleObject>;
347
+ findRoute(opts?: {
348
+ path?: string;
349
+ key?: string;
350
+ id?: string;
351
+ }): Route<{
352
+ [key: string]: any;
353
+ }, SimpleObject>;
354
+ createRouteList(force?: boolean, filter?: (route: Route) => boolean): void;
355
+ /**
356
+ * 等待程序运行, 获取到message的数据,就执行
357
+ * params 是预设参数
358
+ * emitter = process
359
+ * -- .exit
360
+ * -- .on
361
+ * -- .send
362
+ */
363
+ wait(params?: {
364
+ path?: string;
365
+ key?: string;
366
+ payload?: any;
367
+ }, opts?: {
368
+ emitter?: any;
369
+ timeout?: number;
370
+ getList?: boolean;
371
+ force?: boolean;
372
+ filter?: (route: Route) => boolean;
373
+ }): Promise<void>;
374
+ }
375
+ interface HandleFn<T = any> {
376
+ (msg: {
377
+ path: string;
378
+ [key: string]: any;
379
+ }, ctx?: any): {
380
+ code: string;
381
+ data?: any;
382
+ message?: string;
383
+ [key: string]: any;
384
+ };
385
+ (res: RouteContext<T>): any;
386
+ }
387
+
388
+ type Cors = {
389
+ /**
390
+ * @default '*''
391
+ */
392
+ origin?: string | undefined;
393
+ };
394
+ type ServerOpts<T = {}> = {
395
+ /**path default `/api/router` */
396
+ path?: string;
397
+ /**handle Fn */
398
+ handle?: (msg?: {
399
+ path: string;
400
+ key?: string;
401
+ [key: string]: any;
402
+ }, ctx?: {
403
+ req: http.IncomingMessage;
404
+ res: http.ServerResponse;
405
+ }) => any;
406
+ cors?: Cors;
407
+ io?: boolean;
408
+ showConnected?: boolean;
409
+ } & T;
410
+ interface ServerType {
411
+ path?: string;
412
+ server?: any;
413
+ handle: ServerOpts['handle'];
414
+ setHandle(handle?: any): void;
415
+ listeners: Listener[];
416
+ listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
417
+ listen(port: number, hostname?: string, listeningListener?: () => void): void;
418
+ listen(port: number, backlog?: number, listeningListener?: () => void): void;
419
+ listen(port: number, listeningListener?: () => void): void;
420
+ listen(path: string, backlog?: number, listeningListener?: () => void): void;
421
+ listen(path: string, listeningListener?: () => void): void;
422
+ listen(handle: any, backlog?: number, listeningListener?: () => void): void;
423
+ listen(handle: any, listeningListener?: () => void): void;
424
+ /**
425
+ * 兜底监听,当除开 `/api/router` 之外的请求,框架只监听一个api,所以有其他的请求都执行其他的监听
426
+ * @description 主要是为了兼容其他的监听
427
+ * @param listener
428
+ */
429
+ on(listener: OnListener): void;
430
+ onWebSocket({ ws, message, pathname, token, id }: OnWebSocketOptions): void;
431
+ onWsClose<T = {}>(ws: WS<T>): void;
432
+ sendConnected<T = {}>(ws: WS<T>): void;
433
+ }
434
+ type OnWebSocketOptions<T = {}> = {
435
+ ws: WS<T>;
436
+ message: string | Buffer;
437
+ pathname: string;
438
+ token?: string;
439
+ id?: string;
440
+ };
441
+ type WS<T = {}> = {
442
+ send: (data: any) => void;
443
+ close: (code?: number, reason?: string) => void;
444
+ data?: {
445
+ url: URL;
446
+ pathname: string;
447
+ token?: string;
448
+ id?: string;
449
+ /**
450
+ * 鉴权后的获取的信息
451
+ */
452
+ userApp?: string;
453
+ } & T;
454
+ };
455
+ type Listener = {
456
+ id?: string;
457
+ io?: boolean;
458
+ path?: string;
459
+ func: WebSocketListenerFun | HttpListenerFun;
460
+ /**
461
+ * @description 是否默认解析为 JSON,如果为 true,则 message 会被 JSON.parse 处理,默认是 true
462
+ */
463
+ json?: boolean;
464
+ };
465
+ type WebSocketListenerFun = (req: WebSocketReq, res: WebSocketRes) => Promise<void> | void;
466
+ type HttpListenerFun = (req: RouterReq, res: RouterRes) => Promise<void> | void;
467
+ type WebSocketReq<T = {}, U = Record<string, any>> = {
468
+ emitter?: EventEmitter;
469
+ ws: WS<T>;
470
+ data?: U;
471
+ message?: string | Buffer;
472
+ pathname?: string;
473
+ token?: string;
474
+ id?: string;
475
+ };
476
+ type WebSocketRes = {
477
+ end: (data: any) => void;
478
+ };
479
+ type ListenerFun = WebSocketListenerFun | HttpListenerFun;
480
+ type OnListener = Listener | ListenerFun | (Listener | ListenerFun)[];
481
+ type RouterReq<T = {}> = {
482
+ url: string;
483
+ method: string;
484
+ headers: Record<string, string>;
485
+ socket?: {
486
+ remoteAddress?: string;
487
+ remotePort?: number;
488
+ };
489
+ body?: string;
490
+ cookies?: Record<string, string>;
491
+ bun?: {
492
+ request: Bun.BunRequest;
493
+ server: Bun.Server<{}>;
494
+ resolve: (response: Response) => void;
495
+ };
496
+ on: (event: 'close', listener: Function) => void;
497
+ } & T;
498
+ type RouterRes<T = {}> = {
499
+ statusCode: number;
500
+ headersSent: boolean;
501
+ _headers: Record<string, string | string[]>;
502
+ _bodyChunks: any[];
503
+ writableEnded: boolean;
504
+ writeHead: (statusCode: number, headers?: Record<string, string>) => void;
505
+ setHeader: (name: string, value: string | string[]) => void;
506
+ cookie: (name: string, value: string, options?: any) => void;
507
+ write: (chunk: any) => void;
508
+ pipe: (stream: ReadableStream) => void;
509
+ end: (data?: any) => void;
510
+ } & T;
511
+
512
+ interface StringifyOptions {
513
+ /**
514
+ * Specifies a function that will be used to encode a [cookie-value](https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1).
515
+ * Since value of a cookie has a limited character set (and must be a simple string), this function can be used to encode
516
+ * a value into a string suited for a cookie's value, and should mirror `decode` when parsing.
517
+ *
518
+ * @default encodeURIComponent
519
+ */
520
+ encode?: (str: string) => string;
521
+ }
522
+ /**
523
+ * Set-Cookie object.
524
+ */
525
+ interface SetCookie {
526
+ /**
527
+ * Specifies the name of the cookie.
528
+ */
529
+ name: string;
530
+ /**
531
+ * Specifies the string to be the value for the cookie.
532
+ */
533
+ value: string | undefined;
534
+ /**
535
+ * Specifies the `number` (in seconds) to be the value for the [`Max-Age` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.2).
536
+ *
537
+ * The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and
538
+ * `maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this,
539
+ * so if both are set, they should point to the same date and time.
540
+ */
541
+ maxAge?: number;
542
+ /**
543
+ * Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.1).
544
+ * When no expiration is set, clients consider this a "non-persistent cookie" and delete it when the current session is over.
545
+ *
546
+ * The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and
547
+ * `maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this,
548
+ * so if both are set, they should point to the same date and time.
549
+ */
550
+ expires?: Date;
551
+ /**
552
+ * Specifies the value for the [`Domain` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3).
553
+ * When no domain is set, clients consider the cookie to apply to the current domain only.
554
+ */
555
+ domain?: string;
556
+ /**
557
+ * Specifies the value for the [`Path` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4).
558
+ * When no path is set, the path is considered the ["default path"](https://tools.ietf.org/html/rfc6265#section-5.1.4).
559
+ */
560
+ path?: string;
561
+ /**
562
+ * Enables the [`HttpOnly` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6).
563
+ * When enabled, clients will not allow client-side JavaScript to see the cookie in `document.cookie`.
564
+ */
565
+ httpOnly?: boolean;
566
+ /**
567
+ * Enables the [`Secure` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5).
568
+ * When enabled, clients will only send the cookie back if the browser has an HTTPS connection.
569
+ */
570
+ secure?: boolean;
571
+ /**
572
+ * Enables the [`Partitioned` `Set-Cookie` attribute](https://tools.ietf.org/html/draft-cutler-httpbis-partitioned-cookies/).
573
+ * When enabled, clients will only send the cookie back when the current domain _and_ top-level domain matches.
574
+ *
575
+ * This is an attribute that has not yet been fully standardized, and may change in the future.
576
+ * This also means clients may ignore this attribute until they understand it. More information
577
+ * about can be found in [the proposal](https://github.com/privacycg/CHIPS).
578
+ */
579
+ partitioned?: boolean;
580
+ /**
581
+ * Specifies the value for the [`Priority` `Set-Cookie` attribute](https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1).
582
+ *
583
+ * - `'low'` will set the `Priority` attribute to `Low`.
584
+ * - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set.
585
+ * - `'high'` will set the `Priority` attribute to `High`.
586
+ *
587
+ * More information about priority levels can be found in [the specification](https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1).
588
+ */
589
+ priority?: "low" | "medium" | "high";
590
+ /**
591
+ * Specifies the value for the [`SameSite` `Set-Cookie` attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7).
592
+ *
593
+ * - `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
594
+ * - `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement.
595
+ * - `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
596
+ * - `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
597
+ *
598
+ * More information about enforcement levels can be found in [the specification](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7).
599
+ */
600
+ sameSite?: boolean | "lax" | "strict" | "none";
601
+ }
602
+ /**
603
+ * Backward compatibility serialize options.
604
+ */
605
+ type SerializeOptions = StringifyOptions & Omit<SetCookie, "name" | "value">;
606
+
607
+ type CookieFn = (name: string, value: string, options?: SerializeOptions, end?: boolean) => void;
608
+ type HandleCtx = {
609
+ req: IncomingMessage & {
610
+ cookies: Record<string, string>;
611
+ };
612
+ res: ServerResponse & {
613
+ /**
614
+ * cookie 函数, end 参数用于设置是否立即设置到响应头,设置了后面的cookie再设置会覆盖前面的
615
+ */
616
+ cookie: CookieFn;
617
+ };
618
+ };
619
+
620
+ type ServerNodeOpts = ServerOpts<{
621
+ httpType?: 'http' | 'https' | 'http2';
622
+ httpsKey?: string;
623
+ httpsCert?: string;
624
+ }>;
625
+
626
+ type RouterHandle = (msg: {
627
+ path: string;
628
+ [key: string]: any;
629
+ }) => {
630
+ code: string;
631
+ data?: any;
632
+ message?: string;
633
+ [key: string]: any;
634
+ };
635
+ type AppOptions<T = {}> = {
636
+ router?: QueryRouter;
637
+ server?: ServerType;
638
+ /** handle msg 关联 */
639
+ routerHandle?: RouterHandle;
640
+ routerContext?: RouteContext<T>;
641
+ serverOptions?: ServerNodeOpts;
642
+ appId?: string;
643
+ };
644
+ type AppRouteContext<T = {}> = HandleCtx & RouteContext<T> & {
645
+ app: App<T>;
646
+ };
647
+ /**
648
+ * 封装了 Router 和 Server 的 App 模块,处理http的请求和响应,内置了 Cookie 和 Token 和 res 的处理
649
+ * U - Route Context的扩展类型
650
+ */
651
+ declare class App<U = {}> extends QueryRouter {
652
+ appId: string;
653
+ router: QueryRouter;
654
+ server: ServerType;
655
+ constructor(opts?: AppOptions<U>);
656
+ listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
657
+ listen(port: number, hostname?: string, listeningListener?: () => void): void;
658
+ listen(port: number, backlog?: number, listeningListener?: () => void): void;
659
+ listen(port: number, listeningListener?: () => void): void;
660
+ listen(path: string, backlog?: number, listeningListener?: () => void): void;
661
+ listen(path: string, listeningListener?: () => void): void;
662
+ listen(handle: any, backlog?: number, listeningListener?: () => void): void;
663
+ listen(handle: any, listeningListener?: () => void): void;
664
+ addRoute(route: Route, opts?: AddOpts): void;
665
+ Route: typeof Route;
666
+ route(opts: RouteOpts<AppRouteContext<U>>): Route<AppRouteContext<U>>;
667
+ route(path: string, key?: string): Route<AppRouteContext<U>>;
668
+ route(path: string, opts?: RouteOpts<AppRouteContext<U>>): Route<AppRouteContext<U>>;
669
+ route(path: string, key?: string, opts?: RouteOpts<AppRouteContext<U>>): Route<AppRouteContext<U>>;
670
+ prompt(description: string): Route<AppRouteContext<U>>;
671
+ prompt(description: Function): Route<AppRouteContext<U>>;
672
+ call(message: {
673
+ id?: string;
674
+ path?: string;
675
+ key?: string;
676
+ payload?: any;
677
+ }, ctx?: AppRouteContext<U> & {
678
+ [key: string]: any;
679
+ }): Promise<any>;
680
+ run(msg: {
681
+ id?: string;
682
+ path?: string;
683
+ key?: string;
684
+ payload?: any;
685
+ }, ctx?: Partial<AppRouteContext<U>> & {
686
+ [key: string]: any;
687
+ }): Promise<{
688
+ code: any;
689
+ data: any;
690
+ message: any;
691
+ }>;
692
+ static handleRequest(req: IncomingMessage$1, res: ServerResponse$1): Promise<{
693
+ cookies: Record<string, string>;
694
+ token: string;
695
+ }>;
696
+ onServerRequest(fn: (req: IncomingMessage$1, res: ServerResponse$1) => void): void;
697
+ }
698
+
699
+ declare const handleCallWsApp: (ws: ReconnectingWebSocket, app: App, message: any) => Promise<void>;
700
+ declare const handleCallApp: (send: (data: any) => void, app: App, message: any) => Promise<void>;
701
+ declare class Ws {
702
+ wsClient: ReconnectingWebSocket;
703
+ app: App;
704
+ showLog: boolean;
705
+ constructor(opts?: ReconnectConfig & {
706
+ url: string;
707
+ app: App;
708
+ showLog?: boolean;
709
+ handleMessage?: (ws: ReconnectingWebSocket, app: App, message: any) => void;
710
+ });
711
+ send(data: any): boolean;
712
+ log(...args: any[]): void;
713
+ }
714
+
715
+ export { ReconnectingWebSocket, Ws, handleCallApp, handleCallWsApp };
716
+ export type { ReconnectConfig };
package/dist/ws.js ADDED
@@ -0,0 +1,186 @@
1
+ import WebSocket from 'ws';
2
+
3
+ /**
4
+ * 一个支持自动重连的 WebSocket 客户端。
5
+ * 在连接断开时会根据配置进行重连尝试,支持指数退避。
6
+ */
7
+ class ReconnectingWebSocket {
8
+ ws = null;
9
+ url;
10
+ config;
11
+ retryCount = 0;
12
+ reconnectTimer = null;
13
+ isManualClose = false;
14
+ messageHandlers = [];
15
+ openHandlers = [];
16
+ closeHandlers = [];
17
+ errorHandlers = [];
18
+ constructor(url, config = {}) {
19
+ this.url = url;
20
+ this.config = {
21
+ maxRetries: config.maxRetries ?? Infinity,
22
+ retryDelay: config.retryDelay ?? 1000,
23
+ maxDelay: config.maxDelay ?? 30000,
24
+ backoffMultiplier: config.backoffMultiplier ?? 2,
25
+ };
26
+ }
27
+ log(...args) {
28
+ console.log('[ReconnectingWebSocket]', ...args);
29
+ }
30
+ error(...args) {
31
+ console.error('[ReconnectingWebSocket]', ...args);
32
+ }
33
+ connect() {
34
+ if (this.ws?.readyState === WebSocket.OPEN) {
35
+ return;
36
+ }
37
+ this.log(`正在连接到 ${this.url}...`);
38
+ this.ws = new WebSocket(this.url);
39
+ this.ws.on('open', () => {
40
+ this.log('WebSocket 连接已打开');
41
+ this.retryCount = 0;
42
+ this.openHandlers.forEach(handler => handler());
43
+ this.send({ type: 'heartbeat', timestamp: new Date().toISOString() });
44
+ });
45
+ this.ws.on('message', (data) => {
46
+ this.messageHandlers.forEach(handler => {
47
+ try {
48
+ const message = JSON.parse(data.toString());
49
+ handler(message);
50
+ }
51
+ catch {
52
+ handler(data.toString());
53
+ }
54
+ });
55
+ });
56
+ this.ws.on('close', (code, reason) => {
57
+ this.log(`WebSocket 连接已关闭: code=${code}, reason=${reason.toString()}`);
58
+ this.closeHandlers.forEach(handler => handler(code, reason));
59
+ if (!this.isManualClose) {
60
+ this.scheduleReconnect();
61
+ }
62
+ });
63
+ this.ws.on('error', (error) => {
64
+ this.error('WebSocket 错误:', error.message);
65
+ this.errorHandlers.forEach(handler => handler(error));
66
+ });
67
+ }
68
+ scheduleReconnect() {
69
+ if (this.reconnectTimer) {
70
+ return;
71
+ }
72
+ if (this.retryCount >= this.config.maxRetries) {
73
+ this.error(`已达到最大重试次数 (${this.config.maxRetries}),停止重连`);
74
+ return;
75
+ }
76
+ // 计算延迟(指数退避)
77
+ const delay = Math.min(this.config.retryDelay * Math.pow(this.config.backoffMultiplier, this.retryCount), this.config.maxDelay);
78
+ this.retryCount++;
79
+ this.log(`将在 ${delay}ms 后进行第 ${this.retryCount} 次重连尝试...`);
80
+ this.reconnectTimer = setTimeout(() => {
81
+ this.reconnectTimer = null;
82
+ this.connect();
83
+ }, delay);
84
+ }
85
+ send(data) {
86
+ if (this.ws?.readyState === WebSocket.OPEN) {
87
+ this.ws.send(JSON.stringify(data));
88
+ return true;
89
+ }
90
+ this.log('WebSocket 未连接,无法发送消息');
91
+ return false;
92
+ }
93
+ onMessage(handler) {
94
+ this.messageHandlers.push(handler);
95
+ }
96
+ onOpen(handler) {
97
+ this.openHandlers.push(handler);
98
+ }
99
+ onClose(handler) {
100
+ this.closeHandlers.push(handler);
101
+ }
102
+ onError(handler) {
103
+ this.errorHandlers.push(handler);
104
+ }
105
+ close() {
106
+ this.isManualClose = true;
107
+ if (this.reconnectTimer) {
108
+ clearTimeout(this.reconnectTimer);
109
+ this.reconnectTimer = null;
110
+ }
111
+ if (this.ws) {
112
+ this.ws.close();
113
+ this.ws = null;
114
+ }
115
+ }
116
+ getReadyState() {
117
+ return this.ws?.readyState ?? WebSocket.CLOSED;
118
+ }
119
+ getRetryCount() {
120
+ return this.retryCount;
121
+ }
122
+ }
123
+ // const ws = new ReconnectingWebSocket('ws://localhost:51516/livecode/ws?id=test-live-app', {
124
+ // maxRetries: Infinity, // 无限重试
125
+ // retryDelay: 1000, // 初始重试延迟 1 秒
126
+ // maxDelay: 30000, // 最大延迟 30 秒
127
+ // backoffMultiplier: 2, // 指数退避倍数
128
+ // });
129
+
130
+ const handleCallWsApp = async (ws, app, message) => {
131
+ return handleCallApp((data) => {
132
+ ws.send(data);
133
+ }, app, message);
134
+ };
135
+ const handleCallApp = async (send, app, message) => {
136
+ if (message.type === 'router' && message.id) {
137
+ const data = message?.data;
138
+ if (!message.id) {
139
+ console.error('Message id is required for router type');
140
+ return;
141
+ }
142
+ if (!data) {
143
+ send({
144
+ type: 'router',
145
+ id: message.id,
146
+ data: { code: 500, message: 'No data received' }
147
+ });
148
+ return;
149
+ }
150
+ const { tokenUser, ...rest } = data || {};
151
+ const res = await app.run(rest, {
152
+ state: { tokenUser },
153
+ appId: app.appId,
154
+ });
155
+ send({
156
+ type: 'router',
157
+ id: message.id,
158
+ data: res
159
+ });
160
+ }
161
+ };
162
+ class Ws {
163
+ wsClient;
164
+ app;
165
+ showLog = true;
166
+ constructor(opts) {
167
+ const { url, app, showLog = true, handleMessage = handleCallWsApp, ...rest } = opts;
168
+ this.wsClient = new ReconnectingWebSocket(url, rest);
169
+ this.app = app;
170
+ this.showLog = showLog;
171
+ this.wsClient.connect();
172
+ const onMessage = async (data) => {
173
+ return handleMessage(this.wsClient, this.app, data);
174
+ };
175
+ this.wsClient.onMessage(onMessage);
176
+ }
177
+ send(data) {
178
+ return this.wsClient.send(data);
179
+ }
180
+ log(...args) {
181
+ if (this.showLog)
182
+ console.log('[Ws]', ...args);
183
+ }
184
+ }
185
+
186
+ export { ReconnectingWebSocket, Ws, handleCallApp, handleCallWsApp };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@kevisual/router",
4
- "version": "0.0.66",
4
+ "version": "0.0.67",
5
5
  "description": "",
6
6
  "type": "module",
7
7
  "main": "./dist/router.js",
@@ -24,17 +24,18 @@
24
24
  "packageManager": "pnpm@10.28.2",
25
25
  "devDependencies": {
26
26
  "@kevisual/context": "^0.0.4",
27
+ "@kevisual/dts": "^0.0.3",
27
28
  "@kevisual/js-filter": "^0.0.5",
28
29
  "@kevisual/local-proxy": "^0.0.8",
29
- "@kevisual/query": "^0.0.38",
30
- "@kevisual/use-config": "^1.0.28",
31
- "@opencode-ai/plugin": "^1.1.47",
30
+ "@kevisual/query": "^0.0.39",
31
+ "@kevisual/use-config": "^1.0.30",
32
+ "@opencode-ai/plugin": "^1.1.48",
32
33
  "@rollup/plugin-alias": "^6.0.0",
33
34
  "@rollup/plugin-commonjs": "29.0.0",
34
35
  "@rollup/plugin-node-resolve": "^16.0.3",
35
36
  "@rollup/plugin-typescript": "^12.3.0",
36
37
  "@types/bun": "^1.3.8",
37
- "@types/node": "^25.1.0",
38
+ "@types/node": "^25.2.0",
38
39
  "@types/send": "^1.2.1",
39
40
  "@types/ws": "^8.18.1",
40
41
  "@types/xml2js": "^0.4.14",
@@ -52,16 +53,14 @@
52
53
  "typescript": "^5.9.3",
53
54
  "ws": "npm:@kevisual/ws",
54
55
  "xml2js": "^0.6.2",
55
- "zod": "^4.3.6"
56
+ "zod": "^4.3.6",
57
+ "hono": "^4.11.7"
56
58
  },
57
59
  "repository": {
58
60
  "type": "git",
59
61
  "url": "git+https://github.com/abearxiong/kevisual-router.git"
60
62
  },
61
- "dependencies": {
62
- "@kevisual/dts": "^0.0.3",
63
- "hono": "^4.11.7"
64
- },
63
+ "dependencies": {},
65
64
  "publishConfig": {
66
65
  "access": "public"
67
66
  },
@@ -88,6 +87,7 @@
88
87
  "require": "./dist/router-define.js",
89
88
  "types": "./dist/router-define.d.ts"
90
89
  },
90
+ "./ws": "./dist/ws.js",
91
91
  "./mod.ts": {
92
92
  "import": "./mod.ts",
93
93
  "require": "./mod.ts",
@@ -102,4 +102,4 @@
102
102
  "require": "./src/modules/*"
103
103
  }
104
104
  }
105
- }
105
+ }
@@ -0,0 +1,170 @@
1
+ import WebSocket from 'ws';
2
+
3
+ export type ReconnectConfig = {
4
+ /**
5
+ * 重连配置选项, 最大重试次数,默认无限
6
+ */
7
+ maxRetries?: number;
8
+ /**
9
+ * 重连配置选项, 重试延迟(ms),默认1000
10
+ */
11
+ retryDelay?: number;
12
+ /**
13
+ * 重连配置选项, 最大延迟(ms),默认30000
14
+ */
15
+ maxDelay?: number;
16
+ /**
17
+ * 重连配置选项, 退避倍数,默认2
18
+ */
19
+ backoffMultiplier?: number;
20
+ };
21
+
22
+ /**
23
+ * 一个支持自动重连的 WebSocket 客户端。
24
+ * 在连接断开时会根据配置进行重连尝试,支持指数退避。
25
+ */
26
+ export class ReconnectingWebSocket {
27
+ private ws: WebSocket | null = null;
28
+ private url: string;
29
+ private config: Required<ReconnectConfig>;
30
+ private retryCount: number = 0;
31
+ private reconnectTimer: NodeJS.Timeout | null = null;
32
+ private isManualClose: boolean = false;
33
+ private messageHandlers: Array<(data: any) => void> = [];
34
+ private openHandlers: Array<() => void> = [];
35
+ private closeHandlers: Array<(code: number, reason: Buffer) => void> = [];
36
+ private errorHandlers: Array<(error: Error) => void> = [];
37
+
38
+ constructor(url: string, config: ReconnectConfig = {}) {
39
+ this.url = url;
40
+ this.config = {
41
+ maxRetries: config.maxRetries ?? Infinity,
42
+ retryDelay: config.retryDelay ?? 1000,
43
+ maxDelay: config.maxDelay ?? 30000,
44
+ backoffMultiplier: config.backoffMultiplier ?? 2,
45
+ };
46
+ }
47
+ log(...args: any[]): void {
48
+ console.log('[ReconnectingWebSocket]', ...args);
49
+ }
50
+ error(...args: any[]): void {
51
+ console.error('[ReconnectingWebSocket]', ...args);
52
+ }
53
+ connect(): void {
54
+ if (this.ws?.readyState === WebSocket.OPEN) {
55
+ return;
56
+ }
57
+
58
+ this.log(`正在连接到 ${this.url}...`);
59
+ this.ws = new WebSocket(this.url);
60
+
61
+ this.ws.on('open', () => {
62
+ this.log('WebSocket 连接已打开');
63
+ this.retryCount = 0;
64
+ this.openHandlers.forEach(handler => handler());
65
+ this.send({ type: 'heartbeat', timestamp: new Date().toISOString() });
66
+ });
67
+
68
+ this.ws.on('message', (data: any) => {
69
+ this.messageHandlers.forEach(handler => {
70
+ try {
71
+ const message = JSON.parse(data.toString());
72
+ handler(message);
73
+ } catch {
74
+ handler(data.toString());
75
+ }
76
+ });
77
+ });
78
+
79
+ this.ws.on('close', (code: number, reason: Buffer) => {
80
+ this.log(`WebSocket 连接已关闭: code=${code}, reason=${reason.toString()}`);
81
+ this.closeHandlers.forEach(handler => handler(code, reason));
82
+
83
+ if (!this.isManualClose) {
84
+ this.scheduleReconnect();
85
+ }
86
+ });
87
+
88
+ this.ws.on('error', (error: Error) => {
89
+ this.error('WebSocket 错误:', error.message);
90
+ this.errorHandlers.forEach(handler => handler(error));
91
+ });
92
+ }
93
+
94
+ private scheduleReconnect(): void {
95
+ if (this.reconnectTimer) {
96
+ return;
97
+ }
98
+
99
+ if (this.retryCount >= this.config.maxRetries) {
100
+ this.error(`已达到最大重试次数 (${this.config.maxRetries}),停止重连`);
101
+ return;
102
+ }
103
+
104
+ // 计算延迟(指数退避)
105
+ const delay = Math.min(
106
+ this.config.retryDelay * Math.pow(this.config.backoffMultiplier, this.retryCount),
107
+ this.config.maxDelay
108
+ );
109
+
110
+ this.retryCount++;
111
+ this.log(`将在 ${delay}ms 后进行第 ${this.retryCount} 次重连尝试...`);
112
+
113
+ this.reconnectTimer = setTimeout(() => {
114
+ this.reconnectTimer = null;
115
+ this.connect();
116
+ }, delay);
117
+ }
118
+
119
+ send(data: any): boolean {
120
+ if (this.ws?.readyState === WebSocket.OPEN) {
121
+ this.ws.send(JSON.stringify(data));
122
+ return true;
123
+ }
124
+ this.log('WebSocket 未连接,无法发送消息');
125
+ return false;
126
+ }
127
+
128
+ onMessage(handler: (data: any) => void): void {
129
+ this.messageHandlers.push(handler);
130
+ }
131
+
132
+ onOpen(handler: () => void): void {
133
+ this.openHandlers.push(handler);
134
+ }
135
+
136
+ onClose(handler: (code: number, reason: Buffer) => void): void {
137
+ this.closeHandlers.push(handler);
138
+ }
139
+
140
+ onError(handler: (error: Error) => void): void {
141
+ this.errorHandlers.push(handler);
142
+ }
143
+
144
+ close(): void {
145
+ this.isManualClose = true;
146
+ if (this.reconnectTimer) {
147
+ clearTimeout(this.reconnectTimer);
148
+ this.reconnectTimer = null;
149
+ }
150
+ if (this.ws) {
151
+ this.ws.close();
152
+ this.ws = null;
153
+ }
154
+ }
155
+
156
+ getReadyState(): number {
157
+ return this.ws?.readyState ?? WebSocket.CLOSED;
158
+ }
159
+
160
+ getRetryCount(): number {
161
+ return this.retryCount;
162
+ }
163
+ }
164
+
165
+ // const ws = new ReconnectingWebSocket('ws://localhost:51516/livecode/ws?id=test-live-app', {
166
+ // maxRetries: Infinity, // 无限重试
167
+ // retryDelay: 1000, // 初始重试延迟 1 秒
168
+ // maxDelay: 30000, // 最大延迟 30 秒
169
+ // backoffMultiplier: 2, // 指数退避倍数
170
+ // });
package/src/ws.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { ReconnectingWebSocket, ReconnectConfig } from "./server/reconnect-ws.ts";
2
+
3
+ export * from "./server/reconnect-ws.ts";
4
+ import type { App } from "./app.ts";
5
+
6
+ export const handleCallWsApp = async (ws: ReconnectingWebSocket, app: App, message: any) => {
7
+ return handleCallApp((data: any) => {
8
+ ws.send(data);
9
+ }, app, message);
10
+ }
11
+ export const handleCallApp = async (send: (data: any) => void, app: App, message: any) => {
12
+ if (message.type === 'router' && message.id) {
13
+ const data = message?.data;
14
+ if (!message.id) {
15
+ console.error('Message id is required for router type');
16
+ return;
17
+ }
18
+ if (!data) {
19
+ send({
20
+ type: 'router',
21
+ id: message.id,
22
+ data: { code: 500, message: 'No data received' }
23
+ });
24
+ return;
25
+ }
26
+ const { tokenUser, ...rest } = data || {};
27
+ const res = await app.run(rest, {
28
+ state: { tokenUser },
29
+ appId: app.appId,
30
+ });
31
+ send({
32
+ type: 'router',
33
+ id: message.id,
34
+ data: res
35
+ });
36
+ }
37
+ }
38
+ export class Ws {
39
+ wsClient: ReconnectingWebSocket;
40
+ app: App;
41
+ showLog: boolean = true;
42
+ constructor(opts?: ReconnectConfig & {
43
+ url: string;
44
+ app: App;
45
+ showLog?: boolean;
46
+ handleMessage?: (ws: ReconnectingWebSocket, app: App, message: any) => void;
47
+ }) {
48
+ const { url, app, showLog = true, handleMessage = handleCallWsApp, ...rest } = opts;
49
+ this.wsClient = new ReconnectingWebSocket(url, rest);
50
+ this.app = app;
51
+ this.showLog = showLog;
52
+ this.wsClient.connect();
53
+ const onMessage = async (data: any) => {
54
+ return handleMessage(this.wsClient, this.app, data);
55
+ }
56
+ this.wsClient.onMessage(onMessage);
57
+ }
58
+ send(data: any): boolean {
59
+ return this.wsClient.send(data);
60
+ }
61
+ log(...args: any[]): void {
62
+ if (this.showLog)
63
+ console.log('[Ws]', ...args);
64
+ }
65
+ }