universal-geocoder
Version:
Universal geocoding abstraction server-side and client-side with multiple built-in providers
181 lines (154 loc) • 5.36 kB
text/typescript
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;
}
}