@yumerijs/core 3.0.2 → 3.0.4
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/context.d.ts +54 -0
- package/dist/context.js +118 -3
- package/dist/core.js +5 -1
- package/dist/route.d.ts +8 -2
- package/dist/route.js +27 -12
- package/package.json +1 -1
package/dist/context.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare class Context {
|
|
|
24
24
|
private i18ns;
|
|
25
25
|
private affects;
|
|
26
26
|
private services;
|
|
27
|
+
private timers;
|
|
28
|
+
private disposed;
|
|
27
29
|
component: Components;
|
|
28
30
|
renderer?: IRenderer;
|
|
29
31
|
module: any;
|
|
@@ -47,6 +49,58 @@ export declare class Context {
|
|
|
47
49
|
* @param callback 销毁回调
|
|
48
50
|
*/
|
|
49
51
|
affect(callback: () => void | Promise<void>): void;
|
|
52
|
+
/**
|
|
53
|
+
* 注册重复定时器(Node 原生 setInterval 的包装)
|
|
54
|
+
*
|
|
55
|
+
* 参数与行为和原生 setInterval 完全一致,额外保证插件卸载后不留副作用:
|
|
56
|
+
* - 定时器句柄会被 Context 记录,插件卸载(dispose)时统一清理,
|
|
57
|
+
* 不会留下继续运行、拖住事件循环的“幽灵定时器”
|
|
58
|
+
* - 卸载后即使还有已经排队的 tick,包装层也会拦截,不再调用插件回调
|
|
59
|
+
* - 回调中的同步异常与 async 回调的 Promise 拒绝会被捕获并写入日志,
|
|
60
|
+
* 不会变成 uncaughtException / unhandledRejection 影响整个进程
|
|
61
|
+
*
|
|
62
|
+
* @param callback 定时执行的回调
|
|
63
|
+
* @param ms 间隔毫秒数
|
|
64
|
+
* @param args 原样透传给回调的额外参数
|
|
65
|
+
* @returns 定时器句柄,可用 ctx.clearInterval 或原生 clearInterval 取消;
|
|
66
|
+
* 若 Context 已卸载则返回 undefined(此时不会创建定时器)
|
|
67
|
+
*/
|
|
68
|
+
setInterval(callback: (...args: any[]) => any, ms?: number, ...args: any[]): NodeJS.Timeout | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* 注册一次性定时器(Node 原生 setTimeout 的包装)
|
|
71
|
+
*
|
|
72
|
+
* 行为与原生 setTimeout 一致,卸载保证同 setInterval:
|
|
73
|
+
* 卸载时清理尚未触发的句柄,已排队但尚未执行的回调同样会被拦截。
|
|
74
|
+
*
|
|
75
|
+
* @param callback 延时执行的回调
|
|
76
|
+
* @param ms 延时毫秒数
|
|
77
|
+
* @param args 原样透传给回调的额外参数
|
|
78
|
+
* @returns 定时器句柄,可用 ctx.clearTimeout 或原生 clearTimeout 取消;
|
|
79
|
+
* 若 Context 已卸载则返回 undefined(此时不会创建定时器)
|
|
80
|
+
*/
|
|
81
|
+
setTimeout(callback: (...args: any[]) => any, ms?: number, ...args: any[]): NodeJS.Timeout | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* 取消由 setInterval 创建的定时器(Node 原生 clearInterval 的包装)
|
|
84
|
+
* @param timer 定时器句柄
|
|
85
|
+
*/
|
|
86
|
+
clearInterval(timer?: NodeJS.Timeout | number | null): void;
|
|
87
|
+
/**
|
|
88
|
+
* 取消由 setTimeout 创建的定时器(Node 原生 clearTimeout 的包装)
|
|
89
|
+
* @param timer 定时器句柄
|
|
90
|
+
*/
|
|
91
|
+
clearTimeout(timer?: NodeJS.Timeout | number | null): void;
|
|
92
|
+
/**
|
|
93
|
+
* 校验回调参数并确认 Context 尚未卸载
|
|
94
|
+
* @param kind 定时器类型,仅用于日志
|
|
95
|
+
* @param callback 用户回调
|
|
96
|
+
*/
|
|
97
|
+
private prepareTimer;
|
|
98
|
+
/**
|
|
99
|
+
* 调用插件定时器回调,并捕获同步异常与 async 拒绝
|
|
100
|
+
* @param callback 用户回调
|
|
101
|
+
* @param args 透传给回调的参数
|
|
102
|
+
*/
|
|
103
|
+
private invokeTimerCallback;
|
|
50
104
|
/**
|
|
51
105
|
* 注册路由
|
|
52
106
|
* @param path 路由路径
|
package/dist/context.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Route } from './route.js';
|
|
2
1
|
import path from 'path';
|
|
2
|
+
import { setInterval as nodeSetInterval, clearInterval as nodeClearInterval, setTimeout as nodeSetTimeout, clearTimeout as nodeClearTimeout } from 'timers';
|
|
3
3
|
/**
|
|
4
4
|
* 插件上下文对象
|
|
5
5
|
* 每个插件一个 Context,用于管理插件注册的命令、路由、事件、组件和中间件
|
|
@@ -16,6 +16,8 @@ export class Context {
|
|
|
16
16
|
i18ns = [];
|
|
17
17
|
affects = [];
|
|
18
18
|
services = [];
|
|
19
|
+
timers = new Set();
|
|
20
|
+
disposed = false;
|
|
19
21
|
component;
|
|
20
22
|
renderer;
|
|
21
23
|
module;
|
|
@@ -51,6 +53,113 @@ export class Context {
|
|
|
51
53
|
return;
|
|
52
54
|
this.affects.push(callback);
|
|
53
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* 注册重复定时器(Node 原生 setInterval 的包装)
|
|
58
|
+
*
|
|
59
|
+
* 参数与行为和原生 setInterval 完全一致,额外保证插件卸载后不留副作用:
|
|
60
|
+
* - 定时器句柄会被 Context 记录,插件卸载(dispose)时统一清理,
|
|
61
|
+
* 不会留下继续运行、拖住事件循环的“幽灵定时器”
|
|
62
|
+
* - 卸载后即使还有已经排队的 tick,包装层也会拦截,不再调用插件回调
|
|
63
|
+
* - 回调中的同步异常与 async 回调的 Promise 拒绝会被捕获并写入日志,
|
|
64
|
+
* 不会变成 uncaughtException / unhandledRejection 影响整个进程
|
|
65
|
+
*
|
|
66
|
+
* @param callback 定时执行的回调
|
|
67
|
+
* @param ms 间隔毫秒数
|
|
68
|
+
* @param args 原样透传给回调的额外参数
|
|
69
|
+
* @returns 定时器句柄,可用 ctx.clearInterval 或原生 clearInterval 取消;
|
|
70
|
+
* 若 Context 已卸载则返回 undefined(此时不会创建定时器)
|
|
71
|
+
*/
|
|
72
|
+
setInterval(callback, ms, ...args) {
|
|
73
|
+
if (!this.prepareTimer('interval', callback))
|
|
74
|
+
return undefined;
|
|
75
|
+
const timer = nodeSetInterval(() => {
|
|
76
|
+
// 兜底:卸载后可能仍有已经排队的 tick,直接丢弃
|
|
77
|
+
if (this.disposed)
|
|
78
|
+
return;
|
|
79
|
+
this.invokeTimerCallback(callback, args);
|
|
80
|
+
}, ms);
|
|
81
|
+
this.timers.add(timer);
|
|
82
|
+
return timer;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 注册一次性定时器(Node 原生 setTimeout 的包装)
|
|
86
|
+
*
|
|
87
|
+
* 行为与原生 setTimeout 一致,卸载保证同 setInterval:
|
|
88
|
+
* 卸载时清理尚未触发的句柄,已排队但尚未执行的回调同样会被拦截。
|
|
89
|
+
*
|
|
90
|
+
* @param callback 延时执行的回调
|
|
91
|
+
* @param ms 延时毫秒数
|
|
92
|
+
* @param args 原样透传给回调的额外参数
|
|
93
|
+
* @returns 定时器句柄,可用 ctx.clearTimeout 或原生 clearTimeout 取消;
|
|
94
|
+
* 若 Context 已卸载则返回 undefined(此时不会创建定时器)
|
|
95
|
+
*/
|
|
96
|
+
setTimeout(callback, ms, ...args) {
|
|
97
|
+
if (!this.prepareTimer('timeout', callback))
|
|
98
|
+
return undefined;
|
|
99
|
+
const timer = nodeSetTimeout(() => {
|
|
100
|
+
// 触发过的句柄不必再被追踪,先摘掉再执行回调
|
|
101
|
+
this.timers.delete(timer);
|
|
102
|
+
if (this.disposed)
|
|
103
|
+
return;
|
|
104
|
+
this.invokeTimerCallback(callback, args);
|
|
105
|
+
}, ms);
|
|
106
|
+
this.timers.add(timer);
|
|
107
|
+
return timer;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 取消由 setInterval 创建的定时器(Node 原生 clearInterval 的包装)
|
|
111
|
+
* @param timer 定时器句柄
|
|
112
|
+
*/
|
|
113
|
+
clearInterval(timer) {
|
|
114
|
+
if (timer === undefined || timer === null)
|
|
115
|
+
return;
|
|
116
|
+
this.timers.delete(timer);
|
|
117
|
+
nodeClearInterval(timer);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 取消由 setTimeout 创建的定时器(Node 原生 clearTimeout 的包装)
|
|
121
|
+
* @param timer 定时器句柄
|
|
122
|
+
*/
|
|
123
|
+
clearTimeout(timer) {
|
|
124
|
+
if (timer === undefined || timer === null)
|
|
125
|
+
return;
|
|
126
|
+
this.timers.delete(timer);
|
|
127
|
+
nodeClearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 校验回调参数并确认 Context 尚未卸载
|
|
131
|
+
* @param kind 定时器类型,仅用于日志
|
|
132
|
+
* @param callback 用户回调
|
|
133
|
+
*/
|
|
134
|
+
prepareTimer(kind, callback) {
|
|
135
|
+
if (typeof callback !== 'function') {
|
|
136
|
+
throw new TypeError('The "callback" argument must be of type function.');
|
|
137
|
+
}
|
|
138
|
+
if (this.disposed) {
|
|
139
|
+
this.core.logger.warn(`Plugin "${this.pluginname}" attempt to create a ${kind} after its context was disposed, ignored.`);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 调用插件定时器回调,并捕获同步异常与 async 拒绝
|
|
146
|
+
* @param callback 用户回调
|
|
147
|
+
* @param args 透传给回调的参数
|
|
148
|
+
*/
|
|
149
|
+
invokeTimerCallback(callback, args) {
|
|
150
|
+
try {
|
|
151
|
+
const result = callback(...args);
|
|
152
|
+
// 兼容 async 回调,避免未处理的 Promise 拒绝
|
|
153
|
+
if (result && typeof result.then === 'function') {
|
|
154
|
+
Promise.resolve(result).catch((error) => {
|
|
155
|
+
this.core.logger.error(`Unhandled rejection in timer callback of plugin "${this.pluginname}":`, error);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
this.core.logger.error(`Unhandled error in timer callback of plugin "${this.pluginname}":`, error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
54
163
|
/**
|
|
55
164
|
* 注册路由
|
|
56
165
|
* @param path 路由路径
|
|
@@ -63,8 +172,9 @@ export class Context {
|
|
|
63
172
|
? 'root'
|
|
64
173
|
: path.posix.join(this.childpath, routepath);
|
|
65
174
|
if (this.core.routes[realpath]) {
|
|
66
|
-
|
|
67
|
-
|
|
175
|
+
// Reuse the registered route so the same path can be split across
|
|
176
|
+
// multiple declarations, e.g. one GET handler and one POST handler.
|
|
177
|
+
return this.core.routes[realpath];
|
|
68
178
|
}
|
|
69
179
|
this.routes.push(realpath);
|
|
70
180
|
return this.core.route(realpath, this);
|
|
@@ -238,6 +348,11 @@ export class Context {
|
|
|
238
348
|
* 卸载插件时清理注册的所有资源
|
|
239
349
|
*/
|
|
240
350
|
async dispose() {
|
|
351
|
+
// 先把自己标记为已卸载并停掉所有定时器(Node 里 clearTimeout/clearInterval 可互换):
|
|
352
|
+
// 这样后续拆卸过程中即使有已排队的 tick 也不会再触发插件回调
|
|
353
|
+
this.disposed = true;
|
|
354
|
+
this.timers.forEach((timer) => nodeClearTimeout(timer));
|
|
355
|
+
this.timers.clear();
|
|
241
356
|
// 删除组件
|
|
242
357
|
this.components.forEach((name) => delete this.core.components[name]);
|
|
243
358
|
// 删除服务
|
package/dist/core.js
CHANGED
|
@@ -228,6 +228,10 @@ export class Core {
|
|
|
228
228
|
const route = this.routes[routePath];
|
|
229
229
|
const result = route.match(pathname, session.client?.headers?.host);
|
|
230
230
|
if (result) {
|
|
231
|
+
const method = session.client?.req?.method ?? 'GET';
|
|
232
|
+
if (!route.hasHandler(method)) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
231
235
|
try {
|
|
232
236
|
this.emit('request:start', {
|
|
233
237
|
path: pathname,
|
|
@@ -254,7 +258,7 @@ export class Core {
|
|
|
254
258
|
}
|
|
255
259
|
else {
|
|
256
260
|
const start = Date.now();
|
|
257
|
-
await route.executeHandler(session, queryParams, result.pathParams, result.hostParams);
|
|
261
|
+
await route.executeHandler(session, queryParams, result.pathParams, result.hostParams, method);
|
|
258
262
|
this.emit('route:end', {
|
|
259
263
|
path: pathname,
|
|
260
264
|
route: routePath,
|
package/dist/route.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { Middleware } from './middleware.js';
|
|
|
8
8
|
import { WebSocketServer } from 'ws';
|
|
9
9
|
import { Context } from './context.js';
|
|
10
10
|
export type RouteHandler = (session: Session, queryParams: URLSearchParams, ...pathParams: string[]) => Promise<void> | void;
|
|
11
|
+
type RouteMethod = string | string[];
|
|
11
12
|
/**
|
|
12
13
|
* Route class using segment-based matching (no fragile capture-group-index mapping).
|
|
13
14
|
* Behavior matches the SimpleRouter in the webpage:
|
|
@@ -20,7 +21,9 @@ export declare class Route {
|
|
|
20
21
|
path: string;
|
|
21
22
|
private segments;
|
|
22
23
|
private paramsInfo;
|
|
23
|
-
private
|
|
24
|
+
private handlers;
|
|
25
|
+
private pendingActionMethods;
|
|
26
|
+
private methodsConfigured;
|
|
24
27
|
middlewares: Middleware[];
|
|
25
28
|
allowedMethods: string[];
|
|
26
29
|
ws: WebSocketServer | null;
|
|
@@ -38,6 +41,7 @@ export declare class Route {
|
|
|
38
41
|
* @returns this
|
|
39
42
|
*/
|
|
40
43
|
action(handler: RouteHandler): this;
|
|
44
|
+
action(method: RouteMethod, handler: RouteHandler): this;
|
|
41
45
|
/**
|
|
42
46
|
* 设置允许的host
|
|
43
47
|
* @param host host列表
|
|
@@ -67,7 +71,8 @@ export declare class Route {
|
|
|
67
71
|
* @param params 查询参数
|
|
68
72
|
* @param pathParams 路由参数
|
|
69
73
|
*/
|
|
70
|
-
executeHandler(session: Session, params: URLSearchParams, pathParams: string[], hostParams: Record<string, string
|
|
74
|
+
executeHandler(session: Session, params: URLSearchParams, pathParams: string[], hostParams: Record<string, string>, method?: string): Promise<void>;
|
|
75
|
+
hasHandler(method: string): boolean;
|
|
71
76
|
/**
|
|
72
77
|
* 设置可用方法
|
|
73
78
|
* @param methods 可用方法
|
|
@@ -80,3 +85,4 @@ export declare class Route {
|
|
|
80
85
|
*/
|
|
81
86
|
wsOn(event: string, handler: (...args: any[]) => void): this;
|
|
82
87
|
}
|
|
88
|
+
export {};
|
package/dist/route.js
CHANGED
|
@@ -225,7 +225,9 @@ export class Route {
|
|
|
225
225
|
path;
|
|
226
226
|
segments;
|
|
227
227
|
paramsInfo;
|
|
228
|
-
|
|
228
|
+
handlers = {};
|
|
229
|
+
pendingActionMethods = null;
|
|
230
|
+
methodsConfigured = false;
|
|
229
231
|
middlewares = [];
|
|
230
232
|
allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'];
|
|
231
233
|
ws = null;
|
|
@@ -243,13 +245,17 @@ export class Route {
|
|
|
243
245
|
this.segments = segments;
|
|
244
246
|
this.paramsInfo = params;
|
|
245
247
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
248
|
+
action(methodOrHandler, maybeHandler) {
|
|
249
|
+
const handler = typeof methodOrHandler === 'function' ? methodOrHandler : maybeHandler;
|
|
250
|
+
if (!handler)
|
|
251
|
+
return this;
|
|
252
|
+
const methods = typeof methodOrHandler === 'function'
|
|
253
|
+
? (this.pendingActionMethods ?? this.allowedMethods)
|
|
254
|
+
: (Array.isArray(methodOrHandler) ? methodOrHandler : [methodOrHandler]);
|
|
255
|
+
for (const method of methods) {
|
|
256
|
+
this.handlers[method.toUpperCase()] = handler;
|
|
257
|
+
}
|
|
258
|
+
this.pendingActionMethods = null;
|
|
253
259
|
return this;
|
|
254
260
|
}
|
|
255
261
|
/**
|
|
@@ -459,18 +465,27 @@ export class Route {
|
|
|
459
465
|
* @param params 查询参数
|
|
460
466
|
* @param pathParams 路由参数
|
|
461
467
|
*/
|
|
462
|
-
async executeHandler(session, params, pathParams, hostParams) {
|
|
463
|
-
|
|
464
|
-
|
|
468
|
+
async executeHandler(session, params, pathParams, hostParams, method) {
|
|
469
|
+
const handler = this.handlers[(method || session.client?.req?.method || 'GET').toUpperCase()];
|
|
470
|
+
if (handler) {
|
|
471
|
+
await handler(session, params, ...pathParams, ...Object.values(hostParams));
|
|
465
472
|
}
|
|
466
473
|
}
|
|
474
|
+
hasHandler(method) {
|
|
475
|
+
return !!this.handlers[method.toUpperCase()];
|
|
476
|
+
}
|
|
467
477
|
/**
|
|
468
478
|
* 设置可用方法
|
|
469
479
|
* @param methods 可用方法
|
|
470
480
|
* @returns this
|
|
471
481
|
*/
|
|
472
482
|
methods(...methods) {
|
|
473
|
-
|
|
483
|
+
const normalized = methods.map(method => method.toUpperCase());
|
|
484
|
+
this.allowedMethods = this.methodsConfigured
|
|
485
|
+
? [...new Set([...this.allowedMethods, ...normalized])]
|
|
486
|
+
: normalized;
|
|
487
|
+
this.methodsConfigured = true;
|
|
488
|
+
this.pendingActionMethods = normalized;
|
|
474
489
|
return this;
|
|
475
490
|
}
|
|
476
491
|
/**
|