xref: /aosp_15_r20/external/perfetto/ui/src/base/shared_disposable.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
15import {assertFalse} from './logging';
16
17/**
18 * Adds reference counting to an AsyncDisposable.
19 *
20 * Allows you to share a disposable amongst multiple different entities which
21 * may have differing lifecycles, and only have the resource cleared up once all
22 * components have called dispose on it.
23 *
24 * @example
25 * ```ts
26 * {
27 *   // Create a shared disposable around an arbitrary disposable resource
28 *   await using sharedResource = SharedAsyncDisposable.wrap(resource);
29 *
30 *   // Pass a the shared resource somewhere else (notice we don't await here,
31 *   // which detaches their lifecycle from ours - i.e. we don't know which task
32 *   // will finish first, us or them)
33 *   doStuff(sharedResource);
34 *
35 *   // Do something with the resource
36 *   await sharedResource.get().doStuff(...);
37 *
38 *   // Our shard resource is disposed here, but the underlying resource will
39 *   // only be disposed once doStuff() is done with it too
40 * }
41 *
42 * // --cut--
43 *
44 * async function doStuff(shared) {
45 *   await using res = shared.clone();
46 *
47 *   // Do stuff with the resource
48 *   await res.get().doStuff(...);
49 *
50 *   // res is automatically disposed here
51 * }
52 * ```
53 */
54export class SharedAsyncDisposable<T extends AsyncDisposable>
55  implements AsyncDisposable
56{
57  // A shared core which is referenced by al instances of this class used to
58  // store the reference count
59  private readonly sharedCore: {refCount: number};
60
61  // This is our underlying disposable
62  private readonly disposable: T;
63
64  // Record of whether this instance is disposed (not whether the underlying
65  // instance is disposed)
66  private _isDisposed = false;
67
68  /**
69   * Create a new shared disposable object from an arbitrary disposable.
70   *
71   * @param disposable The disposable object to wrap.
72   * @returns A new SharedAsyncDisposable object.
73   */
74  static wrap<T extends AsyncDisposable>(
75    disposable: T,
76  ): SharedAsyncDisposable<T> {
77    return new SharedAsyncDisposable(disposable);
78  }
79
80  private constructor(disposable: T, sharedCore?: {refCount: number}) {
81    this.disposable = disposable;
82    if (!sharedCore) {
83      this.sharedCore = {refCount: 1};
84    } else {
85      this.sharedCore = sharedCore;
86    }
87  }
88
89  /**
90   * Check whether this is disposed. If true, clone() and
91   * [Symbol.asyncDispose]() will return throw. Can be used to check state
92   * before cloning.
93   */
94  get isDisposed(): boolean {
95    return this._isDisposed;
96  }
97
98  /**
99   * @returns The underlying disposable.
100   */
101  get(): T {
102    return this.disposable;
103  }
104
105  /**
106   * Create a clone of this object, incrementing the reference count.
107   *
108   * @returns A new shared disposable instance.
109   */
110  clone(): SharedAsyncDisposable<T> {
111    // Cloning again after dispose indicates invalid usage
112    assertFalse(this._isDisposed);
113
114    this.sharedCore.refCount++;
115    return new SharedAsyncDisposable(this.disposable, this.sharedCore);
116  }
117
118  /**
119   * Dispose of this object, decrementing the reference count. If the reference
120   * count drops to 0, the underlying disposable is disposed.
121   */
122  async [Symbol.asyncDispose](): Promise<void> {
123    // Disposing multiple times indicates invalid usage
124    assertFalse(this._isDisposed);
125
126    this._isDisposed = true;
127    this.sharedCore.refCount--;
128
129    if (this.sharedCore.refCount === 0) {
130      await this.disposable[Symbol.asyncDispose]();
131    }
132  }
133}
134