Back to home page

EIC code displayed by LXR

 
 

    


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 }