diff --git a/ghost/domain-events/.eslintrc.js b/ghost/domain-events/.eslintrc.js new file mode 100644 index 0000000000..f6f9d0db65 --- /dev/null +++ b/ghost/domain-events/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + parser: '@babel/eslint-parser', + parserOptions: { + requireConfigFile: false + }, + plugins: ['ghost'], + extends: [ + 'plugin:ghost/node' + ] +}; diff --git a/ghost/domain-events/LICENSE b/ghost/domain-events/LICENSE new file mode 100644 index 0000000000..366ae5f624 --- /dev/null +++ b/ghost/domain-events/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2021 Ghost Foundation + +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/ghost/domain-events/README.md b/ghost/domain-events/README.md new file mode 100644 index 0000000000..a88d5f6433 --- /dev/null +++ b/ghost/domain-events/README.md @@ -0,0 +1,55 @@ +# Domain Events + +## Install + +`npm install @tryghost/domain-events --save` + +or + +`yarn add @tryghost/domain-events` + + +## Usage + +```js +const DomainEvents = require('@tryghost/domain-events'); + +class MyEvent { + constructor(message) { + this.timestamp = new Date(); + this.data = { + message + }; + } +} + +DomainEvents.subscribe(MyEvent, function handler(event) { + console.log(event.data.message); +}); + +const event = new MyEvent('hello world'); + +DomainEvents.dispatch(event); +``` + + +## Develop + +This is a mono repository, managed with [lerna](https://lernajs.io/). + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `yarn` to install top-level dependencies. + + +## Run + +- `yarn dev` + + +## Test + +- `yarn lint` run just eslint +- `yarn test` run lint and tests + + diff --git a/ghost/domain-events/index.js b/ghost/domain-events/index.js new file mode 100644 index 0000000000..e7a906a8ad --- /dev/null +++ b/ghost/domain-events/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/DomainEvents'); diff --git a/ghost/domain-events/jsconfig.json b/ghost/domain-events/jsconfig.json new file mode 100644 index 0000000000..0c0b6f07ca --- /dev/null +++ b/ghost/domain-events/jsconfig.json @@ -0,0 +1,14 @@ +{ + "include": [ + "index.js", + "lib/**/*.js" + ], + "compilerOptions": { + "checkJs": true, + "module": "commonjs", + "target": "es2018", + "moduleResolution": "node", + "strictFunctionTypes": true, + "noImplicitAny": true + } +} diff --git a/ghost/domain-events/lib/DomainEvents.js b/ghost/domain-events/lib/DomainEvents.js new file mode 100644 index 0000000000..6c74bdb14b --- /dev/null +++ b/ghost/domain-events/lib/DomainEvents.js @@ -0,0 +1,44 @@ +const EventEmitter = require('events').EventEmitter; + +/** + * @template T + * @typedef {import('./').ConstructorOf} ConstructorOf + */ + +/** + * @template Data + * @typedef {object} IEvent + * @prop {Date} timestamp + * @prop {Data} data + */ + +class DomainEvents { + /** + * @private + * @type EventEmitter + */ + static ee = new EventEmitter; + + /** + * @template Data + * @template {IEvent} EventClass + * @param {ConstructorOf} Event + * @param {(event: EventClass) => void} handler + * + * @returns {void} + */ + static subscribe(Event, handler) { + DomainEvents.ee.on(Event.name, handler); + } + + /** + * @template Data + * @param {IEvent} event + * @returns {void} + */ + static dispatch(event) { + DomainEvents.ee.emit(event.constructor.name, event); + } +} + +module.exports = DomainEvents; diff --git a/ghost/domain-events/lib/index.d.ts b/ghost/domain-events/lib/index.d.ts new file mode 100644 index 0000000000..b17c0aa55c --- /dev/null +++ b/ghost/domain-events/lib/index.d.ts @@ -0,0 +1 @@ +export declare type ConstructorOf = new (...args: any[]) => T diff --git a/ghost/domain-events/package.json b/ghost/domain-events/package.json new file mode 100644 index 0000000000..19f4a61081 --- /dev/null +++ b/ghost/domain-events/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tryghost/domain-events", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Members/tree/main/packages/domain-events", + "author": "Ghost Foundation", + "license": "MIT", + "main": "index.js", + "types": "types", + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing c8 --check-coverage mocha './test/**/*.test.js'", + "lint": "eslint . --ext .js --cache", + "posttest": "yarn lint" + }, + "files": [ + "index.js", + "lib" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.15.4", + "c8": "7.9.0", + "mocha": "9.1.1", + "should": "13.2.3", + "sinon": "11.1.2" + }, + "dependencies": {} +} diff --git a/ghost/domain-events/test/.eslintrc.js b/ghost/domain-events/test/.eslintrc.js new file mode 100644 index 0000000000..829b601eb0 --- /dev/null +++ b/ghost/domain-events/test/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/test' + ] +}; diff --git a/ghost/domain-events/test/DomainEvents.test.js b/ghost/domain-events/test/DomainEvents.test.js new file mode 100644 index 0000000000..4f55cc0647 --- /dev/null +++ b/ghost/domain-events/test/DomainEvents.test.js @@ -0,0 +1,49 @@ +const should = require('should'); +const DomainEvents = require('../'); + +class TestEvent { + /** + * @param {string} message + */ + constructor(message) { + this.timestamp = new Date(); + this.data = { + message + }; + } +} + +describe('DomainEvents', function () { + it('Will call multiple subscribers with the event when it is dispatched', function (done) { + const event = new TestEvent('Hello, world!'); + + let called = 0; + + /** + * @param {TestEvent} receivedEvent + */ + function handler1(receivedEvent) { + should.equal(receivedEvent, event); + called += 1; + if (called === 2) { + done(); + } + } + + /** + * @param {TestEvent} receivedEvent + */ + function handler2(receivedEvent) { + should.equal(receivedEvent, event); + called += 1; + if (called === 2) { + done(); + } + } + + DomainEvents.subscribe(TestEvent, handler1); + DomainEvents.subscribe(TestEvent, handler2); + + DomainEvents.dispatch(event); + }); +});