Skip to content

Commit

Permalink
implement ObservableV2 with better typings
Browse files Browse the repository at this point in the history
  • Loading branch information
dmonad committed Sep 12, 2023
1 parent e8a6689 commit 2c95d08
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 0 deletions.
82 changes: 82 additions & 0 deletions observable.js
Expand Up @@ -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<string, Set<any>>}
*/
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<EVENTS[NAME]>} 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 {
Expand Down Expand Up @@ -77,3 +158,4 @@ export class Observable {
this._observers = map.create()
}
}
/* c8 ignore end */
50 changes: 50 additions & 0 deletions 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)
}
2 changes: 2 additions & 0 deletions test.js
Expand Up @@ -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'
Expand Down Expand Up @@ -65,6 +66,7 @@ runTests({
time,
pair,
object,
observable,
math,
number,
buffer,
Expand Down

0 comments on commit 2c95d08

Please sign in to comment.