82 行
1.9 KiB
TypeScript
82 行
1.9 KiB
TypeScript
export type AppEvent = {
|
|
id?: string;
|
|
type: string;
|
|
invoiceId?: string;
|
|
status?: string | null;
|
|
updatedAt?: string | null;
|
|
createdAt?: string | null;
|
|
};
|
|
|
|
type SubscribeAppEventsOptions = {
|
|
accessToken: string;
|
|
onEvent: (event: AppEvent) => void;
|
|
onError?: (error: unknown) => void;
|
|
};
|
|
|
|
function parseSseMessage(message: string): AppEvent | null {
|
|
const dataLines = message
|
|
.split(/\r?\n/)
|
|
.filter((line) => line.startsWith("data:"))
|
|
.map((line) => line.slice("data:".length).trimStart());
|
|
|
|
if (!dataLines.length) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(dataLines.join("\n")) as AppEvent;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function subscribeAppEvents(options: SubscribeAppEventsOptions) {
|
|
const controller = new AbortController();
|
|
|
|
void (async () => {
|
|
try {
|
|
const response = await fetch("/api/events", {
|
|
method: "GET",
|
|
credentials: "include",
|
|
headers: {
|
|
authorization: `Bearer ${options.accessToken}`,
|
|
accept: "text/event-stream",
|
|
},
|
|
signal: controller.signal,
|
|
});
|
|
|
|
if (!response.ok || !response.body) {
|
|
throw new Error(`events_stream_unavailable:${response.status}`);
|
|
}
|
|
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = "";
|
|
|
|
while (!controller.signal.aborted) {
|
|
const { done, value } = await reader.read();
|
|
if (done) {
|
|
break;
|
|
}
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const chunks = buffer.split(/\r?\n\r?\n/);
|
|
buffer = chunks.pop() ?? "";
|
|
|
|
for (const chunk of chunks) {
|
|
const event = parseSseMessage(chunk);
|
|
if (event) {
|
|
options.onEvent(event);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (!controller.signal.aborted) {
|
|
options.onError?.(error);
|
|
}
|
|
}
|
|
})();
|
|
|
|
return () => controller.abort();
|
|
}
|