feat(FN-2865): add notification provider abstractions

- Add shared notification event/payload/provider config types in core
- Introduce notification module exports with provider interface and dispatcher implementation
- Extend settings schema to accept notificationProviders configuration
- Add dispatcher unit coverage for provider filtering, failures, and delivery behavior
This commit is contained in:
Fusion
2026-04-28 08:53:36 -07:00
committed by gsxdsm
parent 6f06214ce6
commit 770818e740
8 changed files with 327 additions and 1 deletions

View File

@@ -0,0 +1,89 @@
import type { NotificationProvider } from "./provider.js";
import type {
NotificationDispatcherConfig,
NotificationEvent,
NotificationPayload,
NotificationResult,
} from "./types.js";
export class NotificationDispatcher {
private readonly providers = new Map<string, NotificationProvider>();
constructor(private readonly config: NotificationDispatcherConfig = {}) {}
registerProvider(provider: NotificationProvider): void {
this.providers.set(provider.getProviderId(), provider);
}
unregisterProvider(providerId: string): void {
this.providers.delete(providerId);
}
getProviders(): readonly NotificationProvider[] {
return [...this.providers.values()];
}
async dispatch(
event: NotificationEvent,
payload: NotificationPayload,
): Promise<NotificationResult[]> {
const providers = this.getProviders().filter((provider) =>
provider.isEventSupported(event),
);
const results = await Promise.all(
providers.map(async (provider): Promise<NotificationResult> => {
const providerId = provider.getProviderId();
try {
return await provider.sendNotification(event, payload);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(
`[notification-dispatcher] Provider ${providerId} failed for event ${event}: ${message}`,
);
return { success: false, providerId, error: message };
}
}),
);
return results;
}
async initializeAll(): Promise<void> {
await Promise.all(
this.getProviders().map(async (provider) => {
if (!provider.initialize) {
return;
}
try {
await provider.initialize(this.config as Record<string, unknown>);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(
`[notification-dispatcher] Provider ${provider.getProviderId()} initialization failed: ${message}`,
);
}
}),
);
}
async shutdownAll(): Promise<void> {
await Promise.all(
this.getProviders().map(async (provider) => {
if (!provider.shutdown) {
return;
}
try {
await provider.shutdown();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(
`[notification-dispatcher] Provider ${provider.getProviderId()} shutdown failed: ${message}`,
);
}
}),
);
}
}

View File

@@ -0,0 +1,3 @@
export * from "./types.js";
export * from "./provider.js";
export * from "./dispatcher.js";

View File

@@ -0,0 +1,13 @@
import type { NotificationEvent, NotificationPayload } from "./types.js";
import type { NotificationResult } from "./types.js";
export interface NotificationProvider {
getProviderId(): string;
sendNotification(
event: NotificationEvent,
payload: NotificationPayload,
): Promise<NotificationResult>;
isEventSupported(event: NotificationEvent): boolean;
initialize?(config: Record<string, unknown>): Promise<void>;
shutdown?(): Promise<void>;
}

View File

@@ -0,0 +1,17 @@
export {
NOTIFICATION_EVENTS,
type NotificationEvent,
type NotificationPayload,
type NotificationProviderConfig,
} from "../types.js";
export interface NotificationResult {
success: boolean;
providerId: string;
error?: string;
}
export interface NotificationDispatcherConfig {
maxRetries?: number;
retryDelayMs?: number;
}