108 lines
2.7 KiB
JavaScript
108 lines
2.7 KiB
JavaScript
export const createOpenCodeWatcherRuntime = (deps) => {
|
|||
|
|
const {
|
||
|
|
waitForOpenCodePort,
|
||
|
|
buildOpenCodeUrl,
|
||
|
|
getOpenCodeAuthHeaders,
|
||
|
|
parseSseDataPayload,
|
||
|
|
onPayload,
|
||
|
|
} = deps;
|
||
|
|
|
||
|
|
let abortController = null;
|
||
|
|
|
||
|
|
const start = async () => {
|
||
|
|
if (abortController) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
await waitForOpenCodePort();
|
||
|
|
|
||
|
|
abortController = new AbortController();
|
||
|
|
const signal = abortController.signal;
|
||
|
|
|
||
|
|
let attempt = 0;
|
||
|
|
const run = async () => {
|
||
|
|
while (!signal.aborted) {
|
||
|
|
attempt += 1;
|
||
|
|
let upstream;
|
||
|
|
let reader;
|
||
|
|
try {
|
||
|
|
const url = buildOpenCodeUrl('/global/event', '');
|
||
|
|
upstream = await fetch(url, {
|
||
|
|
headers: {
|
||
|
|
Accept: 'text/event-stream',
|
||
|
|
'Cache-Control': 'no-cache',
|
||
|
|
Connection: 'keep-alive',
|
||
|
|
...getOpenCodeAuthHeaders(),
|
||
|
|
},
|
||
|
|
signal,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!upstream.ok || !upstream.body) {
|
||
|
|
throw new Error(`bad status ${upstream.status}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('[PushWatcher] connected');
|
||
|
|
|
||
|
|
const decoder = new TextDecoder();
|
||
|
|
reader = upstream.body.getReader();
|
||
|
|
let buffer = '';
|
||
|
|
|
||
|
|
while (!signal.aborted) {
|
||
|
|
const { value, done } = await reader.read();
|
||
|
|
if (done) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, '\n');
|
||
|
|
|
||
|
|
let separatorIndex = buffer.indexOf('\n\n');
|
||
|
|
while (separatorIndex !== -1) {
|
||
|
|
const block = buffer.slice(0, separatorIndex);
|
||
|
|
buffer = buffer.slice(separatorIndex + 2);
|
||
|
|
separatorIndex = buffer.indexOf('\n\n');
|
||
|
|
const payload = parseSseDataPayload(block);
|
||
|
|
onPayload(payload);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
if (signal.aborted) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
console.warn('[PushWatcher] disconnected', error?.message ?? error);
|
||
|
|
} finally {
|
||
|
|
try {
|
||
|
|
if (reader) {
|
||
|
|
await reader.cancel();
|
||
|
|
reader.releaseLock();
|
||
|
|
} else if (upstream?.body && !upstream.body.locked) {
|
||
|
|
await upstream.body.cancel();
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const backoffMs = Math.min(1000 * Math.pow(2, Math.min(attempt, 5)), 30000);
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
void run();
|
||
|
|
};
|
||
|
|
|
||
|
|
const stop = () => {
|
||
|
|
if (!abortController) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
abortController.abort();
|
||
|
|
} catch {
|
||
|
|
}
|
||
|
|
abortController = null;
|
||
|
|
};
|
||
|
|
|
||
|
|
return {
|
||
|
|
start,
|
||
|
|
stop,
|
||
|
|
};
|
||
|
|
};
|