feat(federation): permanent-failure callback registry
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
registerPermanentFailureCallback,
|
||||||
|
invokePermanentFailureCallback,
|
||||||
|
_resetCallbacks,
|
||||||
|
} from './federationRollback.js';
|
||||||
|
|
||||||
|
describe('permanent-failure callback registry', () => {
|
||||||
|
beforeEach(() => _resetCallbacks());
|
||||||
|
|
||||||
|
it('invokes the registered callback when called by eventType', () => {
|
||||||
|
const cb = vi.fn();
|
||||||
|
registerPermanentFailureCallback('friend_request_create', cb);
|
||||||
|
invokePermanentFailureCallback('friend_request_create', 'msg-123', 'recipient_not_found');
|
||||||
|
expect(cb).toHaveBeenCalledWith('msg-123', 'recipient_not_found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is a no-op for unregistered event types', () => {
|
||||||
|
expect(() =>
|
||||||
|
invokePermanentFailureCallback('unknown_event_type', 'msg-1', 'whatever')
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces a previously-registered callback for the same eventType', () => {
|
||||||
|
const old = vi.fn();
|
||||||
|
const fresh = vi.fn();
|
||||||
|
registerPermanentFailureCallback('e1', old);
|
||||||
|
registerPermanentFailureCallback('e1', fresh);
|
||||||
|
invokePermanentFailureCallback('e1', 'msg-9', 'reason');
|
||||||
|
expect(old).not.toHaveBeenCalled();
|
||||||
|
expect(fresh).toHaveBeenCalledWith('msg-9', 'reason');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('swallows errors thrown by the callback (logs but does not throw)', () => {
|
||||||
|
registerPermanentFailureCallback('boom', () => { throw new Error('test'); });
|
||||||
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
expect(() => invokePermanentFailureCallback('boom', 'msg-x', 'r')).not.toThrow();
|
||||||
|
expect(errSpy).toHaveBeenCalled();
|
||||||
|
errSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Permanent-failure callback registry for outbox events.
|
||||||
|
*
|
||||||
|
* When the federation worker observes a receiver-acknowledged terminal
|
||||||
|
* rejection (4xx with a recognized reason like 'recipient_not_found'),
|
||||||
|
* it invokes the registered callback for the eventType so the originating
|
||||||
|
* instance can roll back any local state created at queue time.
|
||||||
|
*
|
||||||
|
* Callbacks are NEVER invoked on transient failures (5xx, network errors,
|
||||||
|
* retry exhaustion). Only on receiver-acknowledged terminal rejections.
|
||||||
|
*
|
||||||
|
* Errors thrown by callbacks are logged but not re-thrown — rollback failure
|
||||||
|
* must not prevent the outbox entry from being deleted.
|
||||||
|
*/
|
||||||
|
type PermanentFailureCallback = (messageId: string, reason: string) => void;
|
||||||
|
|
||||||
|
const callbacks = new Map<string, PermanentFailureCallback>();
|
||||||
|
|
||||||
|
export function registerPermanentFailureCallback(eventType: string, cb: PermanentFailureCallback): void {
|
||||||
|
callbacks.set(eventType, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invokePermanentFailureCallback(eventType: string, messageId: string, reason: string): void {
|
||||||
|
const cb = callbacks.get(eventType);
|
||||||
|
if (!cb) return;
|
||||||
|
try {
|
||||||
|
cb(messageId, reason);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`[federation-rollback] callback for ${eventType} (msg=${messageId}, reason=${reason}) threw:`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test-only: clear the registry between tests. */
|
||||||
|
export function _resetCallbacks(): void {
|
||||||
|
callbacks.clear();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user