├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── build-lint-test.yml │ ├── create-release-pr.yml │ ├── main.yml │ ├── publish-release.yml │ └── security-code-scanner.yml ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.test.ts ├── index.ts ├── jest.config.js ├── package.json ├── scripts └── rename-esm.sh ├── tsconfig.esm.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | indent_size = 4 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !.eslintrc.js 3 | !jest.config.js 4 | node_modules 5 | dist 6 | coverage 7 | *.d.ts 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | '@metamask/eslint-config', 5 | '@metamask/eslint-config/config/jest', 6 | '@metamask/eslint-config/config/nodejs', 7 | '@metamask/eslint-config/config/typescript', 8 | ], 9 | overrides: [{ 10 | files: [ 11 | '.eslintrc.js', 12 | 'jest.config.js', 13 | ], 14 | parserOptions: { 15 | sourceType: 'script', 16 | }, 17 | }], 18 | }; 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.d.ts linguist-generated=true 3 | 4 | # Reviewing the lockfile contents is an important step in verifying that 5 | # we're using the dependencies we expect to be using 6 | package-lock.json linguist-generated=false 7 | yarn.lock linguist-generated=false 8 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | * @MetaMask/devs 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: 'npm' 7 | directory: '/' 8 | schedule: 9 | interval: 'daily' 10 | time: '06:00' 11 | allow: 12 | - dependency-name: '@metamask/*' 13 | target-branch: 'main' 14 | versioning-strategy: 'increase-if-necessary' 15 | open-pull-requests-limit: 10 16 | -------------------------------------------------------------------------------- /.github/workflows/build-lint-test.yml: -------------------------------------------------------------------------------- 1 | name: Build, Lint, and Test 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | build-lint-test: 8 | name: Build, Lint, and Test 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [12.x, 14.x, 16.x, 18.x, 20.x] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | cache: yarn 20 | - run: yarn --frozen-lockfile --ignore-scripts 21 | - run: yarn build 22 | - run: yarn lint 23 | - run: yarn test 24 | - name: Validate RC changelog 25 | if: ${{ startsWith(github.head_ref, 'release/') }} 26 | run: yarn auto-changelog validate --rc 27 | - name: Validate changelog 28 | if: ${{ !startsWith(github.head_ref, 'release/') }} 29 | run: yarn auto-changelog validate 30 | -------------------------------------------------------------------------------- /.github/workflows/create-release-pr.yml: -------------------------------------------------------------------------------- 1 | name: Create Release Pull Request 2 | 3 | permissions: 4 | contents: write 5 | pull-requests: write 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | base-branch: 11 | description: 'The base branch for git operations and the pull request.' 12 | default: 'main' 13 | required: true 14 | release-type: 15 | description: 'A SemVer version diff, i.e. major, minor, patch, prerelease etc. Mutually exclusive with "release-version".' 16 | required: false 17 | release-version: 18 | description: 'A specific version to bump to. Mutually exclusive with "release-type".' 19 | required: false 20 | 21 | jobs: 22 | create-release-pr: 23 | runs-on: ubuntu-latest 24 | permissions: 25 | contents: write 26 | pull-requests: write 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | # This is to guarantee that the most recent tag is fetched. 31 | # This can be configured to a more reasonable value by consumers. 32 | fetch-depth: 0 33 | # We check out the specified branch, which will be used as the base 34 | # branch for all git operations and the release PR. 35 | ref: ${{ github.event.inputs.base-branch }} 36 | - name: Setup Node.js 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version-file: '.nvmrc' 40 | - uses: MetaMask/action-create-release-pr@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | release-type: ${{ github.event.inputs.release-type }} 45 | release-version: ${{ github.event.inputs.release-version }} 46 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | build-lint-test: 10 | name: Test 11 | uses: ./.github/workflows/build-lint-test.yml 12 | 13 | all-jobs-completed: 14 | name: All jobs completed 15 | runs-on: ubuntu-latest 16 | needs: 17 | - build-lint-test 18 | outputs: 19 | PASSED: ${{ steps.set-output.outputs.PASSED }} 20 | steps: 21 | - name: Set PASSED output 22 | id: set-output 23 | run: echo "PASSED=true" >> "$GITHUB_OUTPUT" 24 | 25 | all-jobs-pass: 26 | name: All jobs pass 27 | if: ${{ always() }} 28 | runs-on: ubuntu-latest 29 | needs: all-jobs-completed 30 | steps: 31 | - name: Check that all jobs have passed 32 | run: | 33 | passed="${{ needs.all-jobs-completed.outputs.PASSED }}" 34 | if [[ $passed != "true" ]]; then 35 | exit 1 36 | fi 37 | 38 | is-release: 39 | # Filtering by `push` events ensures that we only release from the `main` branch, which is a 40 | # requirement for our npm publishing environment. 41 | # The commit author should always be 'github-actions' for releases created by the 42 | # 'create-release-pr' workflow, so we filter by that as well to prevent accidentally 43 | # triggering a release. 44 | if: github.event_name == 'push' && startsWith(github.event.head_commit.author.name, 'github-actions') 45 | needs: all-jobs-pass 46 | outputs: 47 | IS_RELEASE: ${{ steps.is-release.outputs.IS_RELEASE }} 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: MetaMask/action-is-release@v2 51 | id: is-release 52 | 53 | publish-release: 54 | needs: is-release 55 | if: needs.is-release.outputs.IS_RELEASE == 'true' 56 | name: Publish release 57 | permissions: 58 | contents: write 59 | uses: ./.github/workflows/publish-release.yml 60 | secrets: 61 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 62 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | NPM_TOKEN: 7 | required: true 8 | 9 | jobs: 10 | publish-release: 11 | permissions: 12 | contents: write 13 | if: | 14 | github.event.pull_request.merged == true && 15 | startsWith(github.event.pull_request.head.ref, 'release/') 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.sha }} 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | # TODO: use default version when node.engines has been bumped to at least v16 in package.json 25 | node-version: '20.x' 26 | - uses: MetaMask/action-publish-release@v2 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Install 30 | run: | 31 | yarn --frozen-lockfile --ignore-scripts 32 | yarn build 33 | - uses: actions/cache@v4 34 | id: restore-build 35 | with: 36 | path: | 37 | ./dist 38 | ./node_modules/.yarn-state.yml 39 | key: ${{ github.sha }} 40 | 41 | publish-npm-dry-run: 42 | runs-on: ubuntu-latest 43 | needs: publish-release 44 | steps: 45 | - uses: actions/checkout@v4 46 | with: 47 | ref: ${{ github.sha }} 48 | - uses: actions/cache@v4 49 | id: restore-build 50 | with: 51 | path: | 52 | ./dist 53 | ./node_modules/.yarn-state.yml 54 | key: ${{ github.sha }} 55 | - name: Dry Run Publish 56 | # omit npm-token token to perform dry run publish 57 | uses: MetaMask/action-npm-publish@v2 58 | env: 59 | SKIP_PREPACK: true 60 | 61 | publish-npm: 62 | environment: npm-publish 63 | runs-on: ubuntu-latest 64 | needs: publish-npm-dry-run 65 | steps: 66 | - uses: actions/checkout@v4 67 | with: 68 | ref: ${{ github.sha }} 69 | - uses: actions/cache@v4 70 | id: restore-build 71 | with: 72 | path: | 73 | ./dist 74 | ./node_modules/.yarn-state.yml 75 | key: ${{ github.sha }} 76 | - name: Publish 77 | uses: MetaMask/action-npm-publish@v2 78 | with: 79 | # This `NPM_TOKEN` needs to be manually set per-repository. 80 | # Look in the repository settings under "Environments", and set this token in the `npm-publish` environment. 81 | npm-token: ${{ secrets.NPM_TOKEN }} 82 | env: 83 | SKIP_PREPACK: true 84 | -------------------------------------------------------------------------------- /.github/workflows/security-code-scanner.yml: -------------------------------------------------------------------------------- 1 | name: MetaMask Security Code Scanner 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | run-security-scan: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | steps: 20 | - name: MetaMask Security Code Scanner 21 | uses: MetaMask/Security-Code-Scanner@main 22 | with: 23 | repo: ${{ github.repository }} 24 | paths_ignored: | 25 | .storybook/ 26 | '**/__snapshots__/' 27 | '**/*.snap' 28 | '**/*.stories.js' 29 | '**/*.stories.tsx' 30 | '**/*.test.browser.ts*' 31 | '**/*.test.js*' 32 | '**/*.test.ts*' 33 | '**/fixtures/' 34 | '**/jest.config.js' 35 | '**/jest.environment.js' 36 | '**/mocks/' 37 | '**/test*/' 38 | docs/ 39 | e2e/ 40 | merged-packages/ 41 | node_modules 42 | storybook/ 43 | test*/ 44 | rules_excluded: example 45 | project_metrics_token: ${{ secrets.SECURITY_SCAN_METRICS_TOKEN }} 46 | slack_webhook: ${{ secrets.APPSEC_BOT_SLACK_WEBHOOK }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | coverage/ 4 | docs/ 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | 39 | # TypeScript cache 40 | *.tsbuildinfo 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Microbundle cache 49 | .rpt2_cache/ 50 | .rts2_cache_cjs/ 51 | .rts2_cache_es/ 52 | .rts2_cache_umd/ 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | .env.test 66 | 67 | # Stores VSCode versions used for testing VSCode extensions 68 | .vscode-test 69 | 70 | # yarn v3 (w/o zero-install) 71 | # See: https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 72 | .pnp.* 73 | .yarn/* 74 | !.yarn/patches 75 | !.yarn/plugins 76 | !.yarn/releases 77 | !.yarn/sdks 78 | !.yarn/versions 79 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [3.1.2] 10 | ### Fixed 11 | - Omit test files from published package ([#149](https://github.com/MetaMask/safe-event-emitter/pull/149)) 12 | 13 | ## [3.1.1] - 2024-03-12 14 | ### Fixed 15 | - Fix ESM module path ([#143](https://github.com/MetaMask/safe-event-emitter/pull/143)) 16 | - Fix ESM source map paths ([#146](https://github.com/MetaMask/safe-event-emitter/pull/146)) 17 | 18 | ## [3.1.0] - 2024-03-08 19 | ### Added 20 | - Add ESM build ([#141](https://github.com/MetaMask/safe-event-emitter/pull/141)) 21 | 22 | ## [3.0.0] - 2023-04-24 23 | ### Changed 24 | - **BREAKING**: Drop support for Node.js < v12 ([#101](https://github.com/MetaMask/safe-event-emitter/pull/101)) 25 | 26 | ## [2.0.0] - 2020-09-02 27 | ### Changed 28 | - Migrate to TypeScript ([#1](https://github.com/MetaMask/safe-event-emitter/pull/1)) 29 | 30 | [Unreleased]: https://github.com/MetaMask/safe-event-emitter/compare/v3.1.2...HEAD 31 | [3.1.2]: https://github.com/MetaMask/safe-event-emitter/compare/v3.1.1...v3.1.2 32 | [3.1.1]: https://github.com/MetaMask/safe-event-emitter/compare/v3.1.0...v3.1.1 33 | [3.1.0]: https://github.com/MetaMask/safe-event-emitter/compare/v3.0.0...v3.1.0 34 | [3.0.0]: https://github.com/MetaMask/safe-event-emitter/compare/v2.0.0...v3.0.0 35 | [2.0.0]: https://github.com/MetaMask/safe-event-emitter/releases/tag/v2.0.0 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020 MetaMask 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # safe-event-emitter 2 | 3 | An `EventEmitter` that isolates the emitter from errors in handlers. If an error is thrown in a handler it is caught and re-thrown inside of a `setTimeout` so as to not interrupt the emitter's code flow. 4 | 5 | The API is the same as a core [`EventEmitter`](https://nodejs.org/api/events.html). 6 | 7 | ### Install 8 | 9 | ```bash 10 | $ yarn add '@metamask/safe-event-emitter' 11 | ``` 12 | 13 | ### Usage 14 | 15 | ```js 16 | import SafeEventEmitter from '@metamask/safe-event-emitter'; 17 | 18 | const ee = new SafeEventEmitter(); 19 | ee.on('boom', () => { throw new Error(); }); 20 | ee.emit('boom'); // No error here 21 | 22 | // Error is thrown after setTimeout 23 | ``` 24 | 25 | ### Release & Publishing 26 | 27 | The project follows the same release process as the other libraries in the MetaMask organization: 28 | 29 | 1. Create a release branch 30 | - For a typical release, this would be based on `master` 31 | - To update an older maintained major version, base the release branch on the major version branch (e.g. `1.x`) 32 | 2. Update the changelog 33 | 3. Update version in package.json file (e.g. `yarn version --minor --no-git-tag-version`) 34 | 4. Create a pull request targeting the base branch (e.g. master or 1.x) 35 | 5. Code review and QA 36 | 6. Once approved, the PR is squashed & merged 37 | 7. The commit on the base branch is tagged 38 | 8. The tag can be published as needed 39 | 40 | ## Running tests 41 | 42 | ```bash 43 | yarn test 44 | ``` 45 | -------------------------------------------------------------------------------- /index.test.ts: -------------------------------------------------------------------------------- 1 | import SafeEventEmitter from '.'; 2 | 3 | describe('SafeEventEmitter', () => { 4 | it('can be constructed without error', () => { 5 | expect(new SafeEventEmitter()).toBeDefined(); 6 | }); 7 | 8 | it('can emit a value with no listeners', () => { 9 | const see = new SafeEventEmitter(); 10 | expect(see.emit('foo', 42)).toBe(false); 11 | }); 12 | 13 | it('can emit a value with 1 listeners', () => { 14 | expect.assertions(2); 15 | 16 | const see = new SafeEventEmitter(); 17 | see.on('foo', (x) => expect(x).toBe(42)); 18 | expect(see.emit('foo', 42)).toBe(true); 19 | }); 20 | 21 | it('can emit a value with 2 listeners', () => { 22 | expect.assertions(3); 23 | 24 | const see = new SafeEventEmitter(); 25 | see.on('foo', (x) => expect(x).toBe(42)); 26 | see.on('foo', (x) => expect(x).toBe(42)); 27 | expect(see.emit('foo', 42)).toBe(true); 28 | }); 29 | 30 | it('returns false when _events is somehow undefined', () => { 31 | const see = new SafeEventEmitter(); 32 | see.on('foo', () => { /* */ }); 33 | delete (see as any)._events; 34 | expect(see.emit('foo', 42)).toBe(false); 35 | }); 36 | 37 | it('throws error from handler after setTimeout', () => { 38 | jest.useFakeTimers(); 39 | const see = new SafeEventEmitter(); 40 | see.on('boom', () => { 41 | throw new Error('foo'); 42 | }); 43 | expect(() => { 44 | see.emit('boom'); 45 | }).not.toThrow(); 46 | expect(() => { 47 | jest.runAllTimers(); 48 | }).toThrow('foo'); 49 | }); 50 | 51 | it('throws error emitted when there is no error handler', () => { 52 | const see = new SafeEventEmitter(); 53 | expect(() => { 54 | see.emit('error', new Error('foo')); 55 | }).toThrow('foo'); 56 | }); 57 | 58 | it('throws error emitted when there is no error handler AND _events is somehow undefined', () => { 59 | const see = new SafeEventEmitter(); 60 | delete (see as any)._events; 61 | expect(() => { 62 | see.emit('error', new Error('foo')); 63 | }).toThrow('foo'); 64 | }); 65 | 66 | it('throws default error when there is no error handler and error event emitted', () => { 67 | const see = new SafeEventEmitter(); 68 | expect(() => { 69 | see.emit('error'); 70 | }).toThrow('Unhandled error.'); 71 | }); 72 | 73 | it('throws error when there is no error handler and error event emitted', () => { 74 | const see = new SafeEventEmitter(); 75 | expect(() => { 76 | see.emit('error', { message: 'foo' }); 77 | }).toThrow('Unhandled error. (foo)'); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | type Handler = (...args: any[]) => void; 4 | interface EventMap { 5 | [k: string]: Handler | Handler[] | undefined; 6 | } 7 | 8 | function safeApply(handler: (this: T, ..._args: A) => void, context: T, args: A): void { 9 | try { 10 | Reflect.apply(handler, context, args); 11 | } catch (err) { 12 | // Throw error after timeout so as not to interrupt the stack 13 | setTimeout(() => { 14 | throw err; 15 | }); 16 | } 17 | } 18 | 19 | function arrayClone(arr: T[]): T[] { 20 | const n = arr.length; 21 | const copy = new Array(n); 22 | for (let i = 0; i < n; i += 1) { 23 | copy[i] = arr[i]; 24 | } 25 | return copy; 26 | } 27 | 28 | export default class SafeEventEmitter extends EventEmitter { 29 | emit(type: string, ...args: any[]): boolean { 30 | let doError = type === 'error'; 31 | 32 | const events: EventMap = (this as any)._events; 33 | if (events !== undefined) { 34 | doError = doError && events.error === undefined; 35 | } else if (!doError) { 36 | return false; 37 | } 38 | 39 | // If there is no 'error' event listener then throw. 40 | if (doError) { 41 | let er; 42 | if (args.length > 0) { 43 | [er] = args; 44 | } 45 | if (er instanceof Error) { 46 | // Note: The comments on the `throw` lines are intentional, they show 47 | // up in Node's output if this results in an unhandled exception. 48 | throw er; // Unhandled 'error' event 49 | } 50 | // At least give some kind of context to the user 51 | const err = new Error(`Unhandled error.${er ? ` (${er.message})` : ''}`); 52 | (err as any).context = er; 53 | throw err; // Unhandled 'error' event 54 | } 55 | 56 | const handler = events[type]; 57 | 58 | if (handler === undefined) { 59 | return false; 60 | } 61 | 62 | if (typeof handler === 'function') { 63 | safeApply(handler, this, args); 64 | } else { 65 | const len = handler.length; 66 | const listeners = arrayClone(handler); 67 | for (let i = 0; i < len; i += 1) { 68 | safeApply(listeners[i], this, args); 69 | } 70 | } 71 | 72 | return true; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | coverageReporters: ['text', 'html'], 4 | coverageThreshold: { 5 | global: { 6 | branches: 100, 7 | functions: 100, 8 | lines: 100, 9 | statements: 100, 10 | }, 11 | }, 12 | moduleFileExtensions: ['ts', 'tsx', 'json', 'js', 'jsx', 'node'], 13 | preset: 'ts-jest', 14 | testEnvironment: 'node', 15 | testRegex: [ 16 | '\\.test\\.ts$', 17 | ], 18 | testTimeout: 5000, 19 | }; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@metamask/safe-event-emitter", 3 | "version": "3.1.2", 4 | "description": "An EventEmitter that isolates the emitter from errors in handlers", 5 | "main": "dist/cjs/index.js", 6 | "module": "dist/esm/index.mjs", 7 | "types": "dist/cjs/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": { 11 | "types": "./dist/esm/index.d.ts", 12 | "default": "./dist/esm/index.mjs" 13 | }, 14 | "require": { 15 | "types": "./dist/cjs/index.d.ts", 16 | "default": "./dist/cjs/index.js" 17 | } 18 | }, 19 | "./package.json": "./package.json" 20 | }, 21 | "publishConfig": { 22 | "registry": "https://registry.npmjs.org/", 23 | "access": "public" 24 | }, 25 | "files": [ 26 | "dist", 27 | "!*.test.*" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/MetaMask/safe-event-emitter.git" 32 | }, 33 | "engines": { 34 | "node": ">=12.0.0" 35 | }, 36 | "scripts": { 37 | "prepublishOnly": "yarn build", 38 | "build": "rimraf dist && yarn build:cjs && yarn build:esm", 39 | "build:cjs": "tsc --project .", 40 | "build:esm": "tsc --project tsconfig.esm.json && yarn build:esm:rename", 41 | "build:esm:rename": "./scripts/rename-esm.sh", 42 | "test": "jest", 43 | "lint": "eslint . --ext .ts,.js" 44 | }, 45 | "author": "", 46 | "license": "ISC", 47 | "devDependencies": { 48 | "@metamask/auto-changelog": "^2.6.1", 49 | "@metamask/eslint-config": "^5.0.0", 50 | "@types/jest": "^26.0.20", 51 | "@types/node": "^14.14.21", 52 | "@typescript-eslint/eslint-plugin": "^4.33.0", 53 | "@typescript-eslint/parser": "^4.33.0", 54 | "eslint": "^7.32.0", 55 | "eslint-plugin-import": "^2.22.1", 56 | "eslint-plugin-jest": "^24.1.3", 57 | "eslint-plugin-node": "^11.1.0", 58 | "jest": "^26.4.2", 59 | "rimraf": "^3.0.2", 60 | "ts-jest": "^26.4.0", 61 | "typescript": "^4.0.5" 62 | }, 63 | "dependencies": {} 64 | } 65 | -------------------------------------------------------------------------------- /scripts/rename-esm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -u 5 | set -o pipefail 6 | 7 | if [[ ${RUNNER_DEBUG:-0} == 1 ]]; then 8 | set -x 9 | fi 10 | 11 | for file in dist/esm/*.js; do 12 | # Rename the `.js` files to `.mjs`. 13 | mv "$file" "${file%.js}.mjs" 14 | 15 | # Replace the sourceMappingURL to point to the new `.mjs.map` file. 16 | sourceMap=$(basename "${file%.js}.mjs.map") 17 | perl -i -pe "s|//# sourceMappingURL=.*?\.js\.map|//# sourceMappingURL=$sourceMap|" "${file%.js}.mjs" 18 | done 19 | 20 | for file in dist/esm/*.js.map; do 21 | # Rename the `.js.map` files to `.mjs.map`. 22 | mv "$file" "${file%.js.map}.mjs.map" 23 | 24 | # Replace the file references in the source map to point to the new `.mjs` file. 25 | perl -i -pe 's/\.js/\.mjs/g' "${file%.js.map}.mjs.map" 26 | done 27 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "ES6", 5 | "moduleResolution": "Node", 6 | "outDir": "dist/esm", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "ES2017" 10 | }, 11 | "include": [ 12 | "./**/*.ts" 13 | ], 14 | "exclude": [ 15 | "node_modules", 16 | "dist" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "CommonJS", 5 | "outDir": "dist/cjs", 6 | "sourceMap": true, 7 | "strict": true, 8 | "target": "ES2017" 9 | }, 10 | "include": [ 11 | "./**/*.ts" 12 | ], 13 | "exclude": [ 14 | "node_modules", 15 | "dist" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------