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,416 @@
1
+ import { themeStyles } from '../styles/theme.css';
2
+ import { TaskSnapshot } from '../../types';
3
+ import { TaskStatus } from '../../constants';
4
+ import { ICONS } from './icons';
5
+ import { t } from '../i18n';
6
+
7
+ export class TaskList extends HTMLElement {
8
+ private _shadow: ShadowRoot;
9
+ private _tasks: TaskSnapshot[] = [];
10
+ private _lastExecutionTimes: Map<string, number> = new Map();
11
+
12
+ constructor() {
13
+ super();
14
+ this._shadow = this.attachShadow({ mode: 'open' });
15
+ }
16
+
17
+ connectedCallback() {
18
+ this.render();
19
+ }
20
+
21
+ set tasks(map: Map<string, TaskSnapshot>) {
22
+ const newTasks = Array.from(map.values());
23
+
24
+ // Track execution count changes to show flash animation
25
+ newTasks.forEach(task => {
26
+ const oldTask = this._tasks.find(t => t.id === task.id);
27
+ if (oldTask && task.executionCount > oldTask.executionCount) {
28
+ this._lastExecutionTimes.set(task.id, Date.now());
29
+ }
30
+ });
31
+
32
+ this._tasks = newTasks;
33
+ this.renderRows();
34
+ }
35
+
36
+ filter(text: string, map: Map<string, TaskSnapshot>) {
37
+ const all = Array.from(map.values());
38
+ if (!text) {
39
+ this._tasks = all;
40
+ } else {
41
+ const lower = text.toLowerCase();
42
+ this._tasks = all.filter(t =>
43
+ t.id.toLowerCase().includes(lower) ||
44
+ t.tags.some(tag => tag.toLowerCase().includes(lower))
45
+ );
46
+ }
47
+ this.renderRows();
48
+ }
49
+
50
+ // Method to update table headers when language changes
51
+ updateHeaders() {
52
+ const thead = this._shadow.querySelector('thead');
53
+ if (thead) {
54
+ thead.innerHTML = `
55
+ <tr>
56
+ <th style="width:40px">#</th>
57
+ <th style="min-width:150px">${t('list.idTags')}</th>
58
+ <th style="width:150px">${t('list.status')}</th>
59
+ <th style="width:70px">${t('list.driver')}</th>
60
+ <th style="width:100px">${t('list.schedule')}</th>
61
+ <th style="width:60px">${t('list.count')}</th>
62
+ <th style="width:100px">${t('list.lastRun')}</th>
63
+ <th style="width:100px">${t('list.actions')}</th>
64
+ </tr>
65
+ `;
66
+ }
67
+
68
+ // Update tip
69
+ const tip = this._shadow.querySelector('.tip');
70
+ if (tip) {
71
+ tip.textContent = t('list.tip');
72
+ }
73
+
74
+ // Re-render rows to update status text
75
+ this.renderRows();
76
+ }
77
+
78
+ private getStatusIcon(status: string, taskId: string) {
79
+ const lastExec = this._lastExecutionTimes.get(taskId);
80
+ const isRecentlyExecuted = lastExec && (Date.now() - lastExec < 1000);
81
+
82
+ // idle = 正在调度中(等待下次执行)
83
+ // running = 正在执行 handler
84
+ // stopped = 已停止,不会再调度(需要手动启动)
85
+ // error = 执行出错
86
+ switch (status) {
87
+ case TaskStatus.RUNNING:
88
+ return `<span style="color:var(--hs-primary)">🔵</span> <strong>${t('status.running')}</strong>`;
89
+ case TaskStatus.STOPPED:
90
+ return `<span style="color:var(--hs-text-secondary)">⚪</span> ${t('status.stopped')}`;
91
+ case TaskStatus.IDLE:
92
+ if (isRecentlyExecuted) {
93
+ return `<span class="status-flash" style="color:var(--hs-success)">🟢</span> ${t('status.idle')}`;
94
+ }
95
+ return `<span style="color:var(--hs-success)">🟢</span> ${t('status.idle')}`;
96
+ case TaskStatus.ERROR:
97
+ return `<span style="color:var(--hs-warning)">🟠</span> ${t('status.error')}`;
98
+ default:
99
+ return status;
100
+ }
101
+ }
102
+
103
+ private formatSchedule(schedule: string | number): string {
104
+ if (typeof schedule === 'number') {
105
+ return `${schedule}ms`;
106
+ }
107
+ if (schedule && (schedule.includes('*') || schedule.includes(' '))) {
108
+ return schedule.length > 15 ? schedule.substring(0, 12) + '...' : schedule;
109
+ }
110
+ return schedule || '-';
111
+ }
112
+
113
+ private formatTime(timestamp: number | null): string {
114
+ if (!timestamp) return '-';
115
+ const date = new Date(timestamp);
116
+ return date.toLocaleTimeString('en-US', {
117
+ hour12: false,
118
+ hour: '2-digit',
119
+ minute: '2-digit',
120
+ second: '2-digit'
121
+ }) + '.' + date.getMilliseconds().toString().padStart(3, '0');
122
+ }
123
+
124
+ private getDriverBadge(driver: 'worker' | 'main' | undefined): string {
125
+ const d = driver || 'worker';
126
+ if (d === 'worker') {
127
+ return `<span class="driver-badge worker" title="${t('list.driverWorker')}">W</span>`;
128
+ }
129
+ return `<span class="driver-badge main" title="${t('list.driverMain')}">M</span>`;
130
+ }
131
+
132
+ private renderRows() {
133
+ const tbody = this._shadow.querySelector('tbody');
134
+ if (!tbody) return;
135
+
136
+ // Simple full re-render for MVP
137
+ tbody.innerHTML = this._tasks.map((task, index) => {
138
+ const lastExec = this._lastExecutionTimes.get(task.id);
139
+ const isRecentlyExecuted = lastExec && (Date.now() - lastExec < 1000);
140
+ const rowClass = isRecentlyExecuted ? 'recently-executed' : '';
141
+
142
+ return `
143
+ <tr data-id="${task.id}" class="${rowClass}">
144
+ <td class="col-num">${index + 1}</td>
145
+ <td class="col-id">
146
+ <div class="task-id">${task.id}</div>
147
+ <div class="tags">
148
+ ${task.tags && task.tags.length > 0
149
+ ? task.tags.map(t => `<span class="tag">${t}</span>`).join('')
150
+ : `<span class="no-tags">${t('list.noTags')}</span>`}
151
+ </div>
152
+ </td>
153
+ <td>${this.getStatusIcon(task.status, task.id)}</td>
154
+ <td>${this.getDriverBadge((task as any).driver)}</td>
155
+ <td>${this.formatSchedule(task.schedule)}</td>
156
+ <td>${task.executionCount || 0}</td>
157
+ <td>${this.formatTime(task.lastRun)}</td>
158
+ <td class="col-actions">
159
+ <div class="action-group">
160
+ <button class="btn-icon" data-action="trigger" title="${t('actions.trigger')}" ${task.status === TaskStatus.RUNNING ? 'disabled' : ''}>${ICONS.trigger}</button>
161
+ ${task.status === TaskStatus.STOPPED || task.status === TaskStatus.ERROR
162
+ ? `<button class="btn-icon success" data-action="start" title="${t('actions.start')}">${ICONS.resume}</button>`
163
+ : `<button class="btn-icon warning" data-action="stop" title="${t('actions.stop')}" ${task.status === TaskStatus.RUNNING ? 'disabled' : ''}>${ICONS.pause}</button>`
164
+ }
165
+ <button class="btn-icon danger" data-action="remove" title="${t('actions.remove')}">${ICONS.remove}</button>
166
+ </div>
167
+ </td>
168
+ </tr>
169
+ `;
170
+ }).join('');
171
+ }
172
+
173
+ render() {
174
+ this._shadow.innerHTML = `
175
+ <style>
176
+ ${themeStyles}
177
+ :host {
178
+ display: flex;
179
+ flex-direction: column;
180
+ height: 100%;
181
+ background: var(--hs-bg);
182
+ }
183
+ .table-container {
184
+ flex: 1;
185
+ min-height: 0;
186
+ overflow-y: auto;
187
+ overflow-x: auto;
188
+ position: relative;
189
+ }
190
+ table {
191
+ width: 100%;
192
+ border-collapse: collapse;
193
+ font-size: var(--hs-font-size);
194
+ color: var(--hs-text);
195
+ }
196
+ thead {
197
+ position: sticky;
198
+ top: 0;
199
+ z-index: 2;
200
+ background: var(--hs-bg);
201
+ }
202
+ th {
203
+ text-align: left;
204
+ padding: 8px 12px;
205
+ border-bottom: 2px solid var(--hs-border);
206
+ color: var(--hs-text-secondary);
207
+ font-weight: 600;
208
+ background: var(--hs-bg);
209
+ font-size: 11px;
210
+ text-transform: uppercase;
211
+ white-space: nowrap;
212
+ }
213
+ th:last-child {
214
+ position: sticky;
215
+ right: 0;
216
+ background: var(--hs-bg);
217
+ box-shadow: -2px 0 4px rgba(0,0,0,0.1);
218
+ }
219
+ td {
220
+ padding: 8px 12px;
221
+ border-bottom: 1px solid var(--hs-border);
222
+ vertical-align: middle;
223
+ }
224
+ tr:hover {
225
+ background: var(--hs-bg-secondary);
226
+ cursor: pointer;
227
+ }
228
+ tr.recently-executed {
229
+ animation: flash-row 1s ease-out;
230
+ }
231
+ @keyframes flash-row {
232
+ 0% { background: rgba(34, 197, 94, 0.2); }
233
+ 100% { background: transparent; }
234
+ }
235
+ .status-flash {
236
+ animation: flash-icon 1s ease-out;
237
+ }
238
+ @keyframes flash-icon {
239
+ 0%, 50% { opacity: 1; }
240
+ 25%, 75% { opacity: 0.3; }
241
+ }
242
+ .tip {
243
+ padding: 12px;
244
+ text-align: center;
245
+ font-size: 11px;
246
+ color: var(--hs-text-secondary);
247
+ border-top: 1px solid var(--hs-border);
248
+ background: var(--hs-bg);
249
+ }
250
+ .task-id {
251
+ font-weight: 600;
252
+ }
253
+ .tags {
254
+ display: flex;
255
+ gap: 4px;
256
+ margin-top: 4px;
257
+ }
258
+ .tag {
259
+ background: var(--hs-bg-secondary);
260
+ border: 1px solid var(--hs-border);
261
+ border-radius: 10px;
262
+ padding: 2px 8px;
263
+ font-size: 10px;
264
+ color: var(--hs-text-secondary);
265
+ }
266
+ .no-tags {
267
+ font-size: 10px;
268
+ color: var(--hs-text-secondary);
269
+ font-style: italic;
270
+ }
271
+ .driver-badge {
272
+ display: inline-flex;
273
+ align-items: center;
274
+ justify-content: center;
275
+ width: 22px;
276
+ height: 22px;
277
+ border-radius: 4px;
278
+ font-size: 11px;
279
+ font-weight: 600;
280
+ font-family: monospace;
281
+ }
282
+ .driver-badge.worker {
283
+ background: rgba(34, 197, 94, 0.15);
284
+ color: var(--hs-success);
285
+ border: 1px solid var(--hs-success);
286
+ }
287
+ .driver-badge.main {
288
+ background: rgba(245, 158, 11, 0.15);
289
+ color: var(--hs-warning);
290
+ border: 1px solid var(--hs-warning);
291
+ }
292
+ .col-num {
293
+ width: 40px;
294
+ color: var(--hs-text-secondary);
295
+ font-size: 11px;
296
+ }
297
+ .col-actions {
298
+ width: 100px;
299
+ position: sticky;
300
+ right: 0;
301
+ background: var(--hs-bg);
302
+ box-shadow: -2px 0 4px rgba(0,0,0,0.1);
303
+ }
304
+ .action-group {
305
+ display: flex;
306
+ gap: 4px;
307
+ }
308
+ .btn-icon {
309
+ background: transparent;
310
+ border: 1px solid var(--hs-border);
311
+ color: var(--hs-text-secondary);
312
+ border-radius: 4px;
313
+ width: 24px;
314
+ height: 24px;
315
+ display: flex;
316
+ align-items: center;
317
+ justify-content: center;
318
+ cursor: pointer;
319
+ padding: 0;
320
+ flex-shrink: 0;
321
+ }
322
+ .btn-icon svg {
323
+ width: 14px;
324
+ height: 14px;
325
+ display: block;
326
+ }
327
+ .btn-icon:hover {
328
+ background: var(--hs-primary);
329
+ color: white;
330
+ border-color: var(--hs-primary);
331
+ }
332
+ .btn-trigger:hover {
333
+ background: var(--hs-success);
334
+ border-color: var(--hs-success);
335
+ }
336
+ .btn-pause:hover {
337
+ background: var(--hs-warning);
338
+ border-color: var(--hs-warning);
339
+ }
340
+ .btn-resume:hover {
341
+ background: var(--hs-success);
342
+ border-color: var(--hs-success);
343
+ }
344
+ .btn-remove:hover {
345
+ color: white;
346
+ background: var(--hs-danger);
347
+ border-color: var(--hs-danger);
348
+ }
349
+ .btn-icon:disabled {
350
+ opacity: 0.4;
351
+ cursor: not-allowed;
352
+ }
353
+ .btn-icon:disabled:hover {
354
+ background: transparent;
355
+ color: var(--hs-text-secondary);
356
+ border-color: var(--hs-border);
357
+ }
358
+ </style>
359
+ <div class="table-container">
360
+ <table>
361
+ <thead>
362
+ <tr>
363
+ <th style="width:40px">#</th>
364
+ <th style="min-width:150px">${t('list.idTags')}</th>
365
+ <th style="width:150px">${t('list.status')}</th>
366
+ <th style="width:70px">${t('list.driver')}</th>
367
+ <th style="width:100px">${t('list.schedule')}</th>
368
+ <th style="width:60px">${t('list.count')}</th>
369
+ <th style="width:100px">${t('list.lastRun')}</th>
370
+ <th style="width:100px">${t('list.actions')}</th>
371
+ </tr>
372
+ </thead>
373
+ <tbody>
374
+ <!-- Rows -->
375
+ </tbody>
376
+ </table>
377
+ </div>
378
+ <div class="tip">
379
+ ${t('list.tip')}
380
+ </div>
381
+ `;
382
+
383
+ this._shadow.addEventListener('click', (e) => {
384
+ const target = e.target as HTMLElement;
385
+ const btn = target.closest('button');
386
+
387
+ // Handle Action Buttons
388
+ if (btn) {
389
+ const action = btn.dataset.action;
390
+ const tr = btn.closest('tr');
391
+ if (!action || !tr) return;
392
+ const id = (tr as HTMLElement).dataset.id;
393
+
394
+ this.dispatchEvent(new CustomEvent('task-action', {
395
+ detail: { action, id },
396
+ bubbles: true,
397
+ composed: true
398
+ }));
399
+ return;
400
+ }
401
+
402
+ // Handle Row Click (Selection)
403
+ const tr = target.closest('tr');
404
+ if (tr && !target.closest('.col-actions')) {
405
+ const id = (tr as HTMLElement).dataset.id;
406
+ this.dispatchEvent(new CustomEvent('task-select', {
407
+ detail: id,
408
+ bubbles: true,
409
+ composed: true
410
+ }));
411
+ }
412
+ });
413
+ }
414
+ }
415
+
416
+ customElements.define('hs-task-list', TaskList);