From aee79cd745441053648f92f2bb59af925e663331 Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Thu, 26 Mar 2026 19:19:08 +0200 Subject: [PATCH] Replace tiny-emitter package with inline ESM implementation --- package-lock.json | 24 ++-- package.json | 1 - src/utils/emitter.js | 49 ++++++-- test/unit-tests/utils/emitter.test.js | 156 ++++++++++++++++++++++++++ 4 files changed, 213 insertions(+), 17 deletions(-) create mode 100644 test/unit-tests/utils/emitter.test.js diff --git a/package-lock.json b/package-lock.json index 2637b244f6..ec17a681b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,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" }, "bin": { @@ -117,6 +116,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2302,6 +2302,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2366,6 +2367,7 @@ "integrity": "sha512-ixiWrCSRi33uqBMRuICcKECW7rtgY43TbsHDpM2XK7lXispd48opW+0IXrBVxv9NMhaz/Ue9kyj6r3NTVyXm8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.20.0" } @@ -2415,6 +2417,7 @@ "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", @@ -2828,6 +2831,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3812,6 +3816,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5262,6 +5267,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5347,6 +5353,7 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5462,6 +5469,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5577,6 +5585,7 @@ "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "builtins": "^5.0.1", @@ -5690,6 +5699,7 @@ "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -8366,6 +8376,7 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -10324,6 +10335,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11018,6 +11030,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12022,12 +12035,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/tiny-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", - "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", - "license": "MIT" - }, "node_modules/tinybench": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.0.0.tgz", @@ -12079,6 +12086,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12420,6 +12428,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12994,6 +13003,7 @@ "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/package.json b/package.json index 07653a8be0..47d5b4a6e0 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/utils/emitter.js b/src/utils/emitter.js index 4d3c3b2fe3..e286bc3beb 100644 --- a/src/utils/emitter.js +++ b/src/utils/emitter.js @@ -1,4 +1,5 @@ -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` @@ -6,14 +7,44 @@ import Emitter from 'tiny-emitter' * @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 } diff --git a/test/unit-tests/utils/emitter.test.js b/test/unit-tests/utils/emitter.test.js new file mode 100644 index 0000000000..c3302e8aa1 --- /dev/null +++ b/test/unit-tests/utils/emitter.test.js @@ -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) + }) +})