From 2c95d087b3d8d3dd7045c9b3709d302709ba41d5 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Tue, 12 Sep 2023 23:02:02 +0200 Subject: [PATCH] implement ObservableV2 with better typings --- observable.js | 82 ++++++++++++++++++++++++++++++++++++++++++++++ observable.test.js | 50 ++++++++++++++++++++++++++++ test.js | 2 ++ 3 files changed, 134 insertions(+) create mode 100644 observable.test.js diff --git a/observable.js b/observable.js index 8d065bb..182957e 100644 --- a/observable.js +++ b/observable.js @@ -10,7 +10,88 @@ import * as array from './array.js' /** * Handles named events. + * @experimental * + * This is basically a (better typed) duplicate of Observable, which will replace Observable in the + * next release. + * + * @template {{[key: string]: function(...any):void}} EVENTS + */ +export class ObservableV2 { + constructor () { + /** + * Some desc. + * @type {Map>} + */ + this._observers = map.create() + } + + /** + * @template {string} NAME + * @param {NAME} name + * @param {EVENTS[NAME]} f + */ + on (name, f) { + map.setIfUndefined(this._observers, /** @type {string} */ (name), set.create).add(f) + return f + } + + /** + * @template {string} NAME + * @param {NAME} name + * @param {EVENTS[NAME]} f + */ + once (name, f) { + /** + * @param {...any} args + */ + const _f = (...args) => { + this.off(name, /** @type {any} */ (_f)) + f(...args) + } + this.on(name, /** @type {any} */ (_f)) + } + + /** + * @template {string} NAME + * @param {NAME} name + * @param {EVENTS[NAME]} f + */ + off (name, f) { + const observers = this._observers.get(name) + if (observers !== undefined) { + observers.delete(f) + if (observers.size === 0) { + this._observers.delete(name) + } + } + } + + /** + * Emit a named event. All registered event listeners that listen to the + * specified name will receive the event. + * + * @todo This should catch exceptions + * + * @template {string} NAME + * @param {NAME} name The event name. + * @param {Parameters} args The arguments that are applied to the event listener. + */ + emit (name, args) { + // copy all listeners to an array first to make sure that no event is emitted to listeners that are subscribed while the event handler is called. + return array.from((this._observers.get(name) || map.create()).values()).forEach(f => f(...args)) + } + + destroy () { + this._observers = map.create() + } +} + +/* c8 ignore start */ +/** + * Handles named events. + * + * @deprecated * @template N */ export class Observable { @@ -77,3 +158,4 @@ export class Observable { this._observers = map.create() } } +/* c8 ignore end */ diff --git a/observable.test.js b/observable.test.js new file mode 100644 index 0000000..bf3487d --- /dev/null +++ b/observable.test.js @@ -0,0 +1,50 @@ +import * as t from './testing.js' +import { ObservableV2 } from './observable.js' + +/** + * @param {t.TestCase} _tc + */ +export const testTypedObservable = _tc => { + /** + * @type {ObservableV2<{ "hey": function(number, string):any, listen: function(string):any }>} + */ + const o = new ObservableV2() + let calls = 0 + /** + * Test "hey" + */ + /** + * @param {number} n + * @param {string} s + */ + const listener = (n, s) => { + t.assert(typeof n === 'number') + t.assert(typeof s === 'string') + calls++ + } + o.on('hey', listener) + o.on('hey', (arg1) => t.assert(typeof arg1 === 'number')) + // o.emit('hey', ['four']) // should emit type error + // o.emit('hey', [4]) // should emit type error + o.emit('hey', [4, 'four']) + t.assert(calls === 1) + o.emit('hey', [5, 'five']) + t.assert(calls === 2) + o.off('hey', listener) + o.emit('hey', [6, 'six']) + t.assert(calls === 2) + /** + * Test "listen" + */ + o.once('listen', n => { + t.assert(typeof n === 'string') + calls++ + }) + // o.emit('listen', [4]) // should emit type error + o.emit('listen', ['four']) + o.emit('listen', ['five']) // shouldn't trigger + t.assert(calls === 3) + o.destroy() + o.emit('hey', [7, 'seven']) + t.assert(calls === 3) +} diff --git a/test.js b/test.js index a5cea28..b40ff7b 100644 --- a/test.js +++ b/test.js @@ -22,6 +22,7 @@ import * as eventloop from './eventloop.test.js' import * as time from './time.test.js' import * as pair from './pair.test.js' import * as object from './object.test.js' +import * as observable from './observable.test.js' import * as math from './math.test.js' import * as number from './number.test.js' import * as buffer from './buffer.test.js' @@ -65,6 +66,7 @@ runTests({ time, pair, object, + observable, math, number, buffer,