mirror your GitHub repos to tangled.org automatically
1

Configure Feed

Select the types of activity you want to include in your feed.

perf: dispatch worker on webhook receipt

+111
+4
server/api/github/webhook.post.ts
··· 9 9 import { verify } from '@octokit/webhooks-methods' 10 10 import { sql } from 'drizzle-orm' 11 11 import { installation, webhookEvent } from '#server/db/schema' 12 + import { kickWorker } from '#server/utils/kick-worker' 12 13 import { enqueue } from '#server/utils/queue' 13 14 import { revokeKeysForInstallation } from '#server/utils/tangled-pubkey' 14 15 ··· 110 111 // the raw webhook body. See PLAN.md "Deferred / follow-ups". 111 112 if (RECOGNISED_EVENTS.has(eventName)) { 112 113 await enqueueForEvent(event, eventName, deliveryId) 114 + // Nudge the worker to drain the just-enqueued job now rather than on the 115 + // next cron tick. Fire-and-forget via waitUntil; cron is the safety net. 116 + kickWorker(event) 113 117 } 114 118 115 119 return { ok: true, deliveryId }
+28
server/utils/kick-worker.ts
··· 1 + import type { H3Event } from 'h3' 2 + 3 + /** 4 + * Nudge the job worker to run now, instead of waiting for the next cron tick. 5 + * 6 + * Fire-and-forget: registered via `event.waitUntil` so the serverless function 7 + * stays alive to complete the kick after the response flushes, but the webhook 8 + * never blocks on it. Any failure is swallowed; the per-minute cron is the 9 + * safety net, so a missed kick only costs latency, not correctness. The worker 10 + * itself is safe to run concurrently with cron (claims are `FOR UPDATE SKIP 11 + * LOCKED` with a lease), so a kick racing a tick can't double-process a job. 12 + * 13 + * Auth uses the same `CRON_SECRET` bearer the worker route already requires. 14 + */ 15 + export function kickWorker(event: H3Event): void { 16 + const cronSecret = process.env.CRON_SECRET 17 + const base = useRuntimeConfig().public.url?.replace(/\/$/, '') 18 + if (!cronSecret || !base) return 19 + 20 + const run = globalThis.fetch(`${base}/api/jobs/run`, { 21 + method: 'GET', 22 + headers: { authorization: `Bearer ${cronSecret}` }, 23 + }) 24 + .then(() => undefined) 25 + .catch(() => undefined) 26 + 27 + event.waitUntil(run) 28 + }
+79
test/unit/kick-worker.spec.ts
··· 1 + import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' 2 + import type { H3Event } from 'h3' 3 + import { kickWorker } from '../../server/utils/kick-worker' 4 + 5 + const ORIGINAL_SECRET = process.env.CRON_SECRET 6 + const ORIGINAL_URL = process.env.NUXT_PUBLIC_URL 7 + 8 + function fakeEvent() { 9 + const waitUntil = vi.fn<(p: Promise<unknown>) => void>() 10 + // eslint-disable-next-line ts/no-unsafe-type-assertion -- only waitUntil is exercised 11 + return { event: { waitUntil } as unknown as H3Event, waitUntil } 12 + } 13 + 14 + describe('kickWorker', () => { 15 + beforeEach(() => { 16 + process.env.CRON_SECRET = 'test-secret' 17 + process.env.NUXT_PUBLIC_URL = 'https://synchub.to' 18 + // kick-worker reads `useRuntimeConfig().public.url`; the Nuxt auto-import 19 + // isn't available in plain unit tests, so stub it to read from env. 20 + vi.stubGlobal('useRuntimeConfig', () => ({ public: { url: process.env.NUXT_PUBLIC_URL ?? '' } })) 21 + }) 22 + 23 + afterEach(() => { 24 + if (ORIGINAL_SECRET === undefined) delete process.env.CRON_SECRET 25 + else process.env.CRON_SECRET = ORIGINAL_SECRET 26 + if (ORIGINAL_URL === undefined) delete process.env.NUXT_PUBLIC_URL 27 + else process.env.NUXT_PUBLIC_URL = ORIGINAL_URL 28 + vi.unstubAllGlobals() 29 + }) 30 + 31 + it('fires a GET to the worker with the cron bearer and registers it via waitUntil', async () => { 32 + const fetchMock = vi.fn<(url: string, init?: RequestInit) => Promise<Response>>(async () => new Response('{}')) 33 + vi.stubGlobal('fetch', fetchMock) 34 + const { event, waitUntil } = fakeEvent() 35 + 36 + kickWorker(event) 37 + 38 + expect(waitUntil).toHaveBeenCalledTimes(1) 39 + expect(fetchMock).toHaveBeenCalledTimes(1) 40 + const [url, init] = fetchMock.mock.calls[0]! 41 + expect(url).toBe('https://synchub.to/api/jobs/run') 42 + expect(init).toMatchObject({ method: 'GET', headers: { authorization: 'Bearer test-secret' } }) 43 + }) 44 + 45 + it('strips a trailing slash from the public URL', () => { 46 + process.env.NUXT_PUBLIC_URL = 'https://synchub.to/' 47 + const fetchMock = vi.fn<(url: string, init?: RequestInit) => Promise<Response>>(async () => new Response('{}')) 48 + vi.stubGlobal('fetch', fetchMock) 49 + const { event } = fakeEvent() 50 + 51 + kickWorker(event) 52 + 53 + expect(fetchMock.mock.calls[0]![0]).toBe('https://synchub.to/api/jobs/run') 54 + }) 55 + 56 + it('no-ops when CRON_SECRET is unset', () => { 57 + delete process.env.CRON_SECRET 58 + const fetchMock = vi.fn<(url: string, init?: RequestInit) => Promise<Response>>() 59 + vi.stubGlobal('fetch', fetchMock) 60 + const { event, waitUntil } = fakeEvent() 61 + 62 + kickWorker(event) 63 + 64 + expect(fetchMock).not.toHaveBeenCalled() 65 + expect(waitUntil).not.toHaveBeenCalled() 66 + }) 67 + 68 + it('swallows a fetch rejection (cron is the safety net)', async () => { 69 + const rejecting = vi.fn<(url: string, init?: RequestInit) => Promise<Response>>(async () => { throw new Error('network down') }) 70 + vi.stubGlobal('fetch', rejecting) 71 + const captured: Promise<unknown>[] = [] 72 + // eslint-disable-next-line ts/no-unsafe-type-assertion -- minimal event stub 73 + const event = { waitUntil: (p: Promise<unknown>) => { captured.push(p) } } as unknown as H3Event 74 + 75 + kickWorker(event) 76 + 77 + await expect(captured[0]).resolves.toBeUndefined() 78 + }) 79 + })