1// Copyright (C) 2024 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15/** 16 * Implementations of DisposableStack and AsyncDisposableStack. 17 * 18 * These are defined in the "ECMAScript Explicit Resource Management" proposal 19 * which is currently at stage 3, which means "No changes to the proposal are 20 * expected, but some necessary changes may still occur due to web 21 * incompatibilities or feedback from production-grade implementations." 22 * 23 * Reference 24 * - https://github.com/tc39/proposal-explicit-resource-management 25 * - https://tc39.es/process-document/ 26 * 27 * These classes are purposely not polyfilled to avoid confusion and aid 28 * debug-ability and traceability. 29 */ 30 31export class DisposableStack implements Disposable { 32 private readonly resources: Disposable[]; 33 private isDisposed = false; 34 35 constructor() { 36 this.resources = []; 37 } 38 39 use<T extends Disposable | null | undefined>(res: T): T { 40 if (res == null) return res; 41 this.resources.push(res); 42 return res; 43 } 44 45 defer(onDispose: () => void) { 46 this.resources.push({ 47 [Symbol.dispose]: onDispose, 48 }); 49 } 50 51 // TODO(stevegolton): Handle error suppression properly 52 // https://github.com/tc39/proposal-explicit-resource-management?tab=readme-ov-file#aggregation 53 [Symbol.dispose](): void { 54 this.isDisposed = true; 55 while (true) { 56 const res = this.resources.pop(); 57 if (res === undefined) { 58 break; 59 } 60 res[Symbol.dispose](); 61 } 62 } 63 64 dispose(): void { 65 this[Symbol.dispose](); 66 } 67 68 adopt<T>(value: T, onDispose: (value: T) => void): T { 69 this.resources.push({ 70 [Symbol.dispose]: () => onDispose(value), 71 }); 72 return value; 73 } 74 75 move(): DisposableStack { 76 const other = new DisposableStack(); 77 for (const res of this.resources) { 78 other.resources.push(res); 79 } 80 this.resources.length = 0; 81 return other; 82 } 83 84 readonly [Symbol.toStringTag]: string = 'DisposableStack'; 85 86 get disposed(): boolean { 87 return this.isDisposed; 88 } 89} 90 91export class AsyncDisposableStack implements AsyncDisposable { 92 private readonly resources: AsyncDisposable[]; 93 private isDisposed = false; 94 95 constructor() { 96 this.resources = []; 97 } 98 99 use<T extends Disposable | AsyncDisposable | null | undefined>(res: T): T { 100 if (res == null) return res; 101 102 if (Symbol.asyncDispose in res) { 103 this.resources.push(res); 104 } else if (Symbol.dispose in res) { 105 this.resources.push({ 106 [Symbol.asyncDispose]: async () => { 107 res[Symbol.dispose](); 108 }, 109 }); 110 } 111 112 return res; 113 } 114 115 defer(onDispose: () => Promise<void>) { 116 this.resources.push({ 117 [Symbol.asyncDispose]: onDispose, 118 }); 119 } 120 121 // TODO(stevegolton): Handle error suppression properly 122 // https://github.com/tc39/proposal-explicit-resource-management?tab=readme-ov-file#aggregation 123 async [Symbol.asyncDispose](): Promise<void> { 124 this.isDisposed = true; 125 while (true) { 126 const res = this.resources.pop(); 127 if (res === undefined) { 128 break; 129 } 130 await res[Symbol.asyncDispose](); 131 } 132 } 133 134 asyncDispose(): Promise<void> { 135 return this[Symbol.asyncDispose](); 136 } 137 138 adopt<T>(value: T, onDispose: (value: T) => Promise<void>): T { 139 this.resources.push({ 140 [Symbol.asyncDispose]: async () => onDispose(value), 141 }); 142 return value; 143 } 144 145 move(): AsyncDisposableStack { 146 const other = new AsyncDisposableStack(); 147 for (const res of this.resources) { 148 other.resources.push(res); 149 } 150 this.resources.length = 0; 151 return other; 152 } 153 154 readonly [Symbol.toStringTag]: string = 'AsyncDisposableStack'; 155 156 get disposed(): boolean { 157 return this.isDisposed; 158 } 159} 160