hyper-scheduler 1.0.0

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.
Files changed (76) hide show
  1. package/.editorconfig +21 -0
  2. package/.eslintrc.cjs +26 -0
  3. package/GEMINI.md +1 -0
  4. package/README.md +38 -0
  5. package/docs/.vitepress/config.ts +52 -0
  6. package/docs/README.md +120 -0
  7. package/docs/api/devtools.md +232 -0
  8. package/docs/api/index.md +178 -0
  9. package/docs/api/scheduler.md +322 -0
  10. package/docs/api/task.md +439 -0
  11. package/docs/api/types.md +365 -0
  12. package/docs/examples/index.md +295 -0
  13. package/docs/guide/best-practices.md +436 -0
  14. package/docs/guide/core-concepts.md +363 -0
  15. package/docs/guide/getting-started.md +138 -0
  16. package/docs/index.md +33 -0
  17. package/docs/public/logo.svg +54 -0
  18. package/examples/browser/index.html +354 -0
  19. package/examples/node/simple.js +36 -0
  20. package/examples/react-demo/index.html +12 -0
  21. package/examples/react-demo/package.json +23 -0
  22. package/examples/react-demo/src/App.css +212 -0
  23. package/examples/react-demo/src/App.jsx +160 -0
  24. package/examples/react-demo/src/main.jsx +9 -0
  25. package/examples/react-demo/vite.config.ts +12 -0
  26. package/examples/react-demo/yarn.lock +752 -0
  27. package/examples/vue-demo/index.html +12 -0
  28. package/examples/vue-demo/package.json +21 -0
  29. package/examples/vue-demo/src/App.vue +373 -0
  30. package/examples/vue-demo/src/main.ts +4 -0
  31. package/examples/vue-demo/vite.config.ts +13 -0
  32. package/examples/vue-demo/yarn.lock +375 -0
  33. package/package.json +51 -0
  34. package/src/constants.ts +18 -0
  35. package/src/core/retry-strategy.ts +28 -0
  36. package/src/core/scheduler.ts +601 -0
  37. package/src/core/task-registry.ts +58 -0
  38. package/src/index.ts +74 -0
  39. package/src/platform/browser/browser-timer.ts +66 -0
  40. package/src/platform/browser/main-thread-timer.ts +16 -0
  41. package/src/platform/browser/worker.ts +31 -0
  42. package/src/platform/node/debug-cli.ts +19 -0
  43. package/src/platform/node/node-timer.ts +15 -0
  44. package/src/platform/timer-strategy.ts +19 -0
  45. package/src/plugins/dev-tools.ts +101 -0
  46. package/src/types.ts +115 -0
  47. package/src/ui/components/devtools.ts +525 -0
  48. package/src/ui/components/floating-trigger.ts +102 -0
  49. package/src/ui/components/icons.ts +16 -0
  50. package/src/ui/components/resizer.ts +129 -0
  51. package/src/ui/components/task-detail.ts +228 -0
  52. package/src/ui/components/task-header.ts +319 -0
  53. package/src/ui/components/task-list.ts +416 -0
  54. package/src/ui/components/timeline.ts +364 -0
  55. package/src/ui/debug-panel.ts +56 -0
  56. package/src/ui/i18n/en.ts +76 -0
  57. package/src/ui/i18n/index.ts +42 -0
  58. package/src/ui/i18n/zh.ts +76 -0
  59. package/src/ui/store/dev-tools-store.ts +191 -0
  60. package/src/ui/styles/theme.css.ts +56 -0
  61. package/src/ui/styles.ts +43 -0
  62. package/src/utils/cron-lite.ts +221 -0
  63. package/src/utils/cron.ts +20 -0
  64. package/src/utils/id.ts +10 -0
  65. package/src/utils/schedule.ts +93 -0
  66. package/src/vite-env.d.ts +1 -0
  67. package/stats.html +4949 -0
  68. package/tests/integration/Debug.test.ts +58 -0
  69. package/tests/unit/Plugin.test.ts +16 -0
  70. package/tests/unit/RetryStrategy.test.ts +21 -0
  71. package/tests/unit/Scheduler.test.ts +38 -0
  72. package/tests/unit/schedule.test.ts +70 -0
  73. package/tests/unit/ui/DevToolsStore.test.ts +67 -0
  74. package/tsconfig.json +28 -0
  75. package/vite.config.ts +51 -0
  76. package/vitest.config.ts +24 -0
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { Scheduler } from '../../src/index';
3
+ // The old DebugPanel logic (if any) is gone or replaced by DevTools plugin.
4
+ // We should update this test to test the new DevTools plugin.
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6
+ import { Scheduler } from '../../src/index';
7
+ import { DevTools } from '../../src/plugins/dev-tools';
8
+
9
+ describe('Debug Integration', () => {
10
+ beforeEach(() => { document.body.innerHTML = ''; // Clear DOM
11
+ // Mock Worker globally for JSDOM
12
+ if (typeof window !== 'undefined') {
13
+ window.Worker = class MockWorker {
14
+ postMessage() {}
15
+ onmessage = null;
16
+ terminate() {}
17
+ addEventListener() {}
18
+ removeEventListener() {}
19
+ dispatchEvent() { return false; }
20
+ } as any;
21
+ }
22
+ });
23
+
24
+ afterEach(() => {
25
+ // Cleanup if needed
26
+ });
27
+
28
+ it('should mount DebugPanel in Browser environment when debug is enabled', async () => {
29
+ // Ensure we are in jsdom
30
+ if (typeof window === 'undefined') {
31
+ console.warn('Skipping browser test in node env');
32
+ return;
33
+ }
34
+
35
+ // Intentionally blank - using git checkout to restore
36
+ // We should update this test to test the new DevTools plugin.
37
+
38
+ const scheduler = new Scheduler({
39
+ debug: true,
40
+ plugins: [new DevTools()]
41
+ });
42
+
43
+ // Wait for dynamic import or initialization
44
+ await new Promise(resolve => setTimeout(resolve, 100));
45
+
46
+ scheduler.createTask({
47
+ id: 'debug-dom-test',
48
+ schedule: '* * * * *',
49
+ handler: async () => {}
50
+ });
51
+
52
+ // New DevTools mounts <hs-devtools>
53
+ const panel = document.querySelector('hs-devtools');
54
+ expect(panel).not.toBeNull();
55
+
56
+ scheduler.stop();
57
+ });
58
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { HyperSchedulerPlugin } from '../../src/types';
3
+
4
+ describe('HyperSchedulerPlugin', () => {
5
+ it('should define the correct interface', () => {
6
+ const plugin: HyperSchedulerPlugin = {
7
+ name: 'TestPlugin',
8
+ init: (scheduler) => {
9
+ // Init logic
10
+ }
11
+ };
12
+
13
+ expect(plugin.name).toBe('TestPlugin');
14
+ expect(typeof plugin.init).toBe('function');
15
+ });
16
+ });
@@ -0,0 +1,21 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { RetryStrategy } from '../../src/core/retry-strategy';
3
+
4
+ describe('RetryStrategy', () => {
5
+ it('should return -1 if no options are provided', () => {
6
+ expect(RetryStrategy.getDelay(0, undefined)).toBe(-1);
7
+ });
8
+
9
+ it('should return correct exponential backoff delay', () => {
10
+ const options = { maxAttempts: 3, initialDelay: 1000, factor: 2 };
11
+
12
+ expect(RetryStrategy.getDelay(0, options)).toBe(1000); // 1000 * 2^0
13
+ expect(RetryStrategy.getDelay(1, options)).toBe(2000); // 1000 * 2^1
14
+ expect(RetryStrategy.getDelay(2, options)).toBe(4000); // 1000 * 2^2
15
+ });
16
+
17
+ it('should return -1 if attempts exceed maxAttempts', () => {
18
+ const options = { maxAttempts: 3, initialDelay: 1000, factor: 2 };
19
+ expect(RetryStrategy.getDelay(3, options)).toBe(-1);
20
+ });
21
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Scheduler } from '../../src/core/scheduler';
3
+ import { NodeTimer } from '../../src/platform/node/node-timer';
4
+ import { HyperSchedulerPlugin } from '../../src/types';
5
+
6
+ describe('Scheduler Constructor Plugin Loading', () => {
7
+ it('should initialize plugins passed in config', () => {
8
+ const initSpy = vi.fn();
9
+ const plugin: HyperSchedulerPlugin = {
10
+ name: 'TestPlugin',
11
+ init: initSpy
12
+ };
13
+
14
+ const scheduler = new Scheduler(new NodeTimer(), {
15
+ plugins: [plugin]
16
+ });
17
+
18
+ expect(initSpy).toHaveBeenCalledTimes(1);
19
+ expect(initSpy).toHaveBeenCalledWith(scheduler);
20
+ });
21
+
22
+ it('should not crash if a plugin throws error during init', () => {
23
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
24
+ const errorPlugin: HyperSchedulerPlugin = {
25
+ name: 'ErrorPlugin',
26
+ init: () => {
27
+ throw new Error('Plugin Init Failed');
28
+ }
29
+ };
30
+
31
+ const scheduler = new Scheduler(new NodeTimer(), {
32
+ plugins: [errorPlugin]
33
+ });
34
+
35
+ expect(consoleSpy).toHaveBeenCalled();
36
+ consoleSpy.mockRestore();
37
+ });
38
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { parseSchedule, getNextRun } from '../../src/utils/schedule';
3
+
4
+ describe('schedule.ts', () => {
5
+ describe('parseSchedule', () => {
6
+ it('should parse valid interval strings (s)', () => {
7
+ const result = parseSchedule('10s');
8
+ expect(result.type).toBe('interval');
9
+ expect(result.value).toBe(10 * 1000);
10
+ });
11
+
12
+ it('should parse valid interval strings (m)', () => {
13
+ const result = parseSchedule('5m');
14
+ expect(result.type).toBe('interval');
15
+ expect(result.value).toBe(5 * 60 * 1000);
16
+ });
17
+
18
+ it('should parse valid interval strings (h)', () => {
19
+ const result = parseSchedule('2h');
20
+ expect(result.type).toBe('interval');
21
+ expect(result.value).toBe(2 * 60 * 60 * 1000);
22
+ });
23
+
24
+ it('should parse valid interval strings (d)', () => {
25
+ const result = parseSchedule('1d');
26
+ expect(result.type).toBe('interval');
27
+ expect(result.value).toBe(1 * 24 * 60 * 60 * 1000);
28
+ });
29
+
30
+ it('should parse valid cron expressions', () => {
31
+ const result = parseSchedule('* * * * *');
32
+ expect(result.type).toBe('cron');
33
+ expect(result.value).toBe('* * * * *');
34
+ });
35
+
36
+ it('should throw error for invalid format', () => {
37
+ expect(() => parseSchedule('invalid')).toThrow(/无效的调度格式/);
38
+ });
39
+
40
+ it('should throw error for invalid cron after interval check', () => {
41
+ expect(() => parseSchedule('invalid-cron')).toThrow(/无效的调度格式/);
42
+ });
43
+ });
44
+
45
+ describe('getNextRun', () => {
46
+ it('should return next run for interval schedule', () => {
47
+ const now = Date.now();
48
+ vi.setSystemTime(now); // Mock current time
49
+ const schedule = parseSchedule('5s');
50
+ const nextRun = getNextRun(schedule, { lastRun: now });
51
+ expect(nextRun.getTime()).toBe(now + 5 * 1000);
52
+ vi.useRealTimers();
53
+ });
54
+
55
+ it('should return next run for cron schedule', () => {
56
+ const now = Date.now();
57
+ vi.setSystemTime(now);
58
+ const schedule = parseSchedule('* * * * *');
59
+ const nextRun = getNextRun(schedule);
60
+ // 应该返回下一分钟的 0 秒
61
+ expect(nextRun.getTime()).toBeGreaterThan(now);
62
+ expect(nextRun.getSeconds()).toBe(0);
63
+ vi.useRealTimers();
64
+ });
65
+
66
+ it('should throw error for invalid cron in getNextRun', () => {
67
+ expect(() => getNextRun('invalid-cron')).toThrow(/无效的调度格式/);
68
+ });
69
+ });
70
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { DevToolsStore } from '../../../src/ui/store/dev-tools-store';
3
+
4
+ describe('DevToolsStore', () => {
5
+ let store: DevToolsStore;
6
+
7
+ beforeEach(() => {
8
+ store = new DevToolsStore();
9
+ });
10
+
11
+ it('should initialize with default state', () => {
12
+ const state = store.getState();
13
+ expect(state.isOpen).toBe(false);
14
+ expect(state.activeTab).toBe('tasks');
15
+ expect(state.theme).toBe('auto');
16
+ expect(state.tasks.size).toBe(0);
17
+ });
18
+
19
+ it('should toggle visibility', () => {
20
+ store.toggle();
21
+ expect(store.getState().isOpen).toBe(true);
22
+ store.toggle();
23
+ expect(store.getState().isOpen).toBe(false);
24
+ });
25
+
26
+ it('should set theme', () => {
27
+ store.setTheme('dark');
28
+ expect(store.getState().theme).toBe('dark');
29
+ });
30
+
31
+ it('should update tasks', () => {
32
+ const mockTask = {
33
+ id: 't1',
34
+ status: 'running',
35
+ lastRun: null,
36
+ nextRun: null,
37
+ executionCount: 0,
38
+ schedule: '1s',
39
+ tags: [],
40
+ error: null
41
+ };
42
+ store.updateTask(mockTask);
43
+ expect(store.getState().tasks.get('t1')).toEqual(mockTask);
44
+ });
45
+
46
+ it('should notify subscribers on state change', () => {
47
+ const listener = vi.fn();
48
+ store.subscribe('isOpen', listener);
49
+
50
+ store.toggle();
51
+ expect(listener).toHaveBeenCalledWith(true);
52
+ });
53
+
54
+ it('should not notify removed subscribers', () => {
55
+ const listener = vi.fn();
56
+ const unsubscribe = store.subscribe('isOpen', listener);
57
+
58
+ unsubscribe();
59
+ store.toggle();
60
+ expect(listener).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it('should switch tabs', () => {
64
+ store.setTab('timeline');
65
+ expect(store.getState().activeTab).toBe('timeline');
66
+ });
67
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+
22
+ "baseUrl": ".",
23
+ "paths": {
24
+ "@/*": ["src/*"]
25
+ }
26
+ },
27
+ "include": ["src"]
28
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { resolve } from 'path';
2
+ import { defineConfig } from 'vite';
3
+ import { visualizer } from 'rollup-plugin-visualizer';
4
+
5
+ export default defineConfig({
6
+ plugins: [
7
+ visualizer({
8
+ open: false,
9
+ gzipSize: true,
10
+ brotliSize: true,
11
+ filename: 'stats.html'
12
+ }),
13
+ ],
14
+ build: {
15
+ lib: {
16
+ entry: resolve(__dirname, 'src/index.ts'),
17
+ name: 'HyperScheduler',
18
+ fileName: 'index',
19
+ formats: ['es', 'cjs', 'umd'],
20
+ },
21
+ minify: 'terser',
22
+ terserOptions: {
23
+ compress: {
24
+ drop_console: false,
25
+ drop_debugger: true,
26
+ },
27
+ format: {
28
+ comments: false,
29
+ },
30
+ },
31
+ rollupOptions: {
32
+ // 确保外部化处理那些你不想打包进库的依赖
33
+ external: [],
34
+ output: {
35
+ // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
36
+ globals: {},
37
+ // 确保 Tree-shaking
38
+ manualChunks: undefined,
39
+ },
40
+ treeshake: {
41
+ moduleSideEffects: true,
42
+ propertyReadSideEffects: false,
43
+ },
44
+ },
45
+ },
46
+ resolve: {
47
+ alias: {
48
+ '@': resolve(__dirname, 'src'),
49
+ },
50
+ },
51
+ });
@@ -0,0 +1,24 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import { resolve } from 'path';
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ globals: true,
7
+ environment: 'jsdom', // Use jsdom for browser-like environment (needed for Web Workers simulation in the future, though node env is default)
8
+ include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
9
+ coverage: {
10
+ provider: 'v8',
11
+ reporter: ['text', 'json', 'html'],
12
+ exclude: [
13
+ 'node_modules/',
14
+ 'dist/',
15
+ 'tests/',
16
+ '**/*.d.ts',
17
+ '**/*.config.ts',
18
+ ],
19
+ },
20
+ alias: {
21
+ '@': resolve(__dirname, 'src'),
22
+ },
23
+ },
24
+ });