disposable.ts (9781B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 declare global { 8 interface SymbolConstructor { 9 /** 10 * A method that is used to release resources held by an object. Called by 11 * the semantics of the `using` statement. 12 */ 13 readonly dispose: unique symbol; 14 15 /** 16 * A method that is used to asynchronously release resources held by an 17 * object. Called by the semantics of the `await using` statement. 18 */ 19 readonly asyncDispose: unique symbol; 20 } 21 22 interface Disposable { 23 [Symbol.dispose](): void; 24 } 25 26 interface AsyncDisposable { 27 [Symbol.asyncDispose](): PromiseLike<void>; 28 } 29 } 30 31 (Symbol as any).dispose ??= Symbol('dispose'); 32 (Symbol as any).asyncDispose ??= Symbol('asyncDispose'); 33 34 /** 35 * @internal 36 */ 37 export const disposeSymbol: typeof Symbol.dispose = Symbol.dispose; 38 39 /** 40 * @internal 41 */ 42 export const asyncDisposeSymbol: typeof Symbol.asyncDispose = 43 Symbol.asyncDispose; 44 45 /** 46 * @internal 47 */ 48 export class DisposableStack { 49 #disposed = false; 50 #stack: Disposable[] = []; 51 52 /** 53 * Returns a value indicating whether the stack has been disposed. 54 */ 55 get disposed(): boolean { 56 return this.#disposed; 57 } 58 59 /** 60 * Alias for `[Symbol.dispose]()`. 61 */ 62 dispose(): void { 63 this[disposeSymbol](); 64 } 65 66 /** 67 * Adds a disposable resource to the top of stack, returning the resource. 68 * Has no effect if provided `null` or `undefined`. 69 * 70 * @param value - A `Disposable` object, `null`, or `undefined`. 71 * `null` and `undefined` will not be added, but will be returned. 72 * @returns The provided `value`. 73 */ 74 use<T extends Disposable | null | undefined>(value: T): T { 75 if (value && typeof value[disposeSymbol] === 'function') { 76 this.#stack.push(value); 77 } 78 return value; 79 } 80 81 /** 82 * Adds a non-disposable resource and a disposal callback to the top of the stack. 83 * 84 * @param value - A resource to be disposed. 85 * @param onDispose - A callback invoked to dispose the provided value. 86 * Will be invoked with `value` as the first parameter. 87 * @returns The provided `value`. 88 */ 89 adopt<T>(value: T, onDispose: (value: T) => void): T { 90 this.#stack.push({ 91 [disposeSymbol]() { 92 onDispose(value); 93 }, 94 }); 95 return value; 96 } 97 98 /** 99 * Add a disposal callback to the top of the stack to be invoked when stack is disposed. 100 * @param onDispose - A callback to invoke when this object is disposed. 101 */ 102 defer(onDispose: () => void): void { 103 this.#stack.push({ 104 [disposeSymbol]() { 105 onDispose(); 106 }, 107 }); 108 } 109 110 /** 111 * Move all resources out of this stack and into a new `DisposableStack`, and 112 * marks this stack as disposed. 113 * @returns The new `DisposableStack`. 114 * 115 * @example 116 * 117 * ```ts 118 * class C { 119 * #res1: Disposable; 120 * #res2: Disposable; 121 * #disposables: DisposableStack; 122 * constructor() { 123 * // stack will be disposed when exiting constructor for any reason 124 * using stack = new DisposableStack(); 125 * 126 * // get first resource 127 * this.#res1 = stack.use(getResource1()); 128 * 129 * // get second resource. If this fails, both `stack` and `#res1` will be disposed. 130 * this.#res2 = stack.use(getResource2()); 131 * 132 * // all operations succeeded, move resources out of `stack` so that 133 * // they aren't disposed when constructor exits 134 * this.#disposables = stack.move(); 135 * } 136 * 137 * [disposeSymbol]() { 138 * this.#disposables.dispose(); 139 * } 140 * } 141 * ``` 142 */ 143 move(): DisposableStack { 144 if (this.#disposed) { 145 throw new ReferenceError('A disposed stack can not use anything new'); 146 } 147 const stack = new DisposableStack(); 148 stack.#stack = this.#stack; 149 this.#stack = []; 150 this.#disposed = true; 151 return stack; 152 } 153 154 /** 155 * Disposes each resource in the stack in last-in-first-out (LIFO) manner. 156 */ 157 [disposeSymbol](): void { 158 if (this.#disposed) { 159 return; 160 } 161 this.#disposed = true; 162 const errors: unknown[] = []; 163 for (const resource of this.#stack.reverse()) { 164 try { 165 resource[disposeSymbol](); 166 } catch (e) { 167 errors.push(e); 168 } 169 } 170 if (errors.length === 1) { 171 throw errors[0]; 172 } else if (errors.length > 1) { 173 let suppressed = null; 174 for (const error of errors.reverse()) { 175 if (suppressed === null) { 176 suppressed = error; 177 } else { 178 suppressed = new SuppressedError(error, suppressed); 179 } 180 } 181 throw suppressed; 182 } 183 } 184 185 readonly [Symbol.toStringTag] = 'DisposableStack'; 186 } 187 188 /** 189 * @internal 190 */ 191 export class AsyncDisposableStack { 192 #disposed = false; 193 #stack: AsyncDisposable[] = []; 194 195 /** 196 * Returns a value indicating whether the stack has been disposed. 197 */ 198 get disposed(): boolean { 199 return this.#disposed; 200 } 201 202 /** 203 * Alias for `[Symbol.asyncDispose]()`. 204 */ 205 async dispose(): Promise<void> { 206 await this[asyncDisposeSymbol](); 207 } 208 209 /** 210 * Adds a AsyncDisposable resource to the top of stack, returning the resource. 211 * Has no effect if provided `null` or `undefined`. 212 * 213 * @param value - A `AsyncDisposable` object, `null`, or `undefined`. 214 * `null` and `undefined` will not be added, but will be returned. 215 * @returns The provided `value`. 216 */ 217 use<T extends AsyncDisposable | Disposable | null | undefined>(value: T): T { 218 if (value) { 219 const asyncDispose = (value as AsyncDisposable)[asyncDisposeSymbol]; 220 const dispose = (value as Disposable)[disposeSymbol]; 221 222 if (typeof asyncDispose === 'function') { 223 this.#stack.push(value as AsyncDisposable); 224 } else if (typeof dispose === 'function') { 225 this.#stack.push({ 226 [asyncDisposeSymbol]: async () => { 227 (value as Disposable)[disposeSymbol](); 228 }, 229 }); 230 } 231 } 232 233 return value; 234 } 235 236 /** 237 * Adds a non-disposable resource and a disposal callback to the top of the stack. 238 * 239 * @param value - A resource to be disposed. 240 * @param onDispose - A callback invoked to dispose the provided value. 241 * Will be invoked with `value` as the first parameter. 242 * @returns The provided `value`. 243 */ 244 adopt<T>(value: T, onDispose: (value: T) => Promise<void>): T { 245 this.#stack.push({ 246 [asyncDisposeSymbol]() { 247 return onDispose(value); 248 }, 249 }); 250 return value; 251 } 252 253 /** 254 * Add a disposal callback to the top of the stack to be invoked when stack is disposed. 255 * @param onDispose - A callback to invoke when this object is disposed. 256 */ 257 defer(onDispose: () => Promise<void>): void { 258 this.#stack.push({ 259 [asyncDisposeSymbol]() { 260 return onDispose(); 261 }, 262 }); 263 } 264 265 /** 266 * Move all resources out of this stack and into a new `DisposableStack`, and 267 * marks this stack as disposed. 268 * @returns The new `AsyncDisposableStack`. 269 * 270 * @example 271 * 272 * ```ts 273 * class C { 274 * #res1: Disposable; 275 * #res2: Disposable; 276 * #disposables: DisposableStack; 277 * constructor() { 278 * // stack will be disposed when exiting constructor for any reason 279 * using stack = new DisposableStack(); 280 * 281 * // get first resource 282 * this.#res1 = stack.use(getResource1()); 283 * 284 * // get second resource. If this fails, both `stack` and `#res1` will be disposed. 285 * this.#res2 = stack.use(getResource2()); 286 * 287 * // all operations succeeded, move resources out of `stack` so that 288 * // they aren't disposed when constructor exits 289 * this.#disposables = stack.move(); 290 * } 291 * 292 * [disposeSymbol]() { 293 * this.#disposables.dispose(); 294 * } 295 * } 296 * ``` 297 */ 298 move(): AsyncDisposableStack { 299 if (this.#disposed) { 300 throw new ReferenceError('A disposed stack can not use anything new'); 301 } 302 const stack = new AsyncDisposableStack(); 303 stack.#stack = this.#stack; 304 this.#stack = []; 305 this.#disposed = true; 306 return stack; 307 } 308 309 /** 310 * Disposes each resource in the stack in last-in-first-out (LIFO) manner. 311 */ 312 async [asyncDisposeSymbol](): Promise<void> { 313 if (this.#disposed) { 314 return; 315 } 316 this.#disposed = true; 317 const errors: unknown[] = []; 318 for (const resource of this.#stack.reverse()) { 319 try { 320 await resource[asyncDisposeSymbol](); 321 } catch (e) { 322 errors.push(e); 323 } 324 } 325 if (errors.length === 1) { 326 throw errors[0]; 327 } else if (errors.length > 1) { 328 let suppressed = null; 329 for (const error of errors.reverse()) { 330 if (suppressed === null) { 331 suppressed = error; 332 } else { 333 suppressed = new SuppressedError(error, suppressed); 334 } 335 } 336 throw suppressed; 337 } 338 } 339 340 readonly [Symbol.toStringTag] = 'AsyncDisposableStack'; 341 } 342 343 /** 344 * @internal 345 * Represents an error that occurs when multiple errors are thrown during 346 * the disposal of resources. This class encapsulates the primary error and 347 * any suppressed errors that occurred subsequently. 348 */ 349 export class SuppressedError extends Error { 350 #error: unknown; 351 #suppressed: unknown; 352 353 constructor( 354 error: unknown, 355 suppressed: unknown, 356 message = 'An error was suppressed during disposal', 357 ) { 358 super(message); 359 this.name = 'SuppressedError'; 360 this.#error = error; 361 this.#suppressed = suppressed; 362 } 363 364 /** 365 * The primary error that occurred during disposal. 366 */ 367 get error(): unknown { 368 return this.#error; 369 } 370 371 /** 372 * The suppressed error i.e. the error that was suppressed 373 * because it occurred later in the flow after the original error. 374 */ 375 get suppressed(): unknown { 376 return this.#suppressed; 377 } 378 }