Warning, /firebird/packages/firebird-ng/display/src/lib/utils/data-fetching.utils.ts is written in an unsupported language. File is not indexed.
0001 import JSZip from 'jszip';
0002
0003 /**
0004 * True for zip member names that can hold a DEX document: `.json` files that
0005 * are not directories, not dot-files (`._events.json`, `.DS_Store`), and not
0006 * inside the `__MACOSX/` folder that the macOS archiver adds.
0007 */
0008 export function isDexZipMember(name: string): boolean {
0009 if (name.endsWith('/')) return false;
0010 const parts = name.split('/');
0011 if (parts.includes('__MACOSX')) return false;
0012 const baseName = parts[parts.length - 1];
0013 if (!baseName || baseName.startsWith('.')) return false;
0014 return baseName.toLowerCase().endsWith('.json');
0015 }
0016
0017 /**
0018 * Reads the DEX document of a zip archive: the first `.json` member in archive
0019 * order that `isDexZipMember()` accepts, the member `pyrobird` reads too. Names
0020 * are filtered from the central directory first, so only that one member is
0021 * inflated.
0022 *
0023 * @param data The archive bytes, or a picked file.
0024 * @param sourceName The file name or URL, for error messages.
0025 * @returns The parsed JSON of the member.
0026 */
0027 export async function readDexZip(data: Blob | ArrayBuffer, sourceName: string): Promise<unknown> {
0028 let archive: JSZip;
0029 try {
0030 archive = await JSZip.loadAsync(data);
0031 } catch (error) {
0032 const head = await new Blob([data]).slice(0, 64).text();
0033 throw new Error(`'${sourceName}' is not a readable zip archive${htmlHint(head)}: ${errorText(error)}`);
0034 }
0035 const member = Object.values(archive.files).find(file => !file.dir && isDexZipMember(file.name));
0036 if (!member) {
0037 throw new Error(`'${sourceName}' holds no .json member`);
0038 }
0039 const timing = `readDexZip: inflating and parsing '${member.name}'`;
0040 console.time(timing);
0041 try {
0042 return JSON.parse(await member.async('string'));
0043 } finally {
0044 console.timeEnd(timing);
0045 }
0046 }
0047
0048 /**
0049 * Reads a DEX document from a picked or dropped file: a `.json` file, or a
0050 * `.zip` read with `readDexZip()`. The file is read in place, never uploaded.
0051 */
0052 export async function readDexFile(file: File): Promise<unknown> {
0053 if (file.name.toLowerCase().endsWith('.zip')) {
0054 return readDexZip(file, file.name);
0055 }
0056 return JSON.parse(await file.text());
0057 }
0058
0059 /**
0060 * Builds the error for a failed HTTP response: status, status text and, when
0061 * the body is a JSON object with an `error` field (pyrobird's convert
0062 * endpoint answers that way), the server's reason.
0063 */
0064 export async function httpError(response: Response, url: string): Promise<Error> {
0065 let reason = '';
0066 try {
0067 const body = JSON.parse(await response.text());
0068 if (body && typeof body.error === 'string') reason = `: ${body.error}`;
0069 } catch {
0070 // Not a JSON body (an HTML error page, for example): status only
0071 }
0072 const status = `HTTP ${response.status}${response.statusText ? ' ' + response.statusText : ''}`;
0073 return new Error(`${status}${reason} (${url})`);
0074 }
0075
0076 /** Fetches a text file; rejects with `httpError()` for a failed response. */
0077 export async function fetchTextFile(fileURL: string, signal?: AbortSignal): Promise<string> {
0078 const timing = `${fetchTextFile.name}: fetching ${fileURL}`;
0079 console.time(timing);
0080 try {
0081 const response = await fetch(fileURL, { signal });
0082 if (!response.ok) throw await httpError(response, fileURL);
0083 return await response.text();
0084 } finally {
0085 console.timeEnd(timing);
0086 }
0087 }
0088
0089 /** Fetches a binary file; rejects with `httpError()` for a failed response. */
0090 export async function fetchBinaryFile(fileURL: string, signal?: AbortSignal): Promise<ArrayBuffer> {
0091 const timing = `${fetchBinaryFile.name}: fetching ${fileURL}`;
0092 console.time(timing);
0093 try {
0094 const response = await fetch(fileURL, { signal });
0095 if (!response.ok) throw await httpError(response, fileURL);
0096 return await response.arrayBuffer();
0097 } finally {
0098 console.timeEnd(timing);
0099 }
0100 }
0101
0102 /** Fetches a zipped DEX document and reads it with `readDexZip()`. */
0103 export async function loadZipFileEvents(fileURL: string, signal?: AbortSignal): Promise<unknown> {
0104 const buffer = await fetchBinaryFile(fileURL, signal);
0105 return readDexZip(buffer, fileURL);
0106 }
0107
0108 /** Fetches a DEX JSON document and parses it. */
0109 export async function loadJSONFileEvents(fileURL: string, signal?: AbortSignal): Promise<unknown> {
0110 const text = await fetchTextFile(fileURL, signal);
0111 const timing = `${loadJSONFileEvents.name}: parsing JSON from '${fileURL}'`;
0112 console.time(timing);
0113 try {
0114 return JSON.parse(text);
0115 } catch (error) {
0116 throw new Error(`'${fileURL}' is not JSON${htmlHint(text)}: ${errorText(error)}`);
0117 } finally {
0118 console.timeEnd(timing);
0119 }
0120 }
0121
0122 /**
0123 * A note for content that is an HTML page: a single-page-app server (pyrobird,
0124 * a static host with a fallback route) answers a missing file with its index
0125 * page and status 200.
0126 */
0127 function htmlHint(text: string): string {
0128 return /^\s*</.test(text.slice(0, 64))
0129 ? ' (the server sent an HTML page instead; the file is probably missing)'
0130 : '';
0131 }
0132
0133 function errorText(error: unknown): string {
0134 return error instanceof Error ? error.message : String(error);
0135 }