- Added cancelable concept that works (rough)

This commit is contained in:
Ferdi Koomen 2021-03-13 18:01:37 +01:00
parent c6d9f56e14
commit 7b5823ce75
33 changed files with 674 additions and 314 deletions

View File

@ -1,16 +1,73 @@
export class CancelablePromise<T> extends Promise<T> {
export class CancelablePromise<T> implements Promise<T> {
readonly [Symbol.toStringTag]: string;
private readonly _cancel: (reason?: any) => void;
private _isPending: boolean = true;
private _isCanceled: boolean = false;
private _promise: Promise<T>;
private _resolve?: (value: T | PromiseLike<T>) => void;
private _reject?: (reason?: unknown) => void;
private _cancelHandler?: () => void;
constructor(executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: () => void
cancel: () => void
) => void) {
super(executor);
constructor(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: unknown) => void, onCancel: (cancelHandler: () => void) => void) => void) {
this._promise = new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (!this._isCanceled && this._resolve) {
this._isPending = false;
this._resolve(value);
}
};
const onReject = (reason?: unknown): void => {
if (this._reject) {
this._isPending = false;
this._reject(reason);
}
};
const onCancel = (cancelHandler: () => void): void => {
if (this._isPending) {
this._cancelHandler = cancelHandler;
}
};
return executor(onResolve, onReject, onCancel);
});
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this._promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult> {
return this._promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | undefined | null): Promise<T> {
return this._promise.finally(onFinally);
}
public cancel() {
//
if (!this._isPending || this._isCanceled) {
return;
}
this._isCanceled = true;
if (this._cancelHandler && this._reject) {
try {
this._cancelHandler();
} catch (error) {
this._reject(error);
return;
}
}
}
public get isCanceled(): boolean {
return this._isCanceled;
}
}

View File

@ -2,6 +2,7 @@ function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
if (options.formData) {
return getFormData(options.formData);
}
if (options.body) {
if (isString(options.body) || isBlob(options.body)) {
return options.body;
@ -9,5 +10,6 @@ function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
return JSON.stringify(options.body);
}
}
return undefined;
}

View File

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

View File

@ -5,5 +5,6 @@ function getResponseHeader(response: Response, responseHeader?: string): string
return content;
}
}
return null;
}

View File

@ -55,21 +55,26 @@ import { OpenAPI } from './OpenAPI';
* @throws ApiError
*/
export function request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return new CancelablePromise((resolve, reject, cancel) => {
const controller = new AbortController();
const url = getUrl(options);
const response = await sendRequest(options, url, controller.signal);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
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 result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
catchErrors(options, result);
catchErrors(options, result);
resolve(result.body);
} catch (error) {
reject(error);
}
});
}

View File

@ -1,12 +1,18 @@
async function sendRequest(options: ApiRequestOptions, url: string, signal: AbortSignal): Promise<Response> {
async function sendRequest(options: ApiRequestOptions, url: string, onCancel: (cancelHandler: () => void) => void): Promise<Response> {
const controller = new AbortController();
const request: RequestInit = {
method: options.method,
headers: await getHeaders(options),
body: getRequestBody(options),
signal,
signal: controller.signal,
};
if (OpenAPI.WITH_CREDENTIALS) {
request.credentials = 'include';
}
onCancel(() => controller.abort());
return await fetch(url, request);
}

View File

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

View File

@ -1,5 +1,6 @@
function getQueryString(params: Record<string, any>): string {
const qs: string[] = [];
Object.keys(params).forEach(key => {
const value = params[key];
if (isDefined(value)) {
@ -12,8 +13,10 @@ function getQueryString(params: Record<string, any>): string {
}
}
});
if (qs.length > 0) {
return `?${qs.join('&')}`;
}
return '';
}

View File

@ -5,5 +5,6 @@ function getUrl(options: ApiRequestOptions): string {
if (options.query) {
return `${url}${getQueryString(options.query)}`;
}
return url;
}

View File

@ -2,6 +2,7 @@ function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
if (options.formData) {
return getFormData(options.formData);
}
if (options.body) {
if (isString(options.body) || isBinary(options.body)) {
return options.body;
@ -9,5 +10,6 @@ function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
return JSON.stringify(options.body);
}
}
return undefined;
}

View File

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

View File

@ -5,5 +5,6 @@ function getResponseHeader(response: Response, responseHeader?: string): string
return content;
}
}
return null;
}

View File

@ -60,21 +60,26 @@ import { OpenAPI } from './OpenAPI';
* @throws ApiError
*/
export function request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return new CancelablePromise((resolve, reject, cancel) => {
const controller = new AbortController();
const url = getUrl(options);
const response = await sendRequest(options, url, controller.signal);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
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 result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
catchErrors(options, result);
catchErrors(options, result);
resolve(result.body);
} catch (error) {
reject(error);
}
});
}

View File

@ -1,9 +1,14 @@
async function sendRequest(options: ApiRequestOptions, url: string, signal: AbortSignal): Promise<Response> {
async function sendRequest(options: ApiRequestOptions, url: string, onCancel: (cancelHandler: () => void) => void): Promise<Response> {
const controller = new AbortController();
const request: RequestInit = {
method: options.method,
headers: await getHeaders(options),
body: getRequestBody(options),
signal,
signal: controller.signal,
};
onCancel(() => controller.abort());
return await fetch(url, request);
}

View File

@ -2,6 +2,7 @@ function getRequestBody(options: ApiRequestOptions): any {
if (options.formData) {
return getFormData(options.formData);
}
if (options.body) {
if (isString(options.body) || isBlob(options.body)) {
return options.body;
@ -9,5 +10,6 @@ function getRequestBody(options: ApiRequestOptions): any {
return JSON.stringify(options.body);
}
}
return undefined;
}

View File

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

View File

@ -5,5 +5,6 @@ function getResponseHeader(xhr: XMLHttpRequest, responseHeader?: string): string
return content;
}
}
return null;
}

View File

@ -58,20 +58,26 @@ import { OpenAPI } from './OpenAPI';
* @throws ApiError
*/
export function request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return new CancelablePromise((resolve, reject, cancel) => {
const url = getUrl(options);
const response = await sendRequest(options, url);
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
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 result: ApiResult = {
url,
ok: isSuccess(response.status),
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
const result: ApiResult = {
url,
ok: isSuccess(response.status),
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
catchErrors(options, result);
catchErrors(options, result);
resolve(result.body);
} catch (error) {
reject(error);
}
});
}

View File

@ -1,5 +1,4 @@
async function sendRequest(options: ApiRequestOptions, url: string): Promise<XMLHttpRequest> {
async function sendRequest(options: ApiRequestOptions, url: string, onCancel: (cancelHandler: () => void) => void): Promise<XMLHttpRequest> {
const xhr = new XMLHttpRequest();
xhr.open(options.method, url, true);
xhr.withCredentials = OpenAPI.WITH_CREDENTIALS;
@ -9,14 +8,19 @@ async function sendRequest(options: ApiRequestOptions, url: string): Promise<XML
xhr.setRequestHeader(key, value);
});
return new Promise<XMLHttpRequest>(resolve => {
return new Promise<XMLHttpRequest>((resolve, reject) => {
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
resolve(xhr);
}
console.log(xhr.readyState);
if (xhr.readyState === XMLHttpRequest.DONE) {
reject();
}
};
xhr.send(getRequestBody(options));
// xhr.abort();
onCancel(() => xhr.abort());
});
}

View File

@ -27,6 +27,7 @@ describe('writeClient', () => {
apiError: () => 'apiError',
apiRequestOptions: () => 'apiRequestOptions',
apiResult: () => 'apiResult',
cancelablePromise: () => 'cancelablePromise',
request: () => 'request',
},
};

View File

@ -27,6 +27,7 @@ describe('writeClientCore', () => {
apiError: () => 'apiError',
apiRequestOptions: () => 'apiRequestOptions',
apiResult: () => 'apiResult',
cancelablePromise: () => 'cancelablePromise',
request: () => 'request',
},
};
@ -37,6 +38,7 @@ describe('writeClientCore', () => {
expect(writeFile).toBeCalledWith('/ApiError.ts', 'apiError');
expect(writeFile).toBeCalledWith('/ApiRequestOptions.ts', 'apiRequestOptions');
expect(writeFile).toBeCalledWith('/ApiResult.ts', 'apiResult');
expect(writeFile).toBeCalledWith('/CancelablePromise.ts', 'cancelablePromise');
expect(writeFile).toBeCalledWith('/request.ts', 'request');
});
});

File diff suppressed because it is too large Load Diff

View File

@ -6,13 +6,21 @@ import { CancelablePromise } from './CancelablePromise';
import { OpenAPI } from './OpenAPI';
export function request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return new CancelablePromise((resolve, reject, cancel) => {
return new CancelablePromise((resolve, reject, onCancel) => {
const url = `${OpenAPI.BASE}${options.path}`;
// Do your request...
try {
// Do your request...
const timeout = setTimeout(() => {
resolve({ ...options });
}, 500);
resolve({
...options
});
// Cancel your request...
onCancel(() => {
clearTimeout(timeout);
});
} catch (e) {
reject(e);
}
});
}

View File

@ -28,16 +28,18 @@ async function start(dir) {
// Although this might not be a 'correct' response, we can use this to test
// the majority of API calls.
app.all('/base/api/*', (req, res) => {
res.json({
method: req.method,
protocol: req.protocol,
hostname: req.hostname,
path: req.path,
url: req.url,
query: req.query,
body: req.body,
headers: req.headers,
});
setTimeout(() => {
res.json({
method: req.method,
protocol: req.protocol,
hostname: req.hostname,
path: req.path,
url: req.url,
query: req.query,
body: req.body,
headers: req.headers,
});
}, 100);
});
server = app.listen(3000, resolve);

View File

@ -37,9 +37,9 @@ describe('v2.fetch', () => {
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
});
expect(result).toBeDefined();

View File

@ -37,11 +37,23 @@ describe('v2.fetch', () => {
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
const result = await browser.evaluate(async () => {
const { SimpleService } = window.api;
const promise = SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
});
expect(result).toBeUndefined();
});
});

View File

@ -30,11 +30,21 @@ describe('v2.node', () => {
const result = await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
const { SimpleService } = require('./generated/v3/node/index.js');
const promise = await SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
const result = await promise;
expect(result).toBeDefined();
});
});

View File

@ -37,11 +37,23 @@ describe('v2.xhr', () => {
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
const result = await browser.evaluate(async () => {
const { SimpleService } = window.api;
const promise = SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
});
expect(result).toBeUndefined();
});
});

View File

@ -50,9 +50,9 @@ describe('v3.fetch', () => {
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
});
expect(result).toBeDefined();

View File

@ -50,11 +50,23 @@ describe('v3.fetch', () => {
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
const result = await browser.evaluate(async () => {
const { SimpleService } = window.api;
const promise = SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
});
expect(result).toBeUndefined();
});
});

View File

@ -41,11 +41,21 @@ describe('v3.node', () => {
const result = await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
const { SimpleService } = require('./generated/v3/node/index.js');
const promise = await SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
const result = await promise;
expect(result).toBeDefined();
});
});

View File

@ -50,11 +50,23 @@ describe('v3.xhr', () => {
return await ComplexService.complexTypes({
first: {
second: {
third: 'Hello World!'
}
}
third: 'Hello World!',
},
},
});
});
expect(result).toBeDefined();
});
it('can abort the request', async () => {
const result = await browser.evaluate(async () => {
const { SimpleService } = window.api;
const promise = SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
});
expect(result).toBeUndefined();
});
});

View File

@ -6,7 +6,7 @@ async function generateV2() {
await OpenAPI.generate({
input: './test/spec/v2.json',
output: './test/generated/v2/',
httpClient: OpenAPI.HttpClient.NODE,
httpClient: OpenAPI.HttpClient.FETCH,
useOptions: false,
useUnionTypes: false,
exportCore: true,
@ -21,7 +21,7 @@ async function generateV3() {
await OpenAPI.generate({
input: './test/spec/v3.json',
output: './test/generated/v3/',
httpClient: OpenAPI.HttpClient.NODE,
httpClient: OpenAPI.HttpClient.FETCH,
useOptions: false,
useUnionTypes: false,
exportCore: true,