@yumerijs/core 3.0.3 → 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,4 +1,5 @@
1
1
  import path from 'path';
2
+ import { setInterval as nodeSetInterval, clearInterval as nodeClearInterval, setTimeout as nodeSetTimeout, clearTimeout as nodeClearTimeout } from 'timers';
2
3
  /**
3
4
  * 插件上下文对象
4
5
  * 每个插件一个 Context,用于管理插件注册的命令、路由、事件、组件和中间件
@@ -15,6 +16,8 @@ export class Context {
15
16
  i18ns = [];
16
17
  affects = [];
17
18
  services = [];
19
+ timers = new Set();
20
+ disposed = false;
18
21
  component;
19
22
  renderer;
20
23
  module;
@@ -50,6 +53,113 @@ export class Context {
50
53
  return;
51
54
  this.affects.push(callback);
52
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
+ }
53
163
  /**
54
164
  * 注册路由
55
165
  * @param path 路由路径
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yumerijs/core",
3
- "version": "3.0.3",
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",