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 const timerId = setTimeout(() => { 131 throw new Error( 132 'asyncDispose timed out. This might be due to a Disposable ' + 133 'resource trying to issue cleanup queries on trace unload, ' + 134 'while the Wasm module was already destroyed ', 135 ); 136 }, 10_000); 137 await res[Symbol.asyncDispose](); 138 clearTimeout(timerId); 139 } 140 } 141 142 asyncDispose(): Promise<void> { 143 return this[Symbol.asyncDispose](); 144 } 145 146 adopt<T>(value: T, onDispose: (value: T) => Promise<void>): T { 147 this.resources.push({ 148 [Symbol.asyncDispose]: async () => onDispose(value), 149 }); 150 return value; 151 } 152 153 move(): AsyncDisposableStack { 154 const other = new AsyncDisposableStack(); 155 for (const res of this.resources) { 156 other.resources.push(res); 157 } 158 this.resources.length = 0; 159 return other; 160 } 161 162 readonly [Symbol.toStringTag]: string = 'AsyncDisposableStack'; 163 164 get disposed(): boolean { 165 return this.isDisposed; 166 } 167} 168