Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"fraction.js": "^5.2.1",
"javascript-natural-sort": "^0.7.1",
"seedrandom": "^3.0.5",
"tiny-emitter": "^2.1.0",
"typed-function": "^4.2.1"
},
"devDependencies": {
Expand Down
49 changes: 40 additions & 9 deletions src/utils/emitter.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,50 @@
import Emitter from 'tiny-emitter'
// Inline ESM replacement for the 'tiny-emitter' package (v2.1.0)
// Original: https://github.com/scottcorgan/tiny-emitter (MIT license)

/**
* Extend given object with emitter functions `on`, `off`, `once`, `emit`
* @param {Object} obj
* @return {Object} obj
*/
export function mixin (obj) {
// create event emitter
const emitter = new Emitter()

// bind methods to obj (we don't want to expose the emitter.e Array...)
obj.on = emitter.on.bind(emitter)
obj.off = emitter.off.bind(emitter)
obj.once = emitter.once.bind(emitter)
obj.emit = emitter.emit.bind(emitter)
const events = {}

obj.on = function (name, callback, ctx) {
(events[name] || (events[name] = [])).push({ fn: callback, ctx })
return obj
}

obj.off = function (name, callback) {
if (!callback) {
delete events[name]
return obj
}

const listeners = events[name]
if (listeners) {
const live = listeners.filter(e => e.fn !== callback && e.fn._ !== callback)
live.length ? (events[name] = live) : delete events[name]
}

return obj
}

obj.once = function (name, callback, ctx) {
function listener (...args) {
obj.off(name, listener)
callback.apply(ctx, args)
}
listener._ = callback
return obj.on(name, listener, ctx)
}

obj.emit = function (name, ...args) {
const listeners = (events[name] || []).slice()
for (let i = 0; i < listeners.length; i++) {
listeners[i].fn.apply(listeners[i].ctx, args)
}
return obj
}

return obj
}
156 changes: 156 additions & 0 deletions test/unit-tests/utils/emitter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import assert from 'assert'
import { mixin } from '../../../src/utils/emitter.js'

describe('emitter', function () {
it('should add on/off/once/emit to an object', function () {
const obj = mixin({})
assert.strictEqual(typeof obj.on, 'function')
assert.strictEqual(typeof obj.off, 'function')
assert.strictEqual(typeof obj.once, 'function')
assert.strictEqual(typeof obj.emit, 'function')
})

it('should subscribe to an event and emit it', function () {
const obj = mixin({})
let called = false
obj.on('test', function () { called = true })
obj.emit('test')
assert.strictEqual(called, true)
})

it('should pass arguments to event listener', function () {
const obj = mixin({})
let receivedArgs
obj.on('test', function (a, b) { receivedArgs = [a, b] })
obj.emit('test', 'arg1', 'arg2')
assert.deepStrictEqual(receivedArgs, ['arg1', 'arg2'])
})

it('should subscribe with context', function () {
const obj = mixin({})
const ctx = { value: 42 }
let receivedValue
obj.on('test', function () { receivedValue = this.value }, ctx)
obj.emit('test')
assert.strictEqual(receivedValue, 42)
})

it('should support multiple listeners', function () {
const obj = mixin({})
let calls = 0
obj.on('test', function () { calls++ })
obj.on('test', function () { calls++ })
obj.emit('test')
assert.strictEqual(calls, 2)
})

it('should subscribe only once with once()', function () {
const obj = mixin({})
let calls = 0
obj.once('test', function () { calls++ })
obj.emit('test')
obj.emit('test')
assert.strictEqual(calls, 1)
})

it('should keep context with once()', function () {
const obj = mixin({})
const ctx = { value: 99 }
let receivedValue
obj.once('test', function () { receivedValue = this.value }, ctx)
obj.emit('test')
assert.strictEqual(receivedValue, 99)
})

it('should unsubscribe all listeners with off(name)', function () {
const obj = mixin({})
let called = false
obj.on('test', function () { called = true })
obj.off('test')
obj.emit('test')
assert.strictEqual(called, false)
})

it('should unsubscribe a specific listener with off(name, fn)', function () {
const obj = mixin({})
let calls = 0
const fn = function () { calls++ }
obj.on('test', fn)
obj.on('test', function () { calls += 10 })
obj.off('test', fn)
obj.emit('test')
assert.strictEqual(calls, 10)
})

it('should unsubscribe duplicate listeners', function () {
const obj = mixin({})
let calls = 0
const fn = function () { calls++ }
obj.on('test', fn)
obj.on('test', fn)
obj.off('test', fn)
obj.emit('test')
assert.strictEqual(calls, 0)
})

it('should unsubscribe a once() listener via off()', function () {
const obj = mixin({})
let called = false
const fn = function () { called = true }
obj.once('test', fn)
obj.off('test', fn)
obj.emit('test')
assert.strictEqual(called, false)
})

it('should handle off() before any events are added', function () {
const obj = mixin({})
// Should not throw
obj.off('test', function () {})
})

it('should handle emit for non-subscribed events', function () {
const obj = mixin({})
// Should not throw
obj.emit('nonexistent', 'data')
})

it('should emit all listeners even if one unsubscribes during emit', function () {
const obj = mixin({})
let calls = 0
const fn = function () {
calls++
obj.off('test', fn)
}
obj.on('test', fn)
obj.on('test', function () { calls++ })
obj.on('test', function () { calls++ })
obj.emit('test')
assert.strictEqual(calls, 3)
})

it('should allow removing an event inside its own callback', function () {
const obj = mixin({})
let called = false
obj.on('test', function () {
obj.off('test')
called = true
})
obj.emit('test')
assert.strictEqual(called, true)
// Second emit should do nothing
let calledAgain = false
obj.on('test', function () { calledAgain = true })
obj.off('test')
obj.emit('test')
assert.strictEqual(calledAgain, false)
})

it('should return the object for chaining from on/off/once/emit', function () {
const obj = mixin({})
assert.strictEqual(obj.on('test', function () {}), obj)
assert.strictEqual(obj.off('test'), obj)
assert.strictEqual(obj.once('test', function () {}), obj)
assert.strictEqual(obj.emit('test'), obj)
})
})