├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bench ├── emit-bind.js ├── emit-context.js ├── emit-deopt.js ├── emit.js ├── index.js ├── latest.md ├── mini-signals@0.0.1.js ├── mini-signals@0.0.2.js ├── mini-signals@1.0.1.js ├── mini-signals@1.1.0.js ├── suite.js └── test-deopt.js ├── docs ├── .nojekyll ├── README.md ├── classes │ └── MiniSignal.md └── modules.md ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── index.ts ├── mini-signals.spec.ts ├── mini-signals.test-d.ts └── mini-signals.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "standard-with-typescript", 9 | "prettier" 10 | ], 11 | "overrides": [ 12 | { 13 | "files": ["*.ts", "*.tsx"], 14 | "parserOptions": { 15 | "project": ["./tsconfig.json"] 16 | } 17 | } 18 | ], 19 | "parserOptions": { 20 | "ecmaVersion": "latest", 21 | "sourceType": "module" 22 | }, 23 | "rules": {} 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | dist/ 30 | .nyc_output/ 31 | .vscode 32 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | bench/**/mini-signals@*.js -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## HEAD (Unreleased) 4 | _(none)_ 5 | 6 | --- 7 | 8 | ## 2.0.0 (2023-03-29) 9 | 10 | - `.add` is now type safe. The type of the listener is checked against the type variable in the constructor as well as an optional "flavor". 11 | - `.add` now returns a node reference instead of a object. The returned node cannot be removed directly; it must be from the signal using `MiniSignal#detach`. 12 | - `.once` has been removed. Use `.add` instead with a call to `.detach` in the callback. 13 | - The `thisArg` parameter has been removed from `.add`. Use `.add` with a call to `.bind` or (preferred) use an arrow function with a closure. 14 | - `.dispatch` now throws an error if the signal is already dispatching. 15 | - `.detach` now throws an error if node reference was not generated from the signal. 16 | 17 | ## 1.2.0 (2017-09-11) 18 | 19 | - Added TS typings 20 | 21 | ## 1.1.1 (2016-02-07) 22 | 23 | - Remove once listeners before calling, prevents loops 24 | 25 | ## 1.1.0 (2015-11-03) 26 | 27 | - Added optional thisArg on MiniSignal#add and MiniSignal#once 28 | - Added MiniSignal#has 29 | - MiniSignal#detach now checks if node bound to itself 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2023 Jayson Harshbarger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mini-signals 2 | 3 | signals, in JavaScript, fast 4 | 5 | [![NPM](https://img.shields.io/npm/v/mini-signals.svg)](https://www.npmjs.com/package/mini-signals) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Hypercubed/mini-signals/blob/master/LICENSE) 6 | 7 | ## Description 8 | 9 | Custom event/messaging system for TypeScript/JavaScript inspired by [js-signals](https://github.com/millermedeiros/js-signals) originally based on [EventEmitter3](https://github.com/primus/eventemitter3) code base. 10 | 11 | There are several advantages to signals over event-emitters (see [Comparison between different Observer Pattern implementations](https://github.com/millermedeiros/js-signals/wiki/Comparison-between-different-Observer-Pattern-implementations)). However, the current implementation of [js-signals](https://github.com/millermedeiros/js-signals) is (arguably) slow compared to other implementations (see [EventsSpeedTests](https://github.com/Hypercubed/EventsSpeedTests)). `mini-signals` is a fast, minimal emitter, with an API similar to [js-signals](https://github.com/millermedeiros/js-signals). 12 | 13 | > Note: Signals here are the type defined by Miller Medeiros in [js-signals](https://github.com/millermedeiros/js-signals) inspired by AS3-Signals. They should not to be confused with [SolidJS](https://www.solidjs.com/tutorial/introduction_signals) or [Angular signals](https://github.com/angular/angular/discussions/49090). 14 | 15 | ## mini-signals 2.0.0 16 | 17 | MiniSignals v2.0.0 has been rewritten in TypeScript and had it's API changed to improve performance and add type safety. 18 | 19 | New features: 20 | 21 | - `.add` now returns a node reference which can be used to remove the listener directly from the signal. Reduces memory leaks. 22 | - `.add` is now type safe. The type of the listener is checked against the type variable in the constructor as well as an optional "flavor". 23 | 24 | Breaking changes: 25 | 26 | - `.add` now returns a node reference instead of an object. The returned node cannot be removed directly; it must be from the signal using `MiniSignal#detach`. 27 | - `.once` has been removed. Use `.add` instead with a call to `.detach` in the callback. 28 | - The `thisArg` parameter has been removed from `.add`. Use `.add` with a call to `.bind` or (preferred) use an arrow function with a closure. 29 | - `.dispatch` now throws an error if the signal is already dispatching. 30 | `.detach` now throws an error if the node reference was not generated from the signal. 31 | 32 | ## Install 33 | 34 | ### npm: 35 | 36 | ```sh 37 | npm install mini-signals 38 | ``` 39 | 40 | ## Examples 41 | 42 | ```ts 43 | import { MiniSignal } from 'mini-signals'; 44 | 45 | const mySignal = new MiniSignal<[string, string]>(); // the type variable optionally and defines the parameters to be dispatched 46 | 47 | const binding = mySignal.add((foo: string, bar: string) => { // add listener, note the parameter types match the type variable in the constructor 48 | console.log('signal dispatched'); 49 | assert(foo === 'foo'); 50 | assert(bar === 'bar'); 51 | }); 52 | 53 | mySignal.dispatch('foo', 'bar'); // dispatch signal passing custom parameters 54 | mySignal.detach(binding); // remove a single listener 55 | ``` 56 | 57 | ### Another Example 58 | 59 | ```ts 60 | const myObject = { 61 | foo: "bar", 62 | updated: new MiniSignal() // in this case the type variable is never, since we are not passing any parameters 63 | }; 64 | 65 | myObject.updated.add(() => { 66 | console.log('signal dispatched'); 67 | assert(myObject.foo === 'baz'); 68 | }); 69 | 70 | myObject.foo = 'baz'; 71 | myObject.updated.dispatch(); // dispatch signal 72 | ``` 73 | 74 | ### Flavoring the signal 75 | 76 | ```ts 77 | import { MiniSignal } from 'mini-signals'; 78 | 79 | const mySignal = new MiniSignal<[string, string], 'mySignal'>(); 80 | const myOtherSignal = new MiniSignal<[string, string], 'myOtherSignal'>(); 81 | 82 | const binding = mySignal.add((foo: string, bar: string) => { 83 | // ... 84 | }); 85 | 86 | myOtherSignal.detach(binding); // TypeScript error: Argument of type 'MiniSignalBinding<[string, string], "mySignal">' is not assignable to parameter of type 'MiniSignalBinding<[string, string], "myOtherSignal">'. 87 | ``` 88 | 89 | ## API 90 | 91 | See [API.md](https://github.com/Hypercubed/mini-signals/blob/master/docs/classes/MiniSignal.md) 92 | 93 | ## License 94 | 95 | Copyright (c) 2015-2023 Jayson Harshbarger 96 | 97 | MIT License 98 | -------------------------------------------------------------------------------- /bench/emit-bind.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Preparation code. 5 | */ 6 | const EventEmitter3 = require('eventemitter3'); 7 | const EventEmitter1 = require('events').EventEmitter; 8 | const {MiniSignal} = require('../src/index'); 9 | const MiniSignal_0_0_1 = require('./mini-signals@0.0.1'); 10 | const MiniSignal_0_0_2 = require('./mini-signals@0.0.2'); 11 | const MiniSignal_1_0_1 = require('./mini-signals@1.0.1'); 12 | const MiniSignal_1_1_0 = require('./mini-signals@1.1.0'); 13 | const Signal = require('signals'); 14 | 15 | /** 16 | * Instances. 17 | */ 18 | const ee1 = new EventEmitter1(); 19 | const ee3 = new EventEmitter3(); 20 | const miniSignal = new MiniSignal(); 21 | const miniSignal_0_0_1 = new MiniSignal_0_0_1(); 22 | const miniSignal_0_0_2 = new MiniSignal_0_0_2(); 23 | const miniSignal_1_0_1 = new MiniSignal_1_0_1(); 24 | const miniSignal_1_1_0 = new MiniSignal_1_1_0(); 25 | const signal = new Signal(); 26 | 27 | var ctx = { 28 | foo: 'bar' 29 | }; 30 | 31 | function handle() { 32 | if (arguments.length > 100) {console.log('damn');} 33 | if (this !== ctx) {console.log('damn'); process.exit(); } 34 | } 35 | 36 | function handle2() { 37 | if (arguments.length > 100) {console.log('damn');} 38 | } 39 | 40 | // events 41 | ee1.on('foo', handle.bind(ctx)); ee1.on('foo', handle2); 42 | ee3.on('foo', handle.bind(ctx)); ee3.on('foo', handle2); 43 | 44 | // signals 45 | signal.add(handle.bind(ctx)); signal.add(handle2); 46 | miniSignal.add(handle.bind(ctx)); miniSignal.add(handle2); 47 | miniSignal_0_0_1.add(handle.bind(ctx)); miniSignal_0_0_1.add(handle2); 48 | miniSignal_0_0_2.add(handle.bind(ctx)); miniSignal_0_0_2.add(handle2); 49 | miniSignal_1_0_1.add(handle.bind(ctx)); miniSignal_1_0_1.add(handle2); 50 | miniSignal_1_1_0.add(handle.bind(ctx)); miniSignal_1_1_0.add(handle2); 51 | 52 | require('./suite')('emit-bind') 53 | .add('Node EventEmitter', () => { 54 | ee1.emit('foo'); 55 | ee1.emit('foo', 'bar'); 56 | ee1.emit('foo', 'bar', 'baz'); 57 | ee1.emit('foo', 'bar', 'baz', 'boom'); 58 | }) 59 | .add('EventEmitter3', () => { 60 | ee3.emit('foo'); 61 | ee3.emit('foo', 'bar'); 62 | ee3.emit('foo', 'bar', 'baz'); 63 | ee3.emit('foo', 'bar', 'baz', 'boom'); 64 | }) 65 | .add('JS-Signals', () => { 66 | signal.dispatch(); 67 | signal.dispatch('bar'); 68 | signal.dispatch('bar', 'baz'); 69 | signal.dispatch('bar', 'baz', 'boom'); 70 | }) 71 | .add('MiniSignals Latest', () => { 72 | miniSignal.dispatch(); 73 | miniSignal.dispatch('bar'); 74 | miniSignal.dispatch('bar', 'baz'); 75 | miniSignal.dispatch('bar', 'baz', 'boom'); 76 | }) 77 | .add('MiniSignals @0.0.1', () => { 78 | miniSignal_0_0_1.dispatch(); 79 | miniSignal_0_0_1.dispatch('bar'); 80 | miniSignal_0_0_1.dispatch('bar', 'baz'); 81 | miniSignal_0_0_1.dispatch('bar', 'baz', 'boom'); 82 | }) 83 | .add('MiniSignals @0.0.2', () => { 84 | miniSignal_0_0_2.dispatch(); 85 | miniSignal_0_0_2.dispatch('bar'); 86 | miniSignal_0_0_2.dispatch('bar', 'baz'); 87 | miniSignal_0_0_2.dispatch('bar', 'baz', 'boom'); 88 | }) 89 | .add('MiniSignals @1.0.1', () => { 90 | miniSignal_1_0_1.dispatch(); 91 | miniSignal_1_0_1.dispatch('bar'); 92 | miniSignal_1_0_1.dispatch('bar', 'baz'); 93 | miniSignal_1_0_1.dispatch('bar', 'baz', 'boom'); 94 | }) 95 | .add('MiniSignals @1.2.0', () => { 96 | miniSignal_1_1_0.dispatch(); 97 | miniSignal_1_1_0.dispatch('bar'); 98 | miniSignal_1_1_0.dispatch('bar', 'baz'); 99 | miniSignal_1_1_0.dispatch('bar', 'baz', 'boom'); 100 | }) 101 | .run(); 102 | -------------------------------------------------------------------------------- /bench/emit-context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Preparation code. 5 | */ 6 | const EventEmitter3 = require('eventemitter3'); 7 | const EventEmitter1 = require('events').EventEmitter; 8 | const {MiniSignal} = require('../src/index'); 9 | const MiniSignal_0_0_1 = require('./mini-signals@0.0.1'); 10 | const MiniSignal_0_0_2 = require('./mini-signals@0.0.2'); 11 | const MiniSignal_1_0_1 = require('./mini-signals@1.0.1'); 12 | const MiniSignal_1_1_0 = require('./mini-signals@1.1.0'); 13 | const Signal = require('signals'); 14 | 15 | /** 16 | * Instances. 17 | */ 18 | const ee1 = new EventEmitter1(); 19 | const ee3 = new EventEmitter3(); 20 | const miniSignal = new MiniSignal(); 21 | const miniSignal_0_0_1 = new MiniSignal_0_0_1(); 22 | const miniSignal_0_0_2 = new MiniSignal_0_0_2(); 23 | const miniSignal_1_0_1 = new MiniSignal_1_0_1(); 24 | const miniSignal_1_1_0 = new MiniSignal_1_1_0(); 25 | const signal = new Signal(); 26 | 27 | var ctx = { 28 | foo: 'bar' 29 | }; 30 | 31 | function handle() { 32 | if (arguments.length > 100) {console.log('damn');} 33 | if (this !== ctx) {console.log('damn'); process.exit(); } 34 | } 35 | 36 | function handle2() { 37 | if (arguments.length > 100) {console.log('damn');} 38 | } 39 | 40 | // events 41 | ee1.on('foo', handle.bind(ctx)); ee1.on('foo', handle2); 42 | ee3.on('foo', handle, ctx); ee3.on('foo', handle2); 43 | 44 | // signals 45 | signal.add(handle, ctx); signal.add(handle2); 46 | miniSignal.add(handle.bind(ctx)); miniSignal.add(handle2); // Note: MiniSignal no loner supports context 47 | miniSignal_0_0_1.add(handle.bind(ctx)); miniSignal_0_0_1.add(handle2); 48 | miniSignal_0_0_2.add(handle.bind(ctx)); miniSignal_0_0_2.add(handle2); 49 | miniSignal_1_0_1.add(handle.bind(ctx)); miniSignal_1_0_1.add(handle2); 50 | miniSignal_1_1_0.add(handle, ctx); miniSignal_1_1_0.add(handle2); 51 | 52 | require('./suite')('emit-context') 53 | .add('Node EventEmitter', () => { 54 | ee1.emit('foo'); 55 | ee1.emit('foo', 'bar'); 56 | ee1.emit('foo', 'bar', 'baz'); 57 | ee1.emit('foo', 'bar', 'baz', 'boom'); 58 | }) 59 | .add('EventEmitter3', () => { 60 | ee3.emit('foo'); 61 | ee3.emit('foo', 'bar'); 62 | ee3.emit('foo', 'bar', 'baz'); 63 | ee3.emit('foo', 'bar', 'baz', 'boom'); 64 | }) 65 | .add('JS-Signals', () => { 66 | signal.dispatch(); 67 | signal.dispatch('bar'); 68 | signal.dispatch('bar', 'baz'); 69 | signal.dispatch('bar', 'baz', 'boom'); 70 | }) 71 | .add('MiniSignals Latest', () => { 72 | miniSignal.dispatch(); 73 | miniSignal.dispatch('bar'); 74 | miniSignal.dispatch('bar', 'baz'); 75 | miniSignal.dispatch('bar', 'baz', 'boom'); 76 | }) 77 | .add('MiniSignals @0.0.1', () => { 78 | miniSignal_0_0_1.dispatch(); 79 | miniSignal_0_0_1.dispatch('bar'); 80 | miniSignal_0_0_1.dispatch('bar', 'baz'); 81 | miniSignal_0_0_1.dispatch('bar', 'baz', 'boom'); 82 | }) 83 | .add('MiniSignals @0.0.2', () => { 84 | miniSignal_0_0_2.dispatch(); 85 | miniSignal_0_0_2.dispatch('bar'); 86 | miniSignal_0_0_2.dispatch('bar', 'baz'); 87 | miniSignal_0_0_2.dispatch('bar', 'baz', 'boom'); 88 | }) 89 | .add('MiniSignals @1.0.1', () => { 90 | miniSignal_1_0_1.dispatch(); 91 | miniSignal_1_0_1.dispatch('bar'); 92 | miniSignal_1_0_1.dispatch('bar', 'baz'); 93 | miniSignal_1_0_1.dispatch('bar', 'baz', 'boom'); 94 | }) 95 | .add('MiniSignals @1.2.0', () => { 96 | miniSignal_1_1_0.dispatch(); 97 | miniSignal_1_1_0.dispatch('bar'); 98 | miniSignal_1_1_0.dispatch('bar', 'baz'); 99 | miniSignal_1_1_0.dispatch('bar', 'baz', 'boom'); 100 | }) 101 | .run(); 102 | -------------------------------------------------------------------------------- /bench/emit-deopt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Preparation code. 5 | */ 6 | const EventEmitter3 = require('eventemitter3'); 7 | const EventEmitter1 = require('events').EventEmitter; 8 | const {MiniSignal} = require('../src/index'); 9 | const MiniSignal_0_0_1 = require('./mini-signals@0.0.1'); 10 | const MiniSignal_0_0_2 = require('./mini-signals@0.0.2'); 11 | const MiniSignal_1_0_1 = require('./mini-signals@1.0.1'); 12 | const MiniSignal_1_1_0 = require('./mini-signals@1.1.0'); 13 | const Signal = require('signals'); 14 | 15 | /** 16 | * Instances. 17 | */ 18 | const ee1 = new EventEmitter1(); 19 | const ee3 = new EventEmitter3(); 20 | const miniSignal = new MiniSignal(); 21 | const miniSignal_0_0_1 = new MiniSignal_0_0_1(); 22 | const miniSignal_0_0_2 = new MiniSignal_0_0_2(); 23 | const miniSignal_1_0_1 = new MiniSignal_1_0_1(); 24 | const miniSignal_1_1_0 = new MiniSignal_1_1_0(); 25 | const signal = new Signal(); 26 | 27 | function handle() { 28 | if (arguments.length > 100) {console.log('damn');} 29 | } 30 | 31 | // Adding a prop to the function to make it deopt 32 | handle.$inject = [1,2,3]; 33 | 34 | function handle2() { 35 | if (arguments.length > 100) {console.log('damn');} 36 | } 37 | 38 | // events 39 | ee1.on('foo', handle); ee1.on('foo', handle2); 40 | ee3.on('foo', handle); ee3.on('foo', handle2); 41 | 42 | // signals 43 | signal.add(handle); signal.add(handle2); 44 | miniSignal.add(handle); miniSignal.add(handle2); 45 | miniSignal_0_0_1.add(handle); miniSignal_0_0_1.add(handle2); 46 | miniSignal_0_0_2.add(handle); miniSignal_0_0_2.add(handle2); 47 | miniSignal_1_0_1.add(handle); miniSignal_1_0_1.add(handle2); 48 | miniSignal_1_1_0.add(handle); miniSignal_1_1_0.add(handle2); 49 | 50 | require('./suite')('emit-deopt') 51 | .add('Node EventEmitter', () => { 52 | ee1.emit('foo'); 53 | ee1.emit('foo', 'bar'); 54 | ee1.emit('foo', 'bar', 'baz'); 55 | ee1.emit('foo', 'bar', 'baz', 'boom'); 56 | }) 57 | .add('EventEmitter3', () => { 58 | ee3.emit('foo'); 59 | ee3.emit('foo', 'bar'); 60 | ee3.emit('foo', 'bar', 'baz'); 61 | ee3.emit('foo', 'bar', 'baz', 'boom'); 62 | }) 63 | .add('JS-Signals', () => { 64 | signal.dispatch(); 65 | signal.dispatch('bar'); 66 | signal.dispatch('bar', 'baz'); 67 | signal.dispatch('bar', 'baz', 'boom'); 68 | }) 69 | .add('MiniSignals Latest', () => { 70 | miniSignal.dispatch(); 71 | miniSignal.dispatch('bar'); 72 | miniSignal.dispatch('bar', 'baz'); 73 | miniSignal.dispatch('bar', 'baz', 'boom'); 74 | }) 75 | .add('MiniSignals @0.0.1', () => { 76 | miniSignal_0_0_1.dispatch(); 77 | miniSignal_0_0_1.dispatch('bar'); 78 | miniSignal_0_0_1.dispatch('bar', 'baz'); 79 | miniSignal_0_0_1.dispatch('bar', 'baz', 'boom'); 80 | }) 81 | .add('MiniSignals @0.0.2', () => { 82 | miniSignal_0_0_2.dispatch(); 83 | miniSignal_0_0_2.dispatch('bar'); 84 | miniSignal_0_0_2.dispatch('bar', 'baz'); 85 | miniSignal_0_0_2.dispatch('bar', 'baz', 'boom'); 86 | }) 87 | .add('MiniSignals @1.0.1', () => { 88 | miniSignal_1_0_1.dispatch(); 89 | miniSignal_1_0_1.dispatch('bar'); 90 | miniSignal_1_0_1.dispatch('bar', 'baz'); 91 | miniSignal_1_0_1.dispatch('bar', 'baz', 'boom'); 92 | }) 93 | .add('MiniSignals @1.2.0', () => { 94 | miniSignal_1_1_0.dispatch(); 95 | miniSignal_1_1_0.dispatch('bar'); 96 | miniSignal_1_1_0.dispatch('bar', 'baz'); 97 | miniSignal_1_1_0.dispatch('bar', 'baz', 'boom'); 98 | }) 99 | .run(); 100 | -------------------------------------------------------------------------------- /bench/emit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Preparation code. 5 | */ 6 | const EventEmitter3 = require('eventemitter3'); 7 | const EventEmitter1 = require('events').EventEmitter; 8 | const {MiniSignal} = require('../src/index'); 9 | const MiniSignal_0_0_1 = require('./mini-signals@0.0.1'); 10 | const MiniSignal_0_0_2 = require('./mini-signals@0.0.2'); 11 | const MiniSignal_1_0_1 = require('./mini-signals@1.0.1'); 12 | const MiniSignal_1_1_0 = require('./mini-signals@1.1.0'); 13 | const Signal = require('signals'); 14 | 15 | /** 16 | * Instances. 17 | */ 18 | const ee1 = new EventEmitter1(); 19 | const ee3 = new EventEmitter3(); 20 | const miniSignal = new MiniSignal(); 21 | const miniSignal_0_0_1 = new MiniSignal_0_0_1(); 22 | const miniSignal_0_0_2 = new MiniSignal_0_0_2(); 23 | const miniSignal_1_0_1 = new MiniSignal_1_0_1(); 24 | const miniSignal_1_1_0 = new MiniSignal_1_1_0(); 25 | const signal = new Signal(); 26 | 27 | function handle() { 28 | if (arguments.length > 100) {console.log('damn');} 29 | } 30 | 31 | function handle2() { 32 | if (arguments.length > 100) {console.log('damn');} 33 | } 34 | 35 | // events 36 | ee1.on('foo', handle); ee1.on('foo', handle2); 37 | ee3.on('foo', handle); ee3.on('foo', handle2); 38 | 39 | // signals 40 | signal.add(handle); signal.add(handle2); 41 | miniSignal.add(handle); miniSignal.add(handle2); 42 | miniSignal_0_0_1.add(handle); miniSignal_0_0_1.add(handle2); 43 | miniSignal_0_0_2.add(handle); miniSignal_0_0_2.add(handle2); 44 | miniSignal_1_0_1.add(handle); miniSignal_1_0_1.add(handle2); 45 | miniSignal_1_1_0.add(handle); miniSignal_1_1_0.add(handle2); 46 | 47 | require('./suite')('emit') 48 | .add('Node EventEmitter', () => { 49 | ee1.emit('foo'); 50 | ee1.emit('foo', 'bar'); 51 | ee1.emit('foo', 'bar', 'baz'); 52 | ee1.emit('foo', 'bar', 'baz', 'boom'); 53 | }) 54 | .add('EventEmitter3', () => { 55 | ee3.emit('foo'); 56 | ee3.emit('foo', 'bar'); 57 | ee3.emit('foo', 'bar', 'baz'); 58 | ee3.emit('foo', 'bar', 'baz', 'boom'); 59 | }) 60 | .add('JS-Signals', () => { 61 | signal.dispatch(); 62 | signal.dispatch('bar'); 63 | signal.dispatch('bar', 'baz'); 64 | signal.dispatch('bar', 'baz', 'boom'); 65 | }) 66 | .add('MiniSignals Latest', () => { 67 | miniSignal.dispatch(); 68 | miniSignal.dispatch('bar'); 69 | miniSignal.dispatch('bar', 'baz'); 70 | miniSignal.dispatch('bar', 'baz', 'boom'); 71 | }) 72 | .add('MiniSignals @0.0.1', () => { 73 | miniSignal_0_0_1.dispatch(); 74 | miniSignal_0_0_1.dispatch('bar'); 75 | miniSignal_0_0_1.dispatch('bar', 'baz'); 76 | miniSignal_0_0_1.dispatch('bar', 'baz', 'boom'); 77 | }) 78 | .add('MiniSignals @0.0.2', () => { 79 | miniSignal_0_0_2.dispatch(); 80 | miniSignal_0_0_2.dispatch('bar'); 81 | miniSignal_0_0_2.dispatch('bar', 'baz'); 82 | miniSignal_0_0_2.dispatch('bar', 'baz', 'boom'); 83 | }) 84 | .add('MiniSignals @1.0.1', () => { 85 | miniSignal_1_0_1.dispatch(); 86 | miniSignal_1_0_1.dispatch('bar'); 87 | miniSignal_1_0_1.dispatch('bar', 'baz'); 88 | miniSignal_1_0_1.dispatch('bar', 'baz', 'boom'); 89 | }) 90 | .add('MiniSignals @1.2.0', () => { 91 | miniSignal_1_1_0.dispatch(); 92 | miniSignal_1_1_0.dispatch('bar'); 93 | miniSignal_1_1_0.dispatch('bar', 'baz'); 94 | miniSignal_1_1_0.dispatch('bar', 'baz', 'boom'); 95 | }) 96 | .run(); 97 | -------------------------------------------------------------------------------- /bench/index.js: -------------------------------------------------------------------------------- 1 | require('./emit.js'); 2 | require('./emit-context.js'); 3 | require('./emit-bind.js'); 4 | require('./emit-deopt.js'); 5 | -------------------------------------------------------------------------------- /bench/latest.md: -------------------------------------------------------------------------------- 1 | 2 | # emit 3 | Node EventEmitter x 4,701,629 ops/sec ±2.71% (93 runs sampled) 4 | EventEmitter3 x 7,993,359 ops/sec ±2.53% (90 runs sampled) 5 | JS-Signals x 965,780 ops/sec ±4.75% (80 runs sampled) 6 | MiniSignals Latest x 8,769,543 ops/sec ±6.81% (69 runs sampled) 7 | MiniSignals @0.0.1 x 6,583,595 ops/sec ±3.78% (88 runs sampled) 8 | MiniSignals @0.0.2 x 11,648,869 ops/sec ±2.56% (90 runs sampled) 9 | MiniSignals @1.0.1 x 10,762,628 ops/sec ±2.73% (87 runs sampled) 10 | MiniSignals @1.2.0 x 10,920,804 ops/sec ±2.25% (93 runs sampled) 11 | *Fastest is MiniSignals @0.0.2* 12 | 13 | # emit-context 14 | Node EventEmitter x 4,944,368 ops/sec ±2.83% (91 runs sampled) 15 | EventEmitter3 x 8,357,685 ops/sec ±2.02% (94 runs sampled) 16 | JS-Signals x 1,082,971 ops/sec ±1.99% (91 runs sampled) 17 | MiniSignals Latest x 11,506,542 ops/sec ±2.48% (87 runs sampled) 18 | MiniSignals @0.0.1 x 6,408,780 ops/sec ±2.31% (90 runs sampled) 19 | MiniSignals @0.0.2 x 6,555,570 ops/sec ±1.34% (92 runs sampled) 20 | MiniSignals @1.0.1 x 10,009,634 ops/sec ±1.64% (92 runs sampled) 21 | MiniSignals @1.2.0 x 10,701,207 ops/sec ±3.00% (90 runs sampled) 22 | *Fastest is MiniSignals Latest* 23 | 24 | # emit-bind 25 | Node EventEmitter x 4,854,877 ops/sec ±2.81% (90 runs sampled) 26 | EventEmitter3 x 7,553,681 ops/sec ±2.09% (93 runs sampled) 27 | JS-Signals x 1,195,863 ops/sec ±1.81% (92 runs sampled) 28 | MiniSignals Latest x 11,507,058 ops/sec ±2.76% (88 runs sampled) 29 | MiniSignals @0.0.1 x 6,320,653 ops/sec ±2.67% (89 runs sampled) 30 | MiniSignals @0.0.2 x 10,280,050 ops/sec ±2.93% (88 runs sampled) 31 | MiniSignals @1.0.1 x 9,873,577 ops/sec ±1.99% (91 runs sampled) 32 | MiniSignals @1.2.0 x 9,978,793 ops/sec ±2.02% (95 runs sampled) 33 | *Fastest is MiniSignals Latest* 34 | 35 | # emit-deopt 36 | Node EventEmitter x 5,125,078 ops/sec ±2.53% (87 runs sampled) 37 | EventEmitter3 x 8,742,556 ops/sec ±0.55% (98 runs sampled) 38 | JS-Signals x 1,208,997 ops/sec ±2.96% (92 runs sampled) 39 | MiniSignals Latest x 13,980,985 ops/sec ±1.06% (94 runs sampled) 40 | MiniSignals @0.0.1 x 6,898,518 ops/sec ±1.99% (91 runs sampled) 41 | MiniSignals @0.0.2 x 11,065,502 ops/sec ±3.09% (92 runs sampled) 42 | MiniSignals @1.0.1 x 9,914,237 ops/sec ±2.69% (86 runs sampled) 43 | MiniSignals @1.2.0 x 9,608,100 ops/sec ±4.62% (85 runs sampled) 44 | *Fastest is MiniSignals Latest* 45 | -------------------------------------------------------------------------------- /bench/mini-signals@0.0.1.js: -------------------------------------------------------------------------------- 1 | "use strict";function EE(t,e){this.fn=t,this.context=e}function MiniSignals(){}MiniSignals.prototype._listeners=void 0,MiniSignals.prototype.listeners=function t(e){var n=this._listeners;if(e)return!!n;if(!n)return[];if(n.fn)return[n.fn];for(var i=0,s=n.length,r=Array(s);i Note: Signals here are the type defined by Miller Medeiros in [js-signals](https://github.com/millermedeiros/js-signals) inspired by AS3-Signals. They should not to be confused with [SolidJS](https://www.solidjs.com/tutorial/introduction_signals) or [Angular signals](https://github.com/angular/angular/discussions/49090). 16 | 17 | ## mini-signals 2.0.0 18 | 19 | MiniSignals v2.0.0 has been rewritten in TypeScript and had it's API changed to improve performance and add type safety. 20 | 21 | New features: 22 | 23 | - `.add` now returns a node reference which can be used to remove the listener directly from the signal. Reduces memory leaks. 24 | - `.add` is now type safe. The type of the listener is checked against the type variable in the constructor as well as an optional "flavor". 25 | 26 | Breaking changes: 27 | 28 | - `.add` now returns a node reference instead of an object. The returned node cannot be removed directly; it must be from the signal using `MiniSignal#detach`. 29 | - `.once` has been removed. Use `.add` instead with a call to `.detach` in the callback. 30 | - The `thisArg` parameter has been removed from `.add`. Use `.add` with a call to `.bind` or (preferred) use an arrow function with a closure. 31 | - `.dispatch` now throws an error if the signal is already dispatching. 32 | `.detach` now throws an error if the node reference was not generated from the signal. 33 | 34 | ## Install 35 | 36 | ### npm: 37 | 38 | ```sh 39 | npm install mini-signals 40 | ``` 41 | 42 | ## Examples 43 | 44 | ```ts 45 | import { MiniSignal } from 'mini-signals'; 46 | 47 | const mySignal = new MiniSignal<[string, string]>(); // the type variable optionally and defines the parameters to be dispatched 48 | 49 | const binding = mySignal.add((foo: string, bar: string) => { // add listener, note the parameter types match the type variable in the constructor 50 | console.log('signal dispatched'); 51 | assert(foo === 'foo'); 52 | assert(bar === 'bar'); 53 | }); 54 | 55 | mySignal.dispatch('foo', 'bar'); // dispatch signal passing custom parameters 56 | mySignal.detach(binding); // remove a single listener 57 | ``` 58 | 59 | ### Another Example 60 | 61 | ```ts 62 | const myObject = { 63 | foo: "bar", 64 | updated: new MiniSignal() // in this case the type variable is never, since we are not passing any parameters 65 | }; 66 | 67 | myObject.updated.add(() => { 68 | console.log('signal dispatched'); 69 | assert(myObject.foo === 'baz'); 70 | }); 71 | 72 | myObject.foo = 'baz'; 73 | myObject.updated.dispatch(); // dispatch signal 74 | ``` 75 | 76 | ### Flavoring the signal 77 | 78 | ```ts 79 | import { MiniSignal } from 'mini-signals'; 80 | 81 | const mySignal = new MiniSignal<[string, string], 'mySignal'>(); 82 | const myOtherSignal = new MiniSignal<[string, string], 'myOtherSignal'>(); 83 | 84 | const binding = mySignal.add((foo: string, bar: string) => { 85 | // ... 86 | }); 87 | 88 | myOtherSignal.detach(binding); // TypeScript error: Argument of type 'MiniSignalBinding<[string, string], "mySignal">' is not assignable to parameter of type 'MiniSignalBinding<[string, string], "myOtherSignal">'. 89 | ``` 90 | 91 | ## API 92 | 93 | See [API.md](https://github.com/Hypercubed/mini-signals/blob/master/docs/classes/MiniSignal.md) 94 | 95 | ## License 96 | 97 | Copyright (c) 2015-2023 Jayson Harshbarger 98 | 99 | MIT License 100 | -------------------------------------------------------------------------------- /docs/classes/MiniSignal.md: -------------------------------------------------------------------------------- 1 | [mini-signals](../README.md) / [Exports](../modules.md) / MiniSignal 2 | 3 | # Class: MiniSignal 4 | 5 | ## Type parameters 6 | 7 | | Name | Type | 8 | | :------ | :------ | 9 | | `T` | extends `any`[] = `any`[] | 10 | | `S` | extends `any` = `Symbol` \| `string` | 11 | 12 | ## Table of contents 13 | 14 | ### Constructors 15 | 16 | - [constructor](MiniSignal.md#constructor) 17 | 18 | ### Properties 19 | 20 | - [\_dispatching](MiniSignal.md#_dispatching) 21 | - [\_head](MiniSignal.md#_head) 22 | - [\_refMap](MiniSignal.md#_refmap) 23 | - [\_symbol](MiniSignal.md#_symbol) 24 | - [\_tail](MiniSignal.md#_tail) 25 | 26 | ### Methods 27 | 28 | - [\_addNode](MiniSignal.md#_addnode) 29 | - [\_createRef](MiniSignal.md#_createref) 30 | - [\_destroyNode](MiniSignal.md#_destroynode) 31 | - [\_disconnectNode](MiniSignal.md#_disconnectnode) 32 | - [\_getRef](MiniSignal.md#_getref) 33 | - [add](MiniSignal.md#add) 34 | - [detach](MiniSignal.md#detach) 35 | - [detachAll](MiniSignal.md#detachall) 36 | - [dispatch](MiniSignal.md#dispatch) 37 | - [hasListeners](MiniSignal.md#haslisteners) 38 | 39 | ## Constructors 40 | 41 | ### constructor 42 | 43 | • **new MiniSignal**<`T`, `S`\>() 44 | 45 | #### Type parameters 46 | 47 | | Name | Type | 48 | | :------ | :------ | 49 | | `T` | extends `any`[] = `any`[] | 50 | | `S` | extends `unknown` = `string` \| `Symbol` | 51 | 52 | ## Properties 53 | 54 | ### \_dispatching 55 | 56 | • `Private` **\_dispatching**: `boolean` = `false` 57 | 58 | #### Defined in 59 | 60 | [mini-signals.ts:34](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L34) 61 | 62 | ___ 63 | 64 | ### \_head 65 | 66 | • `Private` `Optional` **\_head**: `MiniSignalNode`<`T`\> = `undefined` 67 | 68 | #### Defined in 69 | 70 | [mini-signals.ts:32](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L32) 71 | 72 | ___ 73 | 74 | ### \_refMap 75 | 76 | • `Private` **\_refMap**: `WeakMap`<`MiniSignalNodeRef`<`T`, `S`\>, `MiniSignalNode`<`T`\>\> 77 | 78 | #### Defined in 79 | 80 | [mini-signals.ts:30](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L30) 81 | 82 | ___ 83 | 84 | ### \_symbol 85 | 86 | • `Private` `Readonly` **\_symbol**: `symbol` 87 | 88 | A Symbol that is used to guarantee the uniqueness of the MiniSignal 89 | instance. 90 | 91 | #### Defined in 92 | 93 | [mini-signals.ts:29](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L29) 94 | 95 | ___ 96 | 97 | ### \_tail 98 | 99 | • `Private` `Optional` **\_tail**: `MiniSignalNode`<`T`\> = `undefined` 100 | 101 | #### Defined in 102 | 103 | [mini-signals.ts:33](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L33) 104 | 105 | ## Methods 106 | 107 | ### \_addNode 108 | 109 | ▸ `Private` **_addNode**(`node`): `MiniSignalNode`<`T`\> 110 | 111 | #### Parameters 112 | 113 | | Name | Type | 114 | | :------ | :------ | 115 | | `node` | `MiniSignalNode`<`T`\> | 116 | 117 | #### Returns 118 | 119 | `MiniSignalNode`<`T`\> 120 | 121 | #### Defined in 122 | 123 | [mini-signals.ts:145](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L145) 124 | 125 | ___ 126 | 127 | ### \_createRef 128 | 129 | ▸ `Private` **_createRef**(`node`): `MiniSignalNodeRef`<`T`, `S`\> 130 | 131 | #### Parameters 132 | 133 | | Name | Type | 134 | | :------ | :------ | 135 | | `node` | `MiniSignalNode`<`T`\> | 136 | 137 | #### Returns 138 | 139 | `MiniSignalNodeRef`<`T`, `S`\> 140 | 141 | #### Defined in 142 | 143 | [mini-signals.ts:159](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L159) 144 | 145 | ___ 146 | 147 | ### \_destroyNode 148 | 149 | ▸ `Private` **_destroyNode**(`node`): `void` 150 | 151 | #### Parameters 152 | 153 | | Name | Type | 154 | | :------ | :------ | 155 | | `node` | `MiniSignalNode`<`T`\> | 156 | 157 | #### Returns 158 | 159 | `void` 160 | 161 | #### Defined in 162 | 163 | [mini-signals.ts:117](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L117) 164 | 165 | ___ 166 | 167 | ### \_disconnectNode 168 | 169 | ▸ `Private` **_disconnectNode**(`node`): `void` 170 | 171 | #### Parameters 172 | 173 | | Name | Type | 174 | | :------ | :------ | 175 | | `node` | `MiniSignalNode`<`T`\> | 176 | 177 | #### Returns 178 | 179 | `void` 180 | 181 | #### Defined in 182 | 183 | [mini-signals.ts:122](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L122) 184 | 185 | ___ 186 | 187 | ### \_getRef 188 | 189 | ▸ `Protected` **_getRef**(`sym`): `undefined` \| `MiniSignalNode`<`T`\> 190 | 191 | #### Parameters 192 | 193 | | Name | Type | 194 | | :------ | :------ | 195 | | `sym` | `MiniSignalNodeRef`<`T`, `S`\> | 196 | 197 | #### Returns 198 | 199 | `undefined` \| `MiniSignalNode`<`T`\> 200 | 201 | #### Defined in 202 | 203 | [mini-signals.ts:165](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L165) 204 | 205 | ___ 206 | 207 | ### add 208 | 209 | ▸ **add**(`fn`): `MiniSignalNodeRef`<`T`, `S`\> 210 | 211 | Register a new listener. 212 | 213 | #### Parameters 214 | 215 | | Name | Type | 216 | | :------ | :------ | 217 | | `fn` | `CallBack`<`T`\> | 218 | 219 | #### Returns 220 | 221 | `MiniSignalNodeRef`<`T`, `S`\> 222 | 223 | #### Defined in 224 | 225 | [mini-signals.ts:65](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L65) 226 | 227 | ___ 228 | 229 | ### detach 230 | 231 | ▸ **detach**(`sym`): [`MiniSignal`](MiniSignal.md)<`T`, `S`\> 232 | 233 | Remove binding object. 234 | 235 | #### Parameters 236 | 237 | | Name | Type | 238 | | :------ | :------ | 239 | | `sym` | `MiniSignalNodeRef`<`T`, `S`\> | 240 | 241 | #### Returns 242 | 243 | [`MiniSignal`](MiniSignal.md)<`T`, `S`\> 244 | 245 | #### Defined in 246 | 247 | [mini-signals.ts:75](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L75) 248 | 249 | ___ 250 | 251 | ### detachAll 252 | 253 | ▸ **detachAll**(): [`MiniSignal`](MiniSignal.md)<`T`, `S`\> 254 | 255 | Detach all listeners. 256 | 257 | #### Returns 258 | 259 | [`MiniSignal`](MiniSignal.md)<`T`, `S`\> 260 | 261 | #### Defined in 262 | 263 | [mini-signals.ts:102](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L102) 264 | 265 | ___ 266 | 267 | ### dispatch 268 | 269 | ▸ **dispatch**(`...args`): `boolean` 270 | 271 | Dispatches a signal to all registered listeners. 272 | 273 | #### Parameters 274 | 275 | | Name | Type | 276 | | :------ | :------ | 277 | | `...args` | `T` | 278 | 279 | #### Returns 280 | 281 | `boolean` 282 | 283 | #### Defined in 284 | 285 | [mini-signals.ts:43](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L43) 286 | 287 | ___ 288 | 289 | ### hasListeners 290 | 291 | ▸ **hasListeners**(): `boolean` 292 | 293 | #### Returns 294 | 295 | `boolean` 296 | 297 | #### Defined in 298 | 299 | [mini-signals.ts:36](https://github.com/Hypercubed/mini-signals/blob/e88de47/src/mini-signals.ts#L36) 300 | -------------------------------------------------------------------------------- /docs/modules.md: -------------------------------------------------------------------------------- 1 | [mini-signals](README.md) / Exports 2 | 3 | # mini-signals 4 | 5 | ## Table of contents 6 | 7 | ### Classes 8 | 9 | - [MiniSignal](classes/MiniSignal.md) 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-signals", 3 | "version": "2.0.0", 4 | "description": "signals, in TypeScript, fast", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "/dist", 9 | "/docs" 10 | ], 11 | "scripts": { 12 | "test": "npm-run-all test:*", 13 | "test:unit": "mocha --reporter spec --require ts-node/register src/**/*.spec.ts -n expose-gc", 14 | "test:deopt": "node --trace_opt --trace_deopt ./bench/test-deopt.js | grep \"disabled optimization\" || true", 15 | "test:types": "tsd -f ./src/mini-signals.test-d.ts", 16 | "build": "npm-run-all build:*", 17 | "build:tsc": "tsc", 18 | "xxx-build:doc": "typedoc --plugin typedoc-plugin-markdown --out docs src/index.ts", 19 | "coverage": "nyc --reporter=html --reporter=text --timeout=3000 npm run test:unit", 20 | "bench": "ts-node ./bench/index.js | tee ./bench/latest.md", 21 | "bench:emit": "ts-node ./bench/emit.js", 22 | "bench:emit-bind": "ts-node ./bench/emit-bind.js", 23 | "bench:emit-context": "ts-node ./bench/emit-context.js", 24 | "bench:emit-depot": "ts-node ./bench/emit-depot.js", 25 | "version": "chg release -y && git add -A CHANGELOG.md", 26 | "np": "npm run build && np", 27 | "fix": "npm-run-all fix:*", 28 | "fix:prettier": "prettier --write './{src, bench}/**/*.*'", 29 | "fix:lint": "eslint './{src, bench}/**/*.*' --fix" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/Hypercubed/mini-signals.git" 34 | }, 35 | "keywords": [ 36 | "events", 37 | "signals", 38 | "fast" 39 | ], 40 | "author": "J. Harshbarger", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/Hypercubed/mini-signals/issues" 44 | }, 45 | "homepage": "https://github.com/Hypercubed/mini-signals", 46 | "devDependencies": { 47 | "@types/chai": "^4.3.4", 48 | "@types/mocha": "^10.0.1", 49 | "@typescript-eslint/eslint-plugin": "^5.57.0", 50 | "benchmark": "^1.0.0", 51 | "chai": "^4.3.7", 52 | "chg": "^0.4.0", 53 | "eslint": "^8.37.0", 54 | "eslint-config-prettier": "^8.8.0", 55 | "eslint-config-standard-with-typescript": "^34.0.1", 56 | "eslint-plugin-import": "^2.27.5", 57 | "eslint-plugin-n": "^15.7.0", 58 | "eslint-plugin-promise": "^6.1.1", 59 | "eventemitter3": "^5.0.0", 60 | "mocha": "^10.2.0", 61 | "np": "^7.7.0", 62 | "npm-check": "^6.0.1", 63 | "npm-run-all": "^4.1.5", 64 | "nyc": "^15.1.0", 65 | "prettier": "2.8.7", 66 | "signals": "^1.0.0", 67 | "ts-node": "^10.9.1", 68 | "tsd": "^0.28.1", 69 | "typedoc": "^0.23.28", 70 | "typedoc-plugin-markdown": "^3.14.0", 71 | "typescript": "^5.0.2" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"] 3 | } 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mini-signals'; 2 | -------------------------------------------------------------------------------- /src/mini-signals.spec.ts: -------------------------------------------------------------------------------- 1 | import { MiniSignal } from '../src/mini-signals'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | describe('MiniSignal', () => { 6 | const pattern: string[] = []; 7 | 8 | const writer = (a: string): void => { 9 | pattern.push(a); 10 | }; 11 | 12 | beforeEach(() => { 13 | pattern.length = 0; 14 | }); 15 | 16 | it('quick test', () => { 17 | const e = new MiniSignal<[string]>(); 18 | 19 | const foo = e.add(writer); 20 | e.add(writer); 21 | const bar = e.add(writer); 22 | 23 | expect(e instanceof MiniSignal); 24 | 25 | e.dispatch('banana'); 26 | e.dispatch('apple'); 27 | 28 | e.detach(foo); 29 | e.detach(bar); 30 | 31 | e.dispatch('pear'); 32 | 33 | e.detachAll(); 34 | 35 | e.dispatch('raspberry'); 36 | 37 | expect(pattern.join(';')).to.equal('banana;banana;banana;apple;apple;apple;pear'); 38 | }); 39 | 40 | describe('Readme Examples', () => { 41 | it('Example Usage', () => { 42 | const mySignal = new MiniSignal<[string, string]>(); 43 | 44 | const binding = mySignal.add(onSignal); // add listener 45 | mySignal.dispatch('foo', 'bar'); // dispatch signal passing custom parameters 46 | mySignal.detach(binding); // remove a single listener 47 | 48 | function onSignal(foo: string, bar: string): void { 49 | expect(foo).to.equal('foo'); 50 | expect(bar).to.equal('bar'); 51 | } 52 | }); 53 | 54 | it('Function#bind example', () => { 55 | const mySignal = new MiniSignal<[string]>(); 56 | const context = {}; 57 | 58 | const cb = function (bar: string) { 59 | expect(arguments).has.length(1); 60 | expect(bar).equals('bar'); 61 | expect(this).equals(context); 62 | }.bind(context); 63 | 64 | mySignal.add(cb); 65 | 66 | mySignal.dispatch('bar'); 67 | }); 68 | 69 | it('Function#bind example with parameters', () => { 70 | const mySignal = new MiniSignal(); 71 | const context = {}; 72 | const cb = function (bar: string) { 73 | expect(arguments).has.length(1); 74 | expect(bar).equals('bar'); 75 | expect(this).equals(context); 76 | }.bind(context, 'bar'); 77 | 78 | mySignal.add(cb); 79 | 80 | mySignal.dispatch(); 81 | }); 82 | }); 83 | 84 | describe('MiniSignal#add', () => { 85 | let e: MiniSignal; 86 | 87 | beforeEach(() => { 88 | e = new MiniSignal<[string]>(); 89 | }); 90 | 91 | it('should throw error for incorrect types', () => { 92 | expect(() => { 93 | // @ts-expect-error testing error 94 | e.add(); 95 | }).throws('MiniSignal#add(): First arg must be a Function.'); 96 | expect(() => { 97 | // @ts-expect-error testing error 98 | e.add(123); 99 | }).throws('MiniSignal#add(): First arg must be a Function.'); 100 | expect(() => { 101 | // @ts-expect-error testing error 102 | e.add(true); 103 | }).throws('MiniSignal#add(): First arg must be a Function.'); 104 | expect(!e.hasListeners); 105 | }); 106 | 107 | // Note: once is deprecated 108 | // These tests use the add method instead 109 | it('should not invoke twice', () => { 110 | const l = e.add(function (arg: string) { 111 | writer(arg); 112 | expect(arg).equals('foo'); 113 | e.detach(l); 114 | }); 115 | 116 | e.dispatch('foo'); 117 | e.dispatch('bar'); 118 | e.dispatch('baz'); 119 | 120 | expect(pattern.join(';')).equals('foo'); 121 | }); 122 | }); 123 | 124 | describe('MiniSignal#dispatch', () => { 125 | let e: MiniSignal; 126 | 127 | beforeEach(() => { 128 | e = new MiniSignal<[string]>(); 129 | }); 130 | 131 | it('should return false when there are not events to dispatch', () => { 132 | expect(e.dispatch('foo')).equals(false); 133 | expect(e.dispatch('bar')).equals(false); 134 | }); 135 | 136 | it('emits with context when function is bound function', () => { 137 | const cb = function (bar: string): void { 138 | expect(bar).equals('bar'); 139 | expect(this).equals(context); 140 | expect(arguments).has.length(1); 141 | }.bind(context); 142 | 143 | e.add(cb); 144 | 145 | e.dispatch('bar'); 146 | }); 147 | 148 | it('can dispatch the function with multiple arguments', () => { 149 | for (let i = 0; i < 100; i++) { 150 | const e = new MiniSignal(); 151 | (function (j) { 152 | const args: number[] = []; 153 | 154 | for (let i = 0; i < j; i++) { 155 | args.push(j); 156 | } 157 | 158 | e.add((..._args) => { 159 | expect(_args.length).equals(args.length); 160 | }); 161 | 162 | e.dispatch.apply(e, args); 163 | })(i); 164 | } 165 | }); 166 | 167 | it('can dispatch the function with multiple arguments, multiple listeners', () => { 168 | for (let i = 0; i < 100; i++) { 169 | const e = new MiniSignal(); 170 | (function (j) { 171 | const args: number[] = []; 172 | 173 | for (let i = 0; i < j; i++) { 174 | args.push(j); 175 | } 176 | 177 | e.add((..._args) => { 178 | expect(_args.length).equals(args.length); 179 | }); 180 | 181 | e.add((..._args) => { 182 | expect(_args.length).equals(args.length); 183 | }); 184 | 185 | e.add((..._args) => { 186 | expect(_args.length).equals(args.length); 187 | }); 188 | 189 | e.add((..._args) => { 190 | expect(_args.length).equals(args.length); 191 | }); 192 | 193 | e.dispatch.apply(e, args); 194 | })(i); 195 | } 196 | }); 197 | 198 | it('can dispatch many listeners', () => { 199 | const N = 10000; 200 | let sum = 0; 201 | 202 | function add(i: number): void { 203 | sum += i; 204 | } 205 | 206 | for (let i = 0; i <= N; i++) { 207 | e.add(add.bind(this, i)); 208 | } 209 | 210 | e.dispatch(); 211 | 212 | expect(sum).equals((N * (N + 1)) / 2); 213 | }); 214 | 215 | it('should return true when there are events to dispatch', function (done) { 216 | e.add(() => { 217 | process.nextTick(done); 218 | }); 219 | 220 | expect(e.dispatch()).equals(true); 221 | }); 222 | 223 | it('should return false when there are no events to dispatch', () => { 224 | expect(e.dispatch()).equals(false); 225 | }); 226 | 227 | it('receives the emitted events', (done) => { 228 | const e = new MiniSignal(); 229 | 230 | e.add(function (a, b, c, undef): void { 231 | expect(a).equals('foo'); 232 | expect(b).equals(e); 233 | expect(c).is.instanceOf(Date); 234 | expect(undef).equals(undefined); 235 | expect(arguments).has.length(3); 236 | 237 | done(); 238 | }); 239 | 240 | e.dispatch('foo', e, new Date()); 241 | }); 242 | 243 | it('emits to all event listeners', () => { 244 | const e = new MiniSignal(); 245 | const pattern: string[] = []; 246 | 247 | e.add(() => { 248 | pattern.push('foo1'); 249 | }); 250 | 251 | e.add(() => { 252 | pattern.push('foo2'); 253 | }); 254 | 255 | e.dispatch(); 256 | 257 | expect(pattern.join(';')).equals('foo1;foo2'); 258 | }); 259 | 260 | it('emits to all event listeners', () => { 261 | const e = new MiniSignal(); 262 | 263 | function foo1(): void { 264 | pattern.push('foo1'); 265 | } 266 | 267 | function foo2(): void { 268 | pattern.push('foo2'); 269 | } 270 | 271 | function foo3(): void { 272 | pattern.push('foo3'); 273 | } 274 | 275 | e.add(foo1); 276 | e.add(foo2); 277 | e.add(foo3); 278 | 279 | e.dispatch(); 280 | 281 | expect(pattern.join(';')).equals('foo1;foo2;foo3'); 282 | }); 283 | 284 | it('emits to all event listeners, removes once', () => { 285 | const e = new MiniSignal(); 286 | 287 | e.add(() => { 288 | pattern.push('foo1'); 289 | }); 290 | 291 | const l = e.add(() => { 292 | pattern.push('foo2'); 293 | e.detach(l); 294 | }); 295 | 296 | e.add(() => { 297 | pattern.push('foo3'); 298 | }); 299 | 300 | e.dispatch(); 301 | e.dispatch(); 302 | 303 | expect(pattern.join(';')).equals('foo1;foo2;foo3;foo1;foo3'); 304 | }); 305 | 306 | it('cannot dispatch while dispatching', () => { 307 | const cb = function (bar: string): void { 308 | expect(bar).equals('bar'); 309 | expect(arguments).has.length(1); 310 | e.dispatch('bar'); 311 | }; 312 | 313 | e.add(cb); 314 | 315 | expect(() => { 316 | e.dispatch('bar'); 317 | }).throws('MiniSignal#dispatch(): Signal already dispatching.'); 318 | }); 319 | }); 320 | 321 | describe('MiniSignal#detach', () => { 322 | /* istanbul ignore next */ 323 | function foo(): void { 324 | pattern.push('foo'); 325 | } 326 | 327 | /* istanbul ignore next */ 328 | function bar(): void { 329 | pattern.push('bar'); 330 | } 331 | 332 | /* istanbul ignore next */ 333 | function a(): void { 334 | pattern.push('a'); 335 | } 336 | 337 | /* istanbul ignore next */ 338 | function b(): void { 339 | pattern.push('b'); 340 | } 341 | 342 | let e: MiniSignal; 343 | let pattern: string[] = []; 344 | 345 | beforeEach(() => { 346 | e = new MiniSignal(); 347 | pattern = []; 348 | }); 349 | 350 | it('should throw an error if not a SignalBinding', () => { 351 | expect(() => { 352 | // @ts-expect-error testing error 353 | e.detach(); 354 | }).throws( 355 | 'MiniSignal#detach(): First arg must be a MiniSignal listener reference.' 356 | ); 357 | expect(() => { 358 | // @ts-expect-error testing error 359 | e.detach(1); 360 | }).throws( 361 | 'MiniSignal#detach(): First arg must be a MiniSignal listener reference.' 362 | ); 363 | expect(() => { 364 | // @ts-expect-error testing error 365 | e.detach(bar); 366 | }).throws( 367 | 'MiniSignal#detach(): First arg must be a MiniSignal listener reference.' 368 | ); 369 | }); 370 | 371 | it('should only remove the event with the specified node', () => { 372 | e.add(a); 373 | e.add(b); 374 | const _bar = e.add(bar); 375 | 376 | expect(e.hasListeners()).equals(true); 377 | e.dispatch(); 378 | expect(pattern.join(';')).equals('a;b;bar'); 379 | 380 | e.detach(_bar); 381 | expect(e.hasListeners()).equals(true); 382 | 383 | e.dispatch(); 384 | expect(pattern.join(';')).equals('a;b;bar;a;b'); 385 | }); 386 | 387 | it('should remove from front', () => { 388 | const _bar = e.add(bar); 389 | e.add(a); 390 | e.add(b); 391 | 392 | expect(e.hasListeners()).equals(true); 393 | e.dispatch(); 394 | expect(pattern.join(';')).equals('bar;a;b'); 395 | 396 | e.detach(_bar); 397 | expect(e.hasListeners()).equals(true); 398 | e.dispatch(); 399 | expect(pattern.join(';')).equals('bar;a;b;a;b'); 400 | }); 401 | 402 | it('should remove from middle', () => { 403 | e.add(a); 404 | const _bar = e.add(bar); 405 | e.add(b); 406 | 407 | expect(e.hasListeners()).equals(true); 408 | e.dispatch(); 409 | expect(pattern.join(';')).equals('a;bar;b'); 410 | 411 | e.detach(_bar); 412 | expect(e.hasListeners()).equals(true); 413 | e.dispatch(); 414 | expect(pattern.join(';')).equals('a;bar;b;a;b'); 415 | }); 416 | 417 | it('emits to all event listeners after removing', () => { 418 | e.add(a); 419 | const _foo = e.add(foo); 420 | e.add(b); 421 | 422 | e.detach(_foo); 423 | e.dispatch(); 424 | 425 | expect(pattern.join(';')).equals('a;b'); 426 | }); 427 | 428 | it('can remove previous node in dispatch', () => { 429 | const _foo = e.add(foo); 430 | e.add(foo2); 431 | e.add(a); 432 | 433 | e.dispatch(); 434 | e.dispatch(); 435 | 436 | expect(pattern.join(';')).equals('foo;foo2;a;foo2;a'); 437 | 438 | function foo2(): void { 439 | pattern.push('foo2'); 440 | e.detach(_foo); 441 | } 442 | }); 443 | 444 | it('can remove next node in dispatch', () => { 445 | e.add(a); 446 | e.add(foo2); 447 | const _foo = e.add(foo); 448 | 449 | e.dispatch(); 450 | e.dispatch(); 451 | 452 | expect(pattern.join(';')).equals('a;foo2;a;foo2'); // will remove node this dispatch (might be unexpected) 453 | 454 | function foo2(): void { 455 | pattern.push('foo2'); 456 | e.detach(_foo); 457 | } 458 | }); 459 | 460 | it('can remove node in dispatch', () => { 461 | e.add(foo2); 462 | e.add(a); 463 | const _foo = e.add(foo); 464 | 465 | e.dispatch(); 466 | e.dispatch(); 467 | 468 | expect(pattern.join(';')).equals('foo2;a;foo2;a'); // will remove node this dispatch (might be unexpected) 469 | 470 | function foo2(): void { 471 | pattern.push('foo2'); 472 | e.detach(_foo); 473 | } 474 | }); 475 | 476 | it('can remove current node in dispatch', () => { 477 | e.add(a); 478 | const _foo = e.add(foo2); 479 | e.add(b); 480 | 481 | e.dispatch(); 482 | e.dispatch(); 483 | 484 | expect(pattern.join(';')).equals('a;foo2;b;a;b'); 485 | 486 | function foo2(): void { 487 | pattern.push('foo2'); 488 | e.detach(_foo); 489 | } 490 | }); 491 | 492 | it('can only detach from same signal', () => { 493 | const e2 = new MiniSignal(); 494 | 495 | const binding = e.add(foo); 496 | 497 | expect(() => { 498 | e2.detach(binding); 499 | }).throws('MiniSignal#detach(): MiniSignal listener does not belong to this MiniSignal.'); 500 | 501 | expect(e.hasListeners()); 502 | }); 503 | 504 | it('can be called multiple times', () => { 505 | const binding = e.add(foo); 506 | e.detach(binding); 507 | e.detach(binding); 508 | e.detach(binding); 509 | }); 510 | }); 511 | 512 | describe('MiniSignal#detachAll', () => { 513 | /* istanbul ignore next */ 514 | function oops(): void { 515 | throw new Error('oops'); 516 | } 517 | 518 | let e: MiniSignal; 519 | 520 | beforeEach(() => { 521 | e = new MiniSignal(); 522 | }); 523 | 524 | it('removes all events', () => { 525 | e.add(oops); 526 | e.add(oops); 527 | e.add(oops); 528 | e.add(oops); 529 | 530 | expect(e.hasListeners()).equals(true); 531 | 532 | expect(e.detachAll()).equals(e); 533 | expect(e.hasListeners()).equals(false); 534 | 535 | expect(e.dispatch()).equals(false); 536 | }); 537 | 538 | it('should not throw an error if no listeners are set', () => { 539 | expect(e.detachAll()).equals(e); 540 | expect(e.hasListeners()).equals(false); 541 | 542 | expect(e.dispatch()).equals(false); 543 | }); 544 | 545 | it('Should not throw error when calling detach after detachAll', () => { 546 | const binding = e.add(oops); 547 | e.detachAll(); 548 | e.detach(binding); 549 | }); 550 | }); 551 | 552 | describe('Garbage Collection', () => { 553 | it('should clean up when signal is destroyed', async () => { 554 | let e = new MiniSignal(); 555 | const eR = new WeakRef(e); 556 | 557 | let fn = () => { 558 | noop(e, w); 559 | }; 560 | 561 | const fR = new WeakRef(fn); 562 | 563 | const w = e.add(fn); 564 | e.add(fn); 565 | e.add(noop); 566 | e.add(() => { 567 | fn(); 568 | noop(); 569 | }); 570 | e.add(() => { 571 | fn(); 572 | noop(); 573 | e.detach(w); 574 | }); 575 | 576 | expect(fR.deref()).to.exist; 577 | e.dispatch(); 578 | 579 | // Removing references in this scope should mark nodes GC 580 | fn = null as any; 581 | e = null as any; 582 | 583 | await new Promise(resolve => setTimeout(resolve, 0)); 584 | global.gc!(); 585 | 586 | expect(fR.deref()).to.be.undefined; 587 | expect(eR.deref()).to.be.undefined; 588 | 589 | // Only the node reference should be left 590 | expect(w).to.exist; 591 | }); 592 | 593 | it('should not leak memory after detach', async () => { 594 | let e = new MiniSignal(); 595 | 596 | const eR = new WeakRef(e); 597 | 598 | let fn = () => { 599 | noop(e, fn); 600 | } 601 | 602 | const fR = new WeakRef(fn); 603 | const w = e.add(fn); 604 | 605 | fn = null as any; 606 | 607 | expect(fR.deref()).to.exist; 608 | e.dispatch(); 609 | expect(fR.deref()).to.exist; 610 | 611 | e.detach(w); 612 | 613 | await new Promise(resolve => setTimeout(resolve, 0)); 614 | global.gc!(); 615 | expect(fR.deref()).to.be.undefined; 616 | 617 | // should not throw an error when detaching gc ref 618 | e.detach(w); 619 | 620 | expect(eR.deref()).to.exist; 621 | 622 | // Also cleans up the signal 623 | e = null as any; 624 | await new Promise(resolve => setTimeout(resolve, 0)); 625 | global.gc!(); 626 | expect(eR.deref()).to.be.undefined; 627 | 628 | // Only the node reference should be left 629 | expect(w).to.exist; 630 | }); 631 | 632 | it('should not leak memory after detach all', async () => { 633 | let e = new MiniSignal(); 634 | 635 | const eR = new WeakRef(e); 636 | 637 | let fn = () => { 638 | noop(e, fn); 639 | } 640 | 641 | const fR = new WeakRef(fn); 642 | const w = e.add(fn); 643 | 644 | fn = null as any; 645 | 646 | expect(fR.deref()).to.exist; 647 | e.dispatch(); 648 | expect(fR.deref()).to.exist; 649 | 650 | e.detach(w); 651 | 652 | await new Promise(resolve => setTimeout(resolve, 0)); 653 | global.gc!(); 654 | expect(fR.deref()).to.be.undefined; 655 | 656 | // should not throw an error when detaching gc ref 657 | e.detachAll(); 658 | 659 | expect(eR.deref()).to.exist; 660 | 661 | // Also cleans up the signal 662 | e = null as any; 663 | await new Promise(resolve => setTimeout(resolve, 0)); 664 | global.gc!(); 665 | expect(eR.deref()).to.be.undefined; 666 | 667 | // Only the node reference should be left 668 | expect(w).to.exist; 669 | }); 670 | 671 | it('should clean up after itself when using add', async () => { 672 | let e = new MiniSignal(); 673 | const eR = new WeakRef(e); 674 | 675 | let w: any; 676 | 677 | let fn = () => { 678 | noop(e, w); 679 | e.detach(w); 680 | }; 681 | 682 | const fR = new WeakRef(fn); 683 | 684 | w = e.add(fn); 685 | 686 | fn = null as any; 687 | 688 | expect(fR.deref()).to.exist; 689 | e.dispatch(); 690 | 691 | 692 | await new Promise(resolve => setTimeout(resolve, 0)); 693 | global.gc!(); 694 | expect(fR.deref()).to.be.undefined; 695 | 696 | // Also cleans up the signal 697 | e = null as any; 698 | await new Promise(resolve => setTimeout(resolve, 0)); 699 | global.gc!(); 700 | expect(eR.deref()).to.be.undefined; 701 | 702 | // Only the node reference should be left 703 | expect(w).to.exist; 704 | }); 705 | }); 706 | }); 707 | 708 | function noop(...args: any[]) { 709 | // empty 710 | } 711 | -------------------------------------------------------------------------------- /src/mini-signals.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectAssignable, expectError, expectType} from 'tsd'; 2 | import { MiniSignal } from './mini-signals'; 3 | 4 | describe('MiniSignal Typing', () => { 5 | it('should have correct types', () => { 6 | const e1 = new MiniSignal<[string]>(); 7 | 8 | const l1 = e1.add(a => { 9 | expectType(a); 10 | }); 11 | 12 | expectType(e1.dispatch('foo')); 13 | 14 | expectType(e1.detach(l1)); 15 | }); 16 | 17 | it('should show TS error on incorrect listeners and dispatch', () => { 18 | const e1 = new MiniSignal<[string]>(); 19 | 20 | expectError(e1.add((a: number) => { /* noop */ })); 21 | 22 | expectError(e1.dispatch(5)); 23 | }); 24 | 25 | it('should show TS error on incorrect binding with different types', () => { 26 | const e1 = new MiniSignal<[string]>(); 27 | const e2 = new MiniSignal<[number]>(); 28 | 29 | const l1 = e1.add(expectType); 30 | const l2 = e2.add(expectType); 31 | 32 | expectType(e1.detach(l1)); 33 | 34 | expectError(e1.detach(l2)); 35 | 36 | expectType(e1.dispatch('foo')); 37 | }); 38 | 39 | it('should show ts error on incorrect branded types using flavors', () => { 40 | const e1 = new MiniSignal<[string], 'e1'>(); 41 | const e2 = new MiniSignal<[string], 'e2'>(); 42 | 43 | const l1 = e1.add(expectType); 44 | const l2 = e2.add(expectType); 45 | 46 | expectType(e1.detach(l1)); 47 | 48 | expectError(e1.detach(l2)); 49 | 50 | expectType(e1.dispatch('foo')); 51 | }); 52 | 53 | it('should show ts error on incorrect branded types using symbols', () => { 54 | const e1s = Symbol('e1'); 55 | const e2s = Symbol('e2'); 56 | 57 | const e1 = new MiniSignal<[string], typeof e1s>(); 58 | const e2 = new MiniSignal<[string], typeof e2s>(); 59 | 60 | const l1 = e1.add(expectType); 61 | const l2 = e2.add(expectType); 62 | 63 | expectType(e1.detach(l1)); 64 | 65 | expectError(e1.detach(l2)); 66 | 67 | expectType(e1.dispatch('foo')); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/mini-signals.ts: -------------------------------------------------------------------------------- 1 | type CallBack = (...x: T) => void; 2 | 3 | const MINI_SIGNAL_KEY = Symbol('SIGNAL'); 4 | 5 | interface MiniSignalNodeRef { 6 | [MINI_SIGNAL_KEY]: Symbol; 7 | __brand?: S; 8 | __type?: T; 9 | } 10 | 11 | interface MiniSignalNode { 12 | fn: CallBack; 13 | next?: MiniSignalNode; 14 | prev?: MiniSignalNode; 15 | } 16 | 17 | function isMiniSignalNodeRef(obj: any): obj is MiniSignalNodeRef { 18 | return typeof obj === 'object' && MINI_SIGNAL_KEY in obj; 19 | } 20 | 21 | export class MiniSignal< 22 | T extends any[] = any[], 23 | S extends any = Symbol | string 24 | > { 25 | /** 26 | * A Symbol that is used to guarantee the uniqueness of the MiniSignal 27 | * instance. 28 | */ 29 | private readonly _symbol = Symbol('MiniSignal'); 30 | private _refMap = new WeakMap, MiniSignalNode>(); 31 | 32 | private _head?: MiniSignalNode = undefined; 33 | private _tail?: MiniSignalNode = undefined; 34 | private _dispatching = false; 35 | 36 | hasListeners(): boolean { 37 | return this._head != null; 38 | } 39 | 40 | /** 41 | * Dispatches a signal to all registered listeners. 42 | */ 43 | dispatch(...args: T): boolean { 44 | if (this._dispatching) { 45 | throw new Error('MiniSignal#dispatch(): Signal already dispatching.'); 46 | } 47 | 48 | let node = this._head; 49 | 50 | if (node == null) return false; 51 | this._dispatching = true; 52 | 53 | while (node != null) { 54 | node.fn(...args); 55 | node = node.next; 56 | } 57 | 58 | this._dispatching = false; 59 | return true; 60 | } 61 | 62 | /** 63 | * Register a new listener. 64 | */ 65 | add(fn: CallBack): MiniSignalNodeRef { 66 | if (typeof fn !== 'function') { 67 | throw new Error('MiniSignal#add(): First arg must be a Function.'); 68 | } 69 | return this._createRef(this._addNode({ fn })); 70 | } 71 | 72 | /** 73 | * Remove binding object. 74 | */ 75 | detach(sym: MiniSignalNodeRef): this { 76 | if (!isMiniSignalNodeRef(sym)) { 77 | throw new Error( 78 | 'MiniSignal#detach(): First arg must be a MiniSignal listener reference.' 79 | ); 80 | } 81 | 82 | if (sym[MINI_SIGNAL_KEY] !== this._symbol) { 83 | throw new Error( 84 | 'MiniSignal#detach(): MiniSignal listener does not belong to this MiniSignal.' 85 | ); 86 | } 87 | 88 | const node = this._refMap.get(sym); 89 | 90 | if (!node) return this; // already detached 91 | 92 | this._refMap.delete(sym); 93 | this._disconnectNode(node); 94 | this._destroyNode(node); 95 | 96 | return this; 97 | } 98 | 99 | /** 100 | * Detach all listeners. 101 | */ 102 | detachAll(): this { 103 | let n = this._head; 104 | if (n == null) return this; 105 | 106 | this._head = this._tail = undefined; 107 | this._refMap = new WeakMap(); 108 | 109 | while (n != null) { 110 | this._destroyNode(n); 111 | n = n.next; 112 | } 113 | 114 | return this; 115 | } 116 | 117 | private _destroyNode(node: MiniSignalNode) { 118 | node.fn = undefined as any; 119 | node.prev = undefined; 120 | } 121 | 122 | private _disconnectNode(node: MiniSignalNode) { 123 | if (node === this._head) { 124 | // first node 125 | this._head = node.next; 126 | if (node.next == null) { 127 | this._tail = undefined; 128 | } 129 | } else if (node === this._tail) { 130 | // last node 131 | this._tail = node.prev; 132 | if (this._tail != null) { 133 | this._tail.next = undefined; 134 | } 135 | } 136 | 137 | if (node.prev != null) { 138 | node.prev.next = node.next; 139 | } 140 | if (node.next != null) { 141 | node.next.prev = node.prev; 142 | } 143 | } 144 | 145 | private _addNode(node: MiniSignalNode): MiniSignalNode { 146 | if (this._head == null) { 147 | this._head = node; 148 | this._tail = node; 149 | } else { 150 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 151 | this._tail!.next = node; 152 | node.prev = this._tail; 153 | this._tail = node; 154 | } 155 | 156 | return node; 157 | } 158 | 159 | private _createRef(node: MiniSignalNode): MiniSignalNodeRef { 160 | const sym = { [MINI_SIGNAL_KEY]: this._symbol } as unknown as MiniSignalNodeRef; 161 | this._refMap.set(sym, node); 162 | return sym; 163 | } 164 | 165 | protected _getRef(sym: MiniSignalNodeRef) { 166 | return this._refMap.get(sym); 167 | } 168 | } 169 | 170 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "strictNullChecks": true, 9 | "lib": ["ESNext"] 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["src/mini-signals.test-d.ts"] 13 | } 14 | --------------------------------------------------------------------------------