@meith/tasks 0.30.1 → 0.32.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/package.json +4 -1
- package/src/builtin.ts +22 -0
- package/src/scheduler.ts +55 -4
- package/src/types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/tasks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"main": "./src/index.ts",
|
|
12
12
|
"types": "./src/index.ts",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@meith/core": "^0.32.0"
|
|
15
|
+
},
|
|
13
16
|
"files": [
|
|
14
17
|
"src",
|
|
15
18
|
"!src/**/*.test.*",
|
package/src/builtin.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface TaskWorkers {
|
|
|
24
24
|
expireWarnings(batchSize: number): Promise<number>
|
|
25
25
|
notifySubscribers(batchSize: number, signal: AbortSignal): Promise<number>
|
|
26
26
|
sendDigests(batchSize: number, signal: AbortSignal): Promise<number>
|
|
27
|
+
sendBoardDigests(batchSize: number, signal: AbortSignal): Promise<number>
|
|
27
28
|
sweepAttachments(batchSize: number): Promise<{ deleted: number; failed: number }>
|
|
28
29
|
sweepAvatars(batchSize: number): Promise<number>
|
|
29
30
|
rollUpStatistics(): Promise<{ memberCount: number; online: number; record: boolean }>
|
|
@@ -361,6 +362,26 @@ function allDefinitions(workers: TaskWorkers): TaskDefinition[] {
|
|
|
361
362
|
},
|
|
362
363
|
},
|
|
363
364
|
|
|
365
|
+
{
|
|
366
|
+
id: 'board.digest_send',
|
|
367
|
+
title: 'Send board activity digests',
|
|
368
|
+
titleKey: 'adminSystem.task.boardDigestSend.title',
|
|
369
|
+
description:
|
|
370
|
+
'Sends the recap a member opted into after they have been away long enough to ' +
|
|
371
|
+
'count as lapsed, and only once their own cadence — weekly or monthly — is due. ' +
|
|
372
|
+
'Content is fetched per recipient through the same permission filter as the rest ' +
|
|
373
|
+
'of the board, so a digest never names a thread its reader could not open. Hourly, ' +
|
|
374
|
+
'for the same reason the subscription digest is: a due check is a query that finds ' +
|
|
375
|
+
'nobody most of the time.',
|
|
376
|
+
descriptionKey: 'adminSystem.task.boardDigestSend.description',
|
|
377
|
+
intervalSeconds: 3_600,
|
|
378
|
+
maxDurationSeconds: 60,
|
|
379
|
+
async run({ signal }) {
|
|
380
|
+
const notified = await workers.sendBoardDigests(50, signal)
|
|
381
|
+
return { detail: { notified } }
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
|
|
364
385
|
{
|
|
365
386
|
id: 'attachments.sweep',
|
|
366
387
|
title: 'Collect abandoned attachment files',
|
|
@@ -446,6 +467,7 @@ const REQUIRED_WORKER: Readonly<Record<string, keyof TaskWorkers>> = {
|
|
|
446
467
|
'warnings.expire': 'expireWarnings',
|
|
447
468
|
'subscriptions.instant': 'notifySubscribers',
|
|
448
469
|
'subscriptions.digest': 'sendDigests',
|
|
470
|
+
'board.digest_send': 'sendBoardDigests',
|
|
449
471
|
'attachments.sweep': 'sweepAttachments',
|
|
450
472
|
'avatars.sweep': 'sweepAvatars',
|
|
451
473
|
'stats.rollup': 'rollUpStatistics',
|
package/src/scheduler.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type CronSchedule, nextRun, parseCron } from '@meith/core'
|
|
2
|
+
|
|
1
3
|
import type { TaskContext, TaskDefinition, TaskResult } from './types'
|
|
2
4
|
|
|
3
5
|
export interface TaskRepository {
|
|
@@ -13,9 +15,17 @@ export interface TaskRepository {
|
|
|
13
15
|
success: boolean
|
|
14
16
|
detail?: Record<string, unknown>
|
|
15
17
|
error?: string
|
|
18
|
+
nextRunAt?: Date
|
|
16
19
|
}): Promise<void>
|
|
17
20
|
|
|
18
|
-
ensureRegistered(
|
|
21
|
+
ensureRegistered(
|
|
22
|
+
tasks: readonly {
|
|
23
|
+
id: string
|
|
24
|
+
intervalSeconds: number
|
|
25
|
+
schedule?: string
|
|
26
|
+
firstRunAt?: Date
|
|
27
|
+
}[],
|
|
28
|
+
): Promise<void>
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
export interface TickOutcome {
|
|
@@ -44,8 +54,35 @@ export async function tick({
|
|
|
44
54
|
onError,
|
|
45
55
|
signal,
|
|
46
56
|
}: TickDeps): Promise<TickOutcome[]> {
|
|
57
|
+
const schedules = new Map<string, { expression: string; parsed: CronSchedule }>()
|
|
58
|
+
const broken = new Map<string, string>()
|
|
59
|
+
for (const task of tasks) {
|
|
60
|
+
if (task.schedule === undefined) continue
|
|
61
|
+
try {
|
|
62
|
+
const parsed = parseCron(task.schedule)
|
|
63
|
+
nextRun(parsed, now)
|
|
64
|
+
schedules.set(task.id, { expression: task.schedule, parsed })
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
67
|
+
broken.set(task.id, message)
|
|
68
|
+
onError?.(task.id, error)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
47
72
|
await repository.ensureRegistered(
|
|
48
|
-
tasks
|
|
73
|
+
tasks
|
|
74
|
+
.filter((t) => !broken.has(t.id))
|
|
75
|
+
.map((t) => {
|
|
76
|
+
const entry = schedules.get(t.id)
|
|
77
|
+
return entry === undefined
|
|
78
|
+
? { id: t.id, intervalSeconds: t.intervalSeconds }
|
|
79
|
+
: {
|
|
80
|
+
id: t.id,
|
|
81
|
+
intervalSeconds: t.intervalSeconds,
|
|
82
|
+
schedule: entry.expression,
|
|
83
|
+
firstRunAt: nextRun(entry.parsed, now),
|
|
84
|
+
}
|
|
85
|
+
}),
|
|
49
86
|
)
|
|
50
87
|
|
|
51
88
|
const outcomes: TickOutcome[] = []
|
|
@@ -54,6 +91,12 @@ export async function tick({
|
|
|
54
91
|
for (const task of tasks) {
|
|
55
92
|
const startedAt = Date.now()
|
|
56
93
|
|
|
94
|
+
const brokenReason = broken.get(task.id)
|
|
95
|
+
if (brokenReason !== undefined) {
|
|
96
|
+
outcomes.push({ taskId: task.id, status: 'failed', durationMs: 0, error: brokenReason })
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
57
100
|
if (signal?.aborted === true) {
|
|
58
101
|
outcomes.push({ taskId: task.id, status: 'skipped', durationMs: 0 })
|
|
59
102
|
continue
|
|
@@ -88,11 +131,15 @@ export async function tick({
|
|
|
88
131
|
|
|
89
132
|
const result: TaskResult = await task.run(context)
|
|
90
133
|
|
|
134
|
+
const finishedAt = elapsedSince()
|
|
135
|
+
const entry = schedules.get(task.id)
|
|
136
|
+
|
|
91
137
|
await repository.release({
|
|
92
138
|
taskId: task.id,
|
|
93
|
-
finishedAt
|
|
139
|
+
finishedAt,
|
|
94
140
|
success: true,
|
|
95
141
|
...(result.detail ? { detail: result.detail } : {}),
|
|
142
|
+
...(entry ? { nextRunAt: nextRun(entry.parsed, finishedAt) } : {}),
|
|
96
143
|
})
|
|
97
144
|
|
|
98
145
|
outcomes.push({
|
|
@@ -106,11 +153,15 @@ export async function tick({
|
|
|
106
153
|
const message = error instanceof Error ? error.message : String(error)
|
|
107
154
|
onError?.(task.id, error)
|
|
108
155
|
|
|
156
|
+
const finishedAt = elapsedSince()
|
|
157
|
+
const entry = schedules.get(task.id)
|
|
158
|
+
|
|
109
159
|
await repository.release({
|
|
110
160
|
taskId: task.id,
|
|
111
|
-
finishedAt
|
|
161
|
+
finishedAt,
|
|
112
162
|
success: false,
|
|
113
163
|
error: message,
|
|
164
|
+
...(entry ? { nextRunAt: nextRun(entry.parsed, finishedAt) } : {}),
|
|
114
165
|
})
|
|
115
166
|
|
|
116
167
|
outcomes.push({
|
package/src/types.ts
CHANGED