Skip to content

Commit 1909574

Browse files
author
Developer
committed
fix(dashboard): replace SSE guest streaming with stable polling mechanism
- src/components/dashboard/guests/GuestDashboardApp.tsx: remove unused guest notification handler - src/components/dashboard/guests/use-guest-dashboard-realtime.ts: implement 25s polling and remove SSE listener logic - src/layouts/DashboardLayout.astro: cleanup layout logic and remove redundant production redirects - src/lib/rsvp/core/stream.ts: delete obsolete SSE pub/sub infrastructure - src/lib/rsvp/services/dashboard-guests.service.ts: remove guest update event publishing - src/lib/rsvp/services/rsvp-submission.service.ts: remove submission event publishing - src/pages/api/dashboard/guests/stream.ts: remove obsolete SSE streaming endpoint - tests/api/dashboard.guests.stream.test.ts: remove discontinued streaming API tests - tests/components/guests.hooks.test.tsx: update dashboard hook tests to reflect removed stream interface - tests/lib/rsvp-v2/stream.test.ts: remove discontinued stream core tests
1 parent b8f8f36 commit 1909574

File tree

10 files changed

+17
-407
lines changed

10 files changed

+17
-407
lines changed

src/components/dashboard/guests/GuestDashboardApp.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,6 @@ const GuestDashboardApp: React.FC<GuestDashboardAppProps> = ({ initialEventId })
2626
'all',
2727
);
2828
const searchInputRef = useRef<HTMLInputElement>(null);
29-
const [notificationSeed, setNotificationSeed] = useState<{
30-
message: string;
31-
type: 'info' | 'success' | 'warning';
32-
} | null>(null);
3329
const {
3430
error,
3531
eventId,
@@ -45,7 +41,6 @@ const GuestDashboardApp: React.FC<GuestDashboardAppProps> = ({ initialEventId })
4541
initialEventId,
4642
search,
4743
status,
48-
onNotification: setNotificationSeed,
4944
});
5045
const {
5146
celebratingGuestId,
@@ -82,12 +77,6 @@ const GuestDashboardApp: React.FC<GuestDashboardAppProps> = ({ initialEventId })
8277
setItems,
8378
});
8479

85-
useEffect(() => {
86-
if (notificationSeed) {
87-
setNotification(notificationSeed);
88-
}
89-
}, [notificationSeed, setNotification]);
90-
9180
useShortcuts(
9281
{
9382
'/': () => searchInputRef.current?.focus(),

src/components/dashboard/guests/use-guest-dashboard-realtime.ts

Lines changed: 14 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useRef, useState } from 'react';
1+
import { useCallback, useEffect, useState } from 'react';
22
import { guestsApi } from '@/lib/dashboard/guests-api';
33
import type {
44
DashboardGuestItem,
@@ -14,7 +14,7 @@ interface HostEventItem {
1414
eventType: EventRecord['eventType'];
1515
}
1616

17-
export type RealtimeState = 'connected' | 'reconnecting' | 'fallback';
17+
export type RealtimeState = 'connected' | 'fallback';
1818

1919
const DEFAULT_TOTALS: DashboardGuestListResponse['totals'] = {
2020
totalInvitations: 0,
@@ -30,18 +30,13 @@ const DEFAULT_TOTALS: DashboardGuestListResponse['totals'] = {
3030
viewed: 0,
3131
};
3232

33-
interface NotificationPayload {
34-
message: string;
35-
type: 'info' | 'success' | 'warning';
36-
}
37-
3833
interface UseGuestDashboardRealtimeOptions {
3934
initialEventId: string;
4035
search: 'all' | string;
4136
status: 'all' | 'pending' | 'confirmed' | 'declined' | 'viewed';
42-
onNotification: (notification: NotificationPayload) => void;
4337
}
4438

39+
const DASHBOARD_POLLING_INTERVAL_MS = 25000; // 25 seconds is a safe, stable interval for Serverless tasks.
4540
function getErrorMessage(error: unknown, fallback: string): string {
4641
return error instanceof Error ? error.message : fallback;
4742
}
@@ -99,7 +94,6 @@ export const useGuestDashboardRealtime = ({
9994
initialEventId,
10095
search,
10196
status,
102-
onNotification,
10397
}: UseGuestDashboardRealtimeOptions) => {
10498
const [eventId, setEventId] = useState<string>(initialEventId || '');
10599
const [hostEvents, setHostEvents] = useState<HostEventItem[]>([]);
@@ -112,9 +106,6 @@ export const useGuestDashboardRealtime = ({
112106
const [eventsDebug, setEventsDebug] = useState<DashboardEventListDebug | null>(null);
113107
const [realtimeState, setRealtimeState] = useState<RealtimeState>('fallback');
114108
const [inviteBaseUrl, setInviteBaseUrl] = useState('');
115-
const reconnectTimerRef = useRef<number | null>(null);
116-
const refreshDebounceRef = useRef<number | null>(null);
117-
const reconnectAttemptRef = useRef(0);
118109

119110
const loadEvents = useCallback(async () => {
120111
try {
@@ -175,55 +166,20 @@ export const useGuestDashboardRealtime = ({
175166
}
176167
}, [eventId, eventsDebug, search, status]);
177168

178-
const connectStream = useCallback(() => {
169+
const setupPolling = useCallback(() => {
179170
if (!eventId) return () => {};
180-
const streamUrl = `/api/dashboard/guests/stream?eventId=${encodeURIComponent(eventId)}`;
181-
const source = new EventSource(streamUrl, { withCredentials: true });
182-
setRealtimeState('reconnecting');
183-
184-
const scheduleRefresh = () => {
185-
if (refreshDebounceRef.current) window.clearTimeout(refreshDebounceRef.current);
186-
refreshDebounceRef.current = window.setTimeout(() => {
187-
void loadGuests();
188-
}, 350);
189-
};
190171

191-
source.addEventListener('guest_updated', () => {
192-
onNotification({ message: 'Cambios detectados en los invitados.', type: 'info' });
193-
scheduleRefresh();
194-
});
172+
// Initial connection simulated.
173+
setRealtimeState('connected');
195174

196-
source.addEventListener('heartbeat', () => {
197-
reconnectAttemptRef.current = 0;
198-
setRealtimeState('connected');
199-
});
200-
201-
source.onerror = (e) => {
202-
source.close();
203-
setRealtimeState('fallback');
204-
if (shouldLogDashboardDebug()) {
205-
console.log('[dashboard][client][stream:error]', e);
206-
}
207-
const nextAttempt = reconnectAttemptRef.current + 1;
208-
reconnectAttemptRef.current = nextAttempt;
209-
// Only retry up to 10 times to prevent infinite loops on permanent schema/auth errors.
210-
if (nextAttempt > 10) {
211-
setGuestsError(
212-
'El sistema de actualizaciones en tiempo real ha fallado tras varios intentos. Revisa tu conexión o contacta a soporte.',
213-
);
214-
return;
215-
}
216-
const backoff = Math.min(10000, [1000, 2000, 5000, 10000][nextAttempt - 1] ?? 10000);
217-
reconnectTimerRef.current = window.setTimeout(() => {
218-
setRealtimeState('reconnecting');
219-
connectStream();
220-
}, backoff);
221-
};
175+
const pollId = window.setInterval(() => {
176+
void loadGuests();
177+
}, DASHBOARD_POLLING_INTERVAL_MS);
222178

223179
return () => {
224-
source.close();
180+
window.clearInterval(pollId);
225181
};
226-
}, [eventId, loadGuests, onNotification]);
182+
}, [eventId, loadGuests]);
227183

228184
useEffect(() => {
229185
void loadEvents();
@@ -251,21 +207,11 @@ export const useGuestDashboardRealtime = ({
251207
}, [eventId]);
252208

253209
useEffect(() => {
254-
const disconnect = connectStream();
210+
const cleanup = setupPolling();
255211
return () => {
256-
disconnect();
257-
if (reconnectTimerRef.current) window.clearTimeout(reconnectTimerRef.current);
258-
if (refreshDebounceRef.current) window.clearTimeout(refreshDebounceRef.current);
212+
cleanup();
259213
};
260-
}, [connectStream]);
261-
262-
useEffect(() => {
263-
if (realtimeState !== 'fallback') return;
264-
const id = window.setInterval(() => {
265-
void loadGuests();
266-
}, 45000);
267-
return () => window.clearInterval(id);
268-
}, [loadGuests, realtimeState]);
214+
}, [setupPolling]);
269215

270216
return {
271217
error: eventsError || guestsError,

src/layouts/DashboardLayout.astro

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ const { title } = Astro.props as Props;
1616
// The layout only reads the session to render user-specific navigation.
1717
const session = await getSessionContextFromRequest(Astro.request);
1818
19-
// Keep a defensive fallback in case the session was stripped before rendering.
19+
// Maintenance Note: Redirection to /login is handled by src/middleware.ts.
20+
// We keep the guard only for type safety of the 'session' variable below.
2021
if (!session) {
21-
return Astro.redirect(`/login?next=${encodeURIComponent(Astro.url.pathname)}`);
22+
return null;
2223
}
2324
2425
// The CSRF token is generated by middleware and forwarded through locals.

src/lib/rsvp/core/stream.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/lib/rsvp/services/dashboard-guests.service.ts

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import type {
1414
} from '@/interfaces/rsvp/domain.interface';
1515
import type { DashboardGuestListResponse } from '@/interfaces/dashboard/guest.interface';
1616
import { ApiError } from '@/lib/rsvp/core/errors';
17-
import { publishGuestStreamEvent } from '@/lib/rsvp/core/stream';
1817
import { mapSupabaseErrorToApiError } from '@/lib/rsvp/repositories/supabase-errors';
1918
import { logAdminAction } from '@/lib/rsvp/services/audit-logger.service';
2019
import {
@@ -180,12 +179,6 @@ export async function createDashboardGuest(input: {
180179
});
181180
}
182181

183-
publishGuestStreamEvent({
184-
type: 'guest_updated',
185-
eventId: event.id,
186-
guestId: created.id,
187-
updatedAt: item.updatedAt,
188-
});
189182
return {
190183
item,
191184
updatedAt: item.updatedAt,
@@ -292,12 +285,6 @@ export async function updateDashboardGuest(input: {
292285
presentation.template,
293286
);
294287

295-
publishGuestStreamEvent({
296-
type: 'guest_updated',
297-
eventId: updated.eventId,
298-
guestId: updated.id,
299-
updatedAt: item.updatedAt,
300-
});
301288
return {
302289
item,
303290
updatedAt: item.updatedAt,
@@ -325,12 +312,6 @@ export async function deleteDashboardGuest(input: {
325312
}
326313

327314
await softDeleteGuestById(input.guestId, input.hostAccessToken);
328-
publishGuestStreamEvent({
329-
type: 'guest_updated',
330-
eventId: existing.eventId,
331-
guestId: existing.id,
332-
updatedAt: new Date().toISOString(),
333-
});
334315
}
335316

336317
export async function markGuestShared(input: {
@@ -370,12 +351,6 @@ export async function markGuestShared(input: {
370351
});
371352
}
372353

373-
publishGuestStreamEvent({
374-
type: 'guest_updated',
375-
eventId: updated.eventId,
376-
guestId: updated.id,
377-
updatedAt: item.updatedAt,
378-
});
379354
return {
380355
item,
381356
updatedAt: item.updatedAt,

src/lib/rsvp/services/rsvp-submission.service.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import type {
1616
ResponseSource,
1717
} from '@/interfaces/rsvp/domain.interface';
1818
import { ApiError } from '@/lib/rsvp/core/errors';
19-
import { publishGuestStreamEvent } from '@/lib/rsvp/core/stream';
2019
import { normalizePhone, sanitize, toSafeAttendeeCount } from '@/lib/rsvp/core/utils';
2120
import { mapSupabaseErrorToApiError } from '@/lib/rsvp/repositories/supabase-errors';
2221

@@ -176,12 +175,6 @@ export async function persistRsvpResponse(
176175
: await updateGuestByIdService(updateBody);
177176

178177
console.info(`[rsvp] Success: RSVP submitted for invite ${updated.inviteId}`);
179-
publishGuestStreamEvent({
180-
type: 'guest_updated',
181-
eventId: updated.eventId,
182-
guestId: updated.id,
183-
updatedAt: updated.updatedAt,
184-
});
185178

186179
return {
187180
attendanceStatus: updated.attendanceStatus,
@@ -250,11 +243,4 @@ export async function trackInvitationView(
250243
is_viewed: true,
251244
view_percentage: nextPercentage,
252245
});
253-
254-
publishGuestStreamEvent({
255-
type: 'guest_updated',
256-
eventId: invitation.eventId,
257-
guestId: invitation.id,
258-
updatedAt: now,
259-
});
260246
}

0 commit comments

Comments
 (0)