├── src ├── interfaces │ ├── inner-instance.interface.ts │ ├── rx-entity.ts │ ├── index.ts │ ├── target-class.interface.ts │ └── descriptor.interface.ts ├── classes │ ├── rx-members │ │ ├── index.ts │ │ ├── rx-class-member-base.ts │ │ ├── rx-member.factory.ts │ │ ├── rx-member-method.ts │ │ └── rx-member-property.ts │ └── rx-entities │ │ ├── index.ts │ │ ├── rx-entity.factory.ts │ │ ├── rx-entity-subscription.ts │ │ ├── rx-entity-base.ts │ │ └── rx-entity-observable.ts ├── auto-unsubscribe.decorator.ts └── auto-unsubscribe.decorator.spec.ts ├── .vscode └── settings.json ├── .travis.yml ├── jasmine.json ├── .prettierrc.json ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /src/interfaces/inner-instance.interface.ts: -------------------------------------------------------------------------------- 1 | export type InnerInstance = Function; 2 | -------------------------------------------------------------------------------- /src/interfaces/rx-entity.ts: -------------------------------------------------------------------------------- 1 | import type { Observable, Subscription } from "rxjs"; 2 | 3 | export type RxEntity = Subscription | Observable; 4 | -------------------------------------------------------------------------------- /src/classes/rx-members/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rx-member.factory'; 2 | export * from './rx-class-member-base'; 3 | export * from './rx-member-method'; 4 | export * from './rx-member-property'; 5 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rx-entity'; 2 | export * from './descriptor.interface'; 3 | export * from './target-class.interface'; 4 | export * from './inner-instance.interface'; 5 | -------------------------------------------------------------------------------- /src/classes/rx-entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rx-entity.factory'; 2 | export * from './rx-entity-base'; 3 | export * from './rx-entity-observable'; 4 | export * from './rx-entity-subscription'; 5 | -------------------------------------------------------------------------------- /src/interfaces/target-class.interface.ts: -------------------------------------------------------------------------------- 1 | import type { Subscription } from "rxjs"; 2 | 3 | export interface TargetClass { 4 | ɵUnsubscriptionHasInitialized: boolean; 5 | ɵSubscriptions: WeakMap; 6 | ngOnDestroy?: () => unknown; 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["Unsubscriber", "Unsubscription"], 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true, 5 | "editor.codeActionsOnSave": { 6 | "source.organizeImports": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: true 7 | node_js: 8 | - "7" 9 | before_script: 10 | - npm prune 11 | script: 12 | - npm run build 13 | branches: 14 | except: 15 | - /^v\d+\.\d+\.\d+$/ 16 | -------------------------------------------------------------------------------- /jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporters": [ 3 | { 4 | "name": "jasmine-spec-reporter#SpecReporter", 5 | "options": { 6 | "displayStacktrace": "all" 7 | } 8 | } 9 | ], 10 | "spec_dir": "src", 11 | "spec_files": ["**/*[sS]pec.ts"] 12 | } -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": false, 4 | "tabWidth": 4, 5 | "arrowParens": "always", 6 | "trailingComma": "all", 7 | "singleQuote": true, 8 | "semi": true, 9 | "htmlWhitespaceSensitivity": "ignore", 10 | "bracketSpacing": true 11 | } 12 | -------------------------------------------------------------------------------- /src/interfaces/descriptor.interface.ts: -------------------------------------------------------------------------------- 1 | import { RxEntity } from './rx-entity'; 2 | 3 | export interface Descriptor { 4 | get: () => unknown; 5 | set: (value: RxEntity) => void; 6 | value?: (...args: unknown[]) => RxEntity; 7 | enumerable: boolean; 8 | configurable: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/classes/rx-members/rx-class-member-base.ts: -------------------------------------------------------------------------------- 1 | import { Descriptor, InnerInstance, TargetClass } from '@src/interfaces'; 2 | 3 | export abstract class RxClassMemberBase { 4 | public abstract check(descriptor: Descriptor): boolean; 5 | 6 | public abstract define(targetClass: TargetClass, key: string, descriptor: Descriptor): void; 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/classes/rx-entities/rx-entity.factory.ts: -------------------------------------------------------------------------------- 1 | import { RxEntity } from '@src/interfaces'; 2 | import { RxEntityBase } from './rx-entity-base'; 3 | import { RxEntityObservable } from './rx-entity-observable'; 4 | import { RxEntitySubscription } from './rx-entity-subscription'; 5 | 6 | const RxClassMemberArray: RxEntityBase[] = [new RxEntityObservable(), new RxEntitySubscription()]; 7 | 8 | export class RxEntityFactory { 9 | public static getInstance(variable: RxEntity): RxEntityBase { 10 | const entity = RxClassMemberArray.find((entity) => entity.check(variable)); 11 | 12 | return entity; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/classes/rx-members/rx-member.factory.ts: -------------------------------------------------------------------------------- 1 | import { Descriptor } from '@src/interfaces'; 2 | import { RxClassMemberBase } from './rx-class-member-base'; 3 | import { RxClassMethod } from './rx-member-method'; 4 | import { RxClassProperty } from './rx-member-property'; 5 | 6 | const RxClassMemberArray: RxClassMemberBase[] = [new RxClassProperty(), new RxClassMethod()]; 7 | 8 | export class RxClassMemberFactory { 9 | public static getInstance(descriptor: Descriptor): RxClassMemberBase { 10 | const member = RxClassMemberArray.find((member) => member.check(descriptor)); 11 | 12 | return member; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/classes/rx-entities/rx-entity-subscription.ts: -------------------------------------------------------------------------------- 1 | import { InnerInstance, RxEntity, TargetClass } from '@src/interfaces'; 2 | import { Subscription } from 'rxjs'; 3 | import { RxEntityBase } from './rx-entity-base'; 4 | 5 | export class RxEntitySubscription extends RxEntityBase { 6 | public check(variable: RxEntity): boolean { 7 | return variable instanceof Subscription; 8 | } 9 | 10 | public process( 11 | instance: InnerInstance, 12 | targetClass: TargetClass, 13 | variable: Subscription, 14 | ): void { 15 | this.defineSubscription(instance, targetClass, variable); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/classes/rx-entities/rx-entity-base.ts: -------------------------------------------------------------------------------- 1 | import type { Subscription } from 'rxjs'; 2 | import { InnerInstance, RxEntity, TargetClass } from '@src/interfaces'; 3 | 4 | export abstract class RxEntityBase { 5 | public abstract check(variable: RxEntity): boolean; 6 | 7 | public abstract process( 8 | instance: InnerInstance, 9 | targetClass: TargetClass, 10 | variable: RxEntity, 11 | ): void; 12 | 13 | protected defineSubscription( 14 | instance: InnerInstance, 15 | targetClass: TargetClass, 16 | subscription: Subscription, 17 | ): void { 18 | const targetSubscriptions = targetClass.ɵSubscriptions.get(instance) || []; 19 | 20 | targetSubscriptions.push(subscription); 21 | targetClass.ɵSubscriptions.set(instance, targetSubscriptions); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /src/classes/rx-members/rx-member-method.ts: -------------------------------------------------------------------------------- 1 | import { Descriptor, RxEntity, TargetClass } from '@src/interfaces'; 2 | import { RxEntityFactory } from '../rx-entities'; 3 | import { RxClassMemberBase } from './rx-class-member-base'; 4 | 5 | export class RxClassMethod extends RxClassMemberBase { 6 | public check(descriptor: Descriptor): boolean { 7 | return !!descriptor?.value; 8 | } 9 | 10 | public define(targetClass: TargetClass, _key: string, descriptor: Descriptor): void { 11 | const originalMethod = descriptor?.value; 12 | 13 | descriptor.value = function (...args: unknown[]): RxEntity { 14 | const result = originalMethod.apply(this, args); 15 | 16 | const entity = RxEntityFactory.getInstance(result); 17 | 18 | entity.process(this, targetClass, result); 19 | 20 | return result; 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "dist/", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": false, 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "sourceMap": true, 11 | "declaration": true, 12 | "downlevelIteration": true, 13 | "experimentalDecorators": true, 14 | "moduleResolution": "node", 15 | "emitDecoratorMetadata": true, 16 | "importHelpers": true, 17 | "target": "es5", 18 | "module": "commonjs", 19 | "lib": ["dom", "es2015", "es2017"], 20 | "paths": { 21 | "@src/*": ["src/*"] 22 | } 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | }, 30 | "exclude": ["**/*.spec.ts"] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nillcon248 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/classes/rx-members/rx-member-property.ts: -------------------------------------------------------------------------------- 1 | import { Descriptor, RxEntity, TargetClass } from '@src/interfaces'; 2 | import { Subscription } from 'rxjs'; 3 | import { RxEntityFactory } from '../rx-entities'; 4 | import { RxClassMemberBase } from './rx-class-member-base'; 5 | 6 | export class RxClassProperty extends RxClassMemberBase { 7 | public check(descriptor: Descriptor): boolean { 8 | return !descriptor; 9 | } 10 | 11 | public define(targetClass: TargetClass, key: string, _descriptor: Descriptor): void { 12 | const newDescriptor = this.getDescriptor(targetClass); 13 | 14 | Object.defineProperty(targetClass, key, newDescriptor); 15 | } 16 | 17 | private getDescriptor(targetClass: TargetClass): Descriptor { 18 | const self = this; 19 | let value: RxEntity; 20 | 21 | const descriptor: Descriptor = { 22 | get: function (): RxEntity { 23 | return value; 24 | }, 25 | set: function (newValue: RxEntity): void { 26 | self.unsubscribeIfPossible(value); 27 | 28 | const entity = RxEntityFactory.getInstance(newValue); 29 | 30 | entity.process(this, targetClass, newValue); 31 | 32 | value = newValue; 33 | }, 34 | enumerable: true, 35 | configurable: true, 36 | }; 37 | 38 | return descriptor; 39 | } 40 | 41 | private unsubscribeIfPossible(value: RxEntity): void { 42 | (value as Subscription)?.unsubscribe(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-auto-unsubscribe-decorator", 3 | "description": "Decorator for auto unsubscribtion from observable", 4 | "version": "1.1.0", 5 | "main": "dist/auto-unsubscribe.decorator.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "jasmine-ts --config=jasmine.json", 10 | "publish:patch": "npm test && npm run build && npm version patch && npm publish", 11 | "publish:minor": "npm test && npm run build && npm version minor && npm publish", 12 | "publish:major": "npm test && npm run build && npm version major && npm publish" 13 | }, 14 | "dependencies": { 15 | "tslib": "^2.1.0" 16 | }, 17 | "devDependencies": { 18 | "@types/jasmine": "~3.6.0", 19 | "@types/node": "^12.11.1", 20 | "rxjs": "^7.5.5", 21 | "jasmine": "^3.7.0", 22 | "jasmine-core": "~3.6.0", 23 | "jasmine-spec-reporter": "~5.0.0", 24 | "jasmine-ts": "^0.3.0", 25 | "protractor": "~7.0.0", 26 | "ts-node": "~8.3.0", 27 | "typescript": "~4.1.2" 28 | }, 29 | "typings": "./dist/auto-unsubscribe.decorator.d.ts", 30 | "author": { 31 | "name": "Nillcon", 32 | "email": "nillcon248@gmail.com", 33 | "url": "https://www.linkedin.com/in/nillcon" 34 | }, 35 | "keywords": [ 36 | "Angular", 37 | "RxJs", 38 | "Auto unsubscribe", 39 | "Auto-unsubscribe", 40 | "Autounsubscribe", 41 | "Unsubscribe", 42 | "Subscription", 43 | "Observable unsubscribe" 44 | ], 45 | "repository": { 46 | "url": "https://github.com/Nillcon248/auto-unsubscribe" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/auto-unsubscribe.decorator.ts: -------------------------------------------------------------------------------- 1 | import type { Subscription } from 'rxjs'; 2 | import { RxClassMemberFactory } from './classes/rx-members'; 3 | import { Descriptor, TargetClass } from './interfaces'; 4 | 5 | export function AutoUnsubscribe(): any { 6 | return function InnerFunction( 7 | targetClass: TargetClass, 8 | key: string, 9 | descriptor: Descriptor, 10 | ): any { 11 | const classMember = RxClassMemberFactory.getInstance(descriptor); 12 | 13 | initializeAutoUnsubscription(targetClass); 14 | 15 | classMember.define(targetClass, key, descriptor); 16 | }; 17 | } 18 | 19 | function initializeAutoUnsubscription(targetClass: TargetClass): void { 20 | if (targetClass?.ɵUnsubscriptionHasInitialized) return; 21 | 22 | targetClass.ɵUnsubscriptionHasInitialized = true; 23 | targetClass.ɵSubscriptions = new WeakMap(); 24 | 25 | overrideNgOnDestroy(targetClass); 26 | } 27 | 28 | function overrideNgOnDestroy(targetClass: TargetClass): void { 29 | const defaultNgOnDestroy = targetClass.ngOnDestroy; 30 | 31 | targetClass.ngOnDestroy = function (): any { 32 | const targetSubscriptions = targetClass.ɵSubscriptions.get(this); 33 | 34 | if (targetSubscriptions?.length) { 35 | targetSubscriptions.forEach((subscription: Subscription, index: number) => { 36 | subscription.unsubscribe(); 37 | 38 | targetSubscriptions[index] = null; 39 | }); 40 | 41 | targetSubscriptions.length = 0; 42 | 43 | targetClass.ɵSubscriptions.delete(this); 44 | } 45 | 46 | if (defaultNgOnDestroy && typeof defaultNgOnDestroy === 'function') { 47 | return defaultNgOnDestroy.apply(this); 48 | } 49 | }; 50 | } 51 | 52 | // Split this method !! 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Angular - Auto unsubscribe decorator 🦄 2 | 3 | [![npm](https://img.shields.io/npm/dt/ngx-auto-unsubscribe-decorator.svg)]() 4 | [![npm](https://img.shields.io/npm/l/ngx-auto-unsubscribe-decorator.svg)]() 5 | [![Build status](https://travis-ci.org/Nillcon248/ngx-base-state.svg?branch=master)](https://travis-ci.org/Nillcon248/ngx-auto-unsubscribe.svg?branch=master) 6 | 7 | # Installation 8 | 9 | `npm i ngx-auto-unsubscribe-decorator` 10 | 11 | ## Idea 💡 12 | 13 | This library has been created for removing 14 | useless code with manually unsubscribes, 15 | for this one was created decorator that works 16 | with all types of subscriptions, you can wrap 17 | observable parameter, a method that returns observable or 18 | a method that returns subscription and doesn't think 19 | about memory leak. 20 | 21 | ## Examples 🧪 22 | 23 | ### Work with parameters 24 | 25 | ```js 26 | export class UserComponent implements OnInit { 27 | @AutoUnsubscribe() // <-- Should be on the target parameter 28 | private userData$ = new BehaviorSubject(/* Some data */); 29 | 30 | public ngOnInit(): void { 31 | // After ngOnDestroy this subscription will unsubscribe 32 | this.userData$.subscribe(); 33 | 34 | // You can override parameter, it will unsubscribe too 35 | this.userData$ = new Subject(); 36 | this.userData$.subscribe(); 37 | } 38 | } 39 | ``` 40 | 41 | ### Work with methods 42 | 43 | ```js 44 | export class UserComponent implements OnInit { 45 | 46 | public ngOnInit(): void { 47 | this.getUserData$().subscribe(); 48 | 49 | this.initUserDataSubscription(); 50 | } 51 | 52 | @AutoUnsubscribe() // <-- Should be on the target method 53 | public getUserData$(): BehaviorSubject { 54 | return new BehaviorSubject(/* Some data */); 55 | } 56 | 57 | @AutoUnsubscribe() // <-- Should be on the target method 58 | public initUserDataSubscription(): BehaviorSubject { 59 | return new BehaviorSubject(/* Some data */).subscribe(); 60 | } 61 | } 62 | 63 | ``` 64 | 65 | ## Demo 66 | 67 | See the [Example 1](https://stackblitz.com/edit/angular-ivy-3xkfrv?file=src/app/unsubscribe-decorator/unsubscribe-decorator.component.ts) [Example 2](https://stackblitz.com/edit/angular-ivy-ujz6vk?devtoolsheight=33&file=src/app/autounsubscribe/autounsubscribe.component.ts) on Stackblitz 68 | -------------------------------------------------------------------------------- /src/classes/rx-entities/rx-entity-observable.ts: -------------------------------------------------------------------------------- 1 | import { InnerInstance, RxEntity, TargetClass } from '@src/interfaces'; 2 | import { Observable, Subject, Subscription } from 'rxjs'; 3 | import { RxEntityBase } from './rx-entity-base'; 4 | 5 | type SubjectKey = keyof Subject; 6 | type ObservableMethod = (...args: any[]) => Observable; 7 | type SubscriptionMethod = (...args: any[]) => Subscription; 8 | 9 | const METHOD_KEYS_TO_OVERRIDE: SubjectKey[] = ['pipe', 'lift', 'asObservable']; 10 | 11 | export class RxEntityObservable extends RxEntityBase { 12 | public check(variable: RxEntity): boolean { 13 | return variable instanceof Observable; 14 | } 15 | 16 | public process( 17 | instance: InnerInstance, 18 | targetClass: TargetClass, 19 | observable: Observable, 20 | ): void { 21 | const subscriptionMethod = this.getOverrideSubscriptionMethod( 22 | instance, 23 | targetClass, 24 | observable, 25 | ); 26 | 27 | observable.subscribe = subscriptionMethod; 28 | 29 | this.overrideMethods(observable, subscriptionMethod); 30 | } 31 | 32 | private getOverrideSubscriptionMethod( 33 | instance: InnerInstance, 34 | targetClass: TargetClass, 35 | observable: Observable, 36 | ): SubscriptionMethod { 37 | const self = this; 38 | const originSubscribeMethod = observable.subscribe; 39 | 40 | return function (...args: unknown[]): Subscription { 41 | const subscription: Subscription = originSubscribeMethod.apply(this, args); 42 | 43 | if (!subscription?.closed) { 44 | self.defineSubscription(instance, targetClass, subscription); 45 | } 46 | 47 | return subscription; 48 | }; 49 | } 50 | 51 | private overrideMethods( 52 | observable: Observable, 53 | subscribeMethod: SubscriptionMethod, 54 | ): void { 55 | METHOD_KEYS_TO_OVERRIDE.forEach((methodName) => 56 | this.processObservableMethod(observable, methodName, subscribeMethod), 57 | ); 58 | } 59 | 60 | private processObservableMethod( 61 | observable: Observable, 62 | methodName: SubjectKey, 63 | subscribeMethod: SubscriptionMethod, 64 | ): void { 65 | const originMethod = observable[methodName] as ObservableMethod; 66 | 67 | observable[methodName] = function (this: unknown, ...args: any[]): unknown { 68 | const result: Observable = originMethod.apply(this, args); 69 | 70 | result.subscribe = subscribeMethod; 71 | 72 | return result; 73 | }; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/auto-unsubscribe.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject, interval, of, Subscription } from 'rxjs'; 2 | import { switchMap } from 'rxjs/operators'; 3 | import { AutoUnsubscribe as AutoUnsubscribeDecorator } from './auto-unsubscribe.decorator'; 4 | 5 | const AutoUnsubscribe = AutoUnsubscribeDecorator; 6 | 7 | class TestComponent { 8 | @AutoUnsubscribe() 9 | public parameter$ = new BehaviorSubject(0); 10 | 11 | @AutoUnsubscribe() 12 | public subscription = interval(1000).subscribe(); 13 | 14 | public ngOnDestroy(): void {} 15 | 16 | @AutoUnsubscribe() 17 | public methodObservable(): BehaviorSubject { 18 | return new BehaviorSubject(0); 19 | } 20 | 21 | @AutoUnsubscribe() 22 | public methodSubscription(): Subscription { 23 | return new BehaviorSubject(0).subscribe(); 24 | } 25 | 26 | @AutoUnsubscribe() 27 | public methodObservableWithPipeSubscription(): Subscription { 28 | return new BehaviorSubject(0) 29 | .asObservable() 30 | .pipe(switchMap(() => new BehaviorSubject(123).pipe())) 31 | .subscribe(); 32 | } 33 | } 34 | 35 | describe('AutoUnsubscribe', () => { 36 | let component: TestComponent; 37 | 38 | beforeEach(() => { 39 | component = new TestComponent(); 40 | }); 41 | 42 | it('should unsubscribe from parameter after ngOnDestroy called', () => { 43 | const subscription = component.parameter$.subscribe(); 44 | 45 | expect(subscription.closed).toBeFalse(); 46 | 47 | component.ngOnDestroy(); 48 | 49 | expect(subscription.closed).toBeTrue(); 50 | }); 51 | 52 | it('should unsubscribe from method result after ngOnDestroy called', () => { 53 | const subscription = component.methodObservable().subscribe(); 54 | 55 | expect(subscription.closed).toBeFalse(); 56 | 57 | component.ngOnDestroy(); 58 | 59 | expect(subscription.closed).toBeTrue(); 60 | }); 61 | 62 | it('should unsubscribe from method thar return subscription after ngOnDestroy called', () => { 63 | const subscription = component.methodSubscription(); 64 | 65 | expect(subscription.closed).toBeFalse(); 66 | 67 | component.ngOnDestroy(); 68 | 69 | expect(subscription.closed).toBeTrue(); 70 | }); 71 | 72 | it('should unsubscribe from overided parameter after ngOnDestroy called', () => { 73 | const subscription1 = component.parameter$.subscribe(); 74 | 75 | component.parameter$ = new BehaviorSubject(1); 76 | const subscription2 = component.parameter$.subscribe(); 77 | 78 | expect(subscription1.closed).toBeFalse(); 79 | expect(subscription2.closed).toBeFalse(); 80 | 81 | component.ngOnDestroy(); 82 | 83 | expect(subscription1.closed).toBeTrue(); 84 | expect(subscription2.closed).toBeTrue(); 85 | }); 86 | 87 | it('should unsubscribe from observables with pipe after ngOnDestroy called', () => { 88 | const subscription = component.parameter$.pipe(switchMap(() => of(1))).subscribe(); 89 | 90 | expect(subscription.closed).toBeFalse(); 91 | 92 | component.ngOnDestroy(); 93 | 94 | expect(subscription.closed).toBeTrue(); 95 | }); 96 | 97 | it('should unsubscribe from observables with lift after ngOnDestroy called', () => { 98 | const subscription = component.parameter$.lift(switchMap(() => of(1))).subscribe(); 99 | 100 | expect(subscription.closed).toBeFalse(); 101 | 102 | component.ngOnDestroy(); 103 | 104 | expect(subscription.closed).toBeTrue(); 105 | }); 106 | 107 | it('should unsubscribe from asObservable() after ngOnDestroy called', () => { 108 | const subscription = component.parameter$.asObservable().subscribe(); 109 | 110 | expect(subscription.closed).toBeFalse(); 111 | 112 | component.ngOnDestroy(); 113 | 114 | expect(subscription.closed).toBeTrue(); 115 | }); 116 | 117 | it('should unsubscribe from subscription of parameter only in destroyed instance', () => { 118 | const subscriptionFirst = component.parameter$.subscribe(); 119 | 120 | const newInstance = new TestComponent(); 121 | const subscriptionSecond = newInstance.parameter$.subscribe(); 122 | 123 | newInstance.ngOnDestroy(); 124 | 125 | expect(subscriptionSecond.closed).toBeTrue(); 126 | expect(subscriptionFirst.closed).toBeFalse(); 127 | }); 128 | 129 | it('should unsubscribe from subscription of method only in destroyed instance', () => { 130 | const subscriptionFirst = component.methodObservable().subscribe(); 131 | 132 | const newInstance = new TestComponent(); 133 | const subscriptionSecond = newInstance.methodObservable().subscribe(); 134 | 135 | newInstance.ngOnDestroy(); 136 | 137 | expect(subscriptionSecond.closed).toBeTrue(); 138 | expect(subscriptionFirst.closed).toBeFalse(); 139 | }); 140 | 141 | it('should unsubscribe from subscription of method with subscription only in destroyed instance', () => { 142 | const subscriptionFirst = component.methodSubscription(); 143 | 144 | const newInstance = new TestComponent(); 145 | const subscriptionSecond = newInstance.methodSubscription(); 146 | 147 | newInstance.ngOnDestroy(); 148 | 149 | expect(subscriptionSecond.closed).toBeTrue(); 150 | expect(subscriptionFirst.closed).toBeFalse(); 151 | }); 152 | 153 | it('should unsubscribe from parameter with subscription', () => { 154 | expect(component.subscription.closed).toBeFalse(); 155 | 156 | component.ngOnDestroy(); 157 | 158 | expect(component.subscription.closed).toBeTrue(); 159 | }); 160 | 161 | it('should unsubscribe from method with pipe with subscription', () => { 162 | const subscription = component.methodObservableWithPipeSubscription(); 163 | 164 | expect(subscription.closed).toBeFalse(); 165 | 166 | component.ngOnDestroy(); 167 | 168 | expect(subscription.closed).toBeTrue(); 169 | }); 170 | 171 | it('should unsubscribe when property with subscription changed to new subscription', () => { 172 | const newSubscription = interval(10000).subscribe(); 173 | 174 | expect(newSubscription.closed).toBeFalse(); 175 | 176 | component.subscription = newSubscription; 177 | 178 | // Rewrite subscription to the new one 179 | component.subscription = interval(20000).subscribe(); 180 | 181 | expect(newSubscription.closed).toBeTrue(); 182 | expect(component.subscription.closed).toBeFalse(); 183 | }); 184 | }); 185 | --------------------------------------------------------------------------------