- Fixed just in time cancelation scenario

This commit is contained in:
Ferdi Koomen 2021-10-24 10:55:01 +02:00
parent 2553b2cf93
commit 63f59e6c39
29 changed files with 364 additions and 210 deletions

View File

@ -11,6 +11,7 @@ export class ApiError extends Error {
constructor(response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;

View File

@ -1,10 +1,29 @@
{{>header}}
export class CancelError extends Error {
constructor(reason: string = 'Promise was canceled') {
super(reason);
this.name = 'CancelError';
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isPending: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
readonly [Symbol.toStringTag]: string;
#isPending: boolean;
#isCanceled: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
@ -14,18 +33,18 @@ export class CancelablePromise<T> implements Promise<T> {
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: (cancelHandler: () => void) => void
onCancel: OnCancel
) => void
) {
this.#isPending = true;
this.#isCanceled = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (!this.#isCanceled) {
if (!this.#isCancelled) {
this.#isPending = false;
this.#resolve?.(value);
}
@ -42,7 +61,15 @@ export class CancelablePromise<T> implements Promise<T> {
}
};
return executor(onResolve, onReject, onCancel);
Object.defineProperty(onCancel, 'isPending', {
get: (): boolean => this.#isPending,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
@ -64,10 +91,10 @@ export class CancelablePromise<T> implements Promise<T> {
}
public cancel(): void {
if (!this.#isPending || this.#isCanceled) {
if (!this.#isPending || this.#isCancelled) {
return;
}
this.#isCanceled = true;
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
@ -80,7 +107,7 @@ export class CancelablePromise<T> implements Promise<T> {
}
}
public get isCanceled(): boolean {
return this.#isCanceled;
public get isCancelled(): boolean {
return this.#isCancelled;
}
}

View File

@ -0,0 +1,6 @@
function getRequestBody(options: ApiRequestOptions): any {
if (options.body) {
return options.body;
}
return;
}

View File

@ -2,5 +2,5 @@ function getResponseBody(response: AxiosResponse<any>): any {
if (response.status !== 204) {
return response.data;
}
return null;
return;
}

View File

@ -1,9 +1,9 @@
function getResponseHeader(response: AxiosResponse<any>, responseHeader?: string): string | null {
function getResponseHeader(response: AxiosResponse<any>, responseHeader?: string): string | undefined {
if (responseHeader) {
const content = response.headers[responseHeader];
if (isString(content)) {
return content;
}
}
return null;
return;
}

View File

@ -7,6 +7,7 @@ import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import { OpenAPI } from './OpenAPI';
{{>functions/isDefined}}
@ -39,6 +40,9 @@ import { OpenAPI } from './OpenAPI';
{{>axios/getHeaders}}
{{>axios/getRequestBody}}
{{>axios/sendRequest}}
@ -61,21 +65,27 @@ export function request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(options);
const response = await sendRequest(options, url, onCancel);
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(options, formData);
const result: ApiResult = {
url,
ok: isSuccess(response.status),
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
if (!onCancel.isCancelled) {
const response = await sendRequest(options, url, formData, body, headers, onCancel);
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
catchErrors(options, result);
const result: ApiResult = {
url,
ok: isSuccess(response.status),
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
resolve(result.body);
catchErrors(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}

View File

@ -1,13 +1,18 @@
async function sendRequest(options: ApiRequestOptions, url: string, onCancel: (cancelHandler: () => void) => void): Promise<AxiosResponse<any>> {
async function sendRequest(
options: ApiRequestOptions,
url: string,
formData: FormData | undefined,
body: any,
headers: Record<string, string>,
onCancel: OnCancel
): Promise<AxiosResponse<any>> {
const source = axios.CancelToken.source();
const formData = options.formData && getFormData(options.formData);
const data = formData || options.body;
const config: AxiosRequestConfig = {
url,
data,
headers,
data: body || formData,
method: options.method,
headers: await getHeaders(options, formData),
cancelToken: source.token,
};

View File

@ -37,5 +37,6 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
headers.append('Content-Type', 'application/json');
}
}
return headers;
}

View File

@ -1,8 +1,4 @@
function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
if (options.formData) {
return getFormData(options.formData);
}
if (options.body) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
@ -12,6 +8,5 @@ function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
return JSON.stringify(options.body);
}
}
return undefined;
return;
}

View File

@ -14,6 +14,5 @@ async function getResponseBody(response: Response): Promise<any> {
console.error(error);
}
}
return null;
return;
}

View File

@ -1,10 +1,9 @@
function getResponseHeader(response: Response, responseHeader?: string): string | null {
function getResponseHeader(response: Response, responseHeader?: string): string | undefined {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return null;
return;
}

View File

@ -4,6 +4,7 @@ import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import { OpenAPI } from './OpenAPI';
{{>functions/isDefined}}
@ -61,21 +62,27 @@ export function request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(options);
const response = await sendRequest(options, url, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(options);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
if (!onCancel.isCancelled) {
const response = await sendRequest(options, url, formData, body, headers, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
catchErrors(options, result);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
resolve(result.body);
catchErrors(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}

View File

@ -1,10 +1,17 @@
async function sendRequest(options: ApiRequestOptions, url: string, onCancel: (cancelHandler: () => void) => void): Promise<Response> {
async function sendRequest(
options: ApiRequestOptions,
url: string,
formData: FormData | undefined,
body: BodyInit | undefined,
headers: Headers,
onCancel: OnCancel
): Promise<Response> {
const controller = new AbortController();
const request: RequestInit = {
headers,
body: body || formData,
method: options.method,
headers: await getHeaders(options),
body: getRequestBody(options),
signal: controller.signal,
};

View File

@ -1,12 +1,15 @@
function getFormData(params: Record<string, any>): FormData {
const formData = new FormData();
function getFormData(options: ApiRequestOptions): FormData | undefined {
if (options.formData) {
const formData = new FormData();
Object.keys(params).forEach(key => {
const value = params[key];
if (isDefined(value)) {
formData.append(key, value);
}
});
Object.keys(options.formData).forEach(key => {
const value = options.formData?.[key];
if (isDefined(value)) {
formData.append(key, value);
}
});
return formData;
return formData;
}
return;
}

View File

@ -1,8 +1,4 @@
function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
if (options.formData) {
return getFormData(options.formData);
}
if (options.body) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
@ -12,6 +8,5 @@ function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
return JSON.stringify(options.body);
}
}
return undefined;
return;
}

View File

@ -14,6 +14,5 @@ async function getResponseBody(response: Response): Promise<any> {
console.error(error);
}
}
return null;
return;
}

View File

@ -1,10 +1,9 @@
function getResponseHeader(response: Response, responseHeader?: string): string | null {
function getResponseHeader(response: Response, responseHeader?: string): string | undefined {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return null;
return;
}

View File

@ -9,6 +9,7 @@ import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import { OpenAPI } from './OpenAPI';
{{>functions/isDefined}}
@ -69,21 +70,27 @@ export function request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(options);
const response = await sendRequest(options, url, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(options);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
if (!onCancel.isCancelled) {
const response = await sendRequest(options, url, formData, body, headers, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
catchErrors(options, result);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
resolve(result.body);
catchErrors(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}

View File

@ -1,10 +1,17 @@
async function sendRequest(options: ApiRequestOptions, url: string, onCancel: (cancelHandler: () => void) => void): Promise<Response> {
async function sendRequest(
options: ApiRequestOptions,
url: string,
formData: FormData | undefined,
body: BodyInit | undefined,
headers: Headers,
onCancel: OnCancel
): Promise<Response> {
const controller = new AbortController();
const request: RequestInit = {
headers,
method: options.method,
headers: await getHeaders(options),
body: getRequestBody(options),
body: body || formData,
signal: controller.signal,
};

View File

@ -1,8 +1,4 @@
function getRequestBody(options: ApiRequestOptions): any {
if (options.formData) {
return getFormData(options.formData);
}
if (options.body) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
@ -13,5 +9,5 @@ function getRequestBody(options: ApiRequestOptions): any {
}
}
return undefined;
return;
}

View File

@ -14,6 +14,5 @@ function getResponseBody(xhr: XMLHttpRequest): any {
console.error(error);
}
}
return null;
return;
}

View File

@ -1,10 +1,9 @@
function getResponseHeader(xhr: XMLHttpRequest, responseHeader?: string): string | null {
function getResponseHeader(xhr: XMLHttpRequest, responseHeader?: string): string | undefined {
if (responseHeader) {
const content = xhr.getResponseHeader(responseHeader);
if (isString(content)) {
return content;
}
}
return null;
return;
}

View File

@ -4,6 +4,7 @@ import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import { OpenAPI } from './OpenAPI';
{{>functions/isDefined}}
@ -64,21 +65,27 @@ export function request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(options);
const response = await sendRequest(options, url, onCancel);
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(options);
const result: ApiResult = {
url,
ok: isSuccess(response.status),
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
if (!onCancel.isCancelled) {
const response = await sendRequest(options, url, formData, body, headers, onCancel);
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
catchErrors(options, result);
const result: ApiResult = {
url,
ok: isSuccess(response.status),
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
resolve(result.body);
catchErrors(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}

View File

@ -1,17 +1,23 @@
async function sendRequest(options: ApiRequestOptions, url: string, onCancel: (cancelHandler: () => void) => void): Promise<XMLHttpRequest> {
async function sendRequest(
options: ApiRequestOptions,
url: string,
formData: FormData | undefined,
body: any,
headers: Headers,
onCancel: OnCancel
): Promise<XMLHttpRequest> {
const xhr = new XMLHttpRequest();
xhr.open(options.method, url, true);
xhr.withCredentials = OpenAPI.WITH_CREDENTIALS;
const headers = await getHeaders(options);
headers.forEach((value: string, key: string) => {
headers.forEach((value, key) => {
xhr.setRequestHeader(key, value);
});
return new Promise<XMLHttpRequest>((resolve, reject) => {
xhr.onload = () => resolve(xhr);
xhr.onabort = () => reject(new Error('The user aborted a request.'));
xhr.send(getRequestBody(options));
xhr.send(body || formData);
onCancel(() => xhr.abort());
});

View File

@ -5,6 +5,7 @@ import templateCoreApiError from '../templates/core/ApiError.hbs';
import templateCoreApiRequestOptions from '../templates/core/ApiRequestOptions.hbs';
import templateCoreApiResult from '../templates/core/ApiResult.hbs';
import axiosGetHeaders from '../templates/core/axios/getHeaders.hbs';
import axiosGetRequestBody from '../templates/core/axios/getRequestBody.hbs';
import axiosGetResponseBody from '../templates/core/axios/getResponseBody.hbs';
import axiosGetResponseHeader from '../templates/core/axios/getResponseHeader.hbs';
import axiosRequest from '../templates/core/axios/request.hbs';
@ -186,6 +187,7 @@ export function registerHandlebarTemplates(root: { httpClient: HttpClient; useOp
// Specific files for the axios client implementation
Handlebars.registerPartial('axios/getHeaders', Handlebars.template(axiosGetHeaders));
Handlebars.registerPartial('axios/getRequestBody', Handlebars.template(axiosGetRequestBody));
Handlebars.registerPartial('axios/getResponseBody', Handlebars.template(axiosGetResponseBody));
Handlebars.registerPartial('axios/getResponseHeader', Handlebars.template(axiosGetResponseHeader));
Handlebars.registerPartial('axios/sendRequest', Handlebars.template(axiosSendRequest));

View File

@ -15,6 +15,7 @@ export class ApiError extends Error {
constructor(response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
@ -58,11 +59,30 @@ exports[`v2 should generate: ./test/generated/v2/core/CancelablePromise.ts 1`] =
"/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export class CancelError extends Error {
constructor(reason: string = 'Promise was canceled') {
super(reason);
this.name = 'CancelError';
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isPending: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
readonly [Symbol.toStringTag]: string;
#isPending: boolean;
#isCanceled: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
@ -72,18 +92,18 @@ export class CancelablePromise<T> implements Promise<T> {
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: (cancelHandler: () => void) => void
onCancel: OnCancel
) => void
) {
this.#isPending = true;
this.#isCanceled = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (!this.#isCanceled) {
if (!this.#isCancelled) {
this.#isPending = false;
this.#resolve?.(value);
}
@ -100,7 +120,15 @@ export class CancelablePromise<T> implements Promise<T> {
}
};
return executor(onResolve, onReject, onCancel);
Object.defineProperty(onCancel, 'isPending', {
get: (): boolean => this.#isPending,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
@ -122,10 +150,10 @@ export class CancelablePromise<T> implements Promise<T> {
}
public cancel(): void {
if (!this.#isPending || this.#isCanceled) {
if (!this.#isPending || this.#isCancelled) {
return;
}
this.#isCanceled = true;
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
@ -138,8 +166,8 @@ export class CancelablePromise<T> implements Promise<T> {
}
}
public get isCanceled(): boolean {
return this.#isCanceled;
public get isCancelled(): boolean {
return this.#isCancelled;
}
}"
`;
@ -184,6 +212,7 @@ import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import { OpenAPI } from './OpenAPI';
function isDefined<T>(value: T | null | undefined): value is Exclude<T, null | undefined> {
@ -243,17 +272,20 @@ function getUrl(options: ApiRequestOptions): string {
return url;
}
function getFormData(params: Record<string, any>): FormData {
const formData = new FormData();
function getFormData(options: ApiRequestOptions): FormData | undefined {
if (options.formData) {
const formData = new FormData();
Object.keys(params).forEach(key => {
const value = params[key];
if (isDefined(value)) {
formData.append(key, value);
}
});
Object.keys(options.formData).forEach(key => {
const value = options.formData?.[key];
if (isDefined(value)) {
formData.append(key, value);
}
});
return formData;
return formData;
}
return;
}
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
@ -304,14 +336,11 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
headers.append('Content-Type', 'application/json');
}
}
return headers;
}
function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
if (options.formData) {
return getFormData(options.formData);
}
if (options.body) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
@ -321,17 +350,23 @@ function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
return JSON.stringify(options.body);
}
}
return undefined;
return;
}
async function sendRequest(options: ApiRequestOptions, url: string, onCancel: (cancelHandler: () => void) => void): Promise<Response> {
async function sendRequest(
options: ApiRequestOptions,
url: string,
formData: FormData | undefined,
body: BodyInit | undefined,
headers: Headers,
onCancel: OnCancel
): Promise<Response> {
const controller = new AbortController();
const request: RequestInit = {
headers,
body: body || formData,
method: options.method,
headers: await getHeaders(options),
body: getRequestBody(options),
signal: controller.signal,
};
@ -344,15 +379,14 @@ async function sendRequest(options: ApiRequestOptions, url: string, onCancel: (c
return await fetch(url, request);
}
function getResponseHeader(response: Response, responseHeader?: string): string | null {
function getResponseHeader(response: Response, responseHeader?: string): string | undefined {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return null;
return;
}
async function getResponseBody(response: Response): Promise<any> {
@ -371,8 +405,7 @@ async function getResponseBody(response: Response): Promise<any> {
console.error(error);
}
}
return null;
return;
}
function catchErrors(options: ApiRequestOptions, result: ApiResult): void {
@ -407,21 +440,27 @@ export function request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(options);
const response = await sendRequest(options, url, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(options);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
if (!onCancel.isCancelled) {
const response = await sendRequest(options, url, formData, body, headers, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
catchErrors(options, result);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
resolve(result.body);
catchErrors(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
@ -2488,6 +2527,7 @@ export class ApiError extends Error {
constructor(response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
@ -2531,11 +2571,30 @@ exports[`v3 should generate: ./test/generated/v3/core/CancelablePromise.ts 1`] =
"/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export class CancelError extends Error {
constructor(reason: string = 'Promise was canceled') {
super(reason);
this.name = 'CancelError';
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isPending: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
readonly [Symbol.toStringTag]: string;
#isPending: boolean;
#isCanceled: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
@ -2545,18 +2604,18 @@ export class CancelablePromise<T> implements Promise<T> {
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: (cancelHandler: () => void) => void
onCancel: OnCancel
) => void
) {
this.#isPending = true;
this.#isCanceled = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (!this.#isCanceled) {
if (!this.#isCancelled) {
this.#isPending = false;
this.#resolve?.(value);
}
@ -2573,7 +2632,15 @@ export class CancelablePromise<T> implements Promise<T> {
}
};
return executor(onResolve, onReject, onCancel);
Object.defineProperty(onCancel, 'isPending', {
get: (): boolean => this.#isPending,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
@ -2595,10 +2662,10 @@ export class CancelablePromise<T> implements Promise<T> {
}
public cancel(): void {
if (!this.#isPending || this.#isCanceled) {
if (!this.#isPending || this.#isCancelled) {
return;
}
this.#isCanceled = true;
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
@ -2611,8 +2678,8 @@ export class CancelablePromise<T> implements Promise<T> {
}
}
public get isCanceled(): boolean {
return this.#isCanceled;
public get isCancelled(): boolean {
return this.#isCancelled;
}
}"
`;
@ -2657,6 +2724,7 @@ import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import { OpenAPI } from './OpenAPI';
function isDefined<T>(value: T | null | undefined): value is Exclude<T, null | undefined> {
@ -2716,17 +2784,20 @@ function getUrl(options: ApiRequestOptions): string {
return url;
}
function getFormData(params: Record<string, any>): FormData {
const formData = new FormData();
function getFormData(options: ApiRequestOptions): FormData | undefined {
if (options.formData) {
const formData = new FormData();
Object.keys(params).forEach(key => {
const value = params[key];
if (isDefined(value)) {
formData.append(key, value);
}
});
Object.keys(options.formData).forEach(key => {
const value = options.formData?.[key];
if (isDefined(value)) {
formData.append(key, value);
}
});
return formData;
return formData;
}
return;
}
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
@ -2777,14 +2848,11 @@ async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
headers.append('Content-Type', 'application/json');
}
}
return headers;
}
function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
if (options.formData) {
return getFormData(options.formData);
}
if (options.body) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
@ -2794,17 +2862,23 @@ function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
return JSON.stringify(options.body);
}
}
return undefined;
return;
}
async function sendRequest(options: ApiRequestOptions, url: string, onCancel: (cancelHandler: () => void) => void): Promise<Response> {
async function sendRequest(
options: ApiRequestOptions,
url: string,
formData: FormData | undefined,
body: BodyInit | undefined,
headers: Headers,
onCancel: OnCancel
): Promise<Response> {
const controller = new AbortController();
const request: RequestInit = {
headers,
body: body || formData,
method: options.method,
headers: await getHeaders(options),
body: getRequestBody(options),
signal: controller.signal,
};
@ -2817,15 +2891,14 @@ async function sendRequest(options: ApiRequestOptions, url: string, onCancel: (c
return await fetch(url, request);
}
function getResponseHeader(response: Response, responseHeader?: string): string | null {
function getResponseHeader(response: Response, responseHeader?: string): string | undefined {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return null;
return;
}
async function getResponseBody(response: Response): Promise<any> {
@ -2844,8 +2917,7 @@ async function getResponseBody(response: Response): Promise<any> {
console.error(error);
}
}
return null;
return;
}
function catchErrors(options: ApiRequestOptions, result: ApiResult): void {
@ -2880,21 +2952,27 @@ export function request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(options);
const response = await sendRequest(options, url, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(options);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
if (!onCancel.isCancelled) {
const response = await sendRequest(options, url, formData, body, headers, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
catchErrors(options, result);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
resolve(result.body);
catchErrors(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}

View File

@ -6,7 +6,7 @@ const compileWithBabel = require('./scripts/compileWithBabel');
const server = require('./scripts/server');
const browser = require('./scripts/browser');
describe('v2.fetch', () => {
describe('v2.babel', () => {
beforeAll(async () => {
await generate('v2/babel', 'v2', 'fetch', true, true);
await copy('v2/babel');

View File

@ -38,7 +38,7 @@ describe('v2.node', () => {
it('can abort the request', async () => {
try {
const { SimpleService } = require('./generated/v3/node/index.js');
const { SimpleService } = require('./generated/v2/node/index.js');
const promise = SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();

View File

@ -6,7 +6,7 @@ const compileWithBabel = require('./scripts/compileWithBabel');
const server = require('./scripts/server');
const browser = require('./scripts/browser');
describe('v3.fetch', () => {
describe('v3.babel', () => {
beforeAll(async () => {
await generate('v3/babel', 'v3', 'fetch', true, true);
await copy('v3/babel');