refactor(jetstream): extract webhook dispatch and bulk-create modules
dispatchAll, fireWebhooks, and the bulk-create debounce buffer
move out of jetstream.ts into src/jetstream/{webhooks,bulk}.ts.
fireWebhooks and the bulk helpers now take their DB lookup and
dispatch as parameters so tests can inject stubs. Top-level
Jetstream construction, event handlers, ensureIndex call, and
the cursor-saving interval are gated behind import.meta.main so
importing jetstream.ts no longer opens a WebSocket.
This commit is contained in:
66
src/jetstream/bulk.ts
Normal file
66
src/jetstream/bulk.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { log } from "../log.ts";
|
||||
import type { GetSubsByVerb, WebhookTarget } from "./webhooks.ts";
|
||||
|
||||
export const BULK_CREATE_DEBOUNCE_MS = 500;
|
||||
|
||||
export type BulkDeps = {
|
||||
getSubs: GetSubsByVerb;
|
||||
dispatch: (
|
||||
webhooks: WebhookTarget[],
|
||||
payload: Record<string, unknown>,
|
||||
label: string,
|
||||
) => Promise<void>;
|
||||
debounceMs?: number;
|
||||
};
|
||||
|
||||
type BulkBuffer = {
|
||||
records: Record<string, unknown>[];
|
||||
timer: number;
|
||||
};
|
||||
|
||||
const bulkBuffers = new Map<string, BulkBuffer>();
|
||||
|
||||
export const flushBulkCreate = async (
|
||||
did: string,
|
||||
deps: BulkDeps,
|
||||
): Promise<void> => {
|
||||
const buffer = bulkBuffers.get(did);
|
||||
if (!buffer) return;
|
||||
bulkBuffers.delete(did);
|
||||
const webhooks = deps.getSubs(did, "bulk-create");
|
||||
log(`[jetstream] bulk-create ${did}: ${buffer.records.length} record(s)`);
|
||||
await deps.dispatch(
|
||||
webhooks,
|
||||
{ event: "bulk-create", did, records: buffer.records },
|
||||
`bulk-create ${did}`,
|
||||
);
|
||||
};
|
||||
|
||||
// Buffered records are not persisted: if jetstream restarts mid-window, those
|
||||
// `bulk-create` notifications are lost. Subscribers reconcile on cold start
|
||||
// because the underlying notes are already saved to the `note` table.
|
||||
export const queueBulkCreate = (
|
||||
did: string,
|
||||
record: Record<string, unknown>,
|
||||
deps: BulkDeps,
|
||||
): void => {
|
||||
const ms = deps.debounceMs ?? BULK_CREATE_DEBOUNCE_MS;
|
||||
const existing = bulkBuffers.get(did);
|
||||
if (existing) {
|
||||
clearTimeout(existing.timer);
|
||||
existing.records.push(record);
|
||||
existing.timer = setTimeout(() => flushBulkCreate(did, deps), ms);
|
||||
return;
|
||||
}
|
||||
bulkBuffers.set(did, {
|
||||
records: [record],
|
||||
timer: setTimeout(() => flushBulkCreate(did, deps), ms),
|
||||
});
|
||||
};
|
||||
|
||||
export const _resetBulkBuffersForTest = (): void => {
|
||||
for (const buffer of bulkBuffers.values()) {
|
||||
clearTimeout(buffer.timer);
|
||||
}
|
||||
bulkBuffers.clear();
|
||||
};
|
||||
49
src/jetstream/webhooks.ts
Normal file
49
src/jetstream/webhooks.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { WebhookVerb } from "../data/db.ts";
|
||||
import { log } from "../log.ts";
|
||||
|
||||
export type WebhookTarget = {
|
||||
method: string;
|
||||
url: string;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export const dispatchAll = async (
|
||||
webhooks: WebhookTarget[],
|
||||
payload: Record<string, unknown>,
|
||||
label: string,
|
||||
): Promise<void> => {
|
||||
if (webhooks.length === 0) return;
|
||||
const results = await Promise.allSettled(
|
||||
webhooks.map(({ method, url, token }) => {
|
||||
const hasBody = method !== "GET" && method !== "HEAD";
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
...(hasBody ? { "Content-Type": "application/json" } : {}),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: hasBody ? JSON.stringify(payload) : undefined,
|
||||
});
|
||||
}),
|
||||
);
|
||||
for (const result of results) {
|
||||
if (result.status === "rejected") {
|
||||
log(`[jetstream] ${label} webhook error:`, result.reason);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export type GetSubsByVerb = (
|
||||
did: string,
|
||||
verb: WebhookVerb,
|
||||
) => WebhookTarget[];
|
||||
|
||||
export const fireWebhooks = async (
|
||||
did: string,
|
||||
verb: WebhookVerb,
|
||||
payload: Record<string, unknown>,
|
||||
getSubs: GetSubsByVerb,
|
||||
): Promise<void> => {
|
||||
const webhooks = getSubs(did, verb);
|
||||
await dispatchAll(webhooks, payload, `${verb} ${did}`);
|
||||
};
|
||||
Reference in New Issue
Block a user