2026-08-18 16:21:22 +00:00
|
|
|
import { CronJob } from 'cron'
|
|
|
|
|
import type { Subscription } from '../alert.js'
|
|
|
|
|
import { getCronExpression } from '../alert.js'
|
|
|
|
|
import { Digest } from '../digest.js'
|
|
|
|
|
import * as db from '../database.js'
|
|
|
|
|
|
|
|
|
|
const jobsById = new Map<string, CronJob>()
|
|
|
|
|
|
2026-09-10 15:28:53 +00:00
|
|
|
// Test-only accessor to inspect stored jobs
|
|
|
|
|
export const getJobCronSource = (id: string): string | undefined => {
|
|
|
|
|
const source = jobsById.get(id)?.cronTime.source
|
|
|
|
|
return typeof source === 'string' ? source : undefined
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 16:21:22 +00:00
|
|
|
export const runJob = async (sub: Subscription) => {
|
|
|
|
|
try {
|
|
|
|
|
if (!sub.confirmed_at || sub.unsubscribed_at) {
|
|
|
|
|
console.log('worker: job skipped', sub.id, 'not confirmed or unsubscribed')
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('worker: job starting', sub.id)
|
|
|
|
|
|
|
|
|
|
const start = Date.now()
|
|
|
|
|
const since = sub.last_digest_at || sub.created_at
|
|
|
|
|
const events = await db.getEventsForSubscription(sub.id, since)
|
|
|
|
|
|
|
|
|
|
if (events.length === 0) {
|
|
|
|
|
console.log('worker: job skipped', sub.id, 'ok', 'no data received')
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const digest = new Digest(sub)
|
|
|
|
|
await digest.sendFromStoredEvents(events)
|
|
|
|
|
|
2026-09-14 17:07:34 +00:00
|
|
|
// Collect the exact IDs that were fetched + sent, then delete ONLY those.
|
|
|
|
|
// Deleting by timestamp (received_at > since) would also remove any event
|
|
|
|
|
// that arrived between the fetch and the delete — the race condition.
|
|
|
|
|
const sentIds = events.map(e => e.id)
|
|
|
|
|
await db.deleteEventsByIds(sub.id, sentIds)
|
2026-08-18 16:21:22 +00:00
|
|
|
await db.updateLastDigestAt(sub.id, Math.floor(Date.now() / 1000))
|
|
|
|
|
|
|
|
|
|
console.log('worker: job completed', sub.id, 'in', Date.now() - start, 'ms')
|
|
|
|
|
return true
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.log('worker: job failed', sub.id, e)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const createJob = (sub: Subscription) => {
|
|
|
|
|
const cron = getCronExpression(sub.frequency)
|
|
|
|
|
|
|
|
|
|
const run = async () => {
|
2026-09-14 17:07:34 +00:00
|
|
|
// Re-fetch the subscription to pick up the latest last_digest_at.
|
|
|
|
|
// The closure-captured `sub` is stale — its last_digest_at never
|
|
|
|
|
// advances, so every tick would re-fetch and re-send old events.
|
|
|
|
|
const fresh = await db.getSubscriptionById(sub.id)
|
|
|
|
|
if (!fresh) return
|
|
|
|
|
await runJob(fresh)
|
2026-08-18 16:21:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return CronJob.from({
|
|
|
|
|
cronTime: cron,
|
|
|
|
|
onTick: run,
|
|
|
|
|
start: true,
|
|
|
|
|
timeZone: 'UTC',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const addJob = (sub: Subscription) => {
|
|
|
|
|
jobsById.get(sub.id)?.stop()
|
|
|
|
|
jobsById.set(sub.id, createJob(sub))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const removeJob = (sub: Subscription) => {
|
|
|
|
|
jobsById.get(sub.id)?.stop()
|
|
|
|
|
jobsById.delete(sub.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Daily purge of events older than 7 days
|
fix: remove pre-eslint unused-variable errors across src/
Remove 21 unused imports/variables that caused eslint failures:
- actions.ts (2): Subscription type import, getCronExpression import
- alert.ts (4): CronExpressionParser, tryCatch, int, HOUR
- digest.ts (12): now, nth, nthEq, dateToSeconds, getIdFilters,
getReplyFilters, Loader, AdapterContext, makeLoader, SocketAdapter,
call, loadRelaySelections
- env.ts (1): netContext
- worker/email.ts (1): purgeJob variable (kept CronJob side-effect)
All unused symbols were pre-existing, not related to changed files.
tsc --noEmit passes; script/checks now returns 0 on the src check.
2026-09-10 15:58:05 +00:00
|
|
|
CronJob.from({
|
2026-08-18 16:21:22 +00:00
|
|
|
cronTime: '0 0 3 * * *', // 3am UTC daily
|
|
|
|
|
onTick: async () => {
|
|
|
|
|
const weekAgo = Math.floor(Date.now() / 1000) - 7 * 24 * 3600
|
|
|
|
|
await db.purgeEventsOlderThan(weekAgo)
|
|
|
|
|
console.log('worker: purged events older than 7 days')
|
|
|
|
|
},
|
|
|
|
|
start: true,
|
|
|
|
|
timeZone: 'UTC',
|
|
|
|
|
})
|