mirror your GitHub repos to tangled.org automatically
1import crypto from 'node:crypto'
2import { dispatch } from '~~/server/utils/job-handlers'
3import { claim, complete, fail } from '~~/server/utils/queue'
4
5const LEASE_MS = 5 * 60_000 // 5 min — generous for a sync job
6const DEFAULT_BUDGET_MS = 25_000 // leave headroom under Vercel's 10s default; pro tiers can override
7
8export default defineEventHandler(async event => {
9 const config = useRuntimeConfig()
10 const cronSecret = config.cronSecret
11 if (!cronSecret) {
12 throw createError({ statusCode: 500, statusMessage: 'cron secret not configured' })
13 }
14
15 const auth = getRequestHeader(event, 'authorization')
16 if (auth !== `Bearer ${cronSecret}`) {
17 throw createError({ statusCode: 401, statusMessage: 'unauthorized' })
18 }
19
20 const workerId = `${process.env.VERCEL_DEPLOYMENT_ID ?? 'local'}:${crypto.randomUUID()}`
21 const budgetMs = Number(config.workerBudgetMs) || DEFAULT_BUDGET_MS
22 const deadline = Date.now() + budgetMs
23
24 let processed = 0
25 let drained = false
26
27 // Sequential by design: each iteration claims one job, runs it, records the
28 // outcome. We don't parallelise because each Vercel invocation is a single
29 // small worker; concurrency comes from cron firing multiple invocations.
30 // eslint-disable-next-line no-await-in-loop
31 while (Date.now() < deadline) {
32 // eslint-disable-next-line no-await-in-loop
33 const job = await claim(workerId, LEASE_MS)
34 if (!job) {
35 drained = true
36 break
37 }
38
39 try {
40 // eslint-disable-next-line no-await-in-loop
41 await dispatch(job)
42 // eslint-disable-next-line no-await-in-loop
43 await complete(job.id)
44 }
45 catch (err) {
46 // eslint-disable-next-line no-await-in-loop
47 await fail(job.id, job.attempts, err)
48 }
49
50 processed++
51 }
52
53 return { ok: true, processed, drained, workerId }
54})