// Licensed to Cloudera, Inc. under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. Cloudera, Inc. licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { onBeforeUnmount, Ref, watch } from 'vue'; import KnockoutObservable from '@types/knockout'; import huePubSub from '../../utils/huePubSub'; export interface Disposable { dispose(): void; } const valueOrNull = (val?: T): T | null => (typeof val === 'undefined' ? null : val); export default class SubscriptionTracker { disposed = false; disposals: (() => void)[] = []; constructor() { onBeforeUnmount(this.dispose.bind(this)); } addDisposable(disposable: Disposable): void { this.disposals.push(disposable.dispose.bind(disposable)); } addEventListener( element: HTMLElement | Document, type: K, listener: (ev: HTMLElementEventMap[K]) => unknown ): void { element.addEventListener(type, listener as EventListenerOrEventListenerObject); this.disposals.push(() => { element.removeEventListener(type, listener as EventListenerOrEventListenerObject); }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any subscribe( subscribable: string | KnockoutSubscribable, callback: (event: T) => void ): void { if (typeof subscribable === 'string') { const pubSub = huePubSub.subscribe(subscribable, callback); this.disposals.push(() => { pubSub.remove(); }); } else if (subscribable.subscribe) { const sub = subscribable.subscribe(callback); this.disposals.push(() => { sub.dispose(); }); } } /** * Helper function to link knockout observable with Vue refs. Note that it will update the Vue ref on changes to * the knockout observable but not the other way around. */ trackObservable( observableRef: Ref>, vueRef: Ref ): void { const whenDef = new Promise>(resolve => { const stop = watch( observableRef, observable => { if (observable) { resolve(observable); stop(); } }, { immediate: true } ); }); whenDef.then(observable => { if (this.disposed) { return; } vueRef.value = valueOrNull(observable()); this.subscribe(observable, (newVal?: T | null) => { vueRef.value = valueOrNull(newVal); }); }); } trackTimeout(timeout: number): void { this.disposals.push(() => { window.clearTimeout(timeout); }); } dispose(): void { this.disposed = true; while (this.disposals.length) { try { const disposeFn = this.disposals.pop(); if (disposeFn) { disposeFn(); } } catch (err) { console.warn(err); } } } }