UNPKG

fauna

Version:

A driver to query Fauna databases in browsers, Node.js, and other Javascript runtimes

182 lines (159 loc) 4.85 kB
import { getServiceError, NetworkError } from "../errors"; import { QueryFailure, QueryRequest } from "../wire-protocol"; import { FaunaAPIPaths } from "./paths"; import { HTTPClient, HTTPClientOptions, HTTPRequest, HTTPResponse, HTTPStreamRequest, HTTPStreamClient, StreamAdapter, } from "./http-client"; /** * An implementation for {@link HTTPClient} that uses the native fetch API */ export class FetchClient implements HTTPClient, HTTPStreamClient { #baseUrl: string; #defaultRequestPath = FaunaAPIPaths.QUERY; #defaultStreamPath = FaunaAPIPaths.STREAM; #keepalive: boolean; constructor({ url, fetch_keepalive }: HTTPClientOptions) { this.#baseUrl = url; this.#keepalive = fetch_keepalive; } #resolveURL(path: string): string { return new URL(path, this.#baseUrl).toString(); } /** {@inheritDoc HTTPClient.request} */ async request<T = QueryRequest>({ data, headers: requestHeaders, method, client_timeout_ms, path = this.#defaultRequestPath, }: HTTPRequest<T>): Promise<HTTPResponse> { const signal = AbortSignal.timeout === undefined ? (() => { const controller = new AbortController(); const signal = controller.signal; setTimeout(() => controller.abort(), client_timeout_ms); return signal; })() : AbortSignal.timeout(client_timeout_ms); const response = await fetch(this.#resolveURL(path), { method, headers: { ...requestHeaders, "Content-Type": "application/json" }, body: JSON.stringify(data), signal, keepalive: this.#keepalive, }).catch((error) => { throw new NetworkError("The network connection encountered a problem.", { cause: error, }); }); const status = response.status; const responseHeaders: Record<string, string> = {}; response.headers.forEach((value, key) => (responseHeaders[key] = value)); const body = await response.text(); return { status, body, headers: responseHeaders, }; } /** {@inheritDoc HTTPStreamClient.stream} */ stream({ data, headers: requestHeaders, method, path = this.#defaultStreamPath, }: HTTPStreamRequest): StreamAdapter { const request = new Request(this.#resolveURL(path), { method, headers: { ...requestHeaders, "Content-Type": "application/json" }, body: JSON.stringify(data), keepalive: this.#keepalive, }); const abortController = new AbortController(); const options = { signal: abortController.signal, }; async function* reader() { const response = await fetch(request, options).catch((error) => { throw new NetworkError( "The network connection encountered a problem.", { cause: error, }, ); }); const status = response.status; if (!(status >= 200 && status < 400)) { const failure: QueryFailure = await response.json(); throw getServiceError(failure, status); } const body = response.body; if (!body) { throw new Error("Response body is undefined."); } const reader = body.getReader(); try { for await (const line of readLines(reader)) { yield line; } } catch (error) { throw new NetworkError( "The network connection encountered a problem while streaming events.", { cause: error }, ); } } return { read: reader(), close: () => { abortController.abort("Stream closed by the client."); }, }; } /** {@inheritDoc HTTPClient.close} */ close() { // no actions at this time } } /** * Get individual lines from the stream * * The stream may be broken into arbitrary chunks, but the events are delimited by a newline character. * * @param reader - The stream reader */ async function* readLines(reader: ReadableStreamDefaultReader<Uint8Array>) { const textDecoder = new TextDecoder(); let partOfLine = ""; for await (const chunk of readChunks(reader)) { const chunkText = textDecoder.decode(chunk); const chunkLines = (partOfLine + chunkText).split("\n"); // Yield all complete lines for (let i = 0; i < chunkLines.length - 1; i++) { yield chunkLines[i].trim(); } // Store the partial line partOfLine = chunkLines[chunkLines.length - 1]; } // Yield the remaining partial line if any if (partOfLine.trim() !== "") { yield partOfLine; } } async function* readChunks(reader: ReadableStreamDefaultReader<Uint8Array>) { let done = false; do { const readResult = await reader.read(); if (readResult.value !== undefined) { yield readResult.value; } done = readResult.done; } while (!done); }