@serenity-is/corelib
Version:
Serenity Core Library
333 lines (279 loc) • 11.2 kB
text/typescript
import { blockUI, blockUndo } from "./blockui";
import { Config } from "./config";
import { getjQuery } from "./environment";
import { ErrorHandling } from "./errorhandling";
import { RequestErrorInfo, ServiceOptions, ServiceResponse } from "./servicetypes";
export function resolveUrl(url: string) {
if (url != null && url.charAt(0) == '~' && url.charAt(1) == '/')
return Config.applicationPath + url.substring(2);
return url;
}
export function resolveServiceUrl(url: string) {
if (url && url.length && url.charAt(0) != '~' && url.charAt(0) != '/' && url.indexOf('://') < 0)
return resolveUrl("~/Services/") + url;
return resolveUrl(url);
}
export function getCookie(name: string) {
let $ = getjQuery();
if (typeof $?.cookie === "function")
return $.cookie(name);
name += '=';
for (var ca = document.cookie.split(/;\s*/), i = ca.length - 1; i >= 0; i--)
if (!ca[i].indexOf(name))
return ca[i].replace(name, '');
}
export function isSameOrigin(url: string) {
var loc = window.location,
a = document.createElement('a');
a.href = url;
return a.hostname == loc.hostname &&
a.port == loc.port &&
a.protocol == loc.protocol;
}
export function getServiceOptions<TResponse extends ServiceResponse>(options: ServiceOptions<TResponse>) {
options = Object.assign(<ServiceOptions<TResponse>>{
allowRedirect: true,
async: true,
blockUI: true,
method: 'POST',
}, options);
const url = options.url = options.service ? resolveServiceUrl(options.service) : resolveUrl(options.url);
delete options.service;
options.headers ??= {};
options.headers["Accept"] ??= "application/json";
options.headers["Content-Type"] ??= "application/json";
if (isSameOrigin(url)) {
var token = getCookie('CSRF-TOKEN');
if (token)
options.headers["X-CSRF-TOKEN"] = token;
}
return options;
}
let activeRequests: number = 0;
export function requestStarting() {
activeRequests++;
let $ = getjQuery();
if ($ && typeof $.active === "number") {
($.active++ === 0) && $.event?.trigger?.("ajaxStart");
}
else if (activeRequests === 1) {
typeof document !== "undefined" && document.dispatchEvent(new Event("ajaxStart"));
}
}
export function requestFinished() {
activeRequests--;
let $ = getjQuery();
if ($ && typeof $.active === "number") {
!(--$.active) && $.event?.trigger?.("ajaxStop");
}
else if (!activeRequests) {
typeof document !== "undefined" && document.dispatchEvent(new Event("ajaxStop"));
}
}
export function getActiveRequests() {
return activeRequests;
}
function serviceFetch<TResponse extends ServiceResponse>(options: ServiceOptions<TResponse>): Promise<TResponse> {
if (typeof fetch !== "function")
return Promise.reject(reason("The fetch method is not available!", "fetch-missing"));
return (async function () {
options = getServiceOptions(options);
const url = options.url;
requestStarting();
try {
options.blockUI && blockUI();
try {
let {
allowRedirect: _1,
async: _2,
blockUI: _3,
request: _4,
service: _5,
url: _6,
onCleanup: _7,
onError: _8,
onSuccess: _9,
...fetchInit
} = options;
fetchInit.body = JSON.stringify(options.request);
let fetchResponse: Response;
try {
fetchResponse = fetchResponse = await fetch(url, fetchInit);
}
catch (ex) {
if (ex.name === "AbortError") {
return Promise.reject(reason(`Service fetch to '${url}' was aborted!`,
"abort", { cause: ex, url }));
}
throw ex;
}
if (!fetchResponse.ok) {
await handleFetchError(fetchResponse, options);
return Promise.reject(reason(`Service fetch to '${url}' resulted in HTTP ${fetchResponse.status} error: ${fetchResponse.statusText}!`,
"http-error", { fetchResponse, url }));
}
let response = await fetchResponse.json() as TResponse;
if (!response)
return Promise.reject(reason(`Received empty response from service fetch to '${url}'!`,
"empty-response", { fetchResponse, url }));
if (response.Error) {
handleError(response ?? {}, { status: fetchResponse.status, statusText: fetchResponse.statusText }, options);
return Promise.reject(reason(`Service fetch to '${url}' resulted in error: ${response.Error.Message ?? response.Error.Code}!`,
"service-error", { response, fetchResponse, url }));
}
options.onSuccess?.(response);
return response;
}
finally {
options.blockUI && blockUndo();
options.onCleanup && options.onCleanup();
}
}
finally {
requestFinished();
}
})();
}
function reason(message: string, kind: string, extra?: any) {
var error: Error;
if (extra?.cause != null) {
error = (Error as any)(message, { cause: extra.cause });
}
else {
error = Error(message);
}
if (kind != null) {
(error as any).kind = kind;
}
(error as any).origin = "serviceCall";
if (extra != null) {
if ((error as any).cause)
delete extra.cause;
Object.assign(error, extra);
}
return error;
}
export function serviceCall<TResponse extends ServiceResponse>(options: ServiceOptions<TResponse>): PromiseLike<TResponse> {
if (options?.async ?? true)
return serviceFetch(options);
let url: string;
return new Promise((resolve, reject) => {
try {
options = getServiceOptions(options);
url = options.url;
var xhr = new XMLHttpRequest();
xhr.open(options.method, url, false);
if (options.cache == "no-store")
options.headers["Cache-Control"] ??= "no-cache, no-store, max-age=0";
else if (options.cache === "no-cache")
options.headers["Cache-Control"] ??= "no-cache";
for (var x in options.headers) {
xhr.setRequestHeader(x, options.headers[x]);
}
requestStarting();
try {
if (options.signal) {
options.signal.addEventListener("abort", () => {
xhr.abort();
}, { once: true });
}
xhr.send(JSON.stringify(options.request));
try {
if (xhr.status !== 200) {
handleXHRError(xhr, options);
return reject(reason(`Service call to '${url}' resulted in HTTP ${xhr.status} error: ${xhr.statusText}!`,
"http-error", { status: xhr.status, statusText: xhr.statusText, url }));
}
let response = JSON.parse(xhr.responseText) as TResponse;
if (!response)
return reject(reason(`Empty response received on service call to '${url}'!`,
"empty-response", { url }));
if (response.Error) {
handleError(response, { status: xhr.status, statusText: xhr.statusText }, options);
return reject(reason(`Service call to '${url}' resulted in error: ${response.Error.Message ?? response.Error.Code}!`,
"service-error", { response, url }));
}
options.onSuccess?.(response);
return resolve(response);
}
finally {
options.blockUI && blockUndo();
options.onCleanup && options.onCleanup();
}
}
finally {
requestFinished();
}
}
catch (exception) {
reject(reason(`Service call to '${url}' thrown exception: ${exception.toString()}`,
"exception", { cause: exception, url }));
}
});
}
export function serviceRequest<TResponse extends ServiceResponse>(service: string, request?: any,
onSuccess?: (response: TResponse) => void, options?: ServiceOptions<TResponse>): PromiseLike<TResponse> {
return serviceCall(Object.assign({
service: service,
request: request,
onSuccess: onSuccess
}, options));
}
function handleError(response: any, errorInfo: RequestErrorInfo, options: ServiceOptions<any>) {
if (Config.notLoggedInHandler != null &&
response &&
response.Error &&
response.Error.Code == 'NotLoggedIn' &&
Config.notLoggedInHandler(options, response)) {
return;
}
if (options?.onError?.(response, errorInfo) ||
options?.errorMode === 'none')
return;
ErrorHandling.showServiceError(response?.Error, errorInfo, options?.errorMode);
};
function handleRedirect(getHeader: (key: string) => string): boolean {
var l: any = null;
try {
l = getHeader('Location');
}
catch ($t1) {
l = null;
}
if (l) {
window.top.location.href = l;
return true;
}
}
async function handleFetchError(response: Response, options: ServiceOptions<any>): Promise<void> {
if (response.status === 403 && options.allowRedirect && handleRedirect(response.headers.get))
return;
if ((response.headers.get('content-type') || '').toLowerCase().indexOf('json') >= 0) {
var json = (await response.json()) as ServiceResponse;
if (json && json.Error) {
handleError(json, {
status: response.status,
statusText: response.statusText
}, options);
return;
}
}
handleError(null, {
status: response.status,
statusText: response.statusText,
responseText: await response.text()
}, options);
}
function handleXHRError(xhr: XMLHttpRequest, options: ServiceOptions<any>) {
if (xhr.status === 403 && options.allowRedirect && handleRedirect(xhr.getResponseHeader))
return;
if ((xhr.getResponseHeader('content-type') || '')
.toLowerCase().indexOf('application/json') >= 0) {
var json = JSON.parse(xhr.responseText);
if (json && json.Error) {
handleError(json, { status: xhr.status, statusText: xhr.statusText }, options);
return;
}
}
handleError(null, { status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText }, options);
}