feat(webhooks): add per-verb subscriptions and bulk-create debounce

Subscriptions now declare a `verb` (create | delete | bulk-create).
POST /:did/webhooks defaults to inserting both create and delete rows
when no verb is given, preserving existing all-events behavior. Update
events fold into the create verb. The new bulk-create verb debounces
creates per DID over 400 ms and delivers a `records` array.

Migration adds the verb column with default 'create' and clones every
existing row for the delete verb so legacy subscriptions keep firing
on both events.
This commit is contained in:
Julien Calixte
2026-05-05 12:25:54 +02:00
parent 8e967ba43c
commit 7b53909c52
4 changed files with 122 additions and 18 deletions

View File

@@ -5,6 +5,7 @@ import {
getNotes,
getNotesByDid,
getNotesByDids,
type WebhookVerb,
} from "./src/data/db.ts";
import { log } from "./src/log.ts";
@@ -84,18 +85,30 @@ router.post("/notes/feed", async (ctx) => {
ctx.response.body = getNotesByDids(dids, cursor, Number(limit) || PAGINATION);
});
const ALLOWED_VERBS = ["create", "delete", "bulk-create"] as const;
router.post("/:did/webhooks", async (ctx) => {
const { did } = ctx.params;
const body = await ctx.request.body.json();
const { method, url, token } = body ?? {};
const { method, url, token, verb } = body ?? {};
if (!method || !url) {
ctx.response.status = 400;
ctx.response.body = { error: "method and url are required" };
return;
}
const subscription = addWebhookSubscription({ did, method, url, token });
if (verb !== undefined && !ALLOWED_VERBS.includes(verb)) {
ctx.response.status = 400;
ctx.response.body = {
error: `verb must be one of ${ALLOWED_VERBS.join(", ")}`,
};
return;
}
const verbsToInsert: WebhookVerb[] = verb ? [verb] : ["create", "delete"];
const subscriptions = verbsToInsert.map((v) =>
addWebhookSubscription({ did, method, url, token, verb: v })
);
ctx.response.status = 201;
ctx.response.body = subscription;
ctx.response.body = subscriptions;
});
router.delete("/:did/webhooks", (ctx) => {