fix(nostr): retry inbound events after handler failures

This commit is contained in:
Vincent Koc
2026-04-13 16:47:52 +01:00
parent 74b4a08592
commit d4824f9a8f
2 changed files with 31 additions and 3 deletions

View File

@@ -235,6 +235,32 @@ describe("startNostrBus inbound guards", () => {
bus.close();
});
it("retries a replayed event after the message handler fails", async () => {
const onMessage = vi
.fn<(sender: string, plaintext: string) => Promise<void>>()
.mockRejectedValueOnce(new Error("boom"))
.mockResolvedValueOnce(undefined);
const bus = await startNostrBus({
privateKey: TEST_HEX_PRIVATE_KEY,
onMessage,
onMetric: () => {},
});
const event = createEvent({
id: "retry-after-handler-failure",
});
await emitEvent(event);
await emitEvent(event);
expect(mockState.verifyEvent).toHaveBeenCalledTimes(2);
expect(mockState.decrypt).toHaveBeenCalledTimes(2);
expect(onMessage).toHaveBeenCalledTimes(2);
expect(bus.getMetrics().eventsProcessed).toBe(1);
bus.close();
});
it("does not rate limit an allowed sender while another authorization is still pending", async () => {
const onMessage = vi.fn(async () => {});
let resolveBlocked: ((value: "block") => void) | undefined;

View File

@@ -617,15 +617,13 @@ export async function startNostrBus(options: NostrBusOptions): Promise<NostrBusH
}
}
// Mark seen AFTER verify (don't cache invalid IDs)
markSeen();
// Decrypt the message
let plaintext: string;
try {
plaintext = decrypt(sk, event.pubkey, event.content);
metrics.emit("decrypt.success");
} catch (err) {
markSeen();
metrics.emit("decrypt.failure");
metrics.emit("event.rejected.decrypt_failed");
onError?.(err as Error, `decrypt from ${event.pubkey}`);
@@ -633,6 +631,7 @@ export async function startNostrBus(options: NostrBusOptions): Promise<NostrBusH
}
if (Buffer.byteLength(plaintext, "utf8") > guardPolicy.maxPlaintextBytes) {
markSeen();
metrics.emit("event.rejected.oversized_plaintext");
return;
}
@@ -643,6 +642,9 @@ export async function startNostrBus(options: NostrBusOptions): Promise<NostrBusH
createdAt: event.created_at,
});
// Only cache successful deliveries so handler failures can retry.
markSeen();
// Mark as processed
metrics.emit("event.processed");