@skyroc/scheduler
协作式任务调度中枢,用单一心跳统一管理应用启动时的初始化链、定时任务和监听器,支持依赖声明、指数退避重试与完整生命周期控制
概述
@skyroc/scheduler 提供一个轻量的任务调度引擎,解决复杂应用启动时的典型困境:
❌ 认证、配置、权限、路由 … 各自 init,执行顺序靠运气
❌ 心跳、上报、轮询、token 刷新 … 每个一个 setInterval,互不感知
❌ resize、online/offline、visibilitychange … 监听器散落各处,清理全靠记忆TaskHub 的答案:一个心跳 + 一个任务注册表 + 依赖关系声明。
- 声明式注册,依赖自动解析,执行顺序由引擎保证
- 永远只有一个
setInterval,无论注册多少任务 hub.stop()一次性清理所有定时器和监听器
零框架依赖,Web / React Native / Node.js 均可使用。
架构
TaskHub.start()
│
▼
┌─ Tick Loop(单一 setInterval)──────────────────────┐
│ │
│ 遍历任务表(按 priority 升序) │
│ ├─ init: deps 全部 done + pending → 执行一次 │
│ ├─ periodic: deps 全部 done + 间隔到了 → 再次执行 │
│ └─ listener: deps 全部 done + pending → 注册一次 │
│ │
│ 检查:所有 init 完成? → 触发 onReady │
│ │
└──────────────────────────────────────────────────────┘
│
▼
TaskHub.stop() → 逆优先级顺序调用所有 cleanup → 清空任务表核心概念
三种任务类型
| 类型 | 行为 | 典型场景 |
|---|---|---|
init | 依赖满足后执行一次 | 认证、加载配置、初始化路由 |
periodic | 依赖满足后按 interval 周期执行 | 心跳、数据上报、token 刷新 |
listener | 依赖满足后注册一次,stop 时自动 cleanup | resize、网络状态、页面可见性 |
任务状态流转
pending → running → done
↘ failed → (retry) → running → done
→ (retries exhausted) → failed(不再调度)periodic 任务完成后状态重置为可再次执行,下个周期到来时再次进入 running。
onReady 触发时机
当所有 init 类型任务均到达 done 状态时,触发一次 onReady 回调。
periodic 和 listener 任务不影响 onReady。
快速上手
import { TaskHub } from '@skyroc/scheduler';
const hub = new TaskHub({
tickInterval: 1000,
onReady: () => {
console.log('所有初始化完成,应用就绪');
},
onTaskError: (name, err) => {
console.error(`任务 ${name} 失败:`, err);
}
});
// 1. 初始化任务(有依赖链)
hub.register({
name: 'auth',
type: 'init',
priority: 1,
run: async () => {
await authService.init();
}
});
hub.register({
name: 'permissions',
type: 'init',
priority: 2,
deps: ['auth'], // auth 完成后才执行
run: async () => {
await permissionService.load();
}
});
hub.register({
name: 'routes',
type: 'init',
priority: 3,
deps: ['permissions'],
run: async () => {
await routerService.initDynamicRoutes();
}
});
// 2. 周期任务
hub.register({
name: 'heartbeat',
type: 'periodic',
interval: 30_000,
deps: ['auth'], // auth 完成后才开始心跳
run: () => {
api.heartbeat();
}
});
// 3. 监听器任务
hub.register({
name: 'network-monitor',
type: 'listener',
run: () => {
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
},
cleanup: () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
}
});
// 启动
hub.start();
// 应用卸载时(React:useEffect 返回值;Vue:onUnmounted)
hub.stop();API
new TaskHub(options?)
创建调度实例。
interface TaskHubOptions {
/** 心跳间隔(ms),默认 1000 */
tickInterval?: number;
/** 失败任务最大重试次数,默认 3,设为 0 禁用重试 */
maxRetries?: number;
/** 重试基础延迟(ms),实际延迟 = baseRetryDelay * 2^retryCount,默认 1000 */
baseRetryDelay?: number;
/** 任务失败回调 */
onTaskError?: (taskName: string, error: unknown) => void;
/** 所有 init 任务完成时触发(仅触发一次) */
onReady?: () => void;
}.register(def) / .registerAll(defs)
注册任务,支持链式调用。重复注册同名任务时跳过并输出警告。
interface TaskDef {
/** 任务唯一标识 */
name: string;
/** 任务类型 */
type: 'init' | 'periodic' | 'listener';
/** 优先级,数字越小越先执行,默认 10 */
priority?: number;
/** 依赖的任务名列表,这些任务 done 后才调度当前任务 */
deps?: string[];
/** 周期间隔(ms),仅 periodic 类型有效,默认 5000 */
interval?: number;
/** 执行体,支持 async */
run: () => void | Promise<void>;
/** 清理函数,TaskHub stop 时调用 */
cleanup?: () => void;
}// 链式注册
hub
.register({ name: 'a', type: 'init', run: initA })
.register({ name: 'b', type: 'init', deps: ['a'], run: initB });
// 批量注册
hub.registerAll([taskA, taskB, taskC]);.start() / .stop()
| 方法 | 说明 |
|---|---|
start() | 启动心跳循环,立即执行首次 tick;重复调用无副作用 |
stop() | 停止心跳,按逆优先级顺序调用所有 cleanup,清空任务表,重置 readyFired |
.pause() / .resume()
| 方法 | 说明 |
|---|---|
pause() | 暂停心跳,保留全部任务状态 |
resume() | 恢复心跳,从当前任务状态继续调度 |
适合页面切到后台时暂停、切回前台时恢复的场景。
.add(def) / .remove(name)
运行时动态增删任务。
// 进入某页面时追加
hub.add({ name: 'page-poll', type: 'periodic', interval: 5000, run: pollData });
// 离开时移除(自动调用 cleanup)
hub.remove('page-poll'); // 返回 boolean,任务不存在时返回 false.snapshot() / .getTask(name)
查看任务状态,适合调试或构建可视化面板。
hub.snapshot();
// 返回值按 priority 升序排列:
// [
// { name: 'auth', type: 'init', status: 'done', lastRun: 1707820800000, deps: [], retryCount: 0 },
// { name: 'permissions', type: 'init', status: 'done', lastRun: 1707820800100, deps: ['auth'], retryCount: 0 },
// { name: 'heartbeat', type: 'periodic', status: 'done', lastRun: 1707820830000, deps: ['auth'], retryCount: 0 },
// { name: 'routes', type: 'init', status: 'failed', lastRun: 1707820800200, deps: ['perm..'], retryCount: 3, error: 'Error: timeout' },
// ]
hub.getTask('auth');
// { name: 'auth', type: 'init', status: 'done', lastRun: 1707820800000, deps: [], retryCount: 0 }
// 任务不存在时返回 undefined.running
只读属性,true 表示心跳循环当前正在运行(已 start 且未 pause)。
重试机制
失败的 init 和 listener 任务会按指数退避自动重试:
第 1 次重试:baseRetryDelay * 1 = 1s 后
第 2 次重试:baseRetryDelay * 2 = 2s 后
第 3 次重试:baseRetryDelay * 4 = 4s 后
超出次数 → 保持 failed,触发 onTaskError,不再调度periodic 任务无需重试机制——下个周期间隔到来时天然会再次执行。
设为 maxRetries: 0 彻底禁用重试:任务失败后直接保持 failed 状态。
依赖关系
通过 deps 声明任务间的前置条件,TaskHub 自动解析执行顺序,无需手动排序。
// 链式依赖:auth → permissions → routes
hub.register({ name: 'auth', type: 'init', run: ... });
hub.register({ name: 'permissions', type: 'init', deps: ['auth'], run: ... });
hub.register({ name: 'routes', type: 'init', deps: ['permissions'], run: ... });
// 共同依赖:heartbeat 和 analytics 都等 auth 完成后才启动
hub.register({ name: 'heartbeat', type: 'periodic', interval: 30_000, deps: ['auth'], run: ... });
hub.register({ name: 'analytics', type: 'periodic', interval: 60_000, deps: ['auth'], run: ... });注意: 若依赖任务 failed 且重试次数耗尽,下游任务将永久停留在 pending。可通过 snapshot() 检查上游任务状态进行排查。
与传统方式对比
| 维度 | N 个 setInterval | TaskHub |
|---|---|---|
| 依赖关系 | 无法表达 | deps 天然支持 DAG |
| 执行顺序 | 靠代码位置,容易出错 | priority + 依赖自动保证 |
| 清理 | 逐一保存 timer id,容易遗漏 | stop() 一次清理全部 |
| 暂停/恢复 | 需自行维护状态 | pause() / resume() |
| 状态观测 | 无 | snapshot() 随时看全貌 |
| Timer 数量 | 随业务线性膨胀 | 永远只有 1 个 |
| 错误处理 | 各自为政 | 统一 onTaskError |
| 重试 | 需手动实现 | 指数退避,开箱即用 |
在 React 中使用
将 TaskHub 的生命周期绑定到应用根组件:
import { useEffect } from 'react';
import { TaskHub } from '@skyroc/scheduler';
// 在模块作用域创建单例(避免 StrictMode 双调用的干扰)
const hub = new TaskHub({
tickInterval: 1000,
onReady: () => store.dispatch(setAppReady(true)),
onTaskError: (name, err) => logger.error(name, err)
});
hub.registerAll([authTask, permissionTask, heartbeatTask, networkTask]);
export function AppScheduler() {
useEffect(() => {
hub.start();
return () => hub.stop();
}, []);
return null;
}测试
# 从 monorepo 根目录
npx vitest run packages/@core/scheduler/__tests__/task-hub.test.ts
# 或在包目录内
cd packages/@core/scheduler && pnpm test
# 含覆盖率报告
pnpm test --coverage测试套件包含 46 个用例,覆盖率 100%(Statements / Branches / Functions / Lines),覆盖:注册校验、三种任务类型调度、依赖解析、失败重试与指数退避、生命周期边界(start / stop / pause / resume)、onReady 触发条件、动态增删、快照查询及错误字段。