chromium/ash/webui/camera_app_ui/resources/js/mojo/util.ts

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {addUnloadCallback} from '../unload.js';
import {WaitableEvent} from '../waitable_event.js';

const windowUnload = new WaitableEvent();

export interface MojoEndpoint {
  $: {close: () => void};
}

addUnloadCallback(() => {
  windowUnload.signal();
});

const NEVER_SETTLED_PROMISE = new Promise<never>(
    () => {
        // This doesn't call the resolve function so the Promise will never
        // be resolved or rejected.
    });

/**
 * Wraps a mojo response promise so that we can handle the situation when the
 * call is dropped by window unload gracefully.
 *
 * @return Returns the mojo response which will be resolved when getting
 *     response or will never be resolved if the window unload is about to
 *     happen.
 */
async function wrapMojoResponse(call: unknown): Promise<unknown> {
  const result = await Promise.race([windowUnload.wait(), call]);
  if (windowUnload.isSignaled()) {
    return NEVER_SETTLED_PROMISE;
  }
  return result;
}

const mojoResponseHandler: ProxyHandler<MojoEndpoint> = {
  get: function(target, property) {
    const val = Reflect.get(target, property);
    if (val instanceof Function) {
      return (...args: unknown[]) => {
        if (windowUnload.isSignaled()) {
          // Don't try to call the mojo function if window is already unloaded,
          // since the connection would have already been closed, and there
          // would be uncaught exception if we try to call the mojo function.
          return NEVER_SETTLED_PROMISE;
        }
        return wrapMojoResponse(Reflect.apply(val, target, args));
      };
    }
    return val;
  },
};

/**
 * Closes the given mojo endpoint once the page is unloaded.
 * Reference b/176139064.
 */
function closeWhenUnload(endpoint: MojoEndpoint) {
  addUnloadCallback(() => closeEndpoint(endpoint));
}

/**
 * Returns a proxy of |endpoint|.
 *
 * The |endpoint| is automatically closed on window unload.
 * Note that the methods on the returned proxy will not be resolved after unload
 * event on window is triggered to avoid race condition during the window
 * unloading.
 */
export function wrapEndpoint<T extends MojoEndpoint>(endpoint: T): T {
  closeWhenUnload(endpoint);
  // The mojoResponseHandler is designed to be able to handle all mojo
  // connection proxies.
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  return new Proxy(endpoint, mojoResponseHandler as ProxyHandler<T>);
}

/**
 * Closes the target mojo |endpoint|.
 */
export function closeEndpoint(endpoint: MojoEndpoint): void {
  endpoint.$.close();
}

/**
 * Returns a fake endpoint using proxy.
 */
export function fakeEndpoint<T>(): T {
  // Disable type assertion since it is intended to make all function calls as
  // no-ops.
  const handler = {
    apply: (): unknown => new Proxy(() => {}, handler),
    get: (): unknown => new Proxy(() => {}, handler),
  };
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  return new Proxy({}, handler) as T;
}