1/* 2 * Copyright 2024 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17/** Function type to be invoked when a disposer clear. */ 18export type DisposableFn = () => void; 19 20/** Objects disposable with an `Disposer`. */ 21export interface Disposable { 22 dispose(): void; 23} 24 25/** 26 * Tracks cleanup code and runs it upon calling `dispose`. 27 * 28 * By default, the disposer must not be used after it has been disposed. This 29 * helps avoid memory leaks by preventing accidental registration of cleanup 30 * code. Create the Disposer with `multiShot` set to 31 */ 32export class Disposer implements Disposable { 33 // null after a one-shot disposer was disposed. 34 private _disposables?: Array<DisposableFn> = []; 35 36 /** 37 * @param multiShot Whether the disposer can be used for multiple dispose 38 * cycles. 39 */ 40 constructor(private multiShot: boolean = false) {} 41 42 /** 43 * Registers the function to be called upon `dispose()`. 44 * 45 * Must not be called on a one-shot Disposer after it has been disposed. 46 */ 47 addFunction(disposable: DisposableFn) { 48 if (!this._disposables) { 49 throw new Error( 50 'Adding new disposabled to already disposed one-shot disposer' 51 ); 52 } 53 this._disposables!.push(disposable); 54 } 55 56 /** 57 * Registers the listener at the `eventTarget` and unregisters it upon 58 * `dispose()`. 59 * 60 * Must not be called on a one-shot Disposer after it has been disposed. 61 */ 62 addListener( 63 eventTarget: EventTarget, 64 type: string, 65 callback: EventListenerOrEventListenerObject, 66 options?: AddEventListenerOptions | boolean 67 ) { 68 this.addFunction(() => 69 eventTarget.removeEventListener(type, callback, options) 70 ); 71 eventTarget.addEventListener(type, callback, options); 72 } 73 74 /** 75 * Registers the Disposable to be cleaned up upon `dispose()`. 76 * 77 * Must not be called on a one-shot Disposer after it has been disposed. 78 */ 79 addDisposable(disposable: Disposable) { 80 this.addFunction(disposable.dispose.bind(disposable)); 81 } 82 83 /** 84 * Disposes all registered cleanup code. 85 * 86 * Must be called exactly once for one-shot Disposers, or at least once for 87 * multi-shot disposers. 88 */ 89 dispose(): void { 90 if (!this._disposables) { 91 throw new Error('dispose() an already disposed one-shot disposer.'); 92 } 93 94 const toDispose = this._disposables; 95 this._disposables = this.multiShot ? [] : undefined; 96 for (const disposable of toDispose) { 97 disposable(); 98 } 99 } 100} 101