import { SignalLike } from '$fresh/src/types.ts' import { JsonValue } from '$std/json/common.ts' 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 export function respondApi< Kind extends ApiPayload['kind'], Payload extends JsonCompatible, >( kind: Kind, payload?: Payload, status?: number, statusText?: string, ): Response { if (kind === 'error') { return Response.json({ kind: 'error', error: String(payload ?? ''), } as ApiPayload, { status: status ?? 500, statusText, }) } return Response.json({ kind: 'success', data: payload ?? null, } as ApiPayload) } export async function requestApi< Payload extends JsonCompatible | undefined, ApiResponse extends JsonCompatible, >( route: string, method: 'GET' | 'POST' | 'DELETE' | 'PATCH', payload?: Payload | null, ): Promise { 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 apiPayload = await response.json() as ApiPayload if (apiPayload.kind === 'error') { throw new Error(`api request error while getting "${endpoint.href}"`, { cause: apiPayload.error, }) } return apiPayload.data } export type ApiPayload = { kind: 'success' data: ApiResponse } | { kind: 'error' 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('=') ) const cookies = Object.fromEntries(cookiesEntries) return cookies[name] } export function base64ToString(base64: string): string { const bytes = decodeBase64(base64) return new TextDecoder().decode(bytes) } export function unwrapSignalOrValue(valueOrSignal: T | SignalLike): T { if (typeof valueOrSignal !== 'object') { return valueOrSignal } if (valueOrSignal === null) { return valueOrSignal } if ( 'value' in valueOrSignal && 'peek' in valueOrSignal && 'subscribe' in valueOrSignal ) { return valueOrSignal.value } return valueOrSignal }