import type { HttpContext } from '@adonisjs/core/http' import SeedOrchestrator, { LeaseLost } from '#services/SeedOrchestrator' import type { SeedBatch } from '#lib/seed/wire' /** * The seed API (CONCEPT.md D38). Nodes make outgoing HTTPS requests only: * lease a range, post batches of what they found, then finish or release * the range. Bodies may be gzip-compressed (Content-Encoding: gzip). */ export default class SeedController { /** POST /api/v1/seed/lease → 200 { range } or 204 when there's nothing to do. */ async lease({ seedNode, response }: HttpContext) { const range = await SeedOrchestrator.lease(seedNode.id) return range ? { range } : response.noContent() } /** POST /api/v1/seed/ranges/:id/batches with a SeedBatch. */ async batch({ seedNode, params, request, response }: HttpContext) { const batch = request.body() as SeedBatch if (!Number.isSafeInteger(batch?.next) || !Array.isArray(batch.files) || !Array.isArray(batch.fragments)) { return response.badRequest({ error: 'Expected { next, headers, files[], fragments[] }' }) } return this.leased(response, async () => { const stored = await SeedOrchestrator.batch(seedNode.id, Number(params.id), { ...batch, headers: Number(batch.headers) || 0, obfuscated: Number(batch.obfuscated) || 0, deferred: Number(batch.deferred) || 0, }) return { stored } }) } /** POST /api/v1/seed/ranges/:id/done */ async done({ seedNode, params, response }: HttpContext) { return this.leased(response, async () => { await SeedOrchestrator.finish(seedNode.id, Number(params.id)) return { ok: true } }) } /** POST /api/v1/seed/ranges/:id/release { error?, uncounted? } */ async release({ seedNode, params, request, response }: HttpContext) { const error = request.input('error') const uncounted = request.input('uncounted') === true return this.leased(response, async () => { await SeedOrchestrator.release(seedNode.id, Number(params.id), typeof error === 'string' ? error : undefined, uncounted) return { ok: true } }) } /** GET /api/v1/seed/status */ async status() { return SeedOrchestrator.status() } /** 409 when the range was given to another node meanwhile; the node should drop it. */ private async leased(response: HttpContext['response'], action: () => Promise) { try { return await action() } catch (error) { if (error instanceof LeaseLost) return response.conflict({ error: error.message }) throw error } } }