Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2
defaults: &defaults
working_directory: ~/repo
docker:
- image: circleci/node:8.14.1
- image: circleci/node:14.15.1

jobs:
build:
Expand Down
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ Your code stays cleaner, as only business logic related code needs to be written
This starter project contains the following packages preconfigured and in a runnable state:

* `@node-ts/bus-core` - The core service bus that manages sending, routing, dispatch, retries etc
* `@node-ts/bus-workflow` - Enables workflow/saga definitions to be written that coordinate higher-order business processes
* `@node-ts/logger-core` - A generic log abstraction that allows consumers to use their preferred logger
* `@node-ts/logger-winston` - An adapter used to log messages using winston
* `inversify` - An IoC framework for Typescript used to manage dependency injection

## Installation

This repository should be cloned or forked into your own repository:
Expand Down Expand Up @@ -69,9 +64,10 @@ The following scripts are available as part of the starter project:

* `build` - transpiles the code to `/dist`
* `dev` - transpiles, runs and watches for code changes
* `dev:rebuild` - transpiles and runs the app
* `lint` - runs linting based on [@node-ts/code-standards](https://github.com/node-ts/code-standards)
* `lint:fix` - attempts to fix linting violations
* `test` - run all tests
* `test:watch` - run tests in watch mode

## Running

Expand Down
12 changes: 12 additions & 0 deletions jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"testRegex": "(src\\/.+\\.|/)(spec|integration)\\.ts$",
"setupFilesAfterEnv": ["<rootDir>/test/setup.ts"],
"transform": {
"^.+\\.ts?$": [
"esbuild-jest",
{
"sourcemap": true
}
]
}
}
7,111 changes: 6,507 additions & 604 deletions package-lock.json

Large diffs are not rendered by default.

24 changes: 16 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,34 @@
"main": "index.js",
"repository": "https://github.com/node-ts/bus-starter.git",
"license": "MIT",
"engines": {
"node": "14.x"
},
"scripts": {
"build": "tsc --project tsconfig.json --outdir dist",
"dev": "nodemon --watch ./src -e ts --exec 'npm run dev:rebuild' dist/index.js",
"dev:rebuild": "npm run build && node dist/index.js",
"build": "esbuild ./src/index.ts --bundle --platform=node --format=cjs --sourcemap=external --outdir=dist",
"dev": "nodemon --watch ./src -e ts --exec 'npm run dev:rebuild'",
"dev:rebuild": "npm run build && node --enable-source-maps dist/index.js",
"lint": "tslint --project tsconfig.json",
"lint:fix": "npm run lint --fix"
"lint:fix": "npm run lint --fix",
"test": "jest --config=jest.config.json",
"test:watch": "npm run test -- --watch"
},
"dependencies": {
"@node-ts/bus-core": "^1.0.0",
"@node-ts/bus-core": "^1.0.3",
"@node-ts/bus-messages": "^1.0.0",
"@node-ts/bus-rabbitmq": "^1.0.1",
"@node-ts/bus-rabbitmq": "^1.0.5",
"reflect-metadata": "^0.1.13",
"uuid": "^7.0.3"
},
"devDependencies": {
"@node-ts/code-standards": "^0.0.10",
"@types/jest": "^27.0.2",
"@types/node": "^16.10.3",
"@types/uuid": "^7.0.5",
"esbuild": "^0.13.8",
"esbuild-jest": "^0.5.0",
"jest": "^27.3.1",
"nodemon": "^2.0.13",
"tslint": "^6.1.3",
"typescript": "^3.9.10"
"tslint": "^6.1.3"
}
}
59 changes: 59 additions & 0 deletions src/bus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { startSirenTestHandler, emailMaintenanceTeamHandler } from './handlers'
import { SirenTestWorkflow } from './workflows'
import { RabbitMqTransportConfiguration, RabbitMqTransport } from '@node-ts/bus-rabbitmq'
import { explainInitializationError } from './error-helpers'
import { Bus, BusInstance } from '@node-ts/bus-core'

const rabbitMqConfiguration: RabbitMqTransportConfiguration = {
queueName: '@node-ts/bus-starter-test',
connectionString: 'amqp://guest:guest@0.0.0.0',
maxRetries: 10
}

const rabbitMq = new RabbitMqTransport(rabbitMqConfiguration)

let busInstance: BusInstance | undefined

/**
* Initializes a new instance of bus
*/
export const initializeBus = async (): Promise<void> => {
if (!!busInstance) {
throw new Error('Bus has already been initialized')
}

try {
busInstance = await Bus.configure()
.withWorkflow(SirenTestWorkflow)
.withHandler(startSirenTestHandler)
.withHandler(emailMaintenanceTeamHandler)
.withTransport(rabbitMq)
.initialize()
} catch (error) {
explainInitializationError(error)
throw error
}

await busInstance.start()
}

/**
* Disposes and removes the current bus instance
*/
export const disposeBus = async () => {
if (!busInstance) {
throw new Error('Cannot dispose bus as it has not been initialized')
}
await busInstance.dispose()
busInstance = undefined
}

/**
* Gets the initialized bus instance
*/
export const bus = (): BusInstance => {
if (!busInstance) {
throw new Error('Bus has not been initialized, call initializeBus() first.')
}
return busInstance
}
17 changes: 17 additions & 0 deletions src/error-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Provided additional helpful logging if the app fails to start up
*/
export const explainInitializationError = (error: Error) => {
const rabbitNotStarted = /ECONNREFUSED/.test(error.message)
if (rabbitNotStarted) {
console.warn(
'RabbitMQ not running on port 5672. '
+ 'Try running `docker run -d -p 8080:15672 -p 5672:5672 rabbitmq:3-management`'
)
}

const rabbitNotReady = /Socket closed abruptly/.test(error.message)
if (rabbitNotReady) {
console.warn('RabbitMQ is still starting up. Wait a minute and then try again.')
}
}
15 changes: 5 additions & 10 deletions src/handlers/email-maintenance-team-handler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { handlerFor, BusInstance } from '@node-ts/bus-core'
import { EmailMaintenanceTeam, MaintenanceTeamEmailed } from '../messages'
import { handlerFor } from '@node-ts/bus-core'
import { EmailMaintenanceTeam } from '../messages'
import * as emailService from '../services/email-service'

export const emailMaintenanceTeamHandler = (bus: () => BusInstance) => handlerFor(
export const emailMaintenanceTeamHandler = handlerFor(
EmailMaintenanceTeam,
async ({ message, sirenId }) => {
console.log('Sending email to maintenance team to fix siren', { message, sirenId })

// Send the email
const maintenanceTeamEmailed = new MaintenanceTeamEmailed(sirenId)
await bus().publish(maintenanceTeamEmailed)
}
async ({ message, sirenId }) => emailService.sendEmail(message, sirenId)
)
5 changes: 3 additions & 2 deletions src/handlers/start-siren-test-handler.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { StartSirenTest, SirenTestFailed, SirenTestPassed, SirenTestStarted } from '../messages'
import { BusInstance, handlerFor } from '@node-ts/bus-core'
import { Uuid } from '../messages/uuid'
import { bus } from '../bus'

const MAX_SIREN_TEST_DURATION = 5000
const MAX_SIREN_TEST_DURATION = 1000
const TEST_FAILURE_THRESHOLD = 0.5

export const startSirenTestHandler = (bus: () => BusInstance) => handlerFor(
export const startSirenTestHandler = handlerFor(
StartSirenTest,
async ({ sirenId }) => {
console.log('StartSirenTest command received, starting siren test...', { sirenId })
Expand Down
30 changes: 3 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,14 @@
import 'reflect-metadata'

import { Bus, BusInstance } from '@node-ts/bus-core'
import { generateUuid } from './messages/uuid'

import { StartSirenTest } from './messages'
import { startSirenTestHandler, emailMaintenanceTeamHandler } from './handlers'
import { SirenTestWorkflow } from './workflows'
import { RabbitMqTransportConfiguration, RabbitMqTransport } from '@node-ts/bus-rabbitmq'

const rabbitMqConfiguration: RabbitMqTransportConfiguration = {
queueName: '@node-ts/bus-starter-test',
connectionString: 'amqp://guest:guest@0.0.0.0',
maxRetries: 10
}

const rabbitMq = new RabbitMqTransport(rabbitMqConfiguration)

let bus: BusInstance
async function initialize (): Promise<void> {
bus = await Bus.configure()
.withWorkflow(SirenTestWorkflow)
.withHandler(startSirenTestHandler(() => bus)) // Late bound so it's available at runtime
.withHandler(emailMaintenanceTeamHandler(() =>bus))
.withTransport(rabbitMq)
.initialize()

await bus.start()
}
import { bus, initializeBus } from './bus'

async function runDemo (): Promise<void> {
await bus.send(new StartSirenTest(generateUuid()))
await bus().send(new StartSirenTest(generateUuid()))
}

initialize()
initializeBus()
.then(runDemo)
.catch(err => {
console.log(err)
Expand Down
9 changes: 9 additions & 0 deletions src/services/email-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { bus } from '../bus'
import { MaintenanceTeamEmailed } from '../messages'

export const sendEmail = async (message: string, sirenId: string) => {
console.log('Sending email to maintenance team', { message, sirenId })

const maintenanceTeamEmailed = new MaintenanceTeamEmailed(sirenId)
await bus().publish(maintenanceTeamEmailed)
}
1 change: 1 addition & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './email-service'
2 changes: 1 addition & 1 deletion src/workflows/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './siren-test-workflow'
export * from './sirent-test-workflow-data'
export * from './siren-test-workflow-data'
32 changes: 32 additions & 0 deletions src/workflows/siren-test-workflow.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { MaintenanceTeamEmailed, SirenTestFailed, SirenTestStarted } from '../messages'
import { bus, disposeBus, initializeBus } from '../bus'
import { generateUuid } from '../messages/uuid'
import { Listener } from '@node-ts/bus-core'
import { Event } from '@node-ts/bus-messages'

describe('SirenTestWorkflow', () => {

beforeAll(initializeBus)
afterAll(disposeBus)

describe('when a siren test is started', () => {
const sirenId = generateUuid()
beforeAll(async () => bus().publish(new SirenTestStarted(sirenId)))

describe('and then the siren test fails', () => {
it('should send an email to the maintenance team', async () => {
const maintenanceEmailReceived = new Promise<void>(resolve => {
const callback: Listener<{ event: Event }> = ({ event }) => {
if (event.$name === MaintenanceTeamEmailed.NAME && (event as MaintenanceTeamEmailed).sirenId === sirenId) {
bus().beforePublish.off(callback)
resolve()
}
}
bus().beforePublish.on(callback)
})
await bus().publish(new SirenTestFailed(sirenId))
await maintenanceEmailReceived
})
})
})
})
21 changes: 11 additions & 10 deletions src/workflows/siren-test-workflow.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { SirenTestWorkflowData } from './sirent-test-workflow-data'
import { SirenTestStarted, SirenTestFailed, SirenTestPassed, EmailMaintenanceTeam, MaintenanceTeamEmailed } from '../messages'
import { BusInstance, Workflow, WorkflowMapper } from '@node-ts/bus-core'
import { SirenTestWorkflowData } from './siren-test-workflow-data'
import {
SirenTestStarted,
SirenTestFailed,
SirenTestPassed,
EmailMaintenanceTeam,
MaintenanceTeamEmailed
} from '../messages'
import { Workflow, WorkflowMapper } from '@node-ts/bus-core'
import { bus } from '../bus'

export class SirenTestWorkflow extends Workflow<SirenTestWorkflowData> {

constructor (
private readonly bus: BusInstance
) {
super()
}

configureWorkflow (mapper: WorkflowMapper<SirenTestWorkflowData, SirenTestWorkflow>) {
mapper
.withState(SirenTestWorkflowData)
Expand Down Expand Up @@ -39,7 +40,7 @@ export class SirenTestWorkflow extends Workflow<SirenTestWorkflowData> {
'A siren has failed its test and requires maintenance',
sirenId
)
await this.bus.send(emailMaintenanceTeam)
await bus().send(emailMaintenanceTeam)
return {}
}

Expand Down
1 change: 1 addition & 0 deletions test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import 'reflect-metadata'