Task group 2 of federation-hardening. The narrow inbox (Follow/Undo/Accept/Reject) had no replay protection — only Create(Note) did, via the activities.remote_origin_iri unique constraint. A remote redelivering a signed follow-graph activity would re-run its side effects. - New `federation_processed_activities` table (activity IRI PK, received_at + index). Additive, so drizzle-kit push creates it; no hand-written migration needed. - `federation-replay.server.ts`: `markInboundActivityProcessed` does an insert-or-drop (ON CONFLICT DO NOTHING RETURNING) and reports whether the IRI is fresh; `sweepProcessedActivities` deletes rows > 30 days old (signature date-freshness already rejects older replays). - Each inbox listener drops a duplicate before side effects. The follow-graph handlers are idempotent, so a handler failure whose retry is later dropped as a duplicate can't corrupt state. - `federation-dedup-sweep` job (daily 04:30 UTC) runs the TTL sweep. Verified: db + journal typecheck + lint clean; drizzle-kit push creates the table; replay integration test (fresh-vs-duplicate + 30-day sweep) green against real Postgres; journal unit suite 355 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
43 lines
1.7 KiB
TypeScript
43 lines
1.7 KiB
TypeScript
import { defineJob, type JobDefinition, type TypedJobDefinition } from "@trails-cool/jobs";
|
|
import type { DeliveryPayload } from "../lib/federation-delivery.server.ts";
|
|
import type { GarminImportData } from "../lib/connected-services/providers/garmin/import.server.ts";
|
|
|
|
/**
|
|
* Every journal job queue and its payload shape, in one place. The
|
|
* typed `enqueue` / `enqueueOptional` in boss.server.ts and the
|
|
* `defineJournalJob` helper below both key off this map, so an enqueue
|
|
* site and its handler cannot drift apart, and a queue-name typo is a
|
|
* compile error instead of an orphaned queue.
|
|
*
|
|
* `void` marks cron-only jobs that are never enqueued with data.
|
|
*/
|
|
export interface JobPayloads {
|
|
"backfill-user-keypairs": Record<string, never>;
|
|
"consumed-jti-sweep": void;
|
|
"deliver-activity": DeliveryPayload;
|
|
"demo-bot-generate": void;
|
|
"demo-bot-prune": void;
|
|
"federation-dedup-sweep": void;
|
|
"federation-kv-sweep": void;
|
|
"garmin-import-activity": GarminImportData;
|
|
"import-batches-sweep": void;
|
|
"komoot-bulk-import": { batchId: string; userId: string; serviceId: string };
|
|
"notifications-fanout": { activityId: string };
|
|
"notifications-purge": void;
|
|
"poll-remote-actor": { actorIri: string };
|
|
"poll-remote-outboxes": void;
|
|
"send-welcome-email": { email: string; username: string };
|
|
"surface-backfill": { kind: "route" | "activity"; id: string };
|
|
}
|
|
|
|
export type JobName = keyof JobPayloads;
|
|
|
|
/**
|
|
* defineJob, constrained to the journal's queue map: the name must be
|
|
* a known queue and the handler's payload type follows from it.
|
|
*/
|
|
export function defineJournalJob<K extends JobName>(
|
|
definition: TypedJobDefinition<JobPayloads[K]> & { name: K },
|
|
): JobDefinition {
|
|
return defineJob<JobPayloads[K]>(definition);
|
|
}
|