summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTao Zhou <taoalpha@google.com>2019-12-03 15:16:29 +0100
committerTao Zhou <taoalpha@google.com>2019-12-10 13:33:53 +0000
commitd936a7a0e0f49da3d0432a4f03e6a008c6e23c54 (patch)
treeb2a1f8f0146d4e3b87ed31099f2093f10915a2c8
parent7e9c6755d6ed7e6ff76a9907134a1fb94ca5089e (diff)
Add event interface to Gerrit
This is to solve problems like communicating among Gerrit components, plugins and Gerrit, and plugins themselves. Original discussion and one classic problem to solve: https://gerrit-review.googlesource.com/c/gerrit/+/242732 ``` // pluginA Gerrit.install(plugin => { // do something Gerrit.emit("event-name", {plugin: plugin}); }); // Gerrit Gerrit.on("event-name", event => { // event.plugin should be pluginA }); ``` Change-Id: I8b9703f5fafd5fc00054d6d4bce4628d4aba6624
-rw-r--r--polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js142
-rw-r--r--polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html146
-rw-r--r--polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html1
-rw-r--r--polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js37
-rw-r--r--polygerrit-ui/app/test/index.html1
5 files changed, 327 insertions, 0 deletions
diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js
new file mode 100644
index 0000000000..d024bb2df3
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js
@@ -0,0 +1,142 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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.
+ */
+
+(function(window) {
+ 'use strict';
+
+ // Avoid duplicate registeration
+ if (window.EventEmitter) return;
+
+ /**
+ * An lite implementation of
+ * https://nodejs.org/api/events.html#events_class_eventemitter.
+ *
+ * This is unrelated to the native DOM events, you should use it when you want
+ * to enable EventEmitter interface on any class.
+ *
+ * @example
+ *
+ * class YourClass extends EventEmitter {
+ * // now all instance of YourClass will have this EventEmitter interface
+ * }
+ *
+ */
+ class EventEmitter {
+ constructor() {
+ /**
+ * Shared events map from name to the listeners.
+ * @type {!Object<string, Array<eventCallback>>}
+ */
+ this._listenersMap = new Map();
+ }
+
+ /**
+ * Register an event listener to an event.
+ *
+ * @param {string} eventName
+ * @param {eventCallback} cb
+ * @returns {Function} Unsubscribe method
+ */
+ addListener(eventName, cb) {
+ if (!eventName || !cb) {
+ console.warn('A valid eventname and callback is required!');
+ return;
+ }
+
+ const listeners = this._listenersMap.get(eventName) || [];
+ listeners.push(cb);
+ this._listenersMap.set(eventName, listeners);
+
+ return () => {
+ this.off(eventName, cb);
+ };
+ }
+
+ // Alias for addListener.
+ on(eventName, cb) {
+ return this.addListener(eventName, cb);
+ }
+
+ // Attach event handler only once. Automatically removed.
+ once(eventName, cb) {
+ const onceWrapper = (...args) => {
+ cb(...args);
+ this.off(eventName, onceWrapper);
+ };
+ return this.on(eventName, onceWrapper);
+ }
+
+ /**
+ * De-register an event listener to an event.
+ *
+ * @param {string} eventName
+ * @param {eventCallback} cb
+ */
+ removeListener(eventName, cb) {
+ let listeners = this._listenersMap.get(eventName) || [];
+ listeners = listeners.filter(listener => listener !== cb);
+ this._listenersMap.set(eventName, listeners);
+ }
+
+ // Alias to removeListener
+ off(eventName, cb) {
+ this.removeListener(eventName, cb);
+ }
+
+ /**
+ * Synchronously calls each of the listeners registered for
+ * the event named eventName, in the order they were registered,
+ * passing the supplied detail to each.
+ *
+ * Returns true if the event had listeners, false otherwise.
+ *
+ * @param {string} eventName
+ * @param {*} detail
+ */
+ emit(eventName, detail) {
+ const listeners = this._listenersMap.get(eventName) || [];
+ for (const listener of listeners) {
+ try {
+ listener(detail);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ return listeners.length !== 0;
+ }
+
+ // Alias to emit.
+ dispatch(eventName, detail) {
+ return this.emit(eventName, detail);
+ }
+
+ /**
+ * Remove listeners for a specific event or all.
+ *
+ * @param {string} eventName if not provided, will remove all
+ */
+ removeAllListeners(eventName) {
+ if (eventName) {
+ this._listenersMap.set(eventName, []);
+ } else {
+ this._listenersMap = new Map();
+ }
+ }
+ }
+
+ window.EventEmitter = EventEmitter;
+})(window); \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
new file mode 100644
index 0000000000..137ed25042
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-api-interface</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="../gr-js-api-interface/gr-js-api-interface.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-js-api-interface></gr-js-api-interface>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-event-interface tests', () => {
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('test on Gerrit', () => {
+ setup(() => {
+ fixture('basic');
+ Gerrit.removeAllListeners();
+ });
+
+ test('communicate between plugin and Gerrit', done => {
+ const eventName = 'test-plugin-event';
+ let p;
+ Gerrit.on(eventName, e => {
+ assert.equal(e.value, 'test');
+ assert.equal(e.plugin, p);
+ done();
+ });
+ Gerrit.install(plugin => {
+ p = plugin;
+ Gerrit.emit(eventName, {value: 'test', plugin});
+ }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ });
+
+ test('listen on events from core', done => {
+ const eventName = 'test-plugin-event';
+ Gerrit.on(eventName, e => {
+ assert.equal(e.value, 'test');
+ done();
+ });
+
+ Gerrit.emit(eventName, {value: 'test'});
+ });
+
+ test('communicate across plugins', done => {
+ const eventName = 'test-plugin-event';
+ Gerrit.install(plugin => {
+ Gerrit.on(eventName, e => {
+ assert.equal(e.plugin.getPluginName(), 'testB');
+ done();
+ });
+ }, '0.1',
+ 'http://test.com/plugins/testA/static/testA.js');
+
+ Gerrit.install(plugin => {
+ Gerrit.emit(eventName, {plugin});
+ }, '0.1',
+ 'http://test.com/plugins/testB/static/testB.js');
+ });
+ });
+
+ suite('test on interfaces', () => {
+ let testObj;
+ class TestClass extends EventEmitter {
+ }
+ setup(() => {
+ testObj = new TestClass();
+ });
+
+ test('on', () => {
+ const cbStub = sinon.stub();
+ testObj.on('test', cbStub);
+ testObj.emit('test');
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledTwice);
+ });
+
+ test('once', () => {
+ const cbStub = sinon.stub();
+ testObj.once('test', cbStub);
+ testObj.emit('test');
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledOnce);
+ });
+
+ test('unsubscribe', () => {
+ const cbStub = sinon.stub();
+ const unsubscribe = testObj.on('test', cbStub);
+ testObj.emit('test');
+ unsubscribe();
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledOnce);
+ });
+
+ test('off', () => {
+ const cbStub = sinon.stub();
+ testObj.on('test', cbStub);
+ testObj.emit('test');
+ testObj.off('test', cbStub);
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledOnce);
+ });
+
+ test('removeAllListeners', () => {
+ const cbStub = sinon.stub();
+ testObj.on('test', cbStub);
+ testObj.removeAllListeners('test');
+ testObj.emit('test');
+ assert.isTrue(cbStub.notCalled);
+ });
+ });
+ });
+</script> \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index d8a662ec37..d95fd0afd6 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -30,6 +30,7 @@ limitations under the License.
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-js-api-interface">
+ <script src="../gr-event-interface/gr-event-interface.js"></script>
<script src="gr-annotation-actions-context.js"></script>
<script src="gr-annotation-actions-js-api.js"></script>
<script src="gr-change-actions-js-api.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 36a428d966..1311105572 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -662,6 +662,43 @@
}
};
+ // TODO(taoalpha): List all internal supported event names.
+ // Also convert this to inherited class once we move Gerrit to class.
+ Gerrit._eventEmitter = new EventEmitter();
+ ['addListener',
+ 'dispatch',
+ 'emit',
+ 'off',
+ 'on',
+ 'once',
+ 'removeAllListeners',
+ 'removeListener',
+ ].forEach(method => {
+ /**
+ * Enabling EventEmitter interface on Gerrit.
+ *
+ * This will enable to signal across different parts of js code without relying on DOM,
+ * including core to core, plugin to plugin and also core to plugin.
+ *
+ * @example
+ *
+ * // Emit this event from pluginA
+ * Gerrit.install(pluginA => {
+ * fetch("some-api").then(() => {
+ * Gerrit.on("your-special-event", {plugin: pluginA});
+ * });
+ * });
+ *
+ * // Listen on your-special-event from pluignB
+ * Gerrit.install(pluginB => {
+ * Gerrit.on("your-special-event", ({plugin}) => {
+ * // do something, plugin is pluginA
+ * });
+ * });
+ */
+ Gerrit[method] = Gerrit._eventEmitter[method].bind(Gerrit._eventEmitter);
+ });
+
window.Gerrit = Gerrit;
// Preloaded plugins should be installed after Gerrit.install() is set,
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 5784eadec7..75ba7054d5 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -149,6 +149,7 @@ limitations under the License.
'settings/gr-settings-view/gr-settings-view_test.html',
'settings/gr-ssh-editor/gr-ssh-editor_test.html',
'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
+ 'shared/gr-event-interface/gr-event-interface_test.html',
'shared/gr-account-label/gr-account-label_test.html',
'shared/gr-account-link/gr-account-link_test.html',
'shared/gr-alert/gr-alert_test.html',