diff --git a/src/utils.ts b/src/utils.ts index 127fa3f..540a723 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,8 @@ import { JsonValue } from '$std/json/common.ts' -import { decodeBase64 } from "@std/encoding/base64" +import { decodeBase64 } from '@std/encoding/base64' +import { JsonStringifyStream } from '@std/json' +import { JsonParseStream } from '@std/json/json-parse-stream' +import { TextLineStream } from '@std/streams/text-line-stream' export type JsonCompatible = JsonValue | { toJSON(): JsonValue } | unknown @@ -71,6 +74,91 @@ export type ApiPayload = { error: string } +export async function respondApiStream< + Payload extends JsonCompatible, +>( + source: + | ReadableStream + | Iterable + | AsyncIterable, +): Promise { + const stream = new TransformStream< + ApiPayload, + ApiPayload + >() + const writer = stream.writable.getWriter() + + try { + await writer.ready + + for await (const data of source) { + writer.write({ kind: 'success', data }) + } + } catch (error) { + writer.write({ kind: 'error', error }) + } finally { + writer.close() + } + + const body = stream.readable + .pipeThrough(new JsonStringifyStream()) + .pipeThrough(new TextEncoderStream()) + + return new Response(body) +} + +export async function* requestApiStream< + Payload extends JsonCompatible | undefined, + ApiResponse extends JsonCompatible, +>( + route: string, + method: 'GET' | 'POST' | 'DELETE' | 'PATCH', + payload?: Payload | null, +): AsyncGenerator { + const csrf = getCookie('_CSRF') ?? '' + + const base = new URL('/api/', location.origin) + const endpoint = new URL( + route.startsWith('/') ? `.${route}` : route, + base.href, + ) + + const response = await fetch(endpoint, { + method, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'X-CSRF-TOKEN': csrf, + }, + body: payload ? JSON.stringify(payload) : null, + }) + + const { body } = response + + if (body === null) { + throw new TypeError(`api response stream is null`) + } + + const stream = body + .pipeThrough(new TextDecoderStream()) // convert Uint8Array to string + .pipeThrough(new TextLineStream()) // transform into a stream where each chunk is divided by a newline + .pipeThrough(new JsonParseStream()) as unknown as ReadableStream< + ApiPayload + > // parse each chunk as JSON + + for await (const payload of stream) { + if (payload.kind === 'error') { + throw new Error( + `api stream error while getting "${endpoint.href}"`, + { + cause: payload.error, + }, + ) + } + + yield payload.data + } +} + function getCookie(name: string): string | undefined { const cookiesEntries = document.cookie.split(';').map((cookie) => cookie.trim().split('=') @@ -82,4 +170,4 @@ function getCookie(name: string): string | undefined { export function base64ToString(base64: string): string { const bytes = decodeBase64(base64) return new TextDecoder().decode(bytes) -} \ No newline at end of file +}