Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): Independent abort signals #43

Merged
merged 1 commit into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .mocharc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"recursive": true,
"require": [
"ts-node/register",
"test/hooks.ts"
"./test/hooks.ts",
"./test/setup.ts"
],
"spec": [
"test/**/*.test.*"
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
},
"packageManager": "[email protected]",
"devDependencies": {
"@assertive-ts/core": "^2.0.0",
"@assertive-ts/core": "^2.1.0",
"@assertive-ts/sinon": "^1.0.0",
"@eslint/compat": "^1.1.0",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.5.0",
Expand Down
81 changes: 11 additions & 70 deletions src/lib/RxjsAxios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ interface Interceptors {
response: AxiosInterceptorManager<AxiosResponse<unknown>>;
}

interface Abortable {
controller: AbortController;
signal: AbortSignal;
}

const logger = pino({ browser: { asObject: false } });

export class RxjsAxios {
Expand Down Expand Up @@ -95,64 +90,44 @@ export class RxjsAxios {
config: AxiosRequestConfig<D>,
): Observable<R> {
const reqConfig = this.validateConfig(config);
const { controller, signal } = this.makeCancellable();

return observify(
() => this.axios.request<T, R, D>({ ...reqConfig, signal }),
controller,
);
return observify(signal => this.axios.request<T, R, D>({ ...reqConfig, signal }));
}

public get<T, R extends AxiosResponse<T> = AxiosResponse<T>, D = unknown>(
url: string,
config?: AxiosRequestConfig<D>,
): Observable<R> {
const reqConfig = this.validateConfig(config);
const { controller, signal } = this.makeCancellable();

return observify(
() => this.axios.get<T, R, D>(url, { ...reqConfig, signal }),
controller,
);
return observify(signal => this.axios.get<T, R, D>(url, { ...reqConfig, signal }));
}

public delete<T, R extends AxiosResponse<T> = AxiosResponse<T>, D = unknown>(
url: string,
config?: AxiosRequestConfig<D>,
): Observable<R> {
const reqConfig = this.validateConfig(config);
const { controller, signal } = this.makeCancellable();

return observify(
() => this.axios.delete<T, R, D>(url, { ...reqConfig, signal }),
controller,
);
return observify(signal => this.axios.delete<T, R, D>(url, { ...reqConfig, signal }));
}

public head<T, R extends AxiosResponse<T> = AxiosResponse<T>, D = unknown>(
url: string,
config?: AxiosRequestConfig<D>,
): Observable<R> {
const reqConfig = this.validateConfig(config);
const { controller, signal } = this.makeCancellable();

return observify(
() => this.axios.head<T, R, D>(url, { ...reqConfig, signal }),
controller,
);
return observify(signal => this.axios.head<T, R, D>(url, { ...reqConfig, signal }));
}

public options<T, R extends AxiosResponse<T> = AxiosResponse<T>, D = unknown>(
url: string,
config?: AxiosRequestConfig<D>,
): Observable<R> {
const reqConfig = this.validateConfig(config);
const { controller, signal } = this.makeCancellable();

return observify(
() => this.axios.options<T, R, D>(url, { ...reqConfig, signal }),
controller,
);
return observify(signal => this.axios.options<T, R, D>(url, { ...reqConfig, signal }));
}

public post<T, R extends AxiosResponse<T> = AxiosResponse<T>, D = unknown>(
Expand All @@ -161,12 +136,8 @@ export class RxjsAxios {
config?: AxiosRequestConfig<D>,
): Observable<R> {
const reqConfig = this.validateConfig(config);
const { controller, signal } = this.makeCancellable();

return observify(
() => this.axios.post<T, R, D>(url, data, { ...reqConfig, signal }),
controller,
);
return observify(signal => this.axios.post<T, R, D>(url, data, { ...reqConfig, signal }));
}

public put<T, R extends AxiosResponse<T> = AxiosResponse<T>, D = unknown>(
Expand All @@ -175,12 +146,8 @@ export class RxjsAxios {
config?: AxiosRequestConfig<D>,
): Observable<R> {
const reqConfig = this.validateConfig(config);
const { controller, signal } = this.makeCancellable();

return observify(
() => this.axios.put<T, R, D>(url, data, { ...reqConfig, signal }),
controller,
);
return observify(signal => this.axios.put<T, R, D>(url, data, { ...reqConfig, signal }));
}

public patch<T, R extends AxiosResponse<T> = AxiosResponse<T>, D = unknown>(
Expand All @@ -189,12 +156,8 @@ export class RxjsAxios {
config?: AxiosRequestConfig<D>,
): Observable<R> {
const reqConfig = this.validateConfig(config);
const { controller, signal } = this.makeCancellable();

return observify(
() => this.axios.patch<T, R, D>(url, data, { ...reqConfig, signal }),
controller,
);
return observify(signal => this.axios.patch<T, R, D>(url, data, { ...reqConfig, signal }));
}

public postForm<T, R extends AxiosResponse<T> = AxiosResponse<T>, D = unknown>(
Expand All @@ -203,12 +166,8 @@ export class RxjsAxios {
config?: AxiosRequestConfig<D>,
): Observable<R> {
const reqConfig = this.validateConfig(config);
const { controller, signal } = this.makeCancellable();

return observify(
() => this.axios.postForm<T, R, D>(url, data, { ...reqConfig, signal }),
controller,
);
return observify(signal => this.axios.postForm<T, R, D>(url, data, { ...reqConfig, signal }));
}

public putForm<T, R extends AxiosResponse<T> = AxiosResponse<T>, D = unknown>(
Expand All @@ -217,12 +176,8 @@ export class RxjsAxios {
config?: AxiosRequestConfig<D>,
): Observable<R> {
const reqConfig = this.validateConfig(config);
const { controller, signal } = this.makeCancellable();

return observify(
() => this.axios.putForm<T, R, D>(url, data, { ...reqConfig, signal }),
controller,
);
return observify(signal => this.axios.putForm<T, R, D>(url, data, { ...reqConfig, signal }));
}

public patchForm<T, R extends AxiosResponse<T> = AxiosResponse<T>, D = unknown>(
Expand All @@ -231,12 +186,8 @@ export class RxjsAxios {
config?: AxiosRequestConfig<D>,
): Observable<R> {
const reqConfig = this.validateConfig(config);
const { controller, signal } = this.makeCancellable();

return observify(
() => this.axios.patchForm<T, R, D>(url, data, { ...reqConfig, signal }),
controller,
);
return observify(signal => this.axios.patchForm<T, R, D>(url, data, { ...reqConfig, signal }));
}

private validateConfig<D>(config?: AxiosRequestConfig<D>): AxiosRequestConfig<D> | undefined {
Expand All @@ -257,14 +208,4 @@ export class RxjsAxios {

return config;
}

private makeCancellable(): Abortable {
const controller = new AbortController();
const signal = controller.signal;

return {
controller,
signal,
};
}
}
13 changes: 7 additions & 6 deletions src/lib/observify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import { Observable } from "rxjs";

import type { AxiosResponse } from "./RxjsAxios";

export function observify<T, R extends AxiosResponse<T>>(
makeRequest: () => Promise<R>,
controller: AbortController,
): Observable<R> {
type RequestFn<R> = (signal: AbortSignal) => Promise<R>;

export function observify<T, R extends AxiosResponse<T>>(makeRequest: RequestFn<R>): Observable<R> {
return new Observable(subscriber => {
makeRequest()
const controller = new AbortController();

makeRequest(controller.signal)
.then(response => subscriber.next(response))
.catch((error: unknown) => subscriber.error(error))
.finally(() => subscriber.complete());

return { unsubscribe: () => controller.abort() };
return () => controller.abort();
});
}
2 changes: 1 addition & 1 deletion test/helpers/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HttpResponse, PathParams, http } from "msw";

interface User {
export interface User {
id?: number;
lastname: string;
name: string;
Expand Down
23 changes: 23 additions & 0 deletions test/lib/RxjsAxios.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { TypeFactories, expect } from "@assertive-ts/core";
import originalAxios from "axios";
import FormData from "form-data";
import { Observable } from "rxjs";
import { delay, map, repeat } from "rxjs/operators";
import Sinon from "sinon";

import { axios } from "../../src";
import { RxjsAxios } from "../../src/lib/RxjsAxios";
import { User } from "../helpers/mocks";

describe("[Unit] RxjsAxios.test.ts", () => {
describe(".of", () => {
Expand Down Expand Up @@ -150,6 +152,27 @@ describe("[Unit] RxjsAxios.test.ts", () => {
done();
});
});

context("when the data is fetched repeatedly", () => {
it("returns the data on each rquest", done => {
const ref = { count: 0 };

axios.get<User[]>("http://localhost:8080/users")
.pipe(
map(({ data }) => data),
delay(10),
repeat(3),
)
.subscribe(users => {
ref.count++;
expect(users).not.toBeEmpty();

if (ref.count >= 3) {
done();
}
});
});
});
});

describe(".delete", () => {
Expand Down
46 changes: 18 additions & 28 deletions test/lib/observify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,12 @@ const RESPONSE: AxiosResponse<string> = {

const REQUEST_ERROR = new AxiosError("Something went wrong", "Bad Request");

const controller = new AbortController();

describe("[Unit] observify.test.ts", () => {
describe(".observify", () => {
context("when the request promise is resolved", () => {
context("and the observable is not unsubscribed", () => {
it("sets the axios response on the next value and completes the observable", done => {
const observable = observify(
() => Promise.resolve<AxiosResponse<unknown>>(RESPONSE),
controller,
);
const observable = observify(() => Promise.resolve<AxiosResponse<unknown>>(RESPONSE));

observable.subscribe({
complete: done,
Expand All @@ -38,35 +33,31 @@ describe("[Unit] observify.test.ts", () => {

context("and the observable is unsubscribed", () => {
it("does not set the next value and cancels the request", done => {
const abort = Sinon.spy(controller, "abort");
const observable = observify(
() => delay(10).then(() => RESPONSE),
controller,
);
const spy = Sinon.spy();
const observable = observify(signal => {
signal.addEventListener("abort", spy);
return delay(10).then(() => RESPONSE);
});

const subscription = observable.subscribe({
complete: done,
error: error => expect(error).not.toBePresent(),
next: response => expect(response).not.toBePresent(),
});

expect(spy).toNeverBeCalled();
subscription.unsubscribe();

delay(20)
.then(() => Sinon.assert.calledOnce(abort))
.then(done)
.catch(done);
expect(spy).toBeCalledOnce();
done();
});
});
});

context("when the request promise is rejected", () => {
context("and the observable is not unsubscribed", () => {
it("sets the axios error on the error value and completes the observable", done => {
const observable = observify(
() => Promise.reject(REQUEST_ERROR),
controller,
);
const observable = observify(() => Promise.reject(REQUEST_ERROR));

observable.subscribe({
complete: done,
Expand All @@ -81,24 +72,23 @@ describe("[Unit] observify.test.ts", () => {

context("and the observable is unsubscribed", () => {
it("does not set the error value and cancels the request", done => {
const abort = Sinon.spy(controller, "abort");
const observable = observify(
() => delay(10).then(() => Promise.reject(REQUEST_ERROR)),
controller,
);
const spy = Sinon.spy();
const observable = observify(signal => {
signal.addEventListener("abort", spy);
return delay(10).then(() => Promise.reject(REQUEST_ERROR));
});

const subscription = observable.subscribe({
complete: done,
error: error => expect(error).not.toBePresent(),
next: response => expect(response).not.toBePresent(),
});

expect(spy).toNeverBeCalled();
subscription.unsubscribe();

delay(20)
.then(() => Sinon.assert.calledOnce(abort))
.then(done)
.catch(done);
expect(spy).toBeCalledOnce();
done();
});
});
});
Expand Down
4 changes: 4 additions & 0 deletions test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { usePlugin } from "@assertive-ts/core";
import { SinonPlugin } from "@assertive-ts/sinon";

usePlugin(SinonPlugin);
Loading
Loading