fauna
Version:
A driver to query Fauna databases in browsers, Node.js, and other Javascript runtimes
182 lines (159 loc) • 4.85 kB
text/typescript
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);
}