@loaders.gl/core
Version:
The core API for working with loaders.gl loaders and writers
138 lines (121 loc) • 4.37 kB
text/typescript
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {isResponse} from '../../javascript-utils/is-type';
import {FetchError} from '../fetch/fetch-error';
import {getResourceContentLength, getResourceUrl, getResourceMIMEType} from './resource-utils';
import {shortenUrlForDisplay} from './url-utils';
/**
* Returns a Response object
* Adds content-length header when possible
*
* @param resource
*/
export async function makeResponse(resource: unknown): Promise<Response> {
if (isResponse(resource)) {
return resource as Response;
}
// Add content-length header if possible
const headers: {[header: string]: string} = {};
const contentLength = getResourceContentLength(resource);
if (contentLength >= 0) {
headers['content-length'] = String(contentLength);
}
// `new Response(File)` does not preserve content-type and URL
// so we add them here
const url = getResourceUrl(resource);
const type = getResourceMIMEType(resource);
if (type) {
headers['content-type'] = type;
}
// Add a custom header with initial bytes if available
const initialDataUrl = await getInitialDataUrl(resource);
if (initialDataUrl) {
headers['x-first-bytes'] = initialDataUrl;
}
// TODO - is this the best way of handling strings?
// Maybe package as data URL instead?
if (typeof resource === 'string') {
// Convert to ArrayBuffer to avoid Response treating it as a URL
resource = new TextEncoder().encode(resource);
}
// Attempt to create a Response from the resource, adding headers and setting url
const response = new Response(resource as any, {headers});
// We can't control `Response.url` via constructor, use a property override to record URL.
Object.defineProperty(response, 'url', {value: url});
return response;
}
/**
* Checks response status (async) and throws a helpful error message if status is not OK.
* @param response
*/
export async function checkResponse(response: Response): Promise<void> {
if (!response.ok) {
const error = await getResponseError(response);
throw error;
}
}
/**
* Checks response status (sync) and throws a helpful error message if status is not OK.
* @param response
*/
export function checkResponseSync(response: Response): void {
if (!response.ok) {
let message = `${response.status} ${response.statusText}`;
message = message.length > 60 ? `${message.slice(0, 60)}...` : message;
throw new Error(message);
}
}
// HELPERS
async function getResponseError(response: Response): Promise<Error> {
const shortUrl = shortenUrlForDisplay(response.url);
let message = `Failed to fetch resource (${response.status}) ${response.statusText}: ${shortUrl}`;
message = message.length > 100 ? `${message.slice(0, 100)}...` : message;
const info = {
reason: response.statusText,
url: response.url,
response
};
// See if we got an error message in the body
try {
const contentType = response.headers.get('Content-Type');
info.reason =
!response.bodyUsed && contentType?.includes('application/json')
? await response.json()
: await response.text();
} catch (error) {
// eslint forbids return in a finally statement, so we just catch here
}
return new FetchError(message, info);
}
async function getInitialDataUrl(
resource: string | Blob | ArrayBuffer | unknown
): Promise<string | null> {
const INITIAL_DATA_LENGTH = 5;
if (typeof resource === 'string') {
return `data:,${resource.slice(0, INITIAL_DATA_LENGTH)}`;
}
if (resource instanceof Blob) {
const blobSlice = resource.slice(0, 5);
return await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (event) => resolve(event?.target?.result as string);
reader.readAsDataURL(blobSlice);
});
}
if (resource instanceof ArrayBuffer) {
const slice = resource.slice(0, INITIAL_DATA_LENGTH);
const base64 = arrayBufferToBase64(slice);
return `data:base64,${base64}`;
}
return null;
}
// https://stackoverflow.com/questions/9267899/arraybuffer-to-base64-encoded-string
function arrayBufferToBase64(buffer: ArrayBuffer): string {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}