├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── tests.ts └── weak-emitter.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "standard" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "@typescript-eslint" 21 | ], 22 | "rules": { 23 | } 24 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | .lvimrc 5 | .tern-port 6 | lib 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | .lvimrc 5 | .tern-port 6 | src 7 | .eslintrc 8 | .travis.yml 9 | tsconfig.json 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12.14.0" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Jacobo Tabernero - github.com/jacoborus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | weak-emitter 2 | ============ 3 | 4 | Contexts based event emitter on ES6 WeakMaps 5 | 6 | [![Build Status](https://travis-ci.org/jacoborus/weak-emitter.svg?branch=master)](https://travis-ci.org/jacoborus/weak-emitter) [![npm version](https://badge.fury.io/js/weak-emitter.svg)](https://www.npmjs.com/package/weak-emitter) 7 | 8 | 9 | ```js 10 | const context = {} 11 | const eventKey = 'propName' 12 | emitter.on(context, eventKey, (...args) => console.log(args)) 13 | emitter.emit(context, eventKey, 'first', 2, true) 14 | // ['first', 2, true] 15 | ``` 16 | 17 | 18 | ## Install 19 | 20 | ```sh 21 | npm i weak-emitter --save 22 | # or: yarn add weak-emitter 23 | ``` 24 | 25 | ## Emitter API 26 | 27 | - [on](#emitter-on-api) 28 | - [emit](#emitter-emit-api) 29 | - [off](#emitter-off-api) 30 | 31 | **Argument types:** 32 | 33 | - `context: object` 34 | - `eventKey: string | object` 35 | - `handler: (...args: []) => void` 36 | 37 | 38 | ### on(context, eventKey, handler) 39 | 40 | Create new event with `eventKey` in `context` that will trigger the `handler`. 41 | Every handler will be added once, despite the number of times it was added to the event. Handlers are invoked in the order they were added. 42 | 43 | Returns an event controller that has three methods: 44 | 45 | - emit(...args): trigger handler 46 | - off(): remove event 47 | - tranfer(destination): moves the event to `destination` context 48 | 49 | ```js 50 | const context = {} 51 | const eventKey = 'propName' 52 | const evController = emitter.on(context, eventKey, (...args) => doSomething(args)) 53 | 54 | const ctx2 = {} 55 | evController.transfer(ctx2) 56 | evController.off() 57 | 58 | ``` 59 | 60 | 61 | 62 | ### emit(context, eventKey[, ...args]) 63 | 64 | Invoke all handlers in `context` tagged with `key`, passing the rest of the arguments 65 | 66 | ```js 67 | const context = {} 68 | const eventKey = 'propName' 69 | emitter.on(context, eventKey, (...args) => console.log(args)) 70 | emitter.emit(context, eventKey, 'first', 2, true) 71 | // ['first', 2, true] 72 | ``` 73 | 74 | 75 | 76 | ### off(context, key, handler) 77 | 78 | Removes `handler` from the event tagged with `key` in `context` 79 | 80 | ```js 81 | emitter.off(context, eventKey, handler) 82 | ``` 83 | 84 | 85 | 86 | ## Testing 87 | 88 | ```sh 89 | npm test 90 | ``` 91 | 92 |

93 | 94 | --- 95 | 96 | © 2020 [Jacobo Tabernero](http://jacoborus.codes) - Released under [MIT License](https://raw.github.com/jacoborus/weak-emitter/master/LICENSE) 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weak-emitter", 3 | "version": "5.2.1", 4 | "description": "Contexts based event emitter on ES6 WeakMaps", 5 | "main": "lib/weak-emitter.js", 6 | "types": "lib/weak-emitter.d.ts", 7 | "scripts": { 8 | "linter": "eslint 'src/*.ts'", 9 | "test": "tape -r ts-node/register src/tests.ts", 10 | "prepublish": "rm -rf lib && tsc", 11 | "build": "rm -rf lib && tsc" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+ssh://git@github.com/jacoborus/weak-emitter.git" 16 | }, 17 | "keywords": [ 18 | "event emitter", 19 | "map", 20 | "ES6" 21 | ], 22 | "author": "Jacobo Tabernero", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/jacoborus/weak-emitter/issues" 26 | }, 27 | "homepage": "https://github.com/jacoborus/weak-emitter", 28 | "devDependencies": { 29 | "@types/tape": "4.2.34", 30 | "@typescript-eslint/eslint-plugin": "2.20.0", 31 | "@typescript-eslint/parser": "2.20.0", 32 | "eslint": "6.8.0", 33 | "eslint-config-standard": "14.1.0", 34 | "eslint-plugin-import": "2.20.1", 35 | "eslint-plugin-node": "11.0.0", 36 | "eslint-plugin-promise": "4.2.1", 37 | "eslint-plugin-standard": "4.0.1", 38 | "tape": "4.13.0", 39 | "ts-node": "8.6.2", 40 | "typescript": "3.7.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/tests.ts: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import { weakEmitter } from './weak-emitter' 3 | 4 | test('on and emit', t => { 5 | const emitter = weakEmitter() 6 | const obj = {} 7 | let control = 0 8 | const fn = () => ++control 9 | emitter.on(obj, obj, fn) 10 | emitter.emit(obj, obj) 11 | t.is(control, 1, 'trigger') 12 | emitter.emit(obj, obj) 13 | t.is(control, 2, 'trigger') 14 | emitter.on(obj, obj, fn) 15 | emitter.emit(obj, obj) 16 | t.is(control, 3, 'trigger') 17 | t.end() 18 | }) 19 | 20 | test('off action', t => { 21 | const emitter = weakEmitter() 22 | const obj = {} 23 | const control = { 24 | a: 0, 25 | b: 0 26 | } 27 | const fn = () => ++control.b 28 | emitter.on(obj, obj, () => ++control.a) 29 | emitter.on(obj, obj, fn) 30 | emitter.emit(obj, obj) 31 | t.is(control.a, 1, 'control a') 32 | t.is(control.b, 1, 'control b') 33 | emitter.off(obj, obj, fn) 34 | emitter.emit(obj, obj) 35 | t.is(control.a, 2, 'control a') 36 | t.is(control.b, 1, 'control b') 37 | t.end() 38 | }) 39 | 40 | test('eventController#emit', t => { 41 | const ee = weakEmitter() 42 | const obj = {} 43 | t.plan(1) 44 | const event = ee.on(obj, 'a', (...change) => { 45 | t.same(change, [1, 2, 3, 4]) 46 | }) 47 | event.emit(1, 2, 3, 4) 48 | t.end() 49 | }) 50 | 51 | test('eventController#off', t => { 52 | const ee = weakEmitter() 53 | const obj = {} 54 | const event = ee.on(obj, 'a', () => { t.fail() }) 55 | event.off() 56 | ee.emit(obj, 'a') 57 | t.pass() 58 | t.end() 59 | }) 60 | 61 | test('eventController#transfer', t => { 62 | t.plan(1) 63 | const ee = weakEmitter() 64 | const pass = () => t.pass() 65 | const fail = () => t.fail() 66 | const control = { 67 | action: pass 68 | } 69 | const obj = {} 70 | const dest = {} 71 | const event = ee.on(obj, 'a', () => { control.action() }) 72 | 73 | event.transfer(dest) 74 | ee.emit(dest, 'a') 75 | 76 | control.action = fail 77 | ee.emit(obj, 'a') 78 | 79 | event.off() 80 | ee.emit(dest, 'a') 81 | 82 | t.end() 83 | }) 84 | 85 | test('emit actions in order', t => { 86 | const emitter = weakEmitter() 87 | const obj = {} 88 | let control = '' 89 | emitter.on(obj, obj, () => { control = control + 'a' }) 90 | emitter.emit(obj, obj) 91 | t.is(control, 'a', 'trigger') 92 | emitter.emit(obj, obj) 93 | t.is(control, 'aa', 'trigger') 94 | 95 | emitter.on(obj, obj, () => { control = control + 'b' }) 96 | emitter.emit(obj, obj) 97 | t.is(control, 'aaab', 'unsubscribe') 98 | 99 | emitter.on(obj, obj, () => { control = control + 'c' }) 100 | emitter.emit(obj, obj) 101 | t.is(control, 'aaababc', 'unsubscribe') 102 | 103 | t.end() 104 | }) 105 | 106 | test('emit with arguments', t => { 107 | const emitter = weakEmitter() 108 | const obj = {} 109 | const control = { 110 | a: 0 111 | } 112 | emitter.on(obj, obj, a => { control.a = a }) 113 | 114 | emitter.emit(obj, obj, 1) 115 | t.is(control.a, 1) 116 | 117 | t.end() 118 | }) 119 | 120 | test('remove listener in a event with muliple listeners', t => { 121 | const emitter = weakEmitter() 122 | const out: number[] = [] 123 | const tt = {} 124 | const f1 = () => out.push(1) 125 | const f3 = () => out.push(3) 126 | const f2 = () => { 127 | out.push(2) 128 | emitter.off(tt, tt, f3) 129 | } 130 | emitter.on(tt, tt, f1) 131 | emitter.on(tt, tt, f2) 132 | emitter.on(tt, tt, f3) 133 | emitter.emit(tt, tt) 134 | t.is(out[0], 1) 135 | t.is(out[1], 2) 136 | t.notOk(out[2]) 137 | emitter.emit(tt, tt) 138 | t.is(out[2], 1) 139 | t.is(out[3], 2) 140 | t.end() 141 | }) 142 | -------------------------------------------------------------------------------- /src/weak-emitter.ts: -------------------------------------------------------------------------------- 1 | type EventHandler = (...args: any[]) => void 2 | type EventKey = PropertyKey | object 3 | type EventStack = Set 4 | type EventContext = Map 5 | type Contexts = WeakMap 6 | 7 | export interface WeakEventController { 8 | emit: EventHandler 9 | off: () => void 10 | transfer: (context: object) => void 11 | } 12 | 13 | export interface WeakEventEmitter { 14 | on: (context: object, key: EventKey, handler: EventHandler) => WeakEventController 15 | emit: (context: object, key: EventKey, ...args: any[]) => void 16 | off: (context: object, key: EventKey, handler: EventHandler) => void 17 | } 18 | 19 | /** 20 | * Create and return a new event emitter 21 | */ 22 | export function weakEmitter (): WeakEventEmitter { 23 | const contexts: Contexts = new WeakMap() 24 | 25 | function newEvent (context: object): EventContext { 26 | const eventContext: EventContext = new Map() 27 | contexts.set(context, eventContext) 28 | return eventContext 29 | } 30 | 31 | function newStack (eventContext: Map, key: EventKey): EventStack { 32 | const stack: EventStack = new Set() 33 | eventContext.set(key, stack) 34 | return stack 35 | } 36 | 37 | function on (context: object, key: EventKey, handler: EventHandler): WeakEventController { 38 | let eventContext = contexts.get(context) || newEvent(context) 39 | let stack = eventContext.get(key) || newStack(eventContext, key) 40 | stack.add(handler) 41 | return { 42 | emit: handler, 43 | off () { off(context, key, handler) }, 44 | transfer (destination: object) { 45 | stack.delete(handler) 46 | if (!stack.size) eventContext.delete(stack) 47 | context = destination 48 | eventContext = contexts.get(context) || newEvent(context) 49 | stack = eventContext.get(key) || newStack(eventContext, key) 50 | stack.add(handler) 51 | } 52 | } 53 | } 54 | 55 | function emit (context: object, key: EventKey, ...args: any[]): void { 56 | const eventContext = contexts.get(context) 57 | if (!eventContext) return 58 | const stack = eventContext.get(key) 59 | if (!stack) return 60 | stack.forEach((handler: EventHandler) => handler(...args)) 61 | } 62 | 63 | function off (context: object, key: EventKey, handler: EventHandler): void { 64 | const eventContext = contexts.get(context) 65 | if (!eventContext) return 66 | const stack = eventContext.get(key) 67 | if (!stack) return 68 | stack.delete(handler) 69 | if (!stack.size) eventContext.delete(stack) 70 | } 71 | 72 | return { on, off, emit } 73 | } 74 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./lib", /* Redirect output structure to the directory. */ 16 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "exclude": ["node_modules", "src/tests.ts"] 67 | } 68 | --------------------------------------------------------------------------------