UNPKG

universal-geocoder

Version:

Universal geocoding abstraction server-side and client-side with multiple built-in providers

181 lines (154 loc) 5.36 kB
import fetch from "cross-fetch"; import { ErrorCallback } from "provider"; import { ResponseError } from "error"; import { isBrowser, filterUndefinedObjectValues } from "utils"; import { PartialSome } from "types"; export interface ExternalLoaderOptions { readonly protocol: string; readonly host?: string; readonly pathname?: string; readonly method: "GET" | "POST"; } export interface ExternalLoaderBody { [param: string]: ExternalLoaderBody | string | number | undefined; } export interface ExternalLoaderParams { [param: string]: string | undefined; jsonpCallback?: string; } export interface ExternalLoaderHeaders { [header: string]: string | undefined; } // eslint-disable-next-line @typescript-eslint/no-explicit-any type ResponseCallback = (response: any) => void; export interface ExternalLoaderInterface { setOptions(options: PartialSome<ExternalLoaderOptions, "method">): void; getOptions(): ExternalLoaderOptions; executeRequest( params: ExternalLoaderParams, callback: ResponseCallback, headers?: ExternalLoaderHeaders, body?: ExternalLoaderBody, errorCallback?: ErrorCallback ): void; } const defaultOptions: ExternalLoaderOptions = { protocol: "http", method: "GET", }; /** * Load data from external geocoding engines. */ export default class ExternalLoader implements ExternalLoaderInterface { private options: ExternalLoaderOptions = defaultOptions; public constructor( options: PartialSome<ExternalLoaderOptions, "method"> = defaultOptions ) { this.setOptions(options); } public setOptions( options: PartialSome<ExternalLoaderOptions, "method"> ): void { this.options = { ...defaultOptions, ...options }; } public getOptions(): ExternalLoaderOptions { return this.options; } public executeRequest( params: ExternalLoaderParams, callback: ResponseCallback, externalLoaderHeaders?: ExternalLoaderHeaders, body?: ExternalLoaderBody, errorCallback?: ErrorCallback ): void { const { protocol, host, pathname, method } = this.options; if (!host) { throw new Error("A host is required for the external loader."); } if (!pathname) { throw new Error("A pathname is required for the external loader."); } const requestUrl = new URL(`${protocol}://${host}/${pathname}`); const { jsonpCallback, ...requestParams } = params; const filteredRequestParams = filterUndefinedObjectValues(requestParams); Object.keys(filteredRequestParams).forEach((paramKey) => requestUrl.searchParams.append( paramKey, filteredRequestParams[paramKey] ?? "" ) ); if (jsonpCallback) { ExternalLoader.runJsonpCallback(requestUrl, callback, jsonpCallback); return; } const headers = filterUndefinedObjectValues(externalLoaderHeaders || {}); fetch(requestUrl.toString(), { headers, method, body: method === "POST" ? JSON.stringify(body) : undefined, }) .then((response: Response) => { if (!response.ok) { throw new ResponseError( `Received HTTP status code ${response.status} when attempting geocoding request.`, response ); } return response.json(); }) // eslint-disable-next-line @typescript-eslint/no-explicit-any .then((data: any) => callback(data)) .catch((error: Error | ResponseError) => { if (errorCallback && error instanceof ResponseError) { errorCallback(error); return; } setTimeout(() => { throw error; }); }); } private static runJsonpCallback( requestUrl: URL, callback: ResponseCallback, jsonpCallback: string ): void { if (!isBrowser()) { throw new Error( '"jsonpCallback" parameter can only be used in a browser environment.' ); } requestUrl.searchParams.append( jsonpCallback, ExternalLoader.generateJsonpCallback(callback) ); // Create a new script element. const scriptElement = document.createElement("script"); // Set its source to the JSONP API. scriptElement.src = requestUrl.toString(); // Stick the script element in the page <head>. document.getElementsByTagName("head")[0].appendChild(scriptElement); } /** * Generates randomly-named function to use as a callback for JSONP requests. * @see https://github.com/OscarGodson/JSONP */ private static generateJsonpCallback(callback: ResponseCallback): string { // Use timestamp + a random factor to account for a lot of requests in a short time. // e.g. jsonp1394571775161. const timestamp = Date.now(); const generatedFunction = `jsonp${Math.round( timestamp + Math.random() * 1000001 )}`; // Generate the temp JSONP function using the name above. // First, call the function the user defined in the callback param [callback(json)]. // Then delete the generated function from the window [delete window[generatedFunction]]. // eslint-disable-next-line @typescript-eslint/no-explicit-any (<any>window)[generatedFunction] = (json: string) => { callback(json); // eslint-disable-next-line @typescript-eslint/no-explicit-any delete (<any>window)[generatedFunction]; }; return generatedFunction; } }