Skip to content

Commit 536c62a

Browse files
committed
fix(web-admin): consolidate OAuth SSE onto shared admin hub
Standalone EventSource connections starved oauth-consent-request delivery through the CF tunnel HTTP/1.1 connection cap. Route OAuth, meta-tool, and backend event channels through admin-sse-hub and enable SSE at sync start. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent a68073c commit 536c62a

9 files changed

Lines changed: 149 additions & 76 deletions

File tree

apps/desktop/src/hooks/useDataSync.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export function useDataSync() {
1616
useEffect(() => {
1717
async function syncData() {
1818
console.log('[useDataSync] Starting data sync...');
19+
if (!isTauri()) {
20+
enableAdminSse();
21+
}
1922
setLoading('spaces', true);
2023
try {
2124
// Refresh OAuth tokens first (before connecting servers)
@@ -38,9 +41,6 @@ export function useDataSync() {
3841
console.error('[useDataSync] Failed to sync:', error);
3942
} finally {
4043
setLoading('spaces', false);
41-
if (!isTauri()) {
42-
enableAdminSse();
43-
}
4444
console.log('[useDataSync] Data sync complete');
4545
}
4646
}

apps/desktop/src/lib/backend/events/admin-sse-hub.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ const allHandlers = new Set<AllEventsCallback>();
3737
const lastEventListeners = new Set<(event: { channel: DomainEventChannel; payload: DomainEventPayload }) => void>();
3838
/** Handlers for non-domain channels (workspace, meta-tool, etc.) sharing the same SSE connection. */
3939
const rawChannelHandlers = new Map<string, Set<RawChannelHandler>>();
40+
/** Raw channels that already have a single dispatcher on `sharedSource`. */
41+
const rawChannelsAttached = new Set<string>();
4042

4143
/**
4244
* Dispatch an SSE frame to all registered handlers.
@@ -70,18 +72,29 @@ function ensureSharedSource(): void {
7072
});
7173
}
7274

73-
for (const [channel, handlers] of rawChannelHandlers) {
74-
source.addEventListener(channel, (event: MessageEvent<string>) => {
75-
try {
76-
const payload = JSON.parse(event.data) as unknown;
77-
handlers.forEach((handler) => handler(payload));
78-
} catch {
79-
// ignore malformed frames
80-
}
81-
});
75+
for (const channel of rawChannelHandlers.keys()) {
76+
attachRawChannelListener(channel);
8277
}
8378
}
8479

80+
/**
81+
* Attach one SSE listener per raw channel; dispatches to all handlers in the set.
82+
*/
83+
function attachRawChannelListener(channel: string): void {
84+
if (!sharedSource || rawChannelsAttached.has(channel)) {
85+
return;
86+
}
87+
rawChannelsAttached.add(channel);
88+
sharedSource.addEventListener(channel, (event: MessageEvent<string>) => {
89+
try {
90+
const payload = JSON.parse(event.data) as unknown;
91+
rawChannelHandlers.get(channel)?.forEach((handler) => handler(payload));
92+
} catch {
93+
// ignore malformed frames
94+
}
95+
});
96+
}
97+
8598
/**
8699
* Close the shared SSE connection when the last consumer detaches.
87100
*/
@@ -91,10 +104,11 @@ function releaseSharedSource(): void {
91104
}
92105
sharedSource.close();
93106
sharedSource = null;
107+
rawChannelsAttached.clear();
94108
}
95109

96110
/**
97-
* Open the shared SSE connection after startup sync (listSpaces) has finished.
111+
* Open the shared SSE connection once web admin startup sync begins.
98112
*/
99113
export function enableAdminSse(): void {
100114
if (isTauri()) {
@@ -178,16 +192,7 @@ export function subscribeAdminSseRaw(channel: string, handler: RawChannelHandler
178192
rawChannelHandlers.set(channel, new Set());
179193
}
180194
rawChannelHandlers.get(channel)!.add(handler);
181-
182-
if (sharedSource) {
183-
sharedSource.addEventListener(channel, (event: MessageEvent<string>) => {
184-
try {
185-
handler(JSON.parse(event.data) as unknown);
186-
} catch {
187-
// ignore malformed frames
188-
}
189-
});
190-
}
195+
attachRawChannelListener(channel);
191196

192197
return () => {
193198
rawChannelHandlers.get(channel)?.delete(handler);

apps/desktop/src/lib/backend/events/use-backend-event-subscription.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { useEffect } from 'react';
22

33
import { isTauri } from '../data/transport';
44

5+
import {
6+
acquireAdminSseConsumer,
7+
releaseAdminSseConsumer,
8+
subscribeAdminSseRaw,
9+
} from './admin-sse-hub';
510
import { listenWhenTauri } from './tauri-adapter';
611

712
/** Options for {@link useBackendEventSubscription}. */
@@ -45,19 +50,19 @@ export function useBackendEventSubscription<T>(
4550
return;
4651
}
4752

48-
const source = new EventSource('/api/v1/events');
49-
const onMessage = (event: MessageEvent<string>) => {
53+
acquireAdminSseConsumer();
54+
55+
const unsubscribe = subscribeAdminSseRaw(channel, (payload) => {
5056
try {
51-
callback(JSON.parse(event.data) as T);
57+
callback(payload as T);
5258
} catch {
5359
// ignore malformed frames
5460
}
55-
};
56-
source.addEventListener(channel, onMessage);
61+
});
5762

5863
return () => {
59-
source.removeEventListener(channel, onMessage);
60-
source.close();
64+
unsubscribe();
65+
releaseAdminSseConsumer();
6166
};
6267
}, [channel, callback, sse]);
6368
}

apps/desktop/src/lib/backend/events/useMetaToolEventsWeb.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ import type { MetaToolAuditEvent } from '@/lib/api/metaTools';
88

99
import { isTauri } from '../data/transport';
1010

11+
import {
12+
acquireAdminSseConsumer,
13+
releaseAdminSseConsumer,
14+
subscribeAdminSseRaw,
15+
} from './admin-sse-hub';
16+
1117
/**
12-
* Subscribe to `meta-tool-invoked` over SSE in web admin mode.
18+
* Subscribe to `meta-tool-invoked` over the shared admin SSE hub in web admin mode.
1319
*/
1420
export function useMetaToolEventsWeb() {
1521
const handlersRef = useRef<Set<(event: MetaToolAuditEvent) => void>>(new Set());
@@ -18,18 +24,22 @@ export function useMetaToolEventsWeb() {
1824
if (isTauri()) {
1925
return;
2026
}
21-
const source = new EventSource('/api/v1/events');
2227

23-
source.addEventListener('meta-tool-invoked', (event: MessageEvent<string>) => {
28+
acquireAdminSseConsumer();
29+
30+
const unsubscribe = subscribeAdminSseRaw('meta-tool-invoked', (payload) => {
2431
try {
25-
const payload = JSON.parse(event.data) as MetaToolAuditEvent;
26-
handlersRef.current.forEach((handler) => handler(payload));
32+
const data = payload as MetaToolAuditEvent;
33+
handlersRef.current.forEach((handler) => handler(data));
2734
} catch {
2835
// ignore malformed frames
2936
}
3037
});
3138

32-
return () => source.close();
39+
return () => {
40+
unsubscribe();
41+
releaseAdminSseConsumer();
42+
};
3343
}, []);
3444

3545
/**

apps/desktop/src/lib/backend/events/useOAuthClientEventsWeb.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ import { useCallback, useEffect, useRef } from 'react';
66

77
import { isTauri } from '../data/transport';
88

9+
import {
10+
acquireAdminSseConsumer,
11+
releaseAdminSseConsumer,
12+
subscribeAdminSseRaw,
13+
} from './admin-sse-hub';
14+
915
import type { OAuthClientChangedPayload } from './useOAuthClientEvents';
1016

1117
/**
12-
* Subscribe to `oauth-client-changed` over SSE in web admin mode.
18+
* Subscribe to `oauth-client-changed` over the shared admin SSE hub in web admin mode.
1319
*/
1420
export function useOAuthClientEventsWeb() {
1521
const handlersRef = useRef<Set<(payload: OAuthClientChangedPayload) => void>>(new Set());
@@ -18,18 +24,22 @@ export function useOAuthClientEventsWeb() {
1824
if (isTauri()) {
1925
return;
2026
}
21-
const source = new EventSource('/api/v1/events');
2227

23-
source.addEventListener('oauth-client-changed', (event: MessageEvent<string>) => {
28+
acquireAdminSseConsumer();
29+
30+
const unsubscribe = subscribeAdminSseRaw('oauth-client-changed', (payload) => {
2431
try {
25-
const payload = JSON.parse(event.data) as OAuthClientChangedPayload;
26-
handlersRef.current.forEach((handler) => handler(payload));
32+
const data = payload as OAuthClientChangedPayload;
33+
handlersRef.current.forEach((handler) => handler(data));
2734
} catch {
2835
// ignore malformed frames
2936
}
3037
});
3138

32-
return () => source.close();
39+
return () => {
40+
unsubscribe();
41+
releaseAdminSseConsumer();
42+
};
3343
}, []);
3444

3545
/**

apps/desktop/src/lib/backend/events/useWorkspaceEventsWeb.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
* SSE workspace event channels for web admin mode.
33
*
44
* Uses the shared admin-sse-hub connection instead of opening a second
5-
* EventSource, avoiding HTTP/1.1 connection starvation and ensuring workspace
6-
* events are gated behind enableAdminSse() like all other domain channels.
5+
* EventSource, avoiding HTTP/1.1 connection starvation.
76
*/
87

98
import { useCallback, useEffect, useRef } from 'react';

apps/desktop/src/lib/backend/shell/index.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import type { ExportConfigRequest } from '@/lib/api/configExport';
88
import type { AdminWebSettings } from '@/lib/api/settings';
99

1010
import { apiCall, isTauri } from '../data/transport';
11+
import {
12+
acquireAdminSseConsumer,
13+
releaseAdminSseConsumer,
14+
subscribeAdminSseRaw,
15+
} from '../events/admin-sse-hub';
1116

1217
export { isTauri };
1318
export type { Event, UnlistenFn, Update };
@@ -201,27 +206,23 @@ export function subscribeOAuthConsentEvents(
201206
};
202207
}
203208

204-
console.log('[OAuth] subscribeOAuthConsentEvents: using SSE /api/v1/events');
205-
const source = new EventSource('/api/v1/events');
206-
source.onopen = () => console.log('[OAuth] SSE connected');
207-
source.onerror = (err) => console.warn('[OAuth] SSE error:', err);
208-
const onConsentRequest = (event: MessageEvent<string>) => {
209-
console.log('[OAuth] SSE consent event raw:', event.data);
210-
try {
211-
const payload = JSON.parse(event.data) as OAuthConsentDeepLinkPayload;
212-
if (payload.requestId) {
213-
console.log('[OAuth] SSE consent event parsed:', payload);
214-
handler(payload);
215-
}
216-
} catch {
217-
console.warn('[OAuth] SSE consent event: malformed payload');
209+
console.log('[OAuth] subscribeOAuthConsentEvents: using shared admin SSE hub');
210+
acquireAdminSseConsumer();
211+
212+
const onConsentRequest = (payload: unknown) => {
213+
const data = payload as OAuthConsentDeepLinkPayload;
214+
if (data.requestId) {
215+
console.log('[OAuth] SSE consent event parsed:', data);
216+
handler(data);
218217
}
219218
};
220-
source.addEventListener('oauth-consent-request', onConsentRequest);
219+
220+
const unsubscribe = subscribeAdminSseRaw('oauth-consent-request', onConsentRequest);
221+
221222
return () => {
222-
console.log('[OAuth] Closing SSE consent listener');
223-
source.removeEventListener('oauth-consent-request', onConsentRequest);
224-
source.close();
223+
console.log('[OAuth] Unsubscribing shared SSE consent listener');
224+
unsubscribe();
225+
releaseAdminSseConsumer();
225226
};
226227
}
227228

apps/desktop/src/stores/metaToolActivityStore.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
import { create } from 'zustand';
1313
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
1414
import type { MetaToolAuditEvent } from '@/lib/api/metaTools';
15+
import {
16+
acquireAdminSseConsumer,
17+
releaseAdminSseConsumer,
18+
subscribeAdminSseRaw,
19+
} from '@/lib/backend/events/admin-sse-hub';
1520
import { isTauri } from '@/lib/backend/data/transport';
1621

1722
/** Ring-buffer size — most recent N invocations kept in memory. */
@@ -38,6 +43,7 @@ export const useMetaToolActivityStore = create<MetaToolActivityState>((set) => (
3843
// changes) and is wired exactly once regardless of how many callers init it.
3944
let listening = false;
4045
let unlistenPromise: Promise<UnlistenFn> | null = null;
46+
let sseUnsubscribe: (() => void) | null = null;
4147

4248
/**
4349
* Start the app-wide `meta-tool-invoked` listener (idempotent). Call once near
@@ -55,21 +61,28 @@ export function startMetaToolActivityListener(): void {
5561
return;
5662
}
5763

58-
const source = new EventSource('/api/v1/events');
59-
source.addEventListener('meta-tool-invoked', (event: MessageEvent<string>) => {
64+
acquireAdminSseConsumer();
65+
66+
sseUnsubscribe = subscribeAdminSseRaw('meta-tool-invoked', (payload) => {
6067
try {
61-
const payload = JSON.parse(event.data) as MetaToolAuditEvent;
62-
useMetaToolActivityStore.getState().push(payload);
68+
const data = payload as MetaToolAuditEvent;
69+
useMetaToolActivityStore.getState().push(data);
6370
} catch {
6471
// ignore malformed frames
6572
}
6673
});
67-
unlistenPromise = Promise.resolve(() => source.close());
74+
75+
unlistenPromise = Promise.resolve(() => {
76+
sseUnsubscribe?.();
77+
sseUnsubscribe = null;
78+
releaseAdminSseConsumer();
79+
});
6880
}
6981

7082
/** Tear down the listener (mainly for tests / hot-reload hygiene). */
7183
export function stopMetaToolActivityListener(): void {
7284
listening = false;
7385
void unlistenPromise?.then((fn) => fn()).catch(() => {});
7486
unlistenPromise = null;
87+
sseUnsubscribe = null;
7588
}

0 commit comments

Comments
 (0)