diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index cf6c636c56fc1892768e539a9d476587f626a793..0000000000000000000000000000000000000000 --- a/src/lib/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -export * from './api/api-debug.component'; -export * from './api/api-debug.interceptor'; -export * from './debug.module'; -export * from './debug-state.service'; -export * from './debug-toggle.component'; -export * from './dump-panel.component'; -export * from './dump-value.component'; -export * from './rxjs/index'; -export * from './spy/spy-display.component'; -export * from './watch/watch.component'; -export * from './watch/watch-display.component'; -export * from './watch/watch.service'; -export * from './configuration'; -export * from './debug.module'; -export * from './debug-state.service'; -export * from './debug-toggle.component'; -export * from './dump-panel.component'; -export * from './dump-value.component'; diff --git a/src/lib/rxjs/hooks.spec.ts b/src/lib/rxjs/hooks.spec.ts deleted file mode 100644 index 2c76ce82d9700de2dbe939bc3e06786440a3b919..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/hooks.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { hookAfter, hookBefore, hookFinally } from './hooks'; - -type Callback = (x: string) => void; - -describe('hooksBefore', () => { - it('should execute the hook before the original method', () => { - class TestClass { - public foo(f: Callback): void { - f('bar'); - } - } - - hookBefore(TestClass.prototype, 'foo', (f: Callback) => f('before')); - - const instance = new TestClass(); - const callback = jasmine.createSpy('callback'); - - instance.foo(callback); - - expect(callback).toHaveBeenCalledTimes(2); - expect(callback.calls.first().args).toEqual(['before']); - expect(callback.calls.mostRecent().args).toEqual(['bar']); - }); -}); - -describe('hookAfter', () => { - it('should execute the hook after the original method', () => { - class TestClass { - public foo(f: Callback): void { - f('bar'); - } - } - - hookAfter(TestClass.prototype, 'foo', (f: Callback) => f('after')); - - const instance = new TestClass(); - const callback = jasmine.createSpy('callback'); - - instance.foo(callback); - - expect(callback).toHaveBeenCalledTimes(2); - expect(callback.calls.first().args).toEqual(['bar']); - expect(callback.calls.mostRecent().args).toEqual(['after']); - }); - - it('should not execute the hook when the original method throws', () => { - const err = new Error('foo'); - - class TestClass { - public foo(f: Callback): void { - throw err; - } - } - - hookAfter(TestClass.prototype, 'foo', (f: Callback) => f('after')); - - const instance = new TestClass(); - const callback = jasmine.createSpy('callback'); - - expect(() => instance.foo(callback)).toThrow(err); - - expect(callback).not.toHaveBeenCalled(); - }); -}); - -describe('hookFinally', () => { - it('should execute the hook after the original method', () => { - class TestClass { - public foo(f: Callback): void { - f('bar'); - } - } - - hookFinally(TestClass.prototype, 'foo', (f: Callback) => f('finally')); - - const instance = new TestClass(); - const callback = jasmine.createSpy('callback'); - - instance.foo(callback); - - expect(callback).toHaveBeenCalledTimes(2); - expect(callback.calls.first().args).toEqual(['bar']); - expect(callback.calls.mostRecent().args).toEqual(['finally']); - }); - - it('should execute the hook even if the original method throws', () => { - const err = new Error('foo'); - - class TestClass { - public foo(f: Callback): void { - throw err; - } - } - - hookFinally(TestClass.prototype, 'foo', (f: Callback) => f('finally')); - - const instance = new TestClass(); - const callback = jasmine.createSpy('callback'); - - expect(() => instance.foo(callback)).toThrow(err); - - expect(callback).toHaveBeenCalledWith('finally'); - }); -}); diff --git a/src/lib/rxjs/hooks.ts b/src/lib/rxjs/hooks.ts deleted file mode 100644 index 27c6fd8e4ae95990aee9ec1604cb564f77ebfbe4..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/hooks.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Fonctions pour "patcher" les méthodes d'une classe. - */ - -type Hookable<K extends string | symbol, M extends (...args: any[]) => any> = { - [X in K]: M -}; - -function isHookable< - K extends string | symbol, - M extends (...args: any[]) => any ->(what: unknown, key: K): what is Hookable<K, M> { - return ( - typeof what === 'object' && - what !== null && - key in what && - typeof (what as any)[key] === 'function' - ); -} - -/** - * Installe un hook. - */ -function installHook< - T extends Hookable<K, M>, - M extends (this: T, ...args: any[]) => void, - K extends string | symbol ->(target: T, name: K, makeHook: (old: M) => M): void { - if (!isHookable<K, M>(target, name)) { - throw new Error(`cannot hook ${name} on ${target}`); - } - if (name === 'constructor') { - throw new Error('cannot hook constructors'); - } - target[name] = <T[K]>makeHook(target[name]); -} - -/** - * Modifie une méthode d'un objet pour éxecuter une fonction avant son éxecution normale. - * - * @param {object} target Le prototype à modifier. - * @param {string|symbol} name Le nom de la méthode à surcharger. - * @param {function} hook La fonction à ajouter. - */ -export function hookBefore< - T extends Hookable<K, M>, - M extends (this: T, ...args: any[]) => void, - K extends string | symbol ->(target: Hookable<K, M>, name: K, hook: M): void { - installHook( - target, - name, - oldMethod => - function(...args: any[]): void { - hook.apply(this, args); - oldMethod.apply(this, args); - } - ); -} - -/** - * Modifie une méthode d'un objet pour éxecuter une fonction après son éxecution normale. - * - * @param {object} target Le prototype à modifier. - * @param {string|symbol} name Le nom de la méthode à surcharger. - * @param {function} hook La fonction à ajouter. - */ -export function hookAfter< - T extends Hookable<K, M>, - M extends (this: T, ...args: any[]) => void, - K extends string | symbol ->(target: T, name: K, hook: M): void { - installHook( - target, - name, - oldMethod => - function(...args: any[]): void { - oldMethod.apply(this, args); - hook.apply(this, args); - } - ); -} - -/** - * Modifie une méthode d'un objet pour éxecuter inconditionnellement une fonction après son éxecution normale. - * - * @param {object} target Le prototype à modifier. - * @param {string|symbol} name Le nom de la méthode à surcharger. - * @param {function} hook La fonction à ajouter. - */ -export function hookFinally< - T extends Hookable<K, M>, - M extends (this: T, ...args: any[]) => void, - K extends string | symbol ->(target: T, name: K, hook: M): void { - installHook( - target, - name, - oldMethod => - function(...args: any[]): void { - try { - oldMethod.apply(this, args); - } finally { - hook.apply(this, args); - } - } - ); -} diff --git a/src/lib/rxjs/index.ts b/src/lib/rxjs/index.ts deleted file mode 100644 index f9565cbb2b50fa2237cbf9730f9b96e57f6c2d7a..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './hooks'; -export * from './lazy.observable'; -export * from './route-param.decorator'; -export * from './safe-combine-latest.observable'; -export * from './safe-fork-join.observable'; -export * from './select.operator'; -export * from './spy.observer'; -export * from './spy.operator'; -export * from './subject-accessors.decorator'; -export * from './subscribe-on-init.decorator'; -export * from './until-destroyed.operator'; diff --git a/src/lib/rxjs/lazy.observable.ts b/src/lib/rxjs/lazy.observable.ts deleted file mode 100644 index 39e85cf78248bf66d3387f629fc0c7fd9efffd06..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/lazy.observable.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defer, Observable, ObservedValueOf } from 'rxjs'; - -/** - * Un observable qui attend le premier abonnement pour fabriquer l'observable qui va vraiment être - * utilisé. - */ -export function lazy<O extends Observable<any>>( - create: () => O -): Observable<ObservedValueOf<O>> { - let obs: O | null = null; - return defer(() => (obs !== null ? obs : (obs = create()))); -} diff --git a/src/lib/rxjs/route-param.decorator.spec.ts b/src/lib/rxjs/route-param.decorator.spec.ts deleted file mode 100644 index 9b3f0e3b630459571b90557309e50c9023eb119c..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/route-param.decorator.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute, convertToParamMap, ParamMap } from '@angular/router'; -import { Observable, ReplaySubject } from 'rxjs'; -import { marbles } from 'rxjs-marbles'; - -import { QueryParam, RouteParam } from './route-param.decorator'; - -function testOne(decorator: any, mapProperty: string) { - function mockActivatedRoute( - map$: Observable<ParamMap>, - snapshot: any - ): ActivatedRoute { - // tslint:disable-next-line:rxjs-finnish - return { - [mapProperty]: map$, - snapshot: { [mapProperty]: snapshot }, - } as any; - } - - class FakeComponent implements OnInit, OnDestroy { - @decorator() - private readonly param$ = new ReplaySubject<string>(1); - - // tslint:disable-next-line:no-unused-variable - public constructor(private readonly route: ActivatedRoute) {} - - public getParam$(): Observable<string> { - return this.param$; - } - - public ngOnInit(): void {} - - public ngOnDestroy(): void {} - } - - it( - 'should subscribe to paramMap on init', - marbles(m => { - const map$ = m.cold(''); - const comp = new FakeComponent( - mockActivatedRoute(map$, convertToParamMap({})) - ); - - comp.ngOnInit(); - - m.expect(map$).toHaveSubscriptions('^'); - - m.flush(); - expect(comp).toBeTruthy(); - }) - ); - - it( - 'should unsubscribe from paramMap on destroy', - marbles(m => { - const map$ = m.cold(''); - const comp = new FakeComponent( - mockActivatedRoute(map$, convertToParamMap({})) - ); - - comp.ngOnInit(); - comp.ngOnDestroy(); - - m.expect(map$).toHaveSubscriptions('(^!)'); - - m.flush(); - expect(comp).toBeTruthy(); - }) - ); - - it( - 'should propagate values to the observable property', - marbles(m => { - const VALUES = { - a: convertToParamMap({ param: 'foo' }), - b: convertToParamMap({ param: 'bar' }), - c: convertToParamMap({ param: 'quz' }), - }; - const map$ = m.hot('--a--b-^-c--|', VALUES); - const comp = new FakeComponent(mockActivatedRoute(map$, VALUES.b)); - - m.expect(comp.getParam$()).toBeObservable('b-c--|', { - b: 'bar', - c: 'quz', - }); - - comp.ngOnInit(); - - m.flush(); - expect(comp).toBeTruthy(); - }) - ); -} - -describe('RouteParam', () => testOne(RouteParam, 'paramMap')); -describe('QueryParam', () => testOne(QueryParam, 'queryParamMap')); diff --git a/src/lib/rxjs/route-param.decorator.ts b/src/lib/rxjs/route-param.decorator.ts deleted file mode 100644 index 9cdad74eb073a19c67bb0f23025861cdfeadb664..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/route-param.decorator.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { ActivatedRoute } from '@angular/router'; -import { Observer } from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { hookBefore, hookFinally } from './hooks'; - -function ParamFromMap<K extends 'paramMap' | 'queryParamMap'>( - param: string = null, - routeProperty: string = 'route', - mapName: K -) { - return (prototype: any, property: string | symbol): void => { - let resolvedParam = param || property.toString(); - if (resolvedParam.endsWith('$')) { - resolvedParam = resolvedParam.substr(0, resolvedParam.length - 1); - } - - const subscription = Symbol.for(`RouteParamSub_${resolvedParam}`); - - hookBefore(prototype, 'ngOnInit', function() { - const route: ActivatedRoute = (this as any)[routeProperty]; - const observer: Observer<string> = (this as any)[property]; - - if (!route || !(mapName in route)) { - throw new Error( - `this.${routeProperty.toString()} must contains an ActivatedRoute` - ); - } - if ( - observer === null || - !('next' in observer) || - typeof observer.next !== 'function' - ) { - throw new Error(`this.${property.toString()} must implement Observer`); - } - - const snapshot = route.snapshot[mapName]; - if (snapshot) { - observer.next(snapshot.get(resolvedParam)); - } - - (this as any)[subscription] = route[mapName] - .pipe(map(p => p.get(resolvedParam))) - .subscribe(observer); - }); - - hookFinally(prototype, 'ngOnDestroy', function() { - this[subscription].unsubscribe(); - }); - }; -} - -/** - * Décorateur pour récuperer un paramètre de la route. - * - * La propriété décorée doit implémenter Observer<string>. - * - * @param {string|null} param Nom du paramètre. Par défaut égal au nom de la propriété minus le '$' final. - * @param {string} routeProperty nom de la propriété du Component contenant l'ActivatedRoute. Par défaut 'route'. - */ -export function RouteParam( - param: string = null, - routeProperty: string = 'route' -) { - return ParamFromMap(param, routeProperty, 'paramMap'); -} - -/** - * Décorateur pour récuperer un paramètre de la requête. - * - * La propriété décorée doit implémenter Observer<string>. - * - * @param {string|null} param Nom du paramètre. Par défaut égal au nom de la propriété minus le '$' final. - * @param {string} routeProperty nom de la propriété du Component contenant l'ActivatedRoute. Par défaut 'route'. - */ -export function QueryParam( - param: string = null, - routeProperty: string = 'route' -) { - return ParamFromMap(param, routeProperty, 'queryParamMap'); -} diff --git a/src/lib/rxjs/safe-combine-latest.observable.spec.ts b/src/lib/rxjs/safe-combine-latest.observable.spec.ts deleted file mode 100644 index 77da580a8ba062b03cc399921dc11153bb4e0658..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/safe-combine-latest.observable.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Observable } from 'rxjs'; - -import { safeCombineLatest } from './safe-combine-latest.observable'; -import { MarbleTestScheduler } from './testing/marbles'; - -describe('safe-combine-latest', () => { - let scheduler: MarbleTestScheduler<any>; - const VALUES: { [key: string]: number | number[] } = { - a: 0, - b: 1, - c: 2, - d: 3, - e: 4, - N: [], - A: [0], - B: [1], - C: [2], - U: [1, 3, 4], - V: [2, 3, 4], - }; - - beforeEach(() => { - scheduler = MarbleTestScheduler.create(VALUES); - }); - - it('should emit an empty array on empty inputs', () => - scheduler.run(({ cold, expectObservable }) => { - const inputs$: Array<Observable<number>> = [ - // EMPTY - ]; - const output = 'N'; - - expectObservable(safeCombineLatest(inputs$)).toBe(output); - })); - - it('should emit an 1-sized array on 1-sized input', () => - scheduler.run(({ cold, expectObservable }) => { - const inputs$: Array<Observable<number>> = [ - // - cold('abc'), - ]; - const output = 'ABC'; - - expectObservable(safeCombineLatest(inputs$)).toBe(output); - })); - - it('should emit combinations of each inputs', () => - scheduler.run(({ cold, expectObservable }) => { - const inputs$: Array<Observable<number>> = [ - // - cold('a-b-c'), - cold('d'), - cold('--e'), - ]; - const output = '--U-V'; - - expectObservable(safeCombineLatest(inputs$)).toBe(output); - })); -}); diff --git a/src/lib/rxjs/safe-combine-latest.observable.ts b/src/lib/rxjs/safe-combine-latest.observable.ts deleted file mode 100644 index 128517a6a920e88b00221a88711da3499999372b..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/safe-combine-latest.observable.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { combineLatest, Observable, ObservableInput } from 'rxjs'; - -/** - * Observable qui envoie un tableau vide et ne complète pas. - */ -// tslint:disable-next-line:rxjs-finnish -const EMPTY_ARRAY_OBSERVABLE: Observable<any[]> = new Observable(subscriber => - subscriber.next([]) -); - -/** - * Variante de combineLatest qui transmets un tableau vide si le tableau des entrées est vide. - */ -export function safeCombineLatest<T>( - inputs$: Array<ObservableInput<T>> -): Observable<T[]> { - if (inputs$.length === 0) { - return EMPTY_ARRAY_OBSERVABLE; - } - - return combineLatest(inputs$); -} diff --git a/src/lib/rxjs/safe-fork-join.observable.spec.ts b/src/lib/rxjs/safe-fork-join.observable.spec.ts deleted file mode 100644 index d4dfadd5d2cdfaf128d9d193b63549a402c668fb..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/safe-fork-join.observable.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Observable } from 'rxjs'; - -import { safeForkJoin } from './safe-fork-join.observable'; -import { MarbleTestScheduler } from './testing/marbles'; - -describe('safe-fork-join', () => { - let scheduler: MarbleTestScheduler<any>; - const VALUES: { [key: string]: number | number[] } = { - a: 0, - b: 1, - c: 2, - d: 3, - e: 4, - N: [], - A: [0], - B: [1], - C: [2], - V: [0, 3, 4], - }; - - beforeEach(() => { - scheduler = MarbleTestScheduler.create(VALUES); - }); - - it('should emit an empty array and complete on empty inputs', () => - scheduler.run(({ cold, expectObservable }) => { - const inputs$: Array<Observable<number>> = [ - // EMPTY - ]; - const output = '(N|)'; - - expectObservable(safeForkJoin(inputs$)).toBe(output); - })); - - it('should emit an 1-sized array on 1-sized input', () => - scheduler.run(({ cold, expectObservable }) => { - const inputs$: Array<Observable<number>> = [ - // - cold('a'), - ]; - const output = '(A|)'; - - expectObservable(safeForkJoin(inputs$)).toBe(output); - })); - - it('should emit only the first value of each inputs', () => - scheduler.run(({ cold, expectObservable }) => { - const inputs$: Array<Observable<number>> = [ - // - // - cold('a-b-c'), - cold('d'), - cold('--e'), - ]; - const output = '--V|'; - - expectObservable(safeForkJoin(inputs$)).toBe(output); - })); -}); diff --git a/src/lib/rxjs/safe-fork-join.observable.ts b/src/lib/rxjs/safe-fork-join.observable.ts deleted file mode 100644 index b9f3150e67e9babd68b387858b4a96bdbddfac3d..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/safe-fork-join.observable.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { forkJoin, from, Observable, ObservableInput, of } from 'rxjs'; -import { take } from 'rxjs/operators'; - -/** - * Variante de forkJoin qui : - * - renvoie un tableau vide si le tableau d'entrée est vide. - * - retourne uniquement la première valeure de chaque entrée. - */ -export function safeForkJoin<T>( - inputs$: Array<ObservableInput<T>> -): Observable<T[]> { - if (inputs$.length === 0) { - return of([]); - } - - return forkJoin(inputs$.map(input$ => from(input$).pipe(take(1)))); -} diff --git a/src/lib/rxjs/select.operator.spec.ts b/src/lib/rxjs/select.operator.spec.ts deleted file mode 100644 index 581af571fd3a8aaf9952a498684f84a39f9d7348..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/select.operator.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { marbles } from 'rxjs-marbles'; - -import { select } from './select.operator'; - -describe('select', () => { - const VALUES = { - a: 1, - b: 2, - A: 4, - B: 13, - }; - - const sub5 = jasmine.createSpy('sub5'); - sub5.and.callFake((x: number) => x - 5); - - const mul8 = jasmine.createSpy('mul8'); - mul8.and.callFake((x: number) => x * 8); - - const const2 = jasmine.createSpy('const2'); - const2.and.callFake(() => 2); - - const add = jasmine.createSpy('add'); - add.and.callFake((x: number, y: number) => x + y); - - beforeEach(() => { - sub5.calls.reset(); - mul8.calls.reset(); - add.calls.reset(); - }); - - it('should produce the expected value', () => { - marbles(m => { - const source$ = m.cold('--a--|', VALUES); - const expected = '--A--|'; - const actual$ = source$.pipe(select([sub5, mul8], add)); - - m.expect(actual$).toBeObservable(expected, VALUES); - })(); - - expect(sub5).toHaveBeenCalledTimes(1); - expect(mul8).toHaveBeenCalledTimes(1); - expect(add).toHaveBeenCalledTimes(1); - - expect(sub5).toHaveBeenCalledWith(VALUES.a); - expect(mul8).toHaveBeenCalledWith(VALUES.a); - expect(add).toHaveBeenCalledWith(VALUES.a - 5, VALUES.a * 8); - }); - - it('should recalculate on different input', () => { - marbles(m => { - const source$ = m.cold('--a--b--|', VALUES); - const expected = '--A--B--|'; - const actual$ = source$.pipe(select([sub5, mul8], add)); - - m.expect(actual$).toBeObservable(expected, VALUES); - })(); - - expect(sub5).toHaveBeenCalledWith(VALUES.a); - expect(mul8).toHaveBeenCalledWith(VALUES.a); - expect(add).toHaveBeenCalledWith(VALUES.a - 5, VALUES.a * 8); - - expect(sub5).toHaveBeenCalledWith(VALUES.b); - expect(mul8).toHaveBeenCalledWith(VALUES.b); - expect(add).toHaveBeenCalledWith(VALUES.b - 5, VALUES.b * 8); - }); - - it('should not recalculate on same input', () => { - marbles(m => { - const source$ = m.cold('--a--a--|', VALUES); - const expected = '--A--A--|'; - const actual$ = source$.pipe(select([sub5, mul8], add)); - - m.expect(actual$).toBeObservable(expected, VALUES); - })(); - - expect(sub5).toHaveBeenCalledTimes(1); - expect(mul8).toHaveBeenCalledTimes(1); - expect(add).toHaveBeenCalledTimes(1); - }); - - it('should not recalculate on same intermediates values', () => { - marbles(m => { - const source$ = m.cold('--a--b--|', VALUES); - const expected = '--A--A--|'; - const actual$ = source$.pipe(select([const2, const2], add)); - - m.expect(actual$).toBeObservable(expected, VALUES); - })(); - - expect(add).toHaveBeenCalledWith(2, 2); - expect(add).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/lib/rxjs/select.operator.ts b/src/lib/rxjs/select.operator.ts deleted file mode 100644 index 8ef0cce6369a32d631fc3452846cb4ba75541ecd..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/select.operator.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Observable, Observer, OperatorFunction } from 'rxjs'; - -type State<T, R, V> = - | { readonly status: 'NEW' } - | { - readonly status: 'OPEN'; - readonly input: T; - readonly output: R; - readonly values: V; - } - | { readonly status: 'CLOSED' }; - -export function select<T, R, V extends any[]>( - selectors: { [I in keyof V]: (input: T) => V[I] }, - projector: (...args: V) => R, - isSame: <X>(a: X, b: X) => boolean = <Y>(a: Y, b: Y) => a === b -): OperatorFunction<T, R> { - return (source$: Observable<T>) => - new Observable<R>(subscriber => { - let state: State<T, R, V> = { status: 'NEW' }; - - function close() { - state = { status: 'CLOSED' }; - } - - const observer: Observer<T> = { - get closed(): boolean { - return state.status === 'CLOSED'; - }, - - next: (input: T) => { - if (state.status === 'CLOSED') { - return; - } - - if (state.status === 'OPEN' && isSame(input, state.input)) { - subscriber.next(state.output); - return; - } - - const values = selectors.map(s => s(input)) as V; - if ( - state.status === 'OPEN' && - state.values.every((v, i) => isSame(v, values[i])) - ) { - subscriber.next(state.output); - return; - } - - const output = projector(...values); - - state = { status: 'OPEN', input, values, output }; - subscriber.next(output); - }, - - error: err => { - subscriber.error(err); - close(); - }, - - complete: () => { - subscriber.complete(); - close(); - }, - }; - - const sub = source$.subscribe(observer); - sub.add(close); - return sub; - }); -} diff --git a/src/lib/rxjs/spy.observer.ts b/src/lib/rxjs/spy.observer.ts deleted file mode 100644 index 5b1132701e22dccf07822ad9dc6570abb31d8da4..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/spy.observer.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Observer } from 'rxjs'; - -// tslint:disable:no-console -export function spyObserver<T>(subject = 'spy'): Observer<T> { - return <Observer<T>>{ - closed: false, - next: value => console.log(subject, 'next:', value), - error: err => console.log(subject, 'error:', err), - complete: () => console.log(subject, 'completed.'), - }; -} diff --git a/src/lib/rxjs/spy.operator.ts b/src/lib/rxjs/spy.operator.ts deleted file mode 100644 index bf3416dbf260e0e02c48c34909fe133bffb4295c..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/spy.operator.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { MonoTypeOperatorFunction, Observable, ReplaySubject } from 'rxjs'; - -export type NotificationType = - | 'next' - | 'error' - | 'complete' - | 'subscribed' - | 'unsubscribed'; - -export interface Notification { - tag: string; - timestamp: number; - type: NotificationType; - payload?: any; -} - -export const notifications$ = new ReplaySubject<Notification>(20); - -const timeOrigin = Date.now(); - -export function spy<T>(tag = 'spy'): MonoTypeOperatorFunction<T> { - function log(type: NotificationType, payload?: any) { - notifications$.next({ - tag, - type, - payload, - timestamp: Date.now() - timeOrigin, - }); - } - - return (source$: Observable<T>): Observable<T> => - new Observable(observer => { - log('subscribed'); - - const sub = source$.subscribe({ - next: value => { - log('next', value); - observer.next(value); - }, - error: err => { - log('error', err); - observer.error(err); - }, - complete: () => log('complete'), - }); - - sub.add(() => log('unsubscribed')); - - return sub; - }); -} diff --git a/src/lib/rxjs/subject-accessors.decorator.ts b/src/lib/rxjs/subject-accessors.decorator.ts deleted file mode 100644 index 743af701428381b9cc8f6b2cf2173dc01803d308..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/subject-accessors.decorator.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Génère un getter et un setter publics pour un attribut de type BehaviorSubject. - * - * Le setter utilise .next() et le getter .getValue(). - * - * Le nom de la propriété doit se terminer par '$'. - */ -export function SubjectAccessors() { - return (prototype: object, observablePropertyName: string | symbol): void => { - Object.defineProperty( - prototype, - getPlainPropertyName(observablePropertyName), - { - get(): any { - return this[observablePropertyName].getValue(); - }, - set(value: any): void { - this[observablePropertyName].next(value); - }, - } - ); - }; -} - -/** - * Génère un setter public pour un attribut de type Subject. Il utilise utilise .next(). - * - * Le nom de la propriété doit se terminer par '$'. - */ -export function SubjectSetter() { - return (prototype: object, observablePropertyName: string | symbol): void => { - Object.defineProperty( - prototype, - getPlainPropertyName(observablePropertyName), - { - set(value: any): void { - this[observablePropertyName].next(value); - }, - } - ); - }; -} - -/** - * Extrait le nom de propriété "normale" d'un nom de propriété de "setter". - */ -function getPlainPropertyName(observablePropertyName: string | symbol): string { - const name = observablePropertyName.toString(); - if (!name.endsWith('$')) { - throw new Error(`Property name must end with '$', '${name}' does noit`); - } - return name.substr(0, name.length - 1); -} diff --git a/src/lib/rxjs/subscribe-on-init.decorator.spec.ts b/src/lib/rxjs/subscribe-on-init.decorator.spec.ts deleted file mode 100644 index 9a9183f2047419210abf507cbf9dadbb27f4eb2c..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/subscribe-on-init.decorator.spec.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { OnDestroy, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; - -import { SubscribeOnInit } from './subscribe-on-init.decorator'; -import { MarbleTestScheduler } from './testing/marbles'; - -describe('@SubscribeOnInit', () => { - const VALUES = { a: 'a' }; - let scheduler: MarbleTestScheduler<string>; - - class PropertyTestClass<T extends Observable<any>> - implements OnInit, OnDestroy { - @SubscribeOnInit() - public readonly obs$: T; - - public constructor( - obs$: T, - private readonly initSpy = (): void => undefined, - private readonly destroySpy = (): void => undefined - ) { - this.obs$ = obs$; - } - - public ngOnInit(): void { - this.initSpy(); - } - - public ngOnDestroy(): void { - this.destroySpy(); - } - } - - beforeEach(() => { - scheduler = MarbleTestScheduler.create(VALUES); - }); - - it('should subscribe on init and unsubscribe on destroy', () => - scheduler.run(({ cold, expectSubscriptions }) => { - const obj = new PropertyTestClass(cold('a')); - - obj.ngOnInit(); - scheduler.schedule(() => obj.ngOnDestroy(), scheduler.createTime('---|')); - - expectSubscriptions(obj.obs$.subscriptions).toBe('^!'); - })); - - it('should be tied only to object instances', () => - scheduler.run(({ cold, expectSubscriptions }) => { - const obj1 = new PropertyTestClass(cold('a')); - const obj2 = new PropertyTestClass(cold('a')); - - obj1.ngOnInit(); - obj2.ngOnInit(); - scheduler.schedule(() => obj1.ngOnDestroy(), scheduler.createTime('--|')); - scheduler.schedule( - () => obj2.ngOnDestroy(), - scheduler.createTime('----|') - ); - - expectSubscriptions(obj1.obs$.subscriptions).toBe('^--!'); - expectSubscriptions(obj2.obs$.subscriptions).toBe('^----!'); - })); - - it('should call original ngOnInit', () => - scheduler.run(({ cold }) => { - const obs$ = cold('a'); - const initSpy = jasmine.createSpy('ngOnInit'); - const obj = new PropertyTestClass(obs$, initSpy); - - obj.ngOnInit(); - - expect(initSpy).toHaveBeenCalled(); - })); - - it('should call original ngOnDestroy', () => - scheduler.run(({ cold }) => { - const obs$ = cold('a'); - const destroySpy = jasmine.createSpy('ngOnDestroy'); - const obj = new PropertyTestClass(obs$, undefined, destroySpy); - - obj.ngOnInit(); - obj.ngOnDestroy(); - - expect(destroySpy).toHaveBeenCalled(); - })); - - it('can be used on more than one property', () => - scheduler.run(({ cold, expectSubscriptions }) => { - class TwoPropertyTestClass implements OnInit, OnDestroy { - @SubscribeOnInit() - public readonly obs1$ = cold('a'); - - @SubscribeOnInit() - public readonly obs2$ = cold('a'); - - public ngOnDestroy(): void {} - - public ngOnInit(): void {} - } - - const obj = new TwoPropertyTestClass(); - - obj.ngOnInit(); - scheduler.schedule(() => obj.ngOnDestroy(), scheduler.createTime('---|')); - - expectSubscriptions(obj.obs1$.subscriptions).toBe('^---!'); - expectSubscriptions(obj.obs2$.subscriptions).toBe('^---!'); - })); - - it('should be inherited', () => - scheduler.run(({ cold, expectSubscriptions }) => { - class ParentClass implements OnInit, OnDestroy { - @SubscribeOnInit() - public readonly obs1$ = cold('a'); - - public ngOnDestroy(): void {} - - public ngOnInit(): void {} - } - - class ChildrenClass extends ParentClass { - @SubscribeOnInit() - public readonly obs2$ = cold('a'); - } - - const obj = new ChildrenClass(); - - obj.ngOnInit(); - scheduler.schedule(() => obj.ngOnDestroy(), scheduler.createTime('---|')); - - expectSubscriptions(obj.obs1$.subscriptions).toBe('^---!'); - expectSubscriptions(obj.obs2$.subscriptions).toBe('^---!'); - })); -}); diff --git a/src/lib/rxjs/subscribe-on-init.decorator.ts b/src/lib/rxjs/subscribe-on-init.decorator.ts deleted file mode 100644 index e998ca5a76c8f0909839bc346849d16dfa008d88..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/subscribe-on-init.decorator.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { OnDestroy, OnInit } from '@angular/core'; -import { Observable, Subscription } from 'rxjs'; - -import { hookBefore, hookFinally } from './hooks'; - -const SUBSCRIPTION_KEY = Symbol.for('SubscribeOnInit'); - -interface AutoSubscribable extends OnInit, OnDestroy {} - -type ObjectKey = string | number | symbol; - -type AutoSubscriber<Name extends ObjectKey> = AutoSubscribable & { - [SUBSCRIPTION_KEY]: Subscription; -} & { - [K in Name]?: Observable<any>; - }; - -/** - * Ce décortaeur est à utiliser sur des propriétés de type Observable dans un composant implémentant OnInit - * et OnDestroy. Il s'assure qu'un abonnnement à l'observavble est fait dans ngOnInit et que le désabonnement - * est géré dans ngOnDestroy(). - */ -export function SubscribeOnInit() { - return <Target extends AutoSubscribable, Name extends ObjectKey>( - target: Target, - name: Name - ): any => { - const prototype = (target as any) as AutoSubscriber<Name>; - - hookBefore(prototype, 'ngOnInit', function() { - if (!(this as any)[SUBSCRIPTION_KEY]) { - (this as any)[SUBSCRIPTION_KEY] = new Subscription( - () => delete (this as any)[SUBSCRIPTION_KEY] - ); - } - if ((this as any)[name]) { - (this as any)[SUBSCRIPTION_KEY].add((this as any)[name].subscribe()); - } - }); - - hookFinally(prototype, 'ngOnDestroy', function() { - if (this[SUBSCRIPTION_KEY]) { - this[SUBSCRIPTION_KEY].unsubscribe(); - } - }); - }; -} diff --git a/src/lib/rxjs/testing/marbles.spec.ts b/src/lib/rxjs/testing/marbles.spec.ts deleted file mode 100644 index b2a240934a5bad41e5c611fe386253ac7ed72880..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/testing/marbles.spec.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* tslint:disable:rxjs-no-internal */ -import { Notification } from 'rxjs'; -import { SubscriptionLog } from 'rxjs/internal/testing/SubscriptionLog'; -import { TestMessage } from 'rxjs/internal/testing/TestMessage'; -import { TestScheduler } from 'rxjs/testing'; - -import { Event, MarbleFormatter } from './marbles'; - -describe('MarbleFormatter', () => { - const VALUES = { a: { x: 'A' }, b: 'B', c: 'C', truc: 5 }; - const ERROR = 'error'; - const FRAME_DURATION = 10; - - let formatter: MarbleFormatter; - - beforeEach(() => { - formatter = new MarbleFormatter(VALUES, ERROR, FRAME_DURATION); - }); - - it('should be created', () => { - expect(formatter).toBeTruthy(); - }); - - describe('.formatMarbles()', () => { - function mkNotif<T = any>( - frame: number, - notification: Notification<T> - ): Event<T> { - return { frame, notification }; - } - - it('should encode empty log to an empty string', () => { - expect(formatter.formatMarbles([])).toEqual(''); - }); - - it('should represented known value with its key', () => { - expect( - formatter.formatMarbles([ - mkNotif(0, Notification.createNext({ x: 'A' })), - ]) - ).toEqual('a'); - }); - - it('should represent values as "o" when no values are specified', () => { - expect( - formatter - .withValues() - .formatMarbles([ - mkNotif(0, Notification.createNext('foo')), - mkNotif(10, Notification.createNext('bar')), - ]) - ).toEqual('oo'); - }); - - it('should throw on unexpected value', () => { - expect(() => - formatter.formatMarbles([mkNotif(10, Notification.createNext('foo'))]) - ).toThrow(new Error('Unexpected value at 10 ms: "foo"')); - }); - - it('should represent complete as "|"', () => { - expect( - formatter.formatMarbles([mkNotif(0, Notification.createComplete())]) - ).toEqual('|'); - }); - - it('should represent errors as "#" when no error is specified', () => { - expect( - formatter - .withError() - .formatMarbles([mkNotif(0, Notification.createError('foo'))]) - ).toEqual('#'); - }); - - it('should represent expected error as "#"', () => { - expect( - formatter.formatMarbles([mkNotif(0, Notification.createError('error'))]) - ).toEqual('#'); - }); - - it('should throw on unexpected error', () => { - expect(() => - formatter.formatMarbles([mkNotif(10, Notification.createError('foo'))]) - ).toThrow(new Error('Unexpected error at 10 ms: "foo"')); - }); - - it('should represent empty time frames as "-"', () => { - expect( - formatter.formatMarbles([ - mkNotif(10, Notification.createNext(VALUES.a)), - mkNotif(30, Notification.createComplete()), - ]) - ).toEqual('-a-|'); - }); - - it('should group events occuring in the same frame', () => { - expect( - formatter.formatMarbles([ - mkNotif(10, Notification.createNext(VALUES.a)), - mkNotif(10, Notification.createNext(VALUES.b)), - mkNotif(10, Notification.createNext(VALUES.c)), - mkNotif(70, Notification.createComplete()), - ]) - ).toEqual('-(abc)-|'); - }); - - describe('should format the messages back to the original marble spec', () => { - const TEST_CASES: Array<[string, TestMessage[]]> = []; - - [ - // Empêche le formateur de tout remettre sur un ligne - '', - '|', - 'a', - '#', - '--a', - '--|', - '--#', - '(ab)--|', - '--(ab)--|', - '(abc)--|', - '-a--#', - '--(a|)', - ].forEach(marbles => { - TEST_CASES.push([ - marbles, - TestScheduler.parseMarbles(marbles, VALUES, ERROR, false, true), - ]); - }); - - for (const [marbles, messages] of TEST_CASES) { - it(marbles, () => { - const result = formatter.formatMarbles(messages); - expect(result).toEqual(marbles); - }); - } - }); - }); - - describe('formatSubscriptions', () => { - it('should format empty subscriptions', () => { - expect( - formatter.formatSubscriptions( - new SubscriptionLog(Number.POSITIVE_INFINITY) - ) - ).toEqual(''); - }); - - it('should format closed subscription', () => { - expect( - formatter.formatSubscriptions(new SubscriptionLog(10, 30)) - ).toEqual(' ^-!'); - }); - - it('should format open subscription', () => { - expect(formatter.formatSubscriptions(new SubscriptionLog(10))).toEqual( - ' ^' - ); - }); - - it('should format zero-duration subscription', () => { - expect( - formatter.formatSubscriptions(new SubscriptionLog(10, 10)) - ).toEqual(' (^!)'); - }); - }); -}); diff --git a/src/lib/rxjs/testing/marbles.ts b/src/lib/rxjs/testing/marbles.ts deleted file mode 100644 index 421b3525a40e5a985d0c53ff33b94f26dab49020..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/testing/marbles.ts +++ /dev/null @@ -1,247 +0,0 @@ -// tslint:disable-next-line:no-reference -///<reference path="node_modules/@types/jasmine/index.d.ts"/> - -/* tslint:disable:rxjs-no-internal */ -import * as _ from 'lodash'; -import { Notification, Observable } from 'rxjs'; -import { ColdObservable } from 'rxjs/internal/testing/ColdObservable'; -import { HotObservable } from 'rxjs/internal/testing/HotObservable'; -import { SubscriptionLog } from 'rxjs/internal/testing/SubscriptionLog'; -import { - observableToBeFn, - RunHelpers, -} from 'rxjs/internal/testing/TestScheduler'; -import { TestScheduler } from 'rxjs/testing'; - -export interface Event<T> { - frame: number; - notification: Notification<T>; -} - -interface Values<T> { - [key: string]: T; -} - -export class MarbleFormatter<T = any> { - public constructor( - public readonly values?: Values<T>, - public readonly error?: any, - public readonly frameDuration = 1 - ) {} - - public withValues<U>(values?: Values<U>): MarbleFormatter<U> { - return new MarbleFormatter<U>(values, this.error, this.frameDuration); - } - - public withError(error?: any): MarbleFormatter<T> { - return new MarbleFormatter<T>(this.values, error, this.frameDuration); - } - - public formatMarbles(events: Array<Event<T>>): string { - let group = ''; - let marbles = ''; - for (const event of events) { - while (this.frameDuration * marbles.length < event.frame) { - if (group) { - if (group.length > 1) { - group = `(${group})`; - } - marbles += group; - group = ''; - } else { - marbles += '-'; - } - } - - event.notification.do( - value => { - const key = this.values - ? _.findKey(this.values, v => _.isEqual(value, v)) - : 'o'; - if (!key) { - throw new Error( - `Unexpected value at ${event.frame} ms: ${JSON.stringify(value)}` - ); - } - group += key; - }, - error => { - if (this.error !== undefined && !_.isEqual(error, this.error)) { - const message = MarbleFormatter.errorMessage(error); - throw new Error( - `Unexpected error at ${event.frame} ms: ${message}` - ); - } - group += '#'; - }, - () => { - group += '|'; - } - ); - } - if (group) { - if (group.length > 1) { - marbles += `(${group})`; - } else { - marbles += group; - } - } - - return marbles; - } - - public formatSubscriptions({ - subscribedFrame, - unsubscribedFrame, - }: SubscriptionLog): string { - if (subscribedFrame === Number.POSITIVE_INFINITY) { - return ''; - } - let marbles = ' '.repeat(subscribedFrame / this.frameDuration); - switch (unsubscribedFrame) { - case Number.POSITIVE_INFINITY: - marbles += '^'; - break; - case subscribedFrame: - marbles += '(^!)'; - break; - default: - marbles += `^${'-'.repeat( - (unsubscribedFrame - subscribedFrame) / this.frameDuration - 1 - )}!`; - } - return marbles; - } - - private static errorMessage(error: any): string { - if (error instanceof Error) { - return error.message; - } - if (typeof error === 'object' && typeof error.toString === 'function') { - return error.ToString(); - } - return JSON.stringify(error); - } -} - -function isSubscriptionLog(data: any): data is SubscriptionLog { - return ( - typeof data === 'object' && - 'subscribedFrame' in data && - 'unsubscribedFrame' in data - ); -} - -function isEvent<T>(data: any): data is Event<T> { - return typeof data === 'object' && 'frame' in data && 'notification' in data; -} - -export class MarbleTestScheduler<T> extends TestScheduler { - public constructor(private readonly formatter: MarbleFormatter<T>) { - super((actuel, expected) => this.compare(actuel, expected)); - } - - public withValues<U>(values?: Values<U>): MarbleTestScheduler<U> { - return new MarbleTestScheduler<U>(this.formatter.withValues(values)); - } - - public withError(error?: any): MarbleTestScheduler<T> { - return new MarbleTestScheduler<T>(this.formatter.withError(error)); - } - - public createColdObservable<X = T>( - marbles: string, - values?: Values<X>, - error?: any - ): ColdObservable<X> { - return super.createColdObservable<X>( - marbles, - (values === undefined ? this.formatter.values : values) as Values<X>, - error === undefined ? this.formatter.error : error - ); - } - - public createHotObservable<X = T>( - marbles: string, - values?: Values<X>, - error?: any - ): HotObservable<X> { - return super.createHotObservable<X>( - marbles, - (values === undefined ? this.formatter.values : values) as Values<X>, - error === undefined ? this.formatter.error : error - ); - } - - public expectObservable( - observable$: Observable<any>, - unsubscriptionMarbles: string = null - ): { toBe: observableToBeFn } { - const { toBe } = super.expectObservable(observable$, unsubscriptionMarbles); - return { - toBe: (marbles, values, error) => - toBe( - marbles, - values === undefined ? this.formatter.values : values, - error === undefined ? this.formatter.error : error - ), - }; - } - - public run<X>(callback: (helpers: MarbleRunHelpers) => X): X { - return super.run(callback); - } - - private compare<U extends Event<T> | SubscriptionLog>( - actual: U[], - expected: U[] - ): void { - if ( - (expected.length === 0 || isSubscriptionLog(expected[0])) && - (actual.length === 0 || isSubscriptionLog(actual[0])) - ) { - const expectedMarbles = expected.map(s => - this.formatter.formatSubscriptions(s as SubscriptionLog) - ); - const actualMarbles = actual.map(s => - this.formatter.formatSubscriptions(s as SubscriptionLog) - ); - expect(actualMarbles).toEqual(expectedMarbles); - return; - } - - if ( - (expected.length === 0 || isEvent(expected[0])) && - (actual.length === 0 || isEvent(actual[0])) - ) { - const expectedMarbles = this.formatter.formatMarbles(expected as Array< - Event<T> - >); - const actualMarbles = this.formatter.formatMarbles(actual as Array< - Event<T> - >); - expect(actualMarbles).toEqual(expectedMarbles); - return; - } - - expect(actual).toEqual(expected); - } - - public static create<T>( - values?: Values<T>, - error?: any, - frameDuration = this.frameTimeFactor - ): MarbleTestScheduler<T> { - return new MarbleTestScheduler<T>( - new MarbleFormatter<T>(values, error, frameDuration) - ); - } -} - -export interface MarbleRunHelpers extends RunHelpers { - cold: typeof MarbleTestScheduler.prototype.createColdObservable; - hot: typeof MarbleTestScheduler.prototype.createHotObservable; - flush: typeof MarbleTestScheduler.prototype.flush; - expectObservable: typeof MarbleTestScheduler.prototype.expectObservable; - expectSubscriptions: typeof MarbleTestScheduler.prototype.expectSubscriptions; -} diff --git a/src/lib/rxjs/until-destroyed.operator.spec.ts b/src/lib/rxjs/until-destroyed.operator.spec.ts deleted file mode 100644 index 386fd75102048b1640d0cd710019c6a1eda3be95..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/until-destroyed.operator.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { OnDestroy, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; -import { marbles } from 'rxjs-marbles'; - -import { untilDestroyed } from './until-destroyed.operator'; - -describe('untilDestroyed', () => { - class TestClass implements OnInit, OnDestroy { - public constructor(private readonly obs$: Observable<any>) {} - - public ngOnInit(): void { - this.obs$.pipe(untilDestroyed(this)).subscribe(); - } - - public ngOnDestroy(): void {} - } - - it( - 'should unsubscribe on destroy', - marbles(m => { - const o$ = m.cold('--a--|'); - const obj = new TestClass(o$); - - obj.ngOnInit(); - obj.ngOnDestroy(); - - m.flush(); - - m.expect(o$).toHaveSubscriptions('(^!)'); - expect(obj).not.toBeUndefined(); - }) - ); -}); diff --git a/src/lib/rxjs/until-destroyed.operator.ts b/src/lib/rxjs/until-destroyed.operator.ts deleted file mode 100644 index 0d32e8752d9f641ef80363cbc2587b4854b99643..0000000000000000000000000000000000000000 --- a/src/lib/rxjs/until-destroyed.operator.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { OnDestroy } from '@angular/core'; -import { MonoTypeOperatorFunction, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; - -import { hookFinally } from './hooks'; - -const OPERATOR = Symbol.for('untilDestroyedOperator'); - -/** - * Opérateur qui interrompt l'observable quand la méthode "ngOnDestroy" de l'objet passé - * en paramètre est appelée. - * - * @param {OnDestroy} target - * @return {MonoTypeOperatorFunction<T>} - * - * @example - * class Foo extends OnInit, OnDestroy { - * private readonly data$: Observable<string>; - * - * public ngOnInit() { - * this.data$ = from(['a', 'b', 'c']).pipe(untilDestroyed(this)); - * } - * - * public ngOnDestroy() {} - * } - */ -export function untilDestroyed<T>( - target: OnDestroy -): MonoTypeOperatorFunction<T> { - if (OPERATOR in target) { - return (target as any)[OPERATOR]; - } - - const signal$ = new Subject<void>(); - const operator = takeUntil<T>(signal$); - - (target as any)[OPERATOR] = operator; - hookFinally(target, 'ngOnDestroy', () => signal$.next(undefined)); - - return operator; -}