blob: 8db7bdf0c82557264c1f7adceec48426cb519401 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @unrestricted
*/
export default class ServiceManager {
/**
* @param {string} serviceName
* @return {!Promise<?Service>}
*/
createRemoteService(serviceName) {
if (!this._remoteConnection) {
const url = Root.Runtime.queryParam('service-backend');
if (!url) {
console.error('No endpoint address specified');
return /** @type {!Promise<?Service>} */ (Promise.resolve(null));
}
this._remoteConnection = new Connection(new RemoteServicePort(url));
}
return this._remoteConnection._createService(serviceName);
}
/**
* @param {string} appName
* @param {string} serviceName
* @return {!Promise<?Service>}
*/
createAppService(appName, serviceName) {
let url = appName + '.js';
const remoteBase = Root.Runtime.queryParam('remoteBase');
const debugFrontend = Root.Runtime.queryParam('debugFrontend');
const isUnderTest = Host.isUnderTest();
const queryParams = [];
if (remoteBase) {
queryParams.push('remoteBase=' + remoteBase);
}
if (debugFrontend) {
queryParams.push('debugFrontend=' + debugFrontend);
}
if (isUnderTest) {
queryParams.push('isUnderTest=true');
}
if (queryParams.length) {
url += `?${queryParams.join('&')}`;
}
const worker = new Worker(url);
const connection = new Connection(new WorkerServicePort(worker));
return connection._createService(serviceName);
}
}
/**
* @unrestricted
*/
class Connection {
/**
* @param {!ServicePort} port
*/
constructor(port) {
this._port = port;
this._port.setHandlers(this._onMessage.bind(this), this._connectionClosed.bind(this));
this._lastId = 1;
/** @type {!Map<number, function(?Object)>}*/
this._callbacks = new Map();
/** @type {!Map<string, !Service>}*/
this._services = new Map();
}
/**
* @param {string} serviceName
* @return {!Promise<?Service>}
*/
_createService(serviceName) {
return this._sendCommand(serviceName + '.create').then(result => {
if (!result) {
console.error('Could not initialize service: ' + serviceName);
return null;
}
const service = new Service(this, serviceName, result.id);
this._services.set(serviceName + ':' + result.id, service);
return service;
});
}
/**
* @param {!Service} service
*/
_serviceDisposed(service) {
this._services.delete(service._serviceName + ':' + service._objectId);
if (!this._services.size) {
// Terminate the connection since it is no longer used.
this._port.close();
}
}
/**
* @param {string} method
* @param {!Object=} params
* @return {!Promise<?Object>}
*/
_sendCommand(method, params) {
const id = this._lastId++;
const message = JSON.stringify({id: id, method: method, params: params || {}});
return this._port.send(message).then(success => {
if (!success) {
return Promise.resolve(null);
}
return new Promise(fulfill => this._callbacks.set(id, fulfill));
});
}
/**
* @param {string} data
*/
_onMessage(data) {
let object;
try {
object = JSON.parse(data);
} catch (e) {
console.error(e);
return;
}
if (object.id) {
if (object.error) {
console.error('Service error: ' + object.error);
}
this._callbacks.get(object.id)(object.error ? null : object.result);
this._callbacks.delete(object.id);
return;
}
const tokens = object.method.split('.');
const serviceName = tokens[0];
const methodName = tokens[1];
const service = this._services.get(serviceName + ':' + object.params.id);
if (!service) {
console.error('Unable to lookup stub for ' + serviceName + ':' + object.params.id);
return;
}
service._dispatchNotification(methodName, object.params);
}
_connectionClosed() {
for (const callback of this._callbacks.values()) {
callback(null);
}
this._callbacks.clear();
for (const service of this._services.values()) {
service._dispatchNotification('disposed');
}
this._services.clear();
}
}
/**
* @unrestricted
*/
export class Service {
/**
* @param {!Connection} connection
* @param {string} serviceName
* @param {string} objectId
*/
constructor(connection, serviceName, objectId) {
this._connection = connection;
this._serviceName = serviceName;
this._objectId = objectId;
/** @type {!Map<string, function(!Object=)>}*/
this._notificationHandlers = new Map();
}
/**
* @return {!Promise}
*/
dispose() {
const params = {id: this._objectId};
return this._connection._sendCommand(this._serviceName + '.dispose', params).then(() => {
this._connection._serviceDisposed(this);
});
}
/**
* @param {string} methodName
* @param {function(!Object=)} handler
*/
on(methodName, handler) {
this._notificationHandlers.set(methodName, handler);
}
/**
* @param {string} methodName
* @param {!Object=} params
* @return {!Promise}
*/
send(methodName, params) {
params = params || {};
params.id = this._objectId;
return this._connection._sendCommand(this._serviceName + '.' + methodName, params);
}
/**
* @param {string} methodName
* @param {!Object=} params
*/
_dispatchNotification(methodName, params) {
const handler = this._notificationHandlers.get(methodName);
if (!handler) {
console.error('Could not report notification \'' + methodName + '\' on \'' + this._objectId + '\'');
return;
}
handler(params);
}
}
/**
* @implements {ServicePort}
* @unrestricted
*/
class RemoteServicePort {
/**
* @param {string} url
*/
constructor(url) {
this._url = url;
}
/**
* @override
* @param {function(string)} messageHandler
* @param {function(string)} closeHandler
*/
setHandlers(messageHandler, closeHandler) {
this._messageHandler = messageHandler;
this._closeHandler = closeHandler;
}
/**
* @return {!Promise<boolean>}
*/
_open() {
if (!this._connectionPromise) {
this._connectionPromise = new Promise(promiseBody.bind(this));
}
return this._connectionPromise;
/**
* @param {function(boolean)} fulfill
* @this {RemoteServicePort}
*/
function promiseBody(fulfill) {
let socket;
try {
socket = new WebSocket(/** @type {string} */ (this._url));
socket.onmessage = onMessage.bind(this);
socket.onclose = onClose.bind(this);
socket.onopen = onConnect.bind(this);
} catch (e) {
fulfill(false);
}
/**
* @this {RemoteServicePort}
*/
function onConnect() {
this._socket = socket;
fulfill(true);
}
/**
* @param {!Event} event
* @this {RemoteServicePort}
*/
function onMessage(event) {
this._messageHandler(event.data);
}
/**
* @this {RemoteServicePort}
*/
function onClose() {
if (!this._socket) {
fulfill(false);
}
this._socketClosed(!!this._socket);
}
}
}
/**
* @override
* @param {string} message
* @return {!Promise<boolean>}
*/
send(message) {
return this._open().then(() => {
if (this._socket) {
this._socket.send(message);
return true;
}
return false;
});
}
/**
* @override
* @return {!Promise}
*/
close() {
return this._open().then(() => {
if (this._socket) {
this._socket.close();
this._socketClosed(true);
}
return true;
});
}
/**
* @param {boolean=} notifyClient
*/
_socketClosed(notifyClient) {
this._socket = null;
delete this._connectionPromise;
if (notifyClient) {
this._closeHandler();
}
}
}
/**
* @implements {ServicePort}
* @unrestricted
*/
class WorkerServicePort {
/**
* @param {!Worker} worker
*/
constructor(worker) {
this._worker = worker;
let fulfill;
this._workerPromise = new Promise(resolve => fulfill = resolve);
this._worker.onmessage = onMessage.bind(this);
this._worker.onclose = this._closeHandler;
/**
* @param {!Event} event
* @this {WorkerServicePort}
*/
function onMessage(event) {
if (event.data === 'workerReady') {
fulfill(true);
return;
}
this._messageHandler(event.data);
}
}
/**
* @override
* @param {function(string)} messageHandler
* @param {function(string)} closeHandler
*/
setHandlers(messageHandler, closeHandler) {
this._messageHandler = messageHandler;
this._closeHandler = closeHandler;
}
/**
* @override
* @param {string} message
* @return {!Promise<boolean>}
*/
send(message) {
return this._workerPromise.then(() => {
try {
this._worker.postMessage(message);
return true;
} catch (e) {
return false;
}
});
}
/**
* @override
* @return {!Promise}
*/
close() {
return this._workerPromise.then(() => {
if (this._worker) {
this._worker.terminate();
}
return false;
});
}
}
/* Legacy exported object */
self.Services = self.Services || {};
/* Legacy exported object */
Services = Services || {};
/** @constructor */
Services.ServiceManager = ServiceManager;
/** @constructor */
Services.ServiceManager.Service = Service;
Services.serviceManager = new ServiceManager();