// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from './assert.js';
import {PromiseResolver} from './promise_resolver.js';
export interface WebUiListener {
eventName: string;
uid: number;
}
/** Counter for use with createUid */
let uidCounter: number = 1;
/** @return A new unique ID. */
function createUid(): number {
return uidCounter++;
}
/**
* The mapping used by the sendWithPromise mechanism to tie the Promise
* returned to callers with the corresponding WebUI response. The mapping is
* from ID to the PromiseResolver helper; the ID is generated by
* sendWithPromise and is unique across all invocations of said method.
*/
const chromeSendResolverMap: {[id: string]: PromiseResolver<any>} = {};
/**
* The named method the WebUI handler calls directly in response to a
* chrome.send call that expects a response. The handler requires no knowledge
* of the specific name of this method, as the name is passed to the handler
* as the first argument in the arguments list of chrome.send. The handler
* must pass the ID, also sent via the chrome.send arguments list, as the
* first argument of the JS invocation; additionally, the handler may
* supply any number of other arguments that will be included in the response.
* @param id The unique ID identifying the Promise this response is
* tied to.
* @param isSuccess Whether the request was successful.
* @param response The response as sent from C++.
*/
export function webUIResponse(id: string, isSuccess: boolean, response: any) {
const resolver = chromeSendResolverMap[id];
assert(resolver);
delete chromeSendResolverMap[id];
if (isSuccess) {
resolver.resolve(response);
} else {
resolver.reject(response);
}
}
/**
* A variation of chrome.send, suitable for messages that expect a single
* response from C++.
* @param methodName The name of the WebUI handler API.
* @param args Variable number of arguments to be forwarded to the
* C++ call.
*/
export function sendWithPromise(
methodName: string, ...args: any[]): Promise<any> {
const promiseResolver = new PromiseResolver();
const id = methodName + '_' + createUid();
chromeSendResolverMap[id] = promiseResolver;
chrome.send(methodName, [id].concat(args));
return promiseResolver.promise;
}
/**
* A map of maps associating event names with listeners. The 2nd level map
* associates a listener ID with the callback function, such that individual
* listeners can be removed from an event without affecting other listeners of
* the same event.
*/
const webUiListenerMap:
{[event: string]: {[listenerId: number]: Function}} = {};
/**
* The named method the WebUI handler calls directly when an event occurs.
* The WebUI handler must supply the name of the event as the first argument
* of the JS invocation; additionally, the handler may supply any number of
* other arguments that will be forwarded to the listener callbacks.
* @param event The name of the event that has occurred.
* @param args Additional arguments passed from C++.
*/
export function webUIListenerCallback(event: string, ...args: any[]) {
const eventListenersMap = webUiListenerMap[event];
if (!eventListenersMap) {
// C++ event sent for an event that has no listeners.
// TODO(dpapad): Should a warning be displayed here?
return;
}
for (const listenerId in eventListenersMap) {
eventListenersMap[listenerId]!.apply(null, args);
}
}
/**
* Registers a listener for an event fired from WebUI handlers. Any number of
* listeners may register for a single event.
* @param eventName The event to listen to.
* @param callback The callback run when the event is fired.
* @return An object to be used for removing a listener via
* removeWebUiListener. Should be treated as read-only.
*/
export function addWebUiListener(
eventName: string, callback: Function): WebUiListener {
webUiListenerMap[eventName] = webUiListenerMap[eventName] || {};
const uid = createUid();
webUiListenerMap[eventName]![uid] = callback;
return {eventName: eventName, uid: uid};
}
/**
* Removes a listener. Does nothing if the specified listener is not found.
* @param listener The listener to be removed (as returned by addWebUiListener).
* @return Whether the given listener was found and actually removed.
*/
export function removeWebUiListener(listener: WebUiListener): boolean {
const listenerExists = webUiListenerMap[listener.eventName] &&
webUiListenerMap[listener.eventName]![listener.uid];
if (listenerExists) {
const map = webUiListenerMap[listener.eventName]!;
delete map[listener.uid];
return true;
}
return false;
}
// Globally expose functions that must be called from C++.
type WindowAndCr = Window&{cr?: object};
assert(!(window as WindowAndCr).cr);
Object.assign(window, {cr: {webUIResponse, webUIListenerCallback}});