@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 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
- this.core.logger.warn(`Plugin "${this.pluginname}" attempt to register route "${path}", but it has already been registered.`);
67
- return new Route(realpath, this);
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 handler;
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>): Promise<void>;
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
- handler = null;
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
- * @param handler 路由处理器
249
- * @returns this
250
- */
251
- action(handler) {
252
- this.handler = handler;
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
- if (this.handler) {
464
- await this.handler(session, params, ...pathParams, ...Object.values(hostParams));
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
- this.allowedMethods = methods;
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yumerijs/core",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "Core module for yumeri",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",