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 cronSecret = process.env.CRON_SECRET
10 if (!cronSecret) {
11 throw createError({ statusCode: 500, statusMessage: 'cron secret not configured' })
12 }
13
14 const auth = getRequestHeader(event, 'authorization')
15 if (auth !== `Bearer ${cronSecret}`) {
16 throw createError({ statusCode: 401, statusMessage: 'unauthorized' })
17 }
18
19 const workerId = `${process.env.VERCEL_DEPLOYMENT_ID ?? 'local'}:${crypto.randomUUID()}`
20 const budgetMs = Number(useRuntimeConfig().workerBudgetMs) || DEFAULT_BUDGET_MS
21 const deadline = Date.now() + budgetMs
22
23 let processed = 0
24 let drained = false
25
26 // Sequential by design: each iteration claims one job, runs it, records the
27 // outcome. We don't parallelise because each Vercel invocation is a single
28 // small worker; concurrency comes from cron firing multiple invocations.
29 // eslint-disable-next-line no-await-in-loop
30 while (Date.now() < deadline) {
31 // eslint-disable-next-line no-await-in-loop
32 const job = await claim(workerId, LEASE_MS)
33 if (!job) {
34 drained = true
35 break
36 }
37
38 try {
39 // eslint-disable-next-line no-await-in-loop
40 await dispatch(job)
41 // eslint-disable-next-line no-await-in-loop
42 await complete(job.id)
43 }
44 catch (err) {
45 // eslint-disable-next-line no-await-in-loop
46 await fail(job.id, job.attempts, err)
47 }
48
49 processed++
50 }
51
52 return { ok: true, processed, drained, workerId }
53})