diff --git a/.mocharc.json b/.mocharc.json index a39508d..1a30dce 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -4,7 +4,8 @@ "recursive": true, "require": [ "ts-node/register", - "test/hooks.ts" + "./test/hooks.ts", + "./test/setup.ts" ], "spec": [ "test/**/*.test.*" diff --git a/package.json b/package.json index 9140e32..aa438f3 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ }, "packageManager": "yarn@4.0.2", "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", diff --git a/src/lib/RxjsAxios.ts b/src/lib/RxjsAxios.ts index efc03ec..9286ee3 100644 --- a/src/lib/RxjsAxios.ts +++ b/src/lib/RxjsAxios.ts @@ -39,11 +39,6 @@ interface Interceptors { response: AxiosInterceptorManager>; } -interface Abortable { - controller: AbortController; - signal: AbortSignal; -} - const logger = pino({ browser: { asObject: false } }); export class RxjsAxios { @@ -95,12 +90,8 @@ export class RxjsAxios { config: AxiosRequestConfig, ): Observable { const reqConfig = this.validateConfig(config); - const { controller, signal } = this.makeCancellable(); - return observify( - () => this.axios.request({ ...reqConfig, signal }), - controller, - ); + return observify(signal => this.axios.request({ ...reqConfig, signal })); } public get = AxiosResponse, D = unknown>( @@ -108,12 +99,8 @@ export class RxjsAxios { config?: AxiosRequestConfig, ): Observable { const reqConfig = this.validateConfig(config); - const { controller, signal } = this.makeCancellable(); - return observify( - () => this.axios.get(url, { ...reqConfig, signal }), - controller, - ); + return observify(signal => this.axios.get(url, { ...reqConfig, signal })); } public delete = AxiosResponse, D = unknown>( @@ -121,12 +108,8 @@ export class RxjsAxios { config?: AxiosRequestConfig, ): Observable { const reqConfig = this.validateConfig(config); - const { controller, signal } = this.makeCancellable(); - return observify( - () => this.axios.delete(url, { ...reqConfig, signal }), - controller, - ); + return observify(signal => this.axios.delete(url, { ...reqConfig, signal })); } public head = AxiosResponse, D = unknown>( @@ -134,12 +117,8 @@ export class RxjsAxios { config?: AxiosRequestConfig, ): Observable { const reqConfig = this.validateConfig(config); - const { controller, signal } = this.makeCancellable(); - return observify( - () => this.axios.head(url, { ...reqConfig, signal }), - controller, - ); + return observify(signal => this.axios.head(url, { ...reqConfig, signal })); } public options = AxiosResponse, D = unknown>( @@ -147,12 +126,8 @@ export class RxjsAxios { config?: AxiosRequestConfig, ): Observable { const reqConfig = this.validateConfig(config); - const { controller, signal } = this.makeCancellable(); - return observify( - () => this.axios.options(url, { ...reqConfig, signal }), - controller, - ); + return observify(signal => this.axios.options(url, { ...reqConfig, signal })); } public post = AxiosResponse, D = unknown>( @@ -161,12 +136,8 @@ export class RxjsAxios { config?: AxiosRequestConfig, ): Observable { const reqConfig = this.validateConfig(config); - const { controller, signal } = this.makeCancellable(); - return observify( - () => this.axios.post(url, data, { ...reqConfig, signal }), - controller, - ); + return observify(signal => this.axios.post(url, data, { ...reqConfig, signal })); } public put = AxiosResponse, D = unknown>( @@ -175,12 +146,8 @@ export class RxjsAxios { config?: AxiosRequestConfig, ): Observable { const reqConfig = this.validateConfig(config); - const { controller, signal } = this.makeCancellable(); - return observify( - () => this.axios.put(url, data, { ...reqConfig, signal }), - controller, - ); + return observify(signal => this.axios.put(url, data, { ...reqConfig, signal })); } public patch = AxiosResponse, D = unknown>( @@ -189,12 +156,8 @@ export class RxjsAxios { config?: AxiosRequestConfig, ): Observable { const reqConfig = this.validateConfig(config); - const { controller, signal } = this.makeCancellable(); - return observify( - () => this.axios.patch(url, data, { ...reqConfig, signal }), - controller, - ); + return observify(signal => this.axios.patch(url, data, { ...reqConfig, signal })); } public postForm = AxiosResponse, D = unknown>( @@ -203,12 +166,8 @@ export class RxjsAxios { config?: AxiosRequestConfig, ): Observable { const reqConfig = this.validateConfig(config); - const { controller, signal } = this.makeCancellable(); - return observify( - () => this.axios.postForm(url, data, { ...reqConfig, signal }), - controller, - ); + return observify(signal => this.axios.postForm(url, data, { ...reqConfig, signal })); } public putForm = AxiosResponse, D = unknown>( @@ -217,12 +176,8 @@ export class RxjsAxios { config?: AxiosRequestConfig, ): Observable { const reqConfig = this.validateConfig(config); - const { controller, signal } = this.makeCancellable(); - return observify( - () => this.axios.putForm(url, data, { ...reqConfig, signal }), - controller, - ); + return observify(signal => this.axios.putForm(url, data, { ...reqConfig, signal })); } public patchForm = AxiosResponse, D = unknown>( @@ -231,12 +186,8 @@ export class RxjsAxios { config?: AxiosRequestConfig, ): Observable { const reqConfig = this.validateConfig(config); - const { controller, signal } = this.makeCancellable(); - return observify( - () => this.axios.patchForm(url, data, { ...reqConfig, signal }), - controller, - ); + return observify(signal => this.axios.patchForm(url, data, { ...reqConfig, signal })); } private validateConfig(config?: AxiosRequestConfig): AxiosRequestConfig | undefined { @@ -257,14 +208,4 @@ export class RxjsAxios { return config; } - - private makeCancellable(): Abortable { - const controller = new AbortController(); - const signal = controller.signal; - - return { - controller, - signal, - }; - } } diff --git a/src/lib/observify.ts b/src/lib/observify.ts index 99204b0..ffe4ba7 100644 --- a/src/lib/observify.ts +++ b/src/lib/observify.ts @@ -2,16 +2,17 @@ import { Observable } from "rxjs"; import type { AxiosResponse } from "./RxjsAxios"; -export function observify>( - makeRequest: () => Promise, - controller: AbortController, -): Observable { +type RequestFn = (signal: AbortSignal) => Promise; + +export function observify>(makeRequest: RequestFn): Observable { 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(); }); } diff --git a/test/helpers/mocks.ts b/test/helpers/mocks.ts index bac6791..6228828 100644 --- a/test/helpers/mocks.ts +++ b/test/helpers/mocks.ts @@ -1,6 +1,6 @@ import { HttpResponse, PathParams, http } from "msw"; -interface User { +export interface User { id?: number; lastname: string; name: string; diff --git a/test/lib/RxjsAxios.test.ts b/test/lib/RxjsAxios.test.ts index b2593a8..a882246 100644 --- a/test/lib/RxjsAxios.test.ts +++ b/test/lib/RxjsAxios.test.ts @@ -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", () => { @@ -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("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", () => { diff --git a/test/lib/observify.test.ts b/test/lib/observify.test.ts index f8b3e17..32ae5eb 100644 --- a/test/lib/observify.test.ts +++ b/test/lib/observify.test.ts @@ -16,17 +16,12 @@ const RESPONSE: AxiosResponse = { 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>(RESPONSE), - controller, - ); + const observable = observify(() => Promise.resolve>(RESPONSE)); observable.subscribe({ complete: done, @@ -38,11 +33,11 @@ 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, @@ -50,12 +45,11 @@ describe("[Unit] observify.test.ts", () => { 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(); }); }); }); @@ -63,10 +57,7 @@ describe("[Unit] observify.test.ts", () => { 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, @@ -81,11 +72,11 @@ 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, @@ -93,12 +84,11 @@ describe("[Unit] observify.test.ts", () => { 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(); }); }); }); diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..41dbe44 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,4 @@ +import { usePlugin } from "@assertive-ts/core"; +import { SinonPlugin } from "@assertive-ts/sinon"; + +usePlugin(SinonPlugin); diff --git a/yarn.lock b/yarn.lock index 095c134..d155433 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,13 +5,30 @@ __metadata: version: 8 cacheKey: 10 -"@assertive-ts/core@npm:^2.0.0": - version: 2.0.0 - resolution: "@assertive-ts/core@npm:2.0.0" +"@assertive-ts/core@npm:^2.1.0": + version: 2.1.0 + resolution: "@assertive-ts/core@npm:2.1.0" + dependencies: + dedent: "npm:^1.5.1" + fast-deep-equal: "npm:^3.1.3" + checksum: c831a60de5aaebbfb6195cd832e0b89db3547ffea6474a5468c83e6594e1c392ae4e85fdc12d41391e51e7284f8fc0f5e1e86f80f85a4b0b78c9cd19db5be44e + languageName: node + linkType: hard + +"@assertive-ts/sinon@npm:^1.0.0": + version: 1.0.0 + resolution: "@assertive-ts/sinon@npm:1.0.0" dependencies: - "@cometlib/dedent": "npm:^0.8.0-es.10" fast-deep-equal: "npm:^3.1.3" - checksum: 472a86bd9eafb91442ad82fe6be82da877d907b2268a19b2d1abdd508ae3d7114808aac37252d08b35df36bc4209e5bbbad23f9f4e4caa6b6e7aab638c8aa3f1 + peerDependencies: + "@assertive-ts/core": ">=2.0.0" + sinon: ">=15.2.0" + peerDependenciesMeta: + "@assertive-ts/core": + optional: false + sinon: + optional: true + checksum: 89279083bde7fc4aec2708c71bf18d25430c8a39b5eb48dcc8a3ffb358c7145ae5e26b1f08e08ac033350de2c56abaa88a51f3991849dd36016c3c33e36f5c0d languageName: node linkType: hard @@ -43,15 +60,6 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.7.2": - version: 7.23.8 - resolution: "@babel/runtime@npm:7.23.8" - dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: ec8f1967a36164da6cac868533ffdff97badd76d23d7d820cc84f0818864accef972f22f9c6a710185db1e3810e353fc18c3da721e5bb3ee8bc61bdbabce03ff - languageName: node - linkType: hard - "@bundled-es-modules/cookie@npm:^2.0.0": version: 2.0.0 resolution: "@bundled-es-modules/cookie@npm:2.0.0" @@ -86,15 +94,6 @@ __metadata: languageName: node linkType: hard -"@cometlib/dedent@npm:^0.8.0-es.10": - version: 0.8.0-es.10 - resolution: "@cometlib/dedent@npm:0.8.0-es.10" - dependencies: - babel-plugin-macros: "npm:^2.8.0" - checksum: e411800737686031007a4a35a69fc7b0d0a5985136bb781a9d70291e46714595f4ae9fa7a85eac35aa450b9a56a3858534039fe196278ad0ee509fdbe621b326 - languageName: node - linkType: hard - "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -1088,13 +1087,6 @@ __metadata: languageName: node linkType: hard -"@types/parse-json@npm:^4.0.0": - version: 4.0.2 - resolution: "@types/parse-json@npm:4.0.2" - checksum: 5bf62eec37c332ad10059252fc0dab7e7da730764869c980b0714777ad3d065e490627be9f40fc52f238ffa3ac4199b19de4127196910576c2fe34dd47c7a470 - languageName: node - linkType: hard - "@types/semver@npm:^7.3.12": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" @@ -1710,17 +1702,6 @@ __metadata: languageName: node linkType: hard -"babel-plugin-macros@npm:^2.8.0": - version: 2.8.0 - resolution: "babel-plugin-macros@npm:2.8.0" - dependencies: - "@babel/runtime": "npm:^7.7.2" - cosmiconfig: "npm:^6.0.0" - resolve: "npm:^1.12.0" - checksum: ef1e7a8870f38ec255b9e85a21fc2f1adc8a8a494c3b715ce01fd34cb36fb58b75fd4701dc01807bd8f0bd475364565eb9d3247b53921e39fedc8511aa647af0 - languageName: node - linkType: hard - "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -2259,19 +2240,6 @@ __metadata: languageName: node linkType: hard -"cosmiconfig@npm:^6.0.0": - version: 6.0.0 - resolution: "cosmiconfig@npm:6.0.0" - dependencies: - "@types/parse-json": "npm:^4.0.0" - import-fresh: "npm:^3.1.0" - parse-json: "npm:^5.0.0" - path-type: "npm:^4.0.0" - yaml: "npm:^1.7.2" - checksum: b184d2bfbced9ba6840fd097dbf3455c68b7258249bb9b1277913823d516d8dfdade8c5ccbf79db0ca8ebd4cc9b9be521ccc06a18396bd242d50023c208f1594 - languageName: node - linkType: hard - "cosmiconfig@npm:^8.1.0": version: 8.3.6 resolution: "cosmiconfig@npm:8.3.6" @@ -2415,6 +2383,18 @@ __metadata: languageName: node linkType: hard +"dedent@npm:^1.5.1": + version: 1.5.3 + resolution: "dedent@npm:1.5.3" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: e5277f6268f288649503125b781a7b7a2c9b22d011139688c0b3619fe40121e600eb1f077c891938d4b2428bdb6326cc3c77a763e4b1cc681bd9666ab1bad2a1 + languageName: node + linkType: hard + "deep-extend@npm:^0.6.0": version: 0.6.0 resolution: "deep-extend@npm:0.6.0" @@ -3824,7 +3804,7 @@ __metadata: languageName: node linkType: hard -"import-fresh@npm:^3.1.0, import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": +"import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" dependencies: @@ -5710,7 +5690,7 @@ __metadata: languageName: node linkType: hard -"parse-json@npm:^5.0.0, parse-json@npm:^5.2.0": +"parse-json@npm:^5.2.0": version: 5.2.0 resolution: "parse-json@npm:5.2.0" dependencies: @@ -6192,13 +6172,6 @@ __metadata: languageName: node linkType: hard -"regenerator-runtime@npm:^0.14.0": - version: 0.14.1 - resolution: "regenerator-runtime@npm:0.14.1" - checksum: 5db3161abb311eef8c45bcf6565f4f378f785900ed3945acf740a9888c792f75b98ecb77f0775f3bf95502ff423529d23e94f41d80c8256e8fa05ed4b07cf471 - languageName: node - linkType: hard - "regexp.prototype.flags@npm:^1.5.2": version: 1.5.2 resolution: "regexp.prototype.flags@npm:1.5.2" @@ -6255,7 +6228,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.12.0, resolve@npm:^1.22.4": +"resolve@npm:^1.22.4": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -6268,7 +6241,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": +"resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -6325,7 +6298,8 @@ __metadata: version: 0.0.0-use.local resolution: "rxjs-axios@workspace:." dependencies: - "@assertive-ts/core": "npm:^2.0.0" + "@assertive-ts/core": "npm:^2.1.0" + "@assertive-ts/sinon": "npm:^1.0.0" "@eslint/compat": "npm:^1.1.0" "@eslint/eslintrc": "npm:^3.1.0" "@eslint/js": "npm:^9.5.0" @@ -7665,13 +7639,6 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^1.7.2": - version: 1.10.2 - resolution: "yaml@npm:1.10.2" - checksum: e088b37b4d4885b70b50c9fa1b7e54bd2e27f5c87205f9deaffd1fb293ab263d9c964feadb9817a7b129a5bf30a06582cb08750f810568ecc14f3cdbabb79cb3 - languageName: node - linkType: hard - "yargs-parser@npm:20.2.4": version: 20.2.4 resolution: "yargs-parser@npm:20.2.4"