Core Docs

@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 时自动 cleanupresize、网络状态、页面可见性

任务状态流转

pending → running → done
                 ↘ failed → (retry) → running → done
                          → (retries exhausted) → failed(不再调度)

periodic 任务完成后状态重置为可再次执行,下个周期到来时再次进入 running

onReady 触发时机

当所有 init 类型任务均到达 done 状态时,触发一次 onReady 回调。
periodiclistener 任务不影响 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)。

重试机制

失败的 initlistener 任务会按指数退避自动重试:

第 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 个 setIntervalTaskHub
依赖关系无法表达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 触发条件、动态增删、快照查询及错误字段。

On this page