@linxin666/dsh-client-ui-task-board 0.1.20 → 0.2.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.
- package/README.i18n.yaml +2 -2
- package/README.md +82 -104
- package/README.zh.md +82 -104
- package/lib/client.js +574 -581
- package/lib/client.js.map +1 -1
- package/lib/index.js +2144 -5
- package/lib/types/client/PluginSettingsCard.d.ts +7 -1
- package/lib/types/client/TaskBoardSettingsCard.d.ts +7 -4
- package/lib/types/client/board/TaskCard.d.ts +5 -2
- package/lib/types/client/host-api.d.ts +18 -0
- package/lib/types/client/locales.d.ts +10 -0
- package/lib/types/core/controller.d.ts +40 -15
- package/lib/types/core/schedule.d.ts +4 -2
- package/lib/types/core/tasks.d.ts +6 -6
- package/lib/types/core/use-cases/task-archive.d.ts +5 -4
- package/lib/types/core/use-cases/task-schedule.d.ts +3 -3
- package/lib/types/dsh-home.d.ts +16 -0
- package/lib/types/host-ledger.d.ts +42 -0
- package/lib/types/host-routes.d.ts +24 -0
- package/lib/types/host-runner.d.ts +30 -0
- package/lib/types/host-service.d.ts +41 -0
- package/lib/types/index.d.ts +17 -9
- package/lib/types/loopback.d.ts +24 -0
- package/lib/types/power-inhibitor.d.ts +73 -0
- package/lib/types/protocol.d.ts +72 -0
- package/package.json +12 -9
- package/src/client/PluginSettingsCard.tsx +10 -0
- package/src/client/TaskBoardSettingsCard.tsx +51 -4
- package/src/client/board/NewTaskModal.tsx +9 -6
- package/src/client/board/TaskBoard.tsx +21 -4
- package/src/client/board/TaskCard.tsx +24 -8
- package/src/client/board/TaskDetail.tsx +83 -50
- package/src/client/host-api.ts +95 -0
- package/src/client/index.ts +9 -78
- package/src/client/locales.ts +24 -4
- package/src/core/controller.ts +205 -24
- package/src/core/schedule.ts +20 -3
- package/src/core/store.ts +4 -8
- package/src/core/tasks.ts +6 -6
- package/src/core/use-cases/task-archive.ts +14 -5
- package/src/core/use-cases/task-schedule.ts +6 -5
- package/src/dsh-home.ts +36 -0
- package/src/host-ledger.ts +478 -0
- package/src/host-routes.ts +188 -0
- package/src/host-runner.ts +134 -0
- package/src/host-service.ts +209 -0
- package/src/index.ts +54 -11
- package/src/loopback.ts +63 -0
- package/src/power-inhibitor.ts +321 -0
- package/src/protocol.ts +213 -0
- package/lib/types/core/scheduler.d.ts +0 -70
- package/src/core/scheduler.ts +0 -161
package/src/core/scheduler.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser-side scheduler: the heartbeat behind scheduled task runs.
|
|
3
|
-
*
|
|
4
|
-
* The board is a pure client plugin with no server channel, so "定时任务"
|
|
5
|
-
* lives in the tab: a timer ticks every minute (plus immediately on tab
|
|
6
|
-
* visibility recovery) and triggers any task whose `schedule.nextRunAt` is
|
|
7
|
-
* due, rolling the schedule forward to the next cron match before triggering
|
|
8
|
-
* so the same tick never double-fires. Missed runs are skipped, never queued:
|
|
9
|
-
* a task still running at its due instant is skipped by the controller's
|
|
10
|
-
* runTask guard and simply waits for the next cron match.
|
|
11
|
-
*
|
|
12
|
-
* A deleted task can never fire again from a stale in-memory copy: every
|
|
13
|
-
* tick first re-reads the persisted ledger through the optional `refresh`
|
|
14
|
-
* hook (the controller reloads its store), so the fire decision and the
|
|
15
|
-
* subsequent roll-forward both run on the freshest persisted truth.
|
|
16
|
-
*
|
|
17
|
-
* The ticker is controlled: `start` arms a single interval guarded by an
|
|
18
|
-
* idempotence check (a second start while running is a no-op, so wiring can
|
|
19
|
-
* never leak a duplicate timer), and `stop`/`dispose` clear the timer and
|
|
20
|
-
* drop the environment listeners. The board unmount path calls stop/dispose,
|
|
21
|
-
* so no interval or listener outlives the board.
|
|
22
|
-
*
|
|
23
|
-
* Framework-free: all runtime access flows through the injected deps
|
|
24
|
-
* (structural faces), so tests drive ticks directly without timers.
|
|
25
|
-
*/
|
|
26
|
-
import { nextRunAtMs } from './schedule.ts'
|
|
27
|
-
import type { TaskRecord } from './tasks.ts'
|
|
28
|
-
|
|
29
|
-
/** Everything the scheduler needs from its host (the board controller). */
|
|
30
|
-
export interface SchedulerDeps {
|
|
31
|
-
/** Read the current task ledger (the controller snapshot). */
|
|
32
|
-
tasks(): readonly TaskRecord[]
|
|
33
|
-
/**
|
|
34
|
-
* Re-sync the task ledger from its persisted source before the tick decides
|
|
35
|
-
* what is due. The wiring passes the controller's store reload so a task
|
|
36
|
-
* deleted elsewhere can never be fired from a stale in-memory copy.
|
|
37
|
-
* Optional: absent in pure in-memory harnesses.
|
|
38
|
-
*/
|
|
39
|
-
refresh?: () => void
|
|
40
|
-
/** Clock; defaults to Date.now in the controller wiring. */
|
|
41
|
-
now(): number
|
|
42
|
-
/** Trigger one task's real execution (the controller's runTask); resolves
|
|
43
|
-
* true when the run was accepted, false when rejected (e.g. the task is
|
|
44
|
-
* already running). */
|
|
45
|
-
runTask(id: string): Promise<boolean>
|
|
46
|
-
/** Persist a rolled-forward schedule (next due + this trigger instant). */
|
|
47
|
-
applySchedule(id: string, nextRunAt: number | undefined, lastTriggeredAt: number | undefined): void
|
|
48
|
-
/** Tick cadence; defaults to 60_000 ms. */
|
|
49
|
-
tickMs?: number
|
|
50
|
-
/**
|
|
51
|
-
* Gate: while false the tick no-ops (e.g. the session list baseline has not
|
|
52
|
-
* arrived on page load, so executions would fail). Defaults to always ready.
|
|
53
|
-
*/
|
|
54
|
-
ready?: () => boolean
|
|
55
|
-
/** Environment listeners for tab-visibility recovery (browser only). */
|
|
56
|
-
environment?: {
|
|
57
|
-
addEventListener(type: 'visibilitychange', listener: () => void): void
|
|
58
|
-
removeEventListener(type: 'visibilitychange', listener: () => void): void
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* The schedule heartbeat (see module doc). `tick` is public so tests and
|
|
64
|
-
* callers can drive a check without waiting for the interval.
|
|
65
|
-
*/
|
|
66
|
-
export class SchedulerService {
|
|
67
|
-
private timer: ReturnType<typeof setInterval> | undefined
|
|
68
|
-
private environmentListener: (() => void) | undefined
|
|
69
|
-
private disposed = false
|
|
70
|
-
private started = false
|
|
71
|
-
/** Cron expressions that matched no instant in the scan window, per task —
|
|
72
|
-
* remembered so a never-matching rule is not re-scanned on every tick. */
|
|
73
|
-
private readonly unmatchableRepair = new Map<string, string>()
|
|
74
|
-
|
|
75
|
-
/** @param deps - tasks/clock/trigger/apply faces (see {@link SchedulerDeps}). */
|
|
76
|
-
constructor(private readonly deps: SchedulerDeps) {}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Start ticking: one immediate check (catch-up after reload) + the interval.
|
|
80
|
-
* Single-instance: a second start while already armed is a no-op, so a
|
|
81
|
-
* re-entrant mount can never stack a duplicate timer.
|
|
82
|
-
*/
|
|
83
|
-
start(): void {
|
|
84
|
-
if (this.disposed) return
|
|
85
|
-
if (this.started) return
|
|
86
|
-
this.started = true
|
|
87
|
-
// Immediate catch-up tick: schedules whose due instant passed while the
|
|
88
|
-
// tab was closed are triggered as soon as the runtime is ready.
|
|
89
|
-
this.tick()
|
|
90
|
-
this.timer = setInterval(() => { this.tick() }, this.deps.tickMs ?? 60_000)
|
|
91
|
-
if (this.deps.environment !== undefined) {
|
|
92
|
-
this.environmentListener = () => { this.tick() }
|
|
93
|
-
this.deps.environment.addEventListener('visibilitychange', this.environmentListener)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Stop ticking and drop listeners (idempotent). Preferred shutdown verb for
|
|
99
|
-
* callers that treat the scheduler as a controlled ticker; `dispose` is an
|
|
100
|
-
* alias.
|
|
101
|
-
*/
|
|
102
|
-
stop(): void {
|
|
103
|
-
this.dispose()
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Stop ticking and drop listeners (idempotent). */
|
|
107
|
-
dispose(): void {
|
|
108
|
-
if (this.disposed && this.timer === undefined && this.environmentListener === undefined) return
|
|
109
|
-
this.disposed = true
|
|
110
|
-
this.started = false
|
|
111
|
-
if (this.timer !== undefined) {
|
|
112
|
-
clearInterval(this.timer)
|
|
113
|
-
this.timer = undefined
|
|
114
|
-
}
|
|
115
|
-
if (this.environmentListener !== undefined && this.deps.environment !== undefined) {
|
|
116
|
-
this.deps.environment.removeEventListener('visibilitychange', this.environmentListener)
|
|
117
|
-
this.environmentListener = undefined
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Check every enabled schedule and trigger the due ones. Idempotent per
|
|
123
|
-
* task per tick: the schedule is rolled forward only after runTask accepts
|
|
124
|
-
* the run, so a rejected run keeps its due slot and is retried on the next
|
|
125
|
-
* tick instead of being silently dropped.
|
|
126
|
-
*/
|
|
127
|
-
async tick(): Promise<void> {
|
|
128
|
-
if (this.disposed) return
|
|
129
|
-
if (this.deps.ready !== undefined && !this.deps.ready()) return
|
|
130
|
-
// Freshest persisted truth before any fire decision: a task deleted in
|
|
131
|
-
// another tab (or a stale in-memory copy) must never be triggered.
|
|
132
|
-
this.deps.refresh?.()
|
|
133
|
-
const now = this.deps.now()
|
|
134
|
-
for (const task of this.deps.tasks()) {
|
|
135
|
-
const schedule = task.schedule
|
|
136
|
-
if (schedule === undefined || !schedule.enabled) continue
|
|
137
|
-
if (schedule.nextRunAt === undefined) {
|
|
138
|
-
// Missing next-run instant (repaired/legacy data): recompute from the
|
|
139
|
-
// cron expression and wait; an unparseable expression is skipped. A
|
|
140
|
-
// grammatically valid rule with no match in the window (e.g. Feb 30)
|
|
141
|
-
// would otherwise re-run the full minute-by-minute scan every tick.
|
|
142
|
-
if (this.unmatchableRepair.get(task.id) === schedule.cron) continue
|
|
143
|
-
const repaired = nextRunAtMs(schedule.cron, now)
|
|
144
|
-
if (repaired === undefined) {
|
|
145
|
-
this.unmatchableRepair.set(task.id, schedule.cron)
|
|
146
|
-
continue
|
|
147
|
-
}
|
|
148
|
-
this.unmatchableRepair.delete(task.id)
|
|
149
|
-
this.deps.applySchedule(task.id, repaired, undefined)
|
|
150
|
-
continue
|
|
151
|
-
}
|
|
152
|
-
if (schedule.nextRunAt > now) continue
|
|
153
|
-
// Advance from the due instant (not this tick's wall-clock) and only
|
|
154
|
-
// after the run is accepted: a rejected run keeps its due slot.
|
|
155
|
-
const next = nextRunAtMs(schedule.cron, schedule.nextRunAt)
|
|
156
|
-
const accepted = await this.deps.runTask(task.id)
|
|
157
|
-
if (accepted) this.deps.applySchedule(task.id, next, now)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|