Share upstream event stream hub
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { createUpstreamSseReader } from './upstream-reader.js';
|
||||
|
||||
export const MESSAGE_STREAM_GLOBAL_REPLAY_LIMIT = 512;
|
||||
|
||||
export function createGlobalMessageStreamHub({
|
||||
buildOpenCodeUrl,
|
||||
getOpenCodeAuthHeaders,
|
||||
fetchImpl = fetch,
|
||||
upstreamStallTimeoutMs,
|
||||
upstreamReconnectDelayMs,
|
||||
replayLimit = MESSAGE_STREAM_GLOBAL_REPLAY_LIMIT,
|
||||
}) {
|
||||
const eventSubscribers = new Set();
|
||||
const statusSubscribers = new Set();
|
||||
const replay = [];
|
||||
|
||||
let controller = null;
|
||||
let reader = null;
|
||||
let connected = false;
|
||||
let everConnected = false;
|
||||
let buildUrlFailed = false;
|
||||
|
||||
const notifyStatus = (status) => {
|
||||
for (const subscriber of Array.from(statusSubscribers)) {
|
||||
subscriber(status);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeEvent = ({ envelope, payload }) => {
|
||||
const directory =
|
||||
typeof envelope?.directory === 'string' && envelope.directory.length > 0 ? envelope.directory : 'global';
|
||||
const eventId = typeof envelope?.eventId === 'string' && envelope.eventId.length > 0 ? envelope.eventId : undefined;
|
||||
return {
|
||||
envelope,
|
||||
payload,
|
||||
directory,
|
||||
eventId,
|
||||
};
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (reader) {
|
||||
return;
|
||||
}
|
||||
|
||||
controller = new AbortController();
|
||||
reader = createUpstreamSseReader({
|
||||
signal: controller.signal,
|
||||
stallTimeoutMs: upstreamStallTimeoutMs,
|
||||
reconnectDelayMs: upstreamReconnectDelayMs,
|
||||
fetchImpl,
|
||||
buildUrl: () => {
|
||||
buildUrlFailed = false;
|
||||
try {
|
||||
return new URL(buildOpenCodeUrl('/global/event', ''));
|
||||
} catch {
|
||||
buildUrlFailed = true;
|
||||
throw new Error('OpenCode service unavailable');
|
||||
}
|
||||
},
|
||||
getHeaders: getOpenCodeAuthHeaders,
|
||||
onConnect() {
|
||||
connected = true;
|
||||
const wasReady = everConnected;
|
||||
everConnected = true;
|
||||
notifyStatus({ type: 'connect', wasReady });
|
||||
},
|
||||
onDisconnect({ reason }) {
|
||||
connected = false;
|
||||
notifyStatus({ type: 'disconnect', reason });
|
||||
},
|
||||
onEvent(event) {
|
||||
const normalized = normalizeEvent(event);
|
||||
if (normalized.eventId) {
|
||||
replay.push(normalized);
|
||||
if (replay.length > replayLimit) {
|
||||
replay.splice(0, replay.length - replayLimit);
|
||||
}
|
||||
}
|
||||
|
||||
for (const subscriber of Array.from(eventSubscribers)) {
|
||||
subscriber(normalized);
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
if (controller?.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
notifyStatus({
|
||||
type: everConnected ? 'error' : 'initial-error',
|
||||
error,
|
||||
buildUrlFailed,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
void reader.start();
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
connected = false;
|
||||
reader?.stop();
|
||||
if (controller && !controller.signal.aborted) {
|
||||
controller.abort();
|
||||
}
|
||||
reader = null;
|
||||
controller = null;
|
||||
everConnected = false;
|
||||
buildUrlFailed = false;
|
||||
};
|
||||
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
isConnected() {
|
||||
return connected;
|
||||
},
|
||||
hasConnected() {
|
||||
return everConnected;
|
||||
},
|
||||
subscribeEvent(subscriber) {
|
||||
eventSubscribers.add(subscriber);
|
||||
return () => {
|
||||
eventSubscribers.delete(subscriber);
|
||||
};
|
||||
},
|
||||
subscribeStatus(subscriber) {
|
||||
statusSubscribers.add(subscriber);
|
||||
return () => {
|
||||
statusSubscribers.delete(subscriber);
|
||||
};
|
||||
},
|
||||
replayAfter(eventId) {
|
||||
if (!eventId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const index = replay.findIndex((entry) => entry.eventId === eventId);
|
||||
return index === -1 ? [] : replay.slice(index + 1);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user