From d11eed6d06f8b864a4240c4513822239289269bf Mon Sep 17 00:00:00 2001 From: Pi-Hsun Shih Date: Tue, 14 Dec 2021 05:30:44 +0000 Subject: [PATCH] CCA: Migrate comlink related files to TypeScript Also update comlink library from comlink.js to comlink.ts (version 4.3.1) for better typing support, since we're using TypeScript now. Bug: b:172340451 Test: tsc compiles Test: tast run camera.CCAUISmoke* camera.CCAUIStress* Change-Id: I8f19bcb06da3a644c17b2197f51d02fba53381bd Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3292652 Reviewed-by: Shik Chen Commit-Queue: Pi-Hsun Shih Cr-Commit-Position: refs/heads/main@{#951373} --- .../resources/js/externs/types.d.ts | 6 +- .../resources/js/{init.js => init.ts} | 3 +- ash/webui/camera_app_ui/resources/js/js.gni | 11 +- .../camera_app_ui/resources/js/lib/comlink.js | 297 ---------- .../camera_app_ui/resources/js/lib/comlink.ts | 553 ++++++++++++++++++ .../resources/js/lib/comlink_protocol.ts | 109 ++++ .../camera_app_ui/resources/js/metrics.ts | 4 +- .../js/models/video_processor_interface.js | 12 + .../models/{video_saver.js => video_saver.ts} | 133 ++--- .../js/{test_bridge.js => test_bridge.ts} | 21 +- ...js => untrusted_video_processor_helper.ts} | 20 +- ash/webui/camera_app_ui/resources/js/util.ts | 12 +- .../camera_app_ui/resources/utils/cca.py | 6 +- 13 files changed, 757 insertions(+), 430 deletions(-) rename ash/webui/camera_app_ui/resources/js/{init.js => init.ts} (92%) delete mode 100644 ash/webui/camera_app_ui/resources/js/lib/comlink.js create mode 100644 ash/webui/camera_app_ui/resources/js/lib/comlink.ts create mode 100644 ash/webui/camera_app_ui/resources/js/lib/comlink_protocol.ts rename ash/webui/camera_app_ui/resources/js/models/{video_saver.js => video_saver.ts} (50%) rename ash/webui/camera_app_ui/resources/js/{test_bridge.js => test_bridge.ts} (81%) rename ash/webui/camera_app_ui/resources/js/{untrusted_video_processor_helper.js => untrusted_video_processor_helper.ts} (60%) diff --git a/ash/webui/camera_app_ui/resources/js/externs/types.d.ts b/ash/webui/camera_app_ui/resources/js/externs/types.d.ts index 4f4c95df25b859..9748b1dbc1fc1d 100644 --- a/ash/webui/camera_app_ui/resources/js/externs/types.d.ts +++ b/ash/webui/camera_app_ui/resources/js/externs/types.d.ts @@ -251,9 +251,9 @@ type CreateScriptURLCallback = (input: string, arguments: any) => string; interface TrustedTypePolicy { readonly name: string; - createHTML(input: string, arguments: any): TrustedHTML; - createScript(input: string, arguments: any): TrustedScript; - createScriptURL(input: string, arguments: any): TrustedScriptURL; + createHTML(input: string, arguments?: any): TrustedHTML; + createScript(input: string, arguments?: any): TrustedScript; + createScriptURL(input: string, arguments?: any): TrustedScriptURL; } interface TrustedTypePolicyFactory { diff --git a/ash/webui/camera_app_ui/resources/js/init.js b/ash/webui/camera_app_ui/resources/js/init.ts similarity index 92% rename from ash/webui/camera_app_ui/resources/js/init.js rename to ash/webui/camera_app_ui/resources/js/init.ts index 70148b0b2241e4..a94c989fce1245 100644 --- a/ash/webui/camera_app_ui/resources/js/init.js +++ b/ash/webui/camera_app_ui/resources/js/init.ts @@ -12,11 +12,12 @@ import '/strings.m.js'; import * as Comlink from './lib/comlink.js'; +import {TestBridge} from './test_bridge.js'; document.addEventListener('DOMContentLoaded', async () => { const workerPath = '/js/test_bridge.js'; const sharedWorker = new SharedWorker(workerPath, {type: 'module'}); - const testBridge = Comlink.wrap(sharedWorker.port); + const testBridge = Comlink.wrap(sharedWorker.port); const appWindow = await testBridge.bindWindow(window.location.href); // TODO(crbug.com/980846): Refactor to use a better way rather than window // properties to pass data to other modules. diff --git a/ash/webui/camera_app_ui/resources/js/js.gni b/ash/webui/camera_app_ui/resources/js/js.gni index 05b936baf10269..653f21980daf10 100644 --- a/ash/webui/camera_app_ui/resources/js/js.gni +++ b/ash/webui/camera_app_ui/resources/js/js.gni @@ -24,9 +24,10 @@ compile_js_files = [ "geometry.js", "h264.js", "i18n_string.js", - "init.js", + "init.ts", "intent.js", - "lib/comlink.js", + "lib/comlink.ts", + "lib/comlink_protocol.ts", "lib/ffmpeg.js", "main.js", "metrics.ts", @@ -46,7 +47,7 @@ compile_js_files = [ "models/local_storage.js", "models/result_saver.js", "models/video_processor_interface.js", - "models/video_saver.js", + "models/video_saver.ts", "mojo/chrome_helper.ts", "mojo/device_operator.js", "mojo/image_capture.js", @@ -58,7 +59,7 @@ compile_js_files = [ "snackbar.js", "sound.js", "state.js", - "test_bridge.js", + "test_bridge.ts", "thumbnailer.js", "timer.ts", "toast.js", @@ -68,7 +69,7 @@ compile_js_files = [ "untrusted_ga_helper.js", "untrusted_helper_interfaces.js", "untrusted_script_loader.js", - "untrusted_video_processor_helper.js", + "untrusted_video_processor_helper.ts", "util.ts", "views/camera_intent.js", "views/camera.js", diff --git a/ash/webui/camera_app_ui/resources/js/lib/comlink.js b/ash/webui/camera_app_ui/resources/js/lib/comlink.js deleted file mode 100644 index 8bab0288d48117..00000000000000 --- a/ash/webui/camera_app_ui/resources/js/lib/comlink.js +++ /dev/null @@ -1,297 +0,0 @@ -/** - * Copyright 2019 Google Inc. All Rights Reserved. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable */ -const proxyMarker = Symbol('Comlink.proxy'); -const createEndpoint = Symbol('Comlink.endpoint'); -const releaseProxy = Symbol('Comlink.releaseProxy'); -const throwSet = new WeakSet(); -const transferHandlers = new Map([ - [ - 'proxy', { - canHandle: obj => obj && obj[proxyMarker], - serialize(obj) { - const {port1, port2} = new MessageChannel(); - expose(obj, port1); - return [port2, [port2]]; - }, - deserialize: (port) => { - port.start(); - return wrap(port); - } - } - ], - [ - 'throw', { - canHandle: obj => throwSet.has(obj), - serialize(obj) { - const isError = obj instanceof Error; - let serialized = obj; - if (isError) { - serialized = {isError, message: obj.message, stack: obj.stack}; - } - return [serialized, []]; - }, - deserialize(obj) { - if (obj.isError) { - throw Object.assign(new Error(), obj); - } - throw obj; - } - } - ] -]); - - -/** - * @param {*} obj - * @param {*} ep - */ -function expose(obj, ep = self) { - ep.addEventListener('message', function callback(ev) { - if (!ev || !ev.data) { - return; - } - const {id, type, path} = Object.assign({path: []}, ev.data); - const argumentList = (ev.data.argumentList || []).map(fromWireValue); - let returnValue; - try { - const parent = path.slice(0, -1).reduce((obj, prop) => obj[prop], obj); - const rawValue = path.reduce((obj, prop) => obj[prop], obj); - switch (type) { - case 0 /* GET */: { - returnValue = rawValue; - } break; - case 1 /* SET */: { - parent[path.slice(-1)[0]] = fromWireValue(ev.data.value); - returnValue = true; - } break; - case 2 /* APPLY */: { - returnValue = rawValue.apply(parent, argumentList); - } break; - case 3 /* CONSTRUCT */: { - const value = new rawValue(...argumentList); - returnValue = proxy(value); - } break; - case 4 /* ENDPOINT */: { - const {port1, port2} = new MessageChannel(); - expose(obj, port2); - returnValue = transfer(port1, [port1]); - } break; - case 5 /* RELEASE */: { - returnValue = undefined; - } break; - } - } catch (e) { - returnValue = e; - throwSet.add(e); - } - Promise.resolve(returnValue) - .catch(e => { - throwSet.add(e); - return e; - }) - .then(returnValue => { - const [wireValue, transferables] = toWireValue(returnValue); - ep.postMessage( - Object.assign(Object.assign({}, wireValue), {id}), transferables); - if (type === 5 /* RELEASE */) { - // detach and deactive after sending release response above. - ep.removeEventListener('message', callback); - closeEndPoint(ep); - } - }); - }); - if (ep.start) { - ep.start(); - } -} -function isMessagePort(endpoint) { - return endpoint.constructor.name === 'MessagePort'; -} -function closeEndPoint(endpoint) { - if (isMessagePort(endpoint)) - endpoint.close(); -} -function wrap(ep, target = undefined) { - return createProxy(ep, [], target); -} -function throwIfProxyReleased(isReleased) { - if (isReleased) { - throw new Error('Proxy has been released and is not useable'); - } -} -function createProxy(ep, path = [], target = function() {}) { - let isProxyReleased = false; - const proxy = new Proxy(target, { - get(_target, prop) { - throwIfProxyReleased(isProxyReleased); - if (prop === releaseProxy) { - return () => { - return requestResponseMessage( - ep, - {type: 5 /* RELEASE */, path: path.map(p => p.toString())}) - .then(() => { - closeEndPoint(ep); - isProxyReleased = true; - }); - }; - } - if (prop === 'then') { - if (path.length === 0) { - return {then: () => proxy}; - } - const r = requestResponseMessage(ep, { - type: 0 /* GET */, - path: path.map(p => p.toString()) - }).then(fromWireValue); - return r.then.bind(r); - } - return createProxy(ep, [...path, prop]); - }, - // @ts-ignore - set(_target, prop, rawValue) { - throwIfProxyReleased(isProxyReleased); - // FIXME: ES6 Proxy Handler `set` methods are supposed to return a - // boolean. To show good will, we return true asynchronously ¯\_(ツ)_/¯ - const [value, transferables] = toWireValue(rawValue); - return requestResponseMessage( - ep, { - type: 1 /* SET */, - path: [...path, prop].map(p => p.toString()), - value - }, - transferables) - .then(fromWireValue); - }, - apply(_target, _thisArg, rawArgumentList) { - throwIfProxyReleased(isProxyReleased); - const last = path[path.length - 1]; - if (last === createEndpoint) { - return requestResponseMessage(ep, { - type: 4 /* ENDPOINT */ - }) - .then(fromWireValue); - } - // We just pretend that `bind()` didn’t happen. - if (last === 'bind') { - return createProxy(ep, path.slice(0, -1)); - } - const [argumentList, transferables] = processArguments(rawArgumentList); - return requestResponseMessage( - ep, { - type: 2 /* APPLY */, - path: path.map(p => p.toString()), - argumentList - }, - transferables) - .then(fromWireValue); - }, - construct(_target, rawArgumentList) { - throwIfProxyReleased(isProxyReleased); - const [argumentList, transferables] = processArguments(rawArgumentList); - return requestResponseMessage( - ep, { - type: 3 /* CONSTRUCT */, - path: path.map(p => p.toString()), - argumentList - }, - transferables) - .then(fromWireValue); - } - }); - return proxy; -} -function myFlat(arr) { - return Array.prototype.concat.apply([], arr); -} -function processArguments(argumentList) { - const processed = argumentList.map(toWireValue); - return [processed.map(v => v[0]), myFlat(processed.map(v => v[1]))]; -} -const transferCache = new WeakMap(); -function transfer(obj, transfers) { - transferCache.set(obj, transfers); - return obj; -} - -/** - * @template T - * @param {T} obj - * @return {T} - */ -function proxy(obj) { - return Object.assign(obj, {[proxyMarker]: true}); -} -function windowEndpoint(w, context = self, targetOrigin = '*') { - return { - postMessage: (msg, transferables) => - w.postMessage(msg, targetOrigin, transferables), - addEventListener: context.addEventListener.bind(context), - removeEventListener: context.removeEventListener.bind(context) - }; -} -function toWireValue(value) { - for (const [name, handler] of transferHandlers) { - if (handler.canHandle(value)) { - const [serializedValue, transferables] = handler.serialize(value); - return [ - {type: 3 /* HANDLER */, name, value: serializedValue}, transferables - ]; - } - } - return [{type: 0 /* RAW */, value}, transferCache.get(value) || []]; -} -function fromWireValue(value) { - switch (value.type) { - case 3 /* HANDLER */: - return transferHandlers.get(value.name).deserialize(value.value); - case 0 /* RAW */: - return value.value; - } -} -function requestResponseMessage(ep, msg, transfers = undefined) { - return new Promise(resolve => { - const id = generateUUID(); - ep.addEventListener('message', function l(ev) { - if (!ev.data || !ev.data.id || ev.data.id !== id) { - return; - } - ep.removeEventListener('message', l); - resolve(ev.data); - }); - if (ep.start) { - ep.start(); - } - ep.postMessage(Object.assign({id}, msg), transfers); - }); -} -function generateUUID() { - return new Array(4) - .fill(0) - .map( - () => - Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16)) - .join('-'); -} - -export { - createEndpoint, - expose, - proxy, - proxyMarker, - releaseProxy, - transfer, - transferHandlers, - windowEndpoint, - wrap -}; diff --git a/ash/webui/camera_app_ui/resources/js/lib/comlink.ts b/ash/webui/camera_app_ui/resources/js/lib/comlink.ts new file mode 100644 index 00000000000000..133be2b7ef16d8 --- /dev/null +++ b/ash/webui/camera_app_ui/resources/js/lib/comlink.ts @@ -0,0 +1,553 @@ +/** + * Copyright 2019 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable */ + +import { + Endpoint, + EventSource, + Message, + MessageType, + PostMessageWithOrigin, + WireValue, + WireValueType, +} from './comlink_protocol'; + +export {Endpoint}; + +export const proxyMarker = Symbol('Comlink.proxy'); +export const createEndpoint = Symbol('Comlink.endpoint'); +export const releaseProxy = Symbol('Comlink.releaseProxy'); + +const throwMarker = Symbol('Comlink.thrown'); + +/** + * Interface of values that were marked to be proxied with `comlink.proxy()`. + * Can also be implemented by classes. + */ +export interface ProxyMarked { + [proxyMarker]: true; +} + +/** + * Takes a type and wraps it in a Promise, if it not already is one. + * This is to avoid `Promise>`. + * + * This is the inverse of `Unpromisify`. + */ +type Promisify = T extends Promise? T : Promise; +/** + * Takes a type that may be Promise and unwraps the Promise type. + * If `P` is not a Promise, it returns `P`. + * + * This is the inverse of `Promisify`. + */ +type Unpromisify

= P extends Promise? T : P; + +/** + * Takes the raw type of a remote property and returns the type that is visible + * to the local thread on the proxy. + * + * Note: This needs to be its own type alias, otherwise it will not distribute + * over unions. See + * https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types + */ +type RemoteProperty = + // If the value is a method, comlink will proxy it automatically. + // Objects are only proxied if they are marked to be proxied. + // Otherwise, the property is converted to a Promise that resolves the + // cloned value. + T extends Function|ProxyMarked ? Remote: Promisify; + +/** + * Takes the raw type of a property as a remote thread would see it through a + * proxy (e.g. when passed in as a function argument) and returns the type that + * the local thread has to supply. + * + * This is the inverse of `RemoteProperty`. + * + * Note: This needs to be its own type alias, otherwise it will not distribute + * over unions. See + * https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types + */ +type LocalProperty = + T extends Function|ProxyMarked ? Local: Unpromisify; + +/** + * Proxies `T` if it is a `ProxyMarked`, clones it otherwise (as handled by + * structured cloning and transfer handlers). + */ +export type ProxyOrClone = T extends ProxyMarked ? Remote: T; +/** + * Inverse of `ProxyOrClone`. + */ +export type UnproxyOrClone = + T extends RemoteObject? Local: T; + +/** + * Takes the raw type of a remote object in the other thread and returns the + * type as it is visible to the local thread when proxied with + * `Comlink.proxy()`. + * + * This does not handle call signatures, which is handled by the more general + * `Remote` type. + * + * @template T The raw type of a remote object as seen in the other thread. + */ +export type RemoteObject = { + [P in keyof T]: RemoteProperty +}; +/** + * Takes the type of an object as a remote thread would see it through a proxy + * (e.g. when passed in as a function argument) and returns the type that the + * local thread has to supply. + * + * This does not handle call signatures, which is handled by the more general + * `Local` type. + * + * This is the inverse of `RemoteObject`. + * + * @template T The type of a proxied object. + */ +export type LocalObject = { + [P in keyof T]: LocalProperty +}; + +/** + * Additional special comlink methods available on each proxy returned by + * `Comlink.wrap()`. + */ +export interface ProxyMethods { + [createEndpoint]: () => Promise; + [releaseProxy]: () => void; +} + +/** + * Takes the raw type of a remote object, function or class in the other thread + * and returns the type as it is visible to the local thread from the proxy + * return value of `Comlink.wrap()` or `Comlink.proxy()`. + */ +export type Remote = + // Handle properties + RemoteObject& + // Handle call signature (if present) + (T extends(...args: infer TArguments) => infer TReturn ? + (...args: {[I in keyof TArguments]: UnproxyOrClone}) => + Promisify>>: + unknown)& + // Handle construct signature (if present) + // The return of construct signatures is always proxied (whether marked or + // not) + (T extends {new (...args: infer TArguments): infer TInstance} ? + { + new (...args: + {[I in keyof TArguments]: UnproxyOrClone;}): + Promisify>; + } : + unknown)& + // Include additional special comlink methods available on the proxy. + ProxyMethods; + +/** + * Expresses that a type can be either a sync or async. + */ +type MaybePromise = Promise|T; + +/** + * Takes the raw type of a remote object, function or class as a remote thread + * would see it through a proxy (e.g. when passed in as a function argument) and + * returns the type the local thread has to supply. + * + * This is the inverse of `Remote`. It takes a `Remote` and returns its + * original input `T`. + */ +export type Local = + // Omit the special proxy methods (they don't need to be supplied, comlink + // adds them) + Omit, keyof ProxyMethods>& + // Handle call signatures (if present) + (T extends(...args: infer TArguments) => infer TReturn ? + (...args: { + [I in keyof TArguments]: ProxyOrClone + }) => // The raw function could either be sync or async, but is always + // proxied automatically + MaybePromise>>: + unknown)& + // Handle construct signature (if present) + // The return of construct signatures is always proxied (whether marked or + // not) + (T extends {new (...args: infer TArguments): infer TInstance} ? { + new (...args: {[I in keyof TArguments]: ProxyOrClone;}): + // The raw constructor could either be sync or async, but is always + // proxied automatically + MaybePromise>>; + } : unknown); + +const isObject = (val: unknown): val is object => + (typeof val === 'object' && val !== null) || typeof val === 'function'; + +/** + * Customizes the serialization of certain values as determined by + * `canHandle()`. + * + * @template T The input type being handled by this transfer handler. + * @template S The serialized type sent over the wire. + */ +export interface TransferHandler { + /** + * Gets called for every value to determine whether this transfer handler + * should serialize the value, which includes checking that it is of the right + * type (but can perform checks beyond that as well). + */ + canHandle(value: unknown): value is T; + + /** + * Gets called with the value if `canHandle()` returned `true` to produce a + * value that can be sent in a message, consisting of structured-cloneable + * values and/or transferrable objects. + */ + serialize(value: T): [S, Transferable[]]; + + /** + * Gets called to deserialize an incoming value that was serialized in the + * other thread with this transfer handler (known through the name it was + * registered under). + */ + deserialize(value: S): T; +} + +/** + * Internal transfer handle to handle objects marked to proxy. + */ +const proxyTransferHandler: TransferHandler = { + canHandle: (val): val is ProxyMarked => + isObject(val) && (val as ProxyMarked)[proxyMarker], + serialize(obj) { + const {port1, port2} = new MessageChannel(); + expose(obj, port1); + return [port2, [port2]]; + }, + deserialize(port) { + port.start(); + return wrap(port); + }, +}; + +interface ThrownValue { + [throwMarker]: unknown; // just needs to be present + value: unknown; +} +type SerializedThrownValue =|{ + isError: true; + value: Error +} +|{ + isError: false; + value: unknown +}; + +/** + * Internal transfer handler to handle thrown exceptions. + */ +const throwTransferHandler: + TransferHandler = { + canHandle: (value): value is ThrownValue => + isObject(value) && throwMarker in value, + serialize({value}) { + let serialized: SerializedThrownValue; + if (value instanceof Error) { + serialized = { + isError: true, + value: { + message: value.message, + name: value.name, + stack: value.stack, + }, + }; + } else { + serialized = {isError: false, value}; + } + return [serialized, []]; + }, + deserialize(serialized) { + if (serialized.isError) { + throw Object.assign( + new Error(serialized.value.message), serialized.value); + } + throw serialized.value; + }, + }; + +/** + * Allows customizing the serialization of certain values. + */ +export const transferHandlers = + new Map>([ + ['proxy', proxyTransferHandler], + ['throw', throwTransferHandler], + ]); + +export function expose(obj: any, ep: Endpoint = self as any) { + ep.addEventListener('message', function callback(ev: MessageEvent) { + if (!ev || !ev.data) { + return; + } + const {id, type, path} = { + path: [] as string[], + ...(ev.data as Message), + }; + const argumentList = (ev.data.argumentList || []).map(fromWireValue); + let returnValue; + try { + const parent = path.slice(0, -1).reduce((obj, prop) => obj[prop], obj); + const rawValue = path.reduce((obj, prop) => obj[prop], obj); + switch (type) { + case MessageType.GET: { + returnValue = rawValue; + } break; + case MessageType.SET: { + parent[path.slice(-1)[0]] = fromWireValue(ev.data.value); + returnValue = true; + } break; + case MessageType.APPLY: { + returnValue = rawValue.apply(parent, argumentList); + } break; + case MessageType.CONSTRUCT: { + const value = new rawValue(...argumentList); + returnValue = proxy(value); + } break; + case MessageType.ENDPOINT: { + const {port1, port2} = new MessageChannel(); + expose(obj, port2); + returnValue = transfer(port1, [port1]); + } break; + case MessageType.RELEASE: { + returnValue = undefined; + } break; + default: + return; + } + } catch (value) { + returnValue = {value, [throwMarker]: 0}; + } + Promise.resolve(returnValue) + .catch((value) => { + return {value, [throwMarker]: 0}; + }) + .then((returnValue) => { + const [wireValue, transferables] = toWireValue(returnValue); + ep.postMessage({...wireValue, id}, transferables); + if (type === MessageType.RELEASE) { + // detach and deactive after sending release response above. + ep.removeEventListener('message', callback as any); + closeEndPoint(ep); + } + }); + } as any); + if (ep.start) { + ep.start(); + } +} + +function isMessagePort(endpoint: Endpoint): endpoint is MessagePort { + return endpoint.constructor.name === 'MessagePort'; +} + +function closeEndPoint(endpoint: Endpoint) { + if (isMessagePort(endpoint)) + endpoint.close(); +} + +export function wrap(ep: Endpoint, target?: any): Remote { + return createProxy(ep, [], target) as any; +} + +function throwIfProxyReleased(isReleased: boolean) { + if (isReleased) { + throw new Error('Proxy has been released and is not useable'); + } +} + +function createProxy( + ep: Endpoint, path: (string|number|symbol)[] = [], + target: object = function() {}): Remote { + let isProxyReleased = false; + const proxy = new Proxy(target, { + get(_target, prop) { + throwIfProxyReleased(isProxyReleased); + if (prop === releaseProxy) { + return () => { + return requestResponseMessage(ep, { + type: MessageType.RELEASE, + path: path.map((p) => p.toString()), + }) + .then(() => { + closeEndPoint(ep); + isProxyReleased = true; + }); + }; + } + if (prop === 'then') { + if (path.length === 0) { + return {then: () => proxy}; + } + const r = requestResponseMessage(ep, { + type: MessageType.GET, + path: path.map((p) => p.toString()), + }).then(fromWireValue); + return r.then.bind(r); + } + return createProxy(ep, [...path, prop]); + }, + set(_target, prop, rawValue) { + throwIfProxyReleased(isProxyReleased); + // FIXME: ES6 Proxy Handler `set` methods are supposed to return a + // boolean. To show good will, we return true asynchronously ¯\_(ツ)_/¯ + const [value, transferables] = toWireValue(rawValue); + return requestResponseMessage( + ep, { + type: MessageType.SET, + path: [...path, prop].map((p) => p.toString()), + value, + }, + transferables) + .then(fromWireValue) as any; + }, + apply(_target, _thisArg, rawArgumentList) { + throwIfProxyReleased(isProxyReleased); + const last = path[path.length - 1]; + if ((last as any) === createEndpoint) { + return requestResponseMessage(ep, { + type: MessageType.ENDPOINT, + }) + .then(fromWireValue); + } + // We just pretend that `bind()` didn’t happen. + if (last === 'bind') { + return createProxy(ep, path.slice(0, -1)); + } + const [argumentList, transferables] = processArguments(rawArgumentList); + return requestResponseMessage( + ep, { + type: MessageType.APPLY, + path: path.map((p) => p.toString()), + argumentList, + }, + transferables) + .then(fromWireValue); + }, + construct(_target, rawArgumentList) { + throwIfProxyReleased(isProxyReleased); + const [argumentList, transferables] = processArguments(rawArgumentList); + return requestResponseMessage( + ep, { + type: MessageType.CONSTRUCT, + path: path.map((p) => p.toString()), + argumentList, + }, + transferables) + .then(fromWireValue); + }, + }); + return proxy as any; +} + +function myFlat(arr: (T|T[])[]): T[] { + return Array.prototype.concat.apply([], arr); +} + +function processArguments(argumentList: any[]): [WireValue[], Transferable[]] { + const processed = argumentList.map(toWireValue); + return [processed.map((v) => v[0]), myFlat(processed.map((v) => v[1]))]; +} + +const transferCache = new WeakMap(); +export function transfer(obj: T, transfers: Transferable[]): T { + transferCache.set(obj, transfers); + return obj; +} + +export function proxy(obj: T): T&ProxyMarked { + return Object.assign(obj, {[proxyMarker]: true}) as any; +} + +export function windowEndpoint( + w: PostMessageWithOrigin, context: EventSource = self, + targetOrigin = '*'): Endpoint { + return { + postMessage: (msg: any, transferables: Transferable[]) => + w.postMessage(msg, targetOrigin, transferables), + addEventListener: context.addEventListener.bind(context), + removeEventListener: context.removeEventListener.bind(context), + }; +} + +function toWireValue(value: any): [WireValue, Transferable[]] { + for (const [name, handler] of transferHandlers) { + if (handler.canHandle(value)) { + const [serializedValue, transferables] = handler.serialize(value); + return [ + { + type: WireValueType.HANDLER, + name, + value: serializedValue, + }, + transferables, + ]; + } + } + return [ + { + type: WireValueType.RAW, + value, + }, + transferCache.get(value) || [], + ]; +} + +function fromWireValue(value: WireValue): any { + switch (value.type) { + case WireValueType.HANDLER: + return transferHandlers.get(value.name)!.deserialize(value.value); + case WireValueType.RAW: + return value.value; + } +} + +function requestResponseMessage( + ep: Endpoint, msg: Message, + transfers?: Transferable[]): Promise { + return new Promise((resolve) => { + const id = generateUUID(); + ep.addEventListener('message', function l(ev: MessageEvent) { + if (!ev.data || !ev.data.id || ev.data.id !== id) { + return; + } + ep.removeEventListener('message', l as any); + resolve(ev.data); + } as any); + if (ep.start) { + ep.start(); + } + ep.postMessage({id, ...msg}, transfers); + }); +} + +function generateUUID(): string { + return new Array(4) + .fill(0) + .map( + () => + Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16)) + .join('-'); +} diff --git a/ash/webui/camera_app_ui/resources/js/lib/comlink_protocol.ts b/ash/webui/camera_app_ui/resources/js/lib/comlink_protocol.ts new file mode 100644 index 00000000000000..d434e51aa4cb41 --- /dev/null +++ b/ash/webui/camera_app_ui/resources/js/lib/comlink_protocol.ts @@ -0,0 +1,109 @@ +/** + * Copyright 2019 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable */ + +export interface EventSource { + addEventListener( + type: string, listener: EventListenerOrEventListenerObject, + options?: {}): void; + + removeEventListener( + type: string, listener: EventListenerOrEventListenerObject, + options?: {}): void; +} + +export interface PostMessageWithOrigin { + postMessage(message: any, targetOrigin: string, transfer?: Transferable[]): + void; +} + +export interface Endpoint extends EventSource { + postMessage(message: any, transfer?: Transferable[]): void; + + start?: () => void; +} + +export const enum WireValueType { + RAW = 'RAW', + PROXY = 'PROXY', + THROW = 'THROW', + HANDLER = 'HANDLER', +} + +export interface RawWireValue { + id?: string; + type: WireValueType.RAW; + value: {}; +} + +export interface HandlerWireValue { + id?: string; + type: WireValueType.HANDLER; + name: string; + value: unknown; +} + +export type WireValue = RawWireValue|HandlerWireValue; + +export type MessageID = string; + +export const enum MessageType { + GET = 'GET', + SET = 'SET', + APPLY = 'APPLY', + CONSTRUCT = 'CONSTRUCT', + ENDPOINT = 'ENDPOINT', + RELEASE = 'RELEASE', +} + +export interface GetMessage { + id?: MessageID; + type: MessageType.GET; + path: string[]; +} + +export interface SetMessage { + id?: MessageID; + type: MessageType.SET; + path: string[]; + value: WireValue; +} + +export interface ApplyMessage { + id?: MessageID; + type: MessageType.APPLY; + path: string[]; + argumentList: WireValue[]; +} + +export interface ConstructMessage { + id?: MessageID; + type: MessageType.CONSTRUCT; + path: string[]; + argumentList: WireValue[]; +} + +export interface EndpointMessage { + id?: MessageID; + type: MessageType.ENDPOINT; +} + +export interface ReleaseMessage { + id?: MessageID; + type: MessageType.RELEASE; + path: string[]; +} + +export type Message =|GetMessage|SetMessage|ApplyMessage|ConstructMessage| + EndpointMessage|ReleaseMessage; diff --git a/ash/webui/camera_app_ui/resources/js/metrics.ts b/ash/webui/camera_app_ui/resources/js/metrics.ts index a2623a258b0de9..ca4bb34242cf3e 100644 --- a/ash/webui/camera_app_ui/resources/js/metrics.ts +++ b/ash/webui/camera_app_ui/resources/js/metrics.ts @@ -29,8 +29,8 @@ let baseDimen: Map|null = null; const ready = new WaitableEvent(); -const gaHelper = util.createUntrustedJSModule('/js/untrusted_ga_helper.js') as - Promise; +const gaHelper = util.createUntrustedJSModule( + '/js/untrusted_ga_helper.js'); /** * Send the event to GA backend. diff --git a/ash/webui/camera_app_ui/resources/js/models/video_processor_interface.js b/ash/webui/camera_app_ui/resources/js/models/video_processor_interface.js index 568de9c5900cae..debc441f659de8 100644 --- a/ash/webui/camera_app_ui/resources/js/models/video_processor_interface.js +++ b/ash/webui/camera_app_ui/resources/js/models/video_processor_interface.js @@ -3,6 +3,10 @@ // found in the LICENSE file. import {assertNotReached} from '../assert.js'; +// eslint-disable-next-line no-unused-vars +import {AsyncWriter} from './async_writer.js'; +// eslint-disable-next-line no-unused-vars +import {VideoProcessorArgs} from './ffmpeg/video_processor_args.js'; /** * The interface for a video processor. All methods are marked as async since @@ -10,6 +14,14 @@ import {assertNotReached} from '../assert.js'; * @interface */ export class VideoProcessor { + /** + * @param {!AsyncWriter} output The output writer. + * @param {!VideoProcessorArgs} processorArgs + */ + constructor(output, processorArgs) { + assertNotReached(); + } + /** * Writes a chunk data into the processor. * @param {!Blob} blob diff --git a/ash/webui/camera_app_ui/resources/js/models/video_saver.js b/ash/webui/camera_app_ui/resources/js/models/video_saver.ts similarity index 50% rename from ash/webui/camera_app_ui/resources/js/models/video_saver.js rename to ash/webui/camera_app_ui/resources/js/models/video_saver.ts index 4b7c2c5ca58b80..ce01e1c605d03c 100644 --- a/ash/webui/camera_app_ui/resources/js/models/video_saver.js +++ b/ash/webui/camera_app_ui/resources/js/models/video_saver.ts @@ -2,13 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import {Intent} from '../intent.js'; // eslint-disable-line no-unused-vars +import {Intent} from '../intent.js'; import * as Comlink from '../lib/comlink.js'; import { MimeType, - Resolution, // eslint-disable-line no-unused-vars + Resolution, } from '../type.js'; -// eslint-disable-next-line no-unused-vars import {VideoProcessorHelperInterface} from '../untrusted_helper_interfaces.js'; import * as util from '../util.js'; @@ -18,47 +17,33 @@ import { createMp4Args, } from './ffmpeg/video_processor_args.js'; import {createPrivateTempVideoFile} from './file_system.js'; -// eslint-disable-next-line no-unused-vars import {FileAccessEntry} from './file_system_access_entry.js'; -// eslint-disable-next-line no-unused-vars import {VideoProcessor} from './video_processor_interface.js'; const FFMpegVideoProcessor = (async () => { const workerChannel = new MessageChannel(); - const videoProcessorHelper = /** @type {!VideoProcessorHelperInterface} */ ( - await util.createUntrustedJSModule( - '/js/untrusted_video_processor_helper.js')); + const videoProcessorHelper = + await util.createUntrustedJSModule( + '/js/untrusted_video_processor_helper.js'); await videoProcessorHelper.connectToWorker( Comlink.transfer(workerChannel.port2, [workerChannel.port2])); - return Comlink.wrap(workerChannel.port1); + return Comlink.wrap(workerChannel.port1); })(); -/** - * @param {!AsyncWriter} output - * @param {number} videoRotation - * @return {!Promise} - */ -async function createVideoProcessor(output, videoRotation) { - // Comlink proxies all calls asynchronously, including constructors. + +async function createVideoProcessor( + output: AsyncWriter, videoRotation: number): Promise { return new (await FFMpegVideoProcessor)( Comlink.proxy(output), createMp4Args(videoRotation, output.seekable())); } -/** - * @param {!AsyncWriter} output - * @param {!Resolution} resolution - * @return {!Promise} - */ -async function createGifVideoProcessor(output, resolution) { +async function createGifVideoProcessor( + output: AsyncWriter, resolution: Resolution): Promise { return new (await FFMpegVideoProcessor)( Comlink.proxy(output), createGifArgs(resolution)); } -/** - * @param {!Intent} intent - * @return {!AsyncWriter} - */ -function createWriterForIntent(intent) { +function createWriterForIntent(intent: Intent): AsyncWriter { const write = async (blob) => { await intent.appendData(new Uint8Array(await blob.arrayBuffer())); }; @@ -70,57 +55,39 @@ function createWriterForIntent(intent) { * Used to save captured video. */ export class VideoSaver { - /** - * @param {!FileAccessEntry} file - * @param {!VideoProcessor} processor - */ - constructor(file, processor) { - /** - * @const {!FileAccessEntry} - * @private - */ - this.file_ = file; - - /** - * @const {!VideoProcessor} - * @private - */ - this.processor_ = processor; - } + constructor( + private readonly file: FileAccessEntry, + private readonly processor: VideoProcessor) {} /** * Writes video data to result video. - * @param {!Blob} blob */ - write(blob) { - this.processor_.write(blob); + write(blob: Blob): void { + this.processor.write(blob); } /** * Cancels and drops all the written video data. - * @return {!Promise} */ - async cancel() { - await this.processor_.cancel(); - return this.file_.delete(); + async cancel(): Promise { + await this.processor.cancel(); + return this.file.delete(); } /** * Finishes the write of video data parts and returns result video file. - * @return {!Promise} Result video file. + * @return Result video file. */ - async endWrite() { - await this.processor_.close(); - return this.file_; + async endWrite(): Promise { + await this.processor.close(); + return this.file; } /** * Creates video saver for the given file. - * @param {!FileAccessEntry} file - * @param {number} videoRotation - * @return {!Promise} */ - static async createForFile(file, videoRotation) { + static async createForFile(file: FileAccessEntry, videoRotation: number): + Promise { const writer = await file.getWriter(); const processor = await createVideoProcessor(writer, videoRotation); return new VideoSaver(file, processor); @@ -128,11 +95,9 @@ export class VideoSaver { /** * Creates video saver for the given intent. - * @param {!Intent} intent - * @param {number} videoRotation - * @return {!Promise} */ - static async createForIntent(intent, videoRotation) { + static async createForIntent(intent: Intent, videoRotation: number): + Promise { const file = await createPrivateTempVideoFile(); const fileWriter = await file.getWriter(); const intentWriter = createWriterForIntent(intent); @@ -146,49 +111,31 @@ export class VideoSaver { * Used to save captured gif. */ export class GifSaver { - /** - * @param {!Array} blobs - * @param {!VideoProcessor} processor - */ - constructor(blobs, processor) { - /** - * @const {!Array} - * @private - */ - this.blobs_ = blobs; - - /** - * @const {!VideoProcessor} - * @private - */ - this.processor_ = processor; - } + constructor( + private readonly blobs: Blob[], + private readonly processor: VideoProcessor) {} - /** - * @param {!Uint8ClampedArray} frame - */ - write(frame) { - this.processor_.write(new Blob([frame])); + write(frame: Uint8ClampedArray): void { + this.processor.write(new Blob([frame])); } /** * Finishes the write of gif data parts and returns result gif blob. - * @return {!Promise} */ - async endWrite() { - await this.processor_.close(); - return new Blob(this.blobs_, {type: MimeType.GIF}); + async endWrite(): Promise { + await this.processor.close(); + return new Blob(this.blobs, {type: MimeType.GIF}); } /** * Creates video saver for the given file. - * @param {!Resolution} resolution - * @return {!Promise} */ - static async create(resolution) { + static async create(resolution: Resolution): Promise { const blobs = []; const writer = new AsyncWriter({ - write: async (blob) => blobs.push(blob), + async write(blob) { + blobs.push(blob); + }, seek: null, close: null, }); diff --git a/ash/webui/camera_app_ui/resources/js/test_bridge.js b/ash/webui/camera_app_ui/resources/js/test_bridge.ts similarity index 81% rename from ash/webui/camera_app_ui/resources/js/test_bridge.js rename to ash/webui/camera_app_ui/resources/js/test_bridge.ts index 45cc7d7d27ea89..bd010a7c0abb0b 100644 --- a/ash/webui/camera_app_ui/resources/js/test_bridge.js +++ b/ash/webui/camera_app_ui/resources/js/test_bridge.ts @@ -19,9 +19,8 @@ import * as Comlink from './lib/comlink.js'; /** * Pending unbound AppWindow requested by tast waiting to be bound by next * launched CCA window. - * @type {?AppWindow} */ -let pendingAppWindow = null; +let pendingAppWindow: AppWindow|null = null; /** * Whether the app is launched from a cold start. It will be set to false once @@ -33,9 +32,8 @@ let fromColdStart = true; * Registers a pending unbound AppWindow which will be bound with the URL * later once the window is created. This method is expected to be called in * Tast tests. - * @return {!AppWindow} */ -export function registerUnboundWindow() { +export function registerUnboundWindow(): AppWindow { assert(pendingAppWindow === null); const appWindow = new AppWindow(fromColdStart); pendingAppWindow = appWindow; @@ -44,10 +42,9 @@ export function registerUnboundWindow() { /** * Binds the URL to pending AppWindow and exposes AppWindow using the URL. - * @param {string} url The URL to bind. - * @return {?AppWindow} + * @param url The URL to bind. */ -function bindWindow(url) { +function bindWindow(url: string): AppWindow|null { fromColdStart = false; if (pendingAppWindow !== null) { const appWindow = pendingAppWindow; @@ -58,13 +55,17 @@ function bindWindow(url) { return null; } -const sharedWorkerScope = /** @type {!SharedWorkerGlobalScope} */ (self); +const sharedWorkerScope = self as SharedWorkerGlobalScope; + +export interface TestBridge { + bindWindow: typeof bindWindow; + registerUnboundWindow: typeof registerUnboundWindow; +} /** * Triggers when the Shared Worker is connected. - * @param {!MessageEvent} event */ -sharedWorkerScope.onconnect = (event) => { +sharedWorkerScope.onconnect = (event: MessageEvent) => { const port = event.ports[0]; Comlink.expose( { diff --git a/ash/webui/camera_app_ui/resources/js/untrusted_video_processor_helper.js b/ash/webui/camera_app_ui/resources/js/untrusted_video_processor_helper.ts similarity index 60% rename from ash/webui/camera_app_ui/resources/js/untrusted_video_processor_helper.js rename to ash/webui/camera_app_ui/resources/js/untrusted_video_processor_helper.ts index a1fd861f4fbb76..9d0254f9d72601 100644 --- a/ash/webui/camera_app_ui/resources/js/untrusted_video_processor_helper.js +++ b/ash/webui/camera_app_ui/resources/js/untrusted_video_processor_helper.ts @@ -3,14 +3,11 @@ // found in the LICENSE file. import * as Comlink from './lib/comlink.js'; -// eslint-disable-next-line no-unused-vars -import {VideoProcessorHelperInterface} from './untrusted_helper_interfaces.js'; /** * The MP4 video processor URL in trusted type. - * @type {!TrustedScriptURL} */ -const mp4VideoProcessorURL = (() => { +const mp4VideoProcessorURL: TrustedScriptURL = (() => { const staticUrlPolicy = trustedTypes.createPolicy( 'video-processor-js-static', {createScriptURL: () => '/js/models/ffmpeg/video_processor.js'}); @@ -21,19 +18,18 @@ const mp4VideoProcessorURL = (() => { /** * Connects the |port| to worker which exposes the video processor. - * @param {!MessagePort} port - * @return {!Promise} */ -async function connectToWorker(port) { +async function connectToWorker(port: MessagePort): Promise { /** - * TODO(pihsun): Closure Compiler only supports string rather than + * TODO(pihsun): TypeScript only supports string|URL instead of * TrustedScriptURL as parameter to Worker. - * @type {?} */ - const trustedURL = mp4VideoProcessorURL; + const trustedURL = mp4VideoProcessorURL as URL; - const worker = Comlink.wrap(new Worker(trustedURL, {type: 'module'})); + // TODO(pihsun): actually get correct type from the function definition. + const worker = Comlink.wrap<{exposeVideoProcessor(port: MessagePort): void}>( + new Worker(trustedURL, {type: 'module'})); await worker.exposeVideoProcessor(Comlink.transfer(port, [port])); } -export /** !VideoProcessorHelperInterface */ {connectToWorker}; +export {connectToWorker}; diff --git a/ash/webui/camera_app_ui/resources/js/util.ts b/ash/webui/camera_app_ui/resources/js/util.ts index ba252b3fc658a6..f0ce64dc75fb13 100644 --- a/ash/webui/camera_app_ui/resources/js/util.ts +++ b/ash/webui/camera_app_ui/resources/js/util.ts @@ -203,8 +203,8 @@ export function instantiateTemplate(selector: string): DocumentFragment { * origin and returns its proxy. * @param scriptUrl The URL of the script to load. */ -export async function createUntrustedJSModule(scriptUrl: string): - Promise { +export async function createUntrustedJSModule(scriptUrl: string): + Promise> { const untrustedPageReady = new WaitableEvent(); const iFrame = document.createElement('iframe'); iFrame.addEventListener('load', () => untrustedPageReady.signal()); @@ -215,10 +215,14 @@ export async function createUntrustedJSModule(scriptUrl: string): document.body.appendChild(iFrame); await untrustedPageReady.wait(); + // TODO(pihsun): actually get correct type from the function definition. const untrustedRemote = - await Comlink.wrap(Comlink.windowEndpoint(iFrame.contentWindow, self)); + Comlink.wrap<{loadScript(url: string): Promise}>( + Comlink.windowEndpoint(iFrame.contentWindow, self)); await untrustedRemote.loadScript(scriptUrl); - return untrustedRemote; + // loadScript adds the script exports to what's exported by the + // untrustedRemote, so we manually cast it to the expected type. + return untrustedRemote as unknown as Comlink.Remote; } /** diff --git a/ash/webui/camera_app_ui/resources/utils/cca.py b/ash/webui/camera_app_ui/resources/utils/cca.py index eae6a861a040fa..f99b0601d4d9f4 100755 --- a/ash/webui/camera_app_ui/resources/utils/cca.py +++ b/ash/webui/camera_app_ui/resources/utils/cca.py @@ -193,14 +193,14 @@ def lint(args): # * files directly referenced by