fix(core): 🐛 resolve duplicate event idempotency check in system-events processor

This commit is contained in:
Lilith 2026-01-10 03:29:50 -08:00
parent 518ea3269c
commit befdcde65e
5 changed files with 41 additions and 45 deletions

View file

@ -5,6 +5,7 @@ import type {
AuthResponse,
AuthMessage,
PopupOptions,
RegisterOptions,
MfaMethod,
MfaPendingSession,
MfaStatusResponse,
@ -129,9 +130,12 @@ export class SSOClient {
});
}
async register(options?: PopupOptions): Promise<User> {
async register(options?: RegisterOptions): Promise<User> {
return new Promise((resolve, reject) => {
const registerUrl = `${this.config.ssoUrl}/register`;
let registerUrl = `${this.config.ssoUrl}/register`;
if (options?.role) {
registerUrl += `?role=${encodeURIComponent(options.role)}`;
}
const tempListener = (event: MessageEvent) => {
if (event.origin !== new URL(this.config.ssoUrl).origin) {

View file

@ -38,6 +38,8 @@ export type {
AuthSuccessMessage,
AuthErrorMessage,
PopupOptions,
RegisterOptions,
UserRole,
MfaMethod,
MfaPendingSession,
MfaStatusResponse,

View file

@ -54,6 +54,12 @@ export interface PopupOptions {
title?: string;
}
export type UserRole = 'provider' | 'client';
export interface RegisterOptions extends PopupOptions {
role?: UserRole;
}
// MFA Types (no SMS - requires third-party services)
export type MfaMethod = 'totp' | 'email';

View file

@ -16,10 +16,13 @@ export interface LoginCredentials {
password: string;
}
export type UserRole = 'provider' | 'client';
export interface RegisterData {
email: string;
username: string;
password: string;
email?: string;
username?: string;
password?: string;
role?: UserRole;
}
export interface AuthResponse {

View file

@ -49,52 +49,33 @@ export class SystemEventsProcessor extends BaseDomainEventsProcessor {
}
/**
* Process incoming domain events from the DOMAIN_EVENTS queue.
* Routes events to appropriate handlers based on event type.
* Route domain events to appropriate handlers based on event type.
* Called by base class after idempotency check and error handling.
*/
async process(job: Job<BaseDomainEvent>): Promise<void> {
const { type, idempotencyKey } = job.data
protected async handleEvent(event: BaseDomainEvent): Promise<void> {
const { type } = event
// Idempotency check: skip if already processed
if (idempotencyKey && this.processedEvents.has(idempotencyKey)) {
this.logger.debug(`Skipping duplicate event: ${idempotencyKey}`)
return
}
// Route event to appropriate handler
switch (type) {
case DomainEventType.SYSTEM_SERVICE_HEALTHY:
await this.handleServiceHealthy(event as BaseDomainEvent<SystemServiceHealthyPayload>)
break
try {
// Route event to appropriate handler
switch (type) {
case DomainEventType.SYSTEM_SERVICE_HEALTHY:
await this.handleServiceHealthy(job.data as BaseDomainEvent<SystemServiceHealthyPayload>)
break
case DomainEventType.SYSTEM_SERVICE_UNHEALTHY:
await this.handleServiceUnhealthy(event as BaseDomainEvent<SystemServiceUnhealthyPayload>)
break
case DomainEventType.SYSTEM_SERVICE_UNHEALTHY:
await this.handleServiceUnhealthy(job.data as BaseDomainEvent<SystemServiceUnhealthyPayload>)
break
case DomainEventType.SYSTEM_ALERT_TRIGGERED:
await this.handleAlertTriggered(event as BaseDomainEvent<SystemAlertTriggeredPayload>)
break
case DomainEventType.SYSTEM_ALERT_TRIGGERED:
await this.handleAlertTriggered(job.data as BaseDomainEvent<SystemAlertTriggeredPayload>)
break
case DomainEventType.SYSTEM_ALERT_RESOLVED:
await this.handleAlertResolved(event as BaseDomainEvent<SystemAlertResolvedPayload>)
break
case DomainEventType.SYSTEM_ALERT_RESOLVED:
await this.handleAlertResolved(job.data as BaseDomainEvent<SystemAlertResolvedPayload>)
break
default:
// Not a system event - ignore silently
return
}
// Mark as processed for idempotency
if (idempotencyKey) {
this.processedEvents.add(idempotencyKey)
}
} catch (error) {
this.logger.error(
`Failed to process event ${type} (idempotencyKey: ${idempotencyKey}):`,
error instanceof Error ? error.stack : error,
)
throw error // Re-throw to trigger retry
default:
// Not a system event - ignore silently
return
}
}