xref: /aosp_15_r20/external/perfetto/ui/src/base/disposable_stack.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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