From ea7552649434f6055a099ddfe53e056eb20cfafd Mon Sep 17 00:00:00 2001 From: Christoph Reiter <reiter.christoph@gmail.com> Date: Thu, 14 Nov 2019 13:42:52 +0100 Subject: [PATCH] Add experimental events API plus tests --- packages/common/events.js | 149 +++++++++++++++++++++++++++++++++ packages/common/test/events.js | 76 +++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 packages/common/events.js create mode 100644 packages/common/test/events.js diff --git a/packages/common/events.js b/packages/common/events.js new file mode 100644 index 00000000..fdda0b18 --- /dev/null +++ b/packages/common/events.js @@ -0,0 +1,149 @@ +export class EventSubscriber { + + /** + * @param {string} eventName The event name used for broadcasting the event + * @param {string} requestName The event name used for requesting the event + */ + constructor(eventName, requestName) { + this._eventName = eventName; + this._requestName = requestName; + this._callbacks = []; + this._eventHandler = this._eventHandler.bind(this); + } + + /** + * Register a callback for the event + * + * @param {Function} callback + */ + subscribe(callback) { + const first = (this._callbacks.length === 0); + this._callbacks.push(callback); + if (first) { + window.addEventListener(this._eventName, this._eventHandler); + } + this._requestUpdate(callback); + } + + /** + * Unregister a callback registered through subscribe() + * + * @param {Function} callback + */ + unsubscribe(callback) { + const index = this._callbacks.indexOf(callback); + if (index == -1) + throw new Error("not subscribed"); + this._callbacks.splice(index, 1); + const last = (this._callbacks.length === 0); + if (last) { + window.removeEventListener(this._eventName, this._eventHandler); + } + } + + /** + * Request an event, might not do anything if there is no emitter. + */ + requestUpdate() { + this._requestUpdate((data) => { + for (const cb of this._callbacks) { + cb(data); + } + }); + } + + /** + * Returns the event data or throws an error if there is no emitter + */ + async requestData() { + return new Promise((resolve, reject) => { + const event = new CustomEvent(this._requestName, { + bubbles: true, + composed: true, + cancelable: true, + detail: {'callback': resolve} + }); + if (window.dispatchEvent(event)) { + reject(new Error("other side not there")); + } + }); + } + + _requestUpdate(callback) { + const event = new CustomEvent(this._requestName, { + bubbles: true, + composed: true, + detail: {callback: callback} + }); + window.dispatchEvent(event); + } + + _eventHandler(event) { + const data = event.detail.data; + for (const cb of this._callbacks) { + cb(data); + } + } +} + +export class EventEmitter { + + /** + * @param {string} eventName The event name used for broadcasting the event + * @param {string} requestName The event name used for requesting the event + */ + constructor(eventName, requestName) { + this._eventName = eventName; + this._requestName = requestName; + } + + /** + * Register a callback that will be called when a new event is generated. + * The callback needs to return an object that is send with the event. + * + * @param {Function} callback + */ + registerCallback(callback) { + if (this._getData !== undefined) + throw new Error("already registered"); + this._getData = callback; + this._onRequest = (event) => { + const callback = event.detail.callback; + if (callback === undefined) { + return; + } + event.preventDefault(); + event.stopImmediatePropagation(); + callback(this._getData()); + }; + window.addEventListener(this._requestName, this._onRequest); + this.emit(); + } + + /** + * Unregister a callback that was passed to registerCallback() + * + * @param {Function} callback + */ + unregisterCallback(callback) { + if (this._getData !== callback) + throw new Error("not registered"); + window.removeEventListener(this._requestName, this._onRequest); + delete this._getData; + delete this._onRequest; + } + + /** + * Force emit an event (when something has changed and new data needs to be send out) + */ + emit() { + if (this._getData === undefined) + throw new Error("no callback registered"); + const event = new CustomEvent(this._eventName, { + bubbles: true, + composed: true, + detail: { data: this._getData() } + }); + window.dispatchEvent(event); + } +} \ No newline at end of file diff --git a/packages/common/test/events.js b/packages/common/test/events.js new file mode 100644 index 00000000..25933976 --- /dev/null +++ b/packages/common/test/events.js @@ -0,0 +1,76 @@ +import {assert} from 'chai'; +import {EventEmitter, EventSubscriber} from '../events.js'; + + +const assertThrowsAsync = async (fn, ...args) => { + let error; + try { + await fn(); + } catch (e) { + error = e; + } + + return assert.throws(() => { + if (error !== undefined) + throw error; + }, ...args); +}; + + +suite('events', () => { + test('emitter basics', () => { + const ev = new EventEmitter("foo", "foo-req"); + assert.throws(ev.emit); + + const cb = () => { return 42; }; + ev.registerCallback(cb); + ev.emit(); + ev.unregisterCallback(cb); + assert.throws(() => {ev.unregisterCallback(cb);}); + assert.throws(ev.emit); + }); + + test('sub basics', async () => { + const sub = new EventSubscriber("foo", "foo-req"); + await assertThrowsAsync(async () => { await sub.requestData();}, /not there/); + + const buffer = []; + const handler = (data) => { + buffer.push(data); + }; + sub.subscribe(handler); + sub.subscribe(handler); + sub.requestUpdate(); + sub.unsubscribe(handler); + sub.unsubscribe(handler); + assert.throws(() => {sub.unsubscribe(handler);}); + assert.deepEqual(buffer, []); + }); + + test('both basics', async () => { + const sub = new EventSubscriber("foo", "foo-req"); + const ev = new EventEmitter("foo", "foo-req"); + let i = 0; + const cb = () => { return i++; }; + ev.registerCallback(cb); + + const buffer = []; + const handler = (data) => { + buffer.push(data); + }; + + sub.subscribe(handler); + assert.deepEqual(buffer, [1]); + sub.requestUpdate(); + assert.deepEqual(buffer, [1, 2]); + sub.requestUpdate(); + assert.deepEqual(buffer, [1, 2, 3]); + ev.emit(); + assert.deepEqual(buffer, [1, 2, 3, 4]); + + sub.unsubscribe(handler); + sub.requestUpdate(); + assert.deepEqual(buffer, [1, 2, 3, 4]); + ev.unregisterCallback(cb); + }); +}); -- GitLab