aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.editorconfig18
-rw-r--r--.eslintrc52
-rw-r--r--.github/workflows/compressed-size.yml12
-rw-r--r--.github/workflows/main.yml25
-rw-r--r--.gitignore5
-rw-r--r--LICENSE21
-rw-r--r--README.md188
-rw-r--r--package.json100
-rw-r--r--src/index.ts239
-rw-r--r--test/index_test.ts204
-rw-r--r--test/test-types-compilation.ts78
-rw-r--r--tsconfig.json17
12 files changed, 784 insertions, 175 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..04d7ef9
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+root = true
+
+[*]
+indent_style = tab
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[{package.json,.*rc,*.yml}]
+indent_style = space
+indent_size = 2
+insert_final_newline = false
+
+[*.md]
+trim_trailing_whitespace = false
+indent_style = space
+indent_size = 2
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..429b835
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,52 @@
+{
+ "ignorePatterns": [
+ "node_modules",
+ "dist",
+ "index.d.ts"
+ ],
+ "extends": [
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended",
+ "developit"
+ ],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "sourceType": "module"
+ },
+ "env": {
+ "browser": true,
+ "mocha": true,
+ "jest": false,
+ "es6": true
+ },
+ "globals": {
+ "expect": true
+ },
+ "rules": {
+ "semi": [
+ 2,
+ "always"
+ ],
+ "brace-style": [
+ 2,
+ "1tbs"
+ ],
+ "quotes": [
+ 2,
+ "single"
+ ],
+ "lines-around-comment": [
+ 2,
+ {
+ "allowBlockStart": true,
+ "allowObjectStart": true
+ }
+ ],
+ "jest/valid-expect": 0,
+ "@typescript-eslint/no-explicit-any": 0,
+ "@typescript-eslint/explicit-function-return-type": 0,
+ "@typescript-eslint/explicit-module-boundary-types": 0,
+ "@typescript-eslint/no-empty-function": 0,
+ "@typescript-eslint/no-non-null-assertion": 0
+ }
+}
diff --git a/.github/workflows/compressed-size.yml b/.github/workflows/compressed-size.yml
new file mode 100644
index 0000000..45cad2f
--- /dev/null
+++ b/.github/workflows/compressed-size.yml
@@ -0,0 +1,12 @@
+name: Compressed Size
+
+on: [pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: preactjs/compressed-size-action@v2
+ with:
+ pattern: "./dist/*.{js,mjs,cjs}"
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..7b73b7d
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,25 @@
+name: CI
+
+on:
+ pull_request:
+ branches:
+ - "**"
+ push:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-node@v2
+ with:
+ node-version: 14
+ - name: npm install, build, and test
+ run: |
+ npm install
+ npm run build --if-present
+ npm test
+ env:
+ CI: true
diff --git a/.gitignore b/.gitignore
index 387153a..a64b10d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,9 @@
/dist
/node_modules
+/npm-debug.log
+/index.d.ts
+package-lock.json
.DS_Store
+.idea
+.vscode
.*cache*
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..84c52fe
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Jason Miller
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index b550b17..584241e 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,21 @@
# Nitt
-> Small, functional event emitter / pubsub.
+> Small functional event emitter / pubsub.
-- **Small:** less than 300 bytes gzipped
-- **Familiar:** similar names (on, off, once, emit) & ideas as [Node's EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
-- **Functional:** methods don't rely on `this`, unless you want to...
-- **No dependency:** Nitt has no external dependencies
+* **Microscopic:** weighs less than 200 bytes gzipped
+* **Useful:** a wildcard `"*"` event type listens to all events
+* **Familiar:** same names & ideas as [Node's EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
+
+nitt was made for the browser, but works in any JavaScript runtime. It has no dependencies and supports IE9+.
## Table of Contents
-- [Nitt](#nitt)
- - [Table of Contents](#table-of-contents)
- - [Install](#install)
- - [Usage](#usage)
- - [API](#api)
- - [nitt](#nitt)
- - [on](#on)
- - [once](#once)
- - [when](#when)
- - [off](#off)
- - [emit](#emit)
- - [Caveats](#caveats)
- - [Contribute](#contribute)
- - [Acknowledments](#acknowledments)
- - [License](#license)
+* [Install](#install)
+* [Usage](#usage)
+* [Examples & Demos](#examples--demos)
+* [API](#api)
+* [Contribute](#contribute)
+* [License](#license)
## Install
@@ -33,107 +25,163 @@ This project uses [node](http://nodejs.org) and [npm](https://npmjs.com). Go che
$ npm install --save nitt
```
+Then with a module bundler like [rollup](http://rollupjs.org/) or [webpack](https://webpack.js.org/), use as you would anything else:
+
+```javascript
+// using ES6 modules
+import nitt from 'nitt'
+
+// using CommonJS modules
+var nitt = require('nitt')
+```
+
+The [UMD](https://github.com/umdjs/umd) build is also available on [unpkg](https://unpkg.com):
+
+```html
+<script src="https://unpkg.com/nitt/dist/nitt.umd.js"></script>
+```
+
+You can find the library on `window.nitt`.
+
## Usage
```js
-import nitt from 'nitt';
+import nitt from 'nitt'
-const emitter = nitt();
+const emitter = nitt()
// listen to an event
-emitter.on('foo', e => console.log('foo', e));
+emitter.on('foo', e => console.log('foo', e) )
// listen to all events
-emitter.on('*', (type, e) => console.log(type, e));
-
-// listen to a single event
-emitter.once('foo', e => console.log('foo', e));
+emitter.on('*', (type, e) => console.log(type, e) )
// fire an event
-emitter.emit('foo', { a: 'b' });
+emitter.emit('foo', { a: 'b' })
+
+// clearing all events
+emitter.all.clear()
// working with handler references:
function onFoo() {}
-emitter.on('foo', onFoo); // listen
-emitter.off('foo', onFoo); // unlisten
-emitter.once('foo', onFoo); // listen to a single event
-
-// using a promise
-const promise = emitter.when('bar');
-promise.then(evt => console.log(evt));
-emitter.emit('bar', 'done'); // prints 'done' in the console
+emitter.on('foo', onFoo) // listen
+emitter.off('foo', onFoo) // unlisten
```
-## API
+### Typescript
-### nitt
+Set `"strict": true` in your tsconfig.json to get improved type inference for `nitt` instance methods.
-Nitt: Small (<300B) functional event emitter / pubsub.
+```ts
+import nitt from 'nitt';
-**Parameters**
+type Events = {
+ foo: string;
+ bar?: number;
+};
-- `all` **EventHandlerMap**
+const emitter = nitt<Events>(); // inferred as emitter<Events>
-Returns **Nitt**
+emitter.on('foo', (e) => {}); // 'e' has inferred type 'string'
-### on
+emitter.emit('foo', 42); // Error: Argument of type 'number' is not assignable to parameter of type 'string'. (2345)
+```
-Register an event handler for the given type.
+Alternatively, you can use the provided `emitter` type:
-**Parameters**
+```ts
+import nitt, { emitter } from 'nitt';
-- `type` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Type of event to listen for, or `"*"` for all events
-- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event
+type Events = {
+ foo: string;
+ bar?: number;
+};
-### once
+const emitter: emitter<Events> = nitt<Events>();
+```
+
+## API
-Register an event handler that is executed just once.
+<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
-**Parameters**
+#### Table of Contents
-- `type` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Type of event to listen for, or `"*"` for all events
-- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event
+* [nitt](#nitt)
+* [all](#all)
+* [on](#on)
+ * [Parameters](#parameters)
+* [off](#off)
+ * [Parameters](#parameters-1)
+* [emit](#emit)
+ * [Parameters](#parameters-2)
+* [once](#once)
+ * [Parameters](#parameters-3)
+* [when](#when)
+ * [Parameters](#parameters-4)
-### when
+### nitt
-Returns a promise for a single event
+Nitt: Tiny (~200b) functional event emitter / pubsub.
+
+Returns **Nitt**&#x20;
-**Parameters**
+### all
-- `type` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Type of event to listen for, or `"*"` for all events
+A Map of event names to registered handler functions.
-Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)**
+### on
+
+Register an event handler for the given type.
+
+#### Parameters
+
+* `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to listen for, or `'*'` for all events
+* `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event
### off
Remove an event handler for the given type.
+If `handler` is omitted, all handlers of the given type are removed.
-**Parameters**
+#### Parameters
-- `type` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Type of event to unregister `handler` from, or `"*"`
-- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Handler function to remove
+* `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to unregister `handler` from (`'*'` to remove a wildcard handler)
+* `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)?** Handler function to remove
### emit
Invoke all handlers for the given type.
-If present, `"*"` handlers are invoked after type-matched handlers.
+If present, `'*'` handlers are invoked after type-matched handlers.
-**Parameters**
+Note: Manually firing '\*' handlers is not supported.
-- `type` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** The event type to invoke
-- `evt` **Any?** Any value (object is recommended and powerful), passed to each handler
+#### Parameters
-## Caveats
+* `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** The event type to invoke
+* `evt` **Any?** Any value (object is recommended and powerful), passed to each handler
-Keep in mind, due to the nature, of the once handlers that self deregister, you are currently not able to remove (off) a once / when handlers.
+### once
-## Contribute
+Register an event handler that is executed just once for the given type.
+
+#### Parameters
+
+* `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to listen for, or `'*'` for all events
+* `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event
+
+### when
-If you want to contribute, I would be happy to look in to your PRs.
+Returns a promise for a single event
+
+#### Parameters
+
+* `type` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Type of event to listen for, or `"*"` for any event
-## Acknowledments
+Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<any>**&#x20;
+
+## Contribute
-I would like to thank [Jason Miller](https://jasonformat.com/) (@developit) for developing `mitt`. `nitt` is no more than just `mitt` with a few extras.
+For reporting issues and submitting patches, send an email.
## License
diff --git a/package.json b/package.json
index d7268b4..ef26fa0 100644
--- a/package.json
+++ b/package.json
@@ -1,34 +1,88 @@
{
"name": "nitt",
- "version": "1.0.2",
- "author": "Marin Ivanov <[email protected]>",
- "license": "MIT",
+ "version": "1.1.0",
+ "description": "Tiny 200b functional Event Emitter / pubsub.",
+ "module": "dist/nitt.mjs",
+ "main": "dist/nitt.js",
+ "jsnext:main": "dist/nitt.mjs",
+ "umd:main": "dist/nitt.umd.js",
+ "source": "src/index.ts",
+ "typings": "index.d.ts",
+ "exports": {
+ "types": "./index.d.ts",
+ "module": "./dist/nitt.mjs",
+ "import": "./dist/nitt.mjs",
+ "require": "./dist/nitt.js",
+ "default": "./dist/nitt.mjs"
+ },
+ "scripts": {
+ "test": "npm-run-all --silent typecheck lint mocha test-types",
+ "mocha": "mocha test",
+ "test-types": "tsc test/test-types-compilation.ts --noEmit --strict",
+ "lint": "eslint src test --ext ts --ext js",
+ "typecheck": "tsc --noEmit",
+ "bundle": "microbundle -f es,cjs,umd",
+ "build": "npm-run-all --silent clean -p bundle -s docs",
+ "clean": "rimraf dist",
+ "docs": "documentation readme src/index.ts --section API -q --parse-extension ts",
+ "release": "npm run -s build -s && npm t && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish"
+ },
"repository": {
"type": "git",
- "url": "https://github.com/metala/nitt.git"
+ "url": "https://pgit.metala.org/lib/nitt.git"
},
"keywords": [
- "nitt",
- "event-listener",
- "event-emitter",
- "eventemitter"
+ "events",
+ "eventemitter",
+ "emitter",
+ "pubsub"
],
- "scripts": {
- "build": "microbundle",
- "dev": "microbundle watch",
- "test": "jest"
+ "authors": [
+ "Marin Ivanov <[email protected]>",
+ "Jason Miller <[email protected]>"
+ ],
+ "license": "MIT",
+ "files": [
+ "dist",
+ "index.d.ts"
+ ],
+ "mocha": {
+ "extension": [
+ "ts"
+ ],
+ "require": [
+ "ts-node/register",
+ "esm"
+ ],
+ "spec": [
+ "test/*_test.ts"
+ ]
+ },
+ "prettier": {
+ "singleQuote": true,
+ "trailingComma": "none"
},
- "source": "src/index.ts",
- "main": "dist/nitt.js",
- "module": "dist/nitt.esm.js",
- "unpkg": "dist/nitt.umd.js",
"devDependencies": {
- "@types/jest": "^24.0.11",
- "@types/node": "^11.13.7",
- "documentation": "^10.1.0",
- "jest": "^24.7.1",
- "microbundle": "^0.11.0",
- "ts-jest": "^24.0.2",
- "typescript": "^3.4.5"
+ "@types/chai": "^4.2.11",
+ "@types/mocha": "^7.0.2",
+ "@types/sinon": "^9.0.4",
+ "@types/sinon-chai": "^3.2.4",
+ "@typescript-eslint/eslint-plugin": "^5.61.0",
+ "@typescript-eslint/parser": "^5.61.0",
+ "chai": "^4.2.0",
+ "documentation": "^14.0.2",
+ "eslint": "^7.32.0",
+ "eslint-config-developit": "^1.2.0",
+ "eslint-plugin-compat": "^4.1.4",
+ "esm": "^3.2.25",
+ "microbundle": "^0.12.3",
+ "mocha": "^8.0.1",
+ "npm-run-all": "^4.1.5",
+ "prettier": "^2.8.8",
+ "rimraf": "^3.0.2",
+ "sinon": "^9.0.2",
+ "sinon-chai": "^3.5.0",
+ "ts-node": "^10.9.1",
+ "typescript": "^4.9.5"
}
}
diff --git a/src/index.ts b/src/index.ts
index 722412b..7aa6826 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,86 +1,167 @@
+export type EventType = string | symbol;
+
// An event handler can take an optional event argument
// and should not return a value
-type EventHandler = (event?: any) => void;
-type WildCardEventHandler = (type: string, event?: any) => void;
+export type Handler<T = unknown> = (event: T) => void;
+export type WildcardHandler<T = Record<string, unknown>> = (
+ type: keyof T,
+ event: T[keyof T]
+) => void;
// An array of all currently registered event handlers for a type
-type EventHandlerList = Array<EventHandler>;
-type WildCardEventHandlerList = Array<WildCardEventHandler>;
+export type EventHandlerList<T = unknown> = Array<Handler<T>>;
+export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<
+ WildcardHandler<T>
+>;
+
// A map of event types and their corresponding event handlers.
-type EventHandlerMap = {
- '*'?: WildCardEventHandlerList;
- [type: string]: EventHandlerList;
-};
-
-/** Nitt: functional event emitter / pubsub.
- * @name nitt
- * @returns {Nitt}
+export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
+ keyof Events | '*',
+ EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
+>;
+
+export interface Emitter<Events extends Record<EventType, unknown>> {
+ all: EventHandlerMap<Events>;
+
+ on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
+ on(type: '*', handler: WildcardHandler<Events>): void;
+
+ once<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
+ once(type: '*', handler: WildcardHandler<Events>): void;
+
+ off<Key extends keyof Events>(
+ type: Key,
+ handler?: Handler<Events[Key]>
+ ): void;
+ off(type: '*', handler: WildcardHandler<Events>): void;
+
+ emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
+ emit<Key extends keyof Events>(
+ type: undefined extends Events[Key] ? Key : never
+ ): void;
+
+ when<Key extends keyof Events>(type: Key): Promise<void>;
+}
+
+/**
+ * Nitt: Tiny (~200b) functional event emitter / pubsub.
+ * @name nitt
+ * @returns {Nitt}
*/
-export default function nitt(all: EventHandlerMap) {
- all = all || Object.create(null);
-
- return {
- /**
- * Register an event handler for the given type.
- *
- * @param {String} type Type of event to listen for, or `"*"` for all events
- * @param {Function} handler Function to call in response to given event
- */
- on(type: string, handler: EventHandler) {
- (all[type] || (all[type] = [])).push(handler);
- },
-
- /**
- * Register an event handler that is executed just once.
- *
- * @param {String} type Type of event to listen for, or `"*"` for any event
- * @param {Function} handler Function to call in response to given event
- */
- once(type: string, handler: EventHandler) {
- const onceHandler = (evt: any) => {
- handler(evt);
- this.off(type, onceHandler);
- };
-
- this.on(type, onceHandler);
- },
-
- /**
- * Returns a promise for a single event
- *
- * @param {String} type Type of event to listen for, or `"*"` for any event
- * @returns {Promise<any>}
- */
- when(type: string): Promise<any> {
- return new Promise<any>(resolve => this.once(type, resolve));
- },
-
- /**
- * Remove an event handler for the given type.
- *
- * @param {String} type Type of event to unregister `handler` from, or `"*"`
- * @param {Function} handler Handler function to remove
- */
- off(type: string, handler: EventHandler) {
- if (all[type]) {
- all[type].splice(all[type].indexOf(handler) >>> 0, 1);
- }
- },
-
- /**
- * Invoke all handlers for the given type.
- * If present, `"*"` handlers are invoked after type-matched handlers.
- *
- * @param {String} type The event type to invoke
- * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
- */
- emit(type: string, evt: any) {
- (all[type] || []).forEach(handler => {
- handler(evt);
- });
- (all['*'] || []).forEach(handler => {
- handler(type, evt);
- });
- },
- };
+export default function nitt<Events extends Record<EventType, unknown>>(
+ all?: EventHandlerMap<Events>
+): Emitter<Events> {
+ type GenericEventHandler =
+ | Handler<Events[keyof Events]>
+ | WildcardHandler<Events>;
+ all = all || new Map();
+
+ return {
+ /**
+ * A Map of event names to registered handler functions.
+ */
+ all,
+
+ /**
+ * Register an event handler for the given type.
+ * @param {string|symbol} type Type of event to listen for, or `'*'` for all events
+ * @param {Function} handler Function to call in response to given event
+ * @memberOf nitt
+ */
+ on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
+ const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
+ if (handlers) {
+ handlers.push(handler);
+ } else {
+ all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
+ }
+ },
+
+ /**
+ * Remove an event handler for the given type.
+ * If `handler` is omitted, all handlers of the given type are removed.
+ * @param {string|symbol} type Type of event to unregister `handler` from (`'*'` to remove a wildcard handler)
+ * @param {Function} [handler] Handler function to remove
+ * @memberOf nitt
+ */
+ off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
+ const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
+ if (handlers) {
+ if (handler) {
+ handlers.splice(handlers.indexOf(handler) >>> 0, 1);
+ } else {
+ all!.set(type, []);
+ }
+ }
+ },
+
+ /**
+ * Invoke all handlers for the given type.
+ * If present, `'*'` handlers are invoked after type-matched handlers.
+ *
+ * Note: Manually firing '*' handlers is not supported.
+ *
+ * @param {string|symbol} type The event type to invoke
+ * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
+ * @memberOf nitt
+ */
+ emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
+ let handlers = all!.get(type);
+ if (handlers) {
+ (handlers as EventHandlerList<Events[keyof Events]>)
+ .slice()
+ .map((handler) => {
+ handler(evt!);
+ });
+ }
+
+ handlers = all!.get('*');
+ if (handlers) {
+ (handlers as WildCardEventHandlerList<Events>)
+ .slice()
+ .map((handler) => {
+ handler(type, evt!);
+ });
+ }
+ },
+
+ /**
+ * Register an event handler that is executed just once for the given type.
+ *
+ * @param {string|symbol} type Type of event to listen for, or `'*'` for all events
+ * @param {Function} handler Function to call in response to given event
+ * @memberOf nitt
+ */
+ once<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
+ if (type === '*') {
+ const onceHandler: WildcardHandler<Events> = (type: keyof Events, event: Events[keyof Events]) => {
+ (handler as WildcardHandler<Events>)(type, event);
+ this.off('*', onceHandler);
+ };
+ this.on('*', onceHandler);
+ } else {
+ const onceHandler = (evt: Events[Key]) => {
+ (handler as Handler<Events[Key]>)(evt);
+ this.off(type, onceHandler);
+ };
+ this.on(type, onceHandler);
+ }
+ },
+
+ /**
+ * Returns a promise for a single event
+ *
+ * @param {String} type Type of event to listen for, or `"*"` for any event
+ * @returns {Promise<any>}
+ */
+ when<Key extends keyof Events>(type: Key): Promise<any> {
+ return new Promise((resolve: GenericEventHandler) => {
+ if (type === '*') {
+ this.once('*', (resolve as WildcardHandler<Events>));
+ } else {
+ this.once(type, (resolve as Handler<Events[Key]>));
+ }
+ });
+ }
+ };
}
diff --git a/test/index_test.ts b/test/index_test.ts
new file mode 100644
index 0000000..4a67fe6
--- /dev/null
+++ b/test/index_test.ts
@@ -0,0 +1,204 @@
+import nitt, { Emitter, EventHandlerMap } from '..';
+import chai, { expect } from 'chai';
+import { spy } from 'sinon';
+import sinonChai from 'sinon-chai';
+chai.use(sinonChai);
+
+describe('nitt', () => {
+ it('should default export be a function', () => {
+ expect(nitt).to.be.a('function');
+ });
+
+ it('should accept an optional event handler map', () => {
+ expect(() => nitt(new Map())).not.to.throw;
+ const map = new Map();
+ const a = spy();
+ const b = spy();
+ map.set('foo', [a, b]);
+ const events = nitt<{ foo: undefined }>(map);
+ events.emit('foo');
+ expect(a).to.have.been.calledOnce;
+ expect(b).to.have.been.calledOnce;
+ });
+});
+
+describe('nitt#', () => {
+ const eventType = Symbol('eventType');
+ type Events = {
+ foo: unknown;
+ constructor: unknown;
+ FOO: unknown;
+ bar: unknown;
+ Bar: unknown;
+ 'baz:bat!': unknown;
+ 'baz:baT!': unknown;
+ Foo: unknown;
+ [eventType]: unknown;
+ };
+ let events: EventHandlerMap<Events>, inst: Emitter<Events>;
+
+ beforeEach(() => {
+ events = new Map();
+ inst = nitt(events);
+ });
+
+ describe('properties', () => {
+ it('should expose the event handler map', () => {
+ expect(inst).to.have.property('all').that.is.a('map');
+ });
+ });
+
+ describe('on()', () => {
+ it('should be a function', () => {
+ expect(inst).to.have.property('on').that.is.a('function');
+ });
+
+ it('should register handler for new type', () => {
+ const foo = () => {};
+ inst.on('foo', foo);
+
+ expect(events.get('foo')).to.deep.equal([foo]);
+ });
+
+ it('should register handlers for any type strings', () => {
+ const foo = () => {};
+ inst.on('constructor', foo);
+
+ expect(events.get('constructor')).to.deep.equal([foo]);
+ });
+
+ it('should append handler for existing type', () => {
+ const foo = () => {};
+ const bar = () => {};
+ inst.on('foo', foo);
+ inst.on('foo', bar);
+
+ expect(events.get('foo')).to.deep.equal([foo, bar]);
+ });
+
+ it('should NOT normalize case', () => {
+ const foo = () => {};
+ inst.on('FOO', foo);
+ inst.on('Bar', foo);
+ inst.on('baz:baT!', foo);
+
+ expect(events.get('FOO')).to.deep.equal([foo]);
+ expect(events.has('foo')).to.equal(false);
+ expect(events.get('Bar')).to.deep.equal([foo]);
+ expect(events.has('bar')).to.equal(false);
+ expect(events.get('baz:baT!')).to.deep.equal([foo]);
+ });
+
+ it('can take symbols for event types', () => {
+ const foo = () => {};
+ inst.on(eventType, foo);
+ expect(events.get(eventType)).to.deep.equal([foo]);
+ });
+
+ // Adding the same listener multiple times should register it multiple times.
+ // See https://nodejs.org/api/events.html#events_emitter_on_eventname_listener
+ it('should add duplicate listeners', () => {
+ const foo = () => {};
+ inst.on('foo', foo);
+ inst.on('foo', foo);
+ expect(events.get('foo')).to.deep.equal([foo, foo]);
+ });
+ });
+
+ describe('off()', () => {
+ it('should be a function', () => {
+ expect(inst).to.have.property('off').that.is.a('function');
+ });
+
+ it('should remove handler for type', () => {
+ const foo = () => {};
+ inst.on('foo', foo);
+ inst.off('foo', foo);
+
+ expect(events.get('foo')).to.be.empty;
+ });
+
+ it('should NOT normalize case', () => {
+ const foo = () => {};
+ inst.on('FOO', foo);
+ inst.on('Bar', foo);
+ inst.on('baz:bat!', foo);
+
+ inst.off('FOO', foo);
+ inst.off('Bar', foo);
+ inst.off('baz:baT!', foo);
+
+ expect(events.get('FOO')).to.be.empty;
+ expect(events.has('foo')).to.equal(false);
+ expect(events.get('Bar')).to.be.empty;
+ expect(events.has('bar')).to.equal(false);
+ expect(events.get('baz:bat!')).to.have.lengthOf(1);
+ });
+
+ it('should remove only the first matching listener', () => {
+ const foo = () => {};
+ inst.on('foo', foo);
+ inst.on('foo', foo);
+ inst.off('foo', foo);
+ expect(events.get('foo')).to.deep.equal([foo]);
+ inst.off('foo', foo);
+ expect(events.get('foo')).to.deep.equal([]);
+ });
+
+ it('off("type") should remove all handlers of the given type', () => {
+ inst.on('foo', () => {});
+ inst.on('foo', () => {});
+ inst.on('bar', () => {});
+ inst.off('foo');
+ expect(events.get('foo')).to.deep.equal([]);
+ expect(events.get('bar')).to.have.length(1);
+ inst.off('bar');
+ expect(events.get('bar')).to.deep.equal([]);
+ });
+ });
+
+ describe('emit()', () => {
+ it('should be a function', () => {
+ expect(inst).to.have.property('emit').that.is.a('function');
+ });
+
+ it('should invoke handler for type', () => {
+ const event = { a: 'b' };
+
+ inst.on('foo', (one, two?: unknown) => {
+ expect(one).to.deep.equal(event);
+ expect(two).to.be.an('undefined');
+ });
+
+ inst.emit('foo', event);
+ });
+
+ it('should NOT ignore case', () => {
+ const onFoo = spy(),
+ onFOO = spy();
+ events.set('Foo', [onFoo]);
+ events.set('FOO', [onFOO]);
+
+ inst.emit('Foo', 'Foo arg');
+ inst.emit('FOO', 'FOO arg');
+
+ expect(onFoo).to.have.been.calledOnce.and.calledWith('Foo arg');
+ expect(onFOO).to.have.been.calledOnce.and.calledWith('FOO arg');
+ });
+
+ it('should invoke * handlers', () => {
+ const star = spy(),
+ ea = { a: 'a' },
+ eb = { b: 'b' };
+
+ events.set('*', [star]);
+
+ inst.emit('foo', ea);
+ expect(star).to.have.been.calledOnce.and.calledWith('foo', ea);
+ star.resetHistory();
+
+ inst.emit('bar', eb);
+ expect(star).to.have.been.calledOnce.and.calledWith('bar', eb);
+ });
+ });
+});
diff --git a/test/test-types-compilation.ts b/test/test-types-compilation.ts
new file mode 100644
index 0000000..25c51cc
--- /dev/null
+++ b/test/test-types-compilation.ts
@@ -0,0 +1,78 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */
+
+import nitt from '..';
+
+interface SomeEventData {
+ name: string;
+}
+
+const emitter = nitt<{
+ foo: string;
+ someEvent: SomeEventData;
+ bar?: number;
+}>();
+
+const barHandler = (x?: number) => {};
+const fooHandler = (x: string) => {};
+const wildcardHandler = (
+ _type: 'foo' | 'bar' | 'someEvent',
+ _event: string | SomeEventData | number | undefined
+) => {};
+
+/*
+ * Check that 'on' args are inferred correctly
+ */
+{
+ // @ts-expect-error
+ emitter.on('foo', barHandler);
+ emitter.on('foo', fooHandler);
+
+ emitter.on('bar', barHandler);
+ // @ts-expect-error
+ emitter.on('bar', fooHandler);
+
+ emitter.on('*', wildcardHandler);
+ // fooHandler is ok, because ('foo' | 'bar' | 'someEvent') extends string
+ emitter.on('*', fooHandler);
+ // @ts-expect-error
+ emitter.on('*', barHandler);
+}
+
+/*
+ * Check that 'off' args are inferred correctly
+ */
+{
+ // @ts-expect-error
+ emitter.off('foo', barHandler);
+ emitter.off('foo', fooHandler);
+
+ emitter.off('bar', barHandler);
+ // @ts-expect-error
+ emitter.off('bar', fooHandler);
+
+ emitter.off('*', wildcardHandler);
+ // fooHandler is ok, because ('foo' | 'bar' | 'someEvent') extends string
+ emitter.off('*', fooHandler);
+ // @ts-expect-error
+ emitter.off('*', barHandler);
+}
+
+/*
+ * Check that 'emit' args are inferred correctly
+ */
+{
+ // @ts-expect-error
+ emitter.emit('someEvent', 'NOT VALID');
+ emitter.emit('someEvent', { name: 'jack' });
+
+ // @ts-expect-error
+ emitter.emit('foo');
+ // @ts-expect-error
+ emitter.emit('foo', 1);
+ emitter.emit('foo', 'string');
+
+ emitter.emit('bar');
+ emitter.emit('bar', 1);
+ // @ts-expect-error
+ emitter.emit('bar', 'string');
+}
diff --git a/tsconfig.json b/tsconfig.json
index 8f3c656..8c3bc08 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,16 @@
{
- "compilerOptions": {
- "lib": ["es2015"]
- }
+ "compileOnSave": false,
+ "compilerOptions": {
+ "lib": ["es2015"],
+ "strict": true,
+ "noEmit": true,
+ "declaration": true,
+ "moduleResolution": "node",
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ },
+ "include": [
+ "src/*.ts",
+ "test/*.ts"
+ ]
}