-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: add Event Emmiter for Danet * enh: register events * fix: remove uneeded BroadcastChannel * enh: add code example for events * enh: add EventEmitterModule * fea: use EventTarget + CustomEvent * enh: cleanup subscribers OnAppClose * enh: logging * enh: update example * fix: deno fmt changes * fix: update remaining listeners * fix: example/events.ts Co-authored-by: Thomas Cruveilher <[email protected]> * enh: add Map for handling registered listeners * fix: typo * fea: move event listeners registration - separate module and service file * enh: cleanup injector dependencies * fix: expose resolvedTypes from injector * enh: code cleanup * enh: add test cases for EventEmitter * enh: add test cases for EventEmitterModule * refactor: use injector.injectables --------- Co-authored-by: Thomas Cruveilher <[email protected]>
- Loading branch information
1 parent
465c68e
commit 28ccfe7
Showing
11 changed files
with
372 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { | ||
Controller, | ||
DanetApplication, | ||
EventEmitter, | ||
EventEmitterModule, | ||
Module, | ||
OnEvent, | ||
Post, | ||
} from '../mod.ts'; | ||
|
||
type User = {}; | ||
|
||
class UserListeners { | ||
@OnEvent('new-user') | ||
notifyUser(user: User) { | ||
console.log('new user created', user); | ||
} | ||
|
||
@OnEvent('new-user') | ||
async sendWelcomeEmail(user: User) { | ||
console.log('send email', user); | ||
} | ||
} | ||
|
||
@Controller('user') | ||
class UserController { | ||
constructor( | ||
private eventEmitter: EventEmitter, | ||
) {} | ||
|
||
@Post() | ||
create() { | ||
const user: User = {}; | ||
this.eventEmitter.emit('new-user', user); | ||
return JSON.stringify(user); | ||
} | ||
} | ||
|
||
@Module({ | ||
imports: [EventEmitterModule], | ||
controllers: [UserController], | ||
injectables: [UserListeners], | ||
}) | ||
class AppModule {} | ||
|
||
const app = new DanetApplication(); | ||
await app.init(AppModule); | ||
|
||
let port = Number(Deno.env.get('PORT')); | ||
if (isNaN(port)) { | ||
port = 3000; | ||
} | ||
app.listen(port); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import { | ||
Controller, | ||
DanetApplication, | ||
EventEmitter, | ||
EventEmitterModule, | ||
Get, | ||
Module, | ||
OnEvent, | ||
} from '../mod.ts'; | ||
import { | ||
assertEquals, | ||
assertSpyCall, | ||
assertThrows, | ||
spy, | ||
} from '../src/deps_test.ts'; | ||
|
||
Deno.test('EventEmitter Service', async (t) => { | ||
await t.step('subscribe multiple listeners for the same topic', async () => { | ||
const emitter = new EventEmitter(); | ||
const fn1 = spy(() => {}); | ||
const fn2 = spy(() => {}); | ||
|
||
emitter.subscribe('test', fn1); | ||
emitter.subscribe('test', fn2); | ||
|
||
emitter.emit('test', 'something'); | ||
|
||
assertSpyCall(fn1, 0, { | ||
args: ['something'], | ||
returned: undefined, | ||
}); | ||
|
||
assertSpyCall(fn2, 0, { | ||
args: ['something'], | ||
returned: undefined, | ||
}); | ||
|
||
emitter.unsubscribe(); | ||
}); | ||
|
||
await t.step('subscribe listeners for the multiple topics', async () => { | ||
const emitter = new EventEmitter(); | ||
const fn1 = spy(() => {}); | ||
const fn2 = spy(() => {}); | ||
|
||
emitter.subscribe('test', fn1); | ||
emitter.subscribe('test-2', fn2); | ||
|
||
emitter.emit('test', 'something'); | ||
|
||
assertSpyCall(fn1, 0, { | ||
args: ['something'], | ||
returned: undefined, | ||
}); | ||
|
||
assertEquals(fn2.calls.length, 0); | ||
|
||
emitter.emit('test-2', 'something'); | ||
|
||
assertSpyCall(fn2, 0, { | ||
args: ['something'], | ||
returned: undefined, | ||
}); | ||
|
||
assertEquals(fn1.calls.length, 1); | ||
|
||
emitter.unsubscribe(); | ||
}); | ||
|
||
await t.step('throw error if emit an event with no listener', async () => { | ||
const emitter = new EventEmitter(); | ||
const fn1 = spy(() => {}); | ||
|
||
assertThrows(() => emitter.emit('test', 'something')); | ||
|
||
emitter.subscribe('test', fn1); | ||
|
||
assertEquals(fn1.calls.length, 0); | ||
|
||
emitter.emit('test', 'something'); | ||
|
||
assertSpyCall(fn1, 0, { | ||
args: ['something'], | ||
returned: undefined, | ||
}); | ||
|
||
emitter.unsubscribe(); | ||
}); | ||
|
||
await t.step('throw error if emit to a unsubscribed topic', async () => { | ||
const emitter = new EventEmitter(); | ||
const fn1 = spy(() => {}); | ||
|
||
assertEquals(fn1.calls.length, 0); | ||
|
||
emitter.subscribe('test', fn1); | ||
emitter.emit('test', 'something'); | ||
|
||
assertSpyCall(fn1, 0, { | ||
args: ['something'], | ||
returned: undefined, | ||
}); | ||
|
||
emitter.unsubscribe('test'); | ||
|
||
assertThrows(() => emitter.emit('test', 'something')); | ||
assertEquals(fn1.calls.length, 1); | ||
|
||
emitter.unsubscribe(); | ||
}); | ||
}); | ||
|
||
Deno.test('EventEmitter Module', async (t) => { | ||
const callback = spy((_payload: any) => {}); | ||
const payload = { name: 'test' }; | ||
|
||
class TestListener { | ||
@OnEvent('trigger') | ||
getSomething(payload: any) { | ||
callback(payload); | ||
} | ||
} | ||
|
||
@Controller('trigger') | ||
class TestController { | ||
constructor(private emitter: EventEmitter) {} | ||
|
||
@Get() | ||
getSomething() { | ||
this.emitter.emit('trigger', payload); | ||
return 'OK'; | ||
} | ||
} | ||
|
||
@Module({ | ||
imports: [EventEmitterModule], | ||
controllers: [TestController], | ||
injectables: [TestListener], | ||
}) | ||
class TestModule {} | ||
|
||
const application = new DanetApplication(); | ||
await application.init(TestModule); | ||
const listenerInfo = await application.listen(0); | ||
|
||
await t.step('validate if api call trigger event', async () => { | ||
assertEquals(callback.calls.length, 0); | ||
|
||
let res = await fetch(`http://localhost:${listenerInfo.port}/trigger`); | ||
|
||
assertEquals(res.status, 200); | ||
assertEquals(await res.text(), 'OK'); | ||
assertEquals(callback.calls.length, 1); | ||
assertSpyCall(callback, 0, { | ||
args: [payload], | ||
}); | ||
|
||
res = await fetch(`http://localhost:${listenerInfo.port}/trigger`); | ||
|
||
assertEquals(res.status, 200); | ||
assertEquals(await res.text(), 'OK'); | ||
assertEquals(callback.calls.length, 2); | ||
}); | ||
|
||
await application.close(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,15 @@ | ||
export { | ||
assertSpyCall, | ||
assertSpyCalls, | ||
spy, | ||
} from 'https://deno.land/[email protected]/testing/mock.ts'; | ||
export { | ||
assertEquals, | ||
assertInstanceOf, | ||
assertNotEquals, | ||
assertObjectMatch, | ||
assertRejects, | ||
assertThrows, | ||
} from 'https://deno.land/[email protected]/testing/asserts.ts'; | ||
export * as path from 'https://deno.land/[email protected]/path/mod.ts'; | ||
export { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const eventListenerMetadataKey = 'event-listener'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { MetadataHelper } from '../metadata/mod.ts'; | ||
import { eventListenerMetadataKey } from './constants.ts'; | ||
|
||
export const OnEvent = (channel: string): MethodDecorator => { | ||
return (_target, _propertyKey, descriptor) => { | ||
MetadataHelper.setMetadata( | ||
eventListenerMetadataKey, | ||
{ channel }, | ||
descriptor.value, | ||
); | ||
return descriptor; | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { Logger } from '../mod.ts'; | ||
|
||
// deno-lint-ignore no-explicit-any | ||
type Listener<P = any> = (payload: P) => void; | ||
|
||
export class EventEmitter { | ||
private logger: Logger = new Logger('EventEmitter'); | ||
private listenersRegistered: Map<string, Listener[]>; | ||
private eventTarget: EventTarget; | ||
|
||
constructor() { | ||
this.listenersRegistered = new Map(); | ||
this.eventTarget = new EventTarget(); | ||
} | ||
|
||
emit<P>(channelName: string, payload: P) { | ||
const channels = Array.from(this.listenersRegistered.keys()); | ||
if (!channels.includes(channelName)) { | ||
throw new Error(`No listener for '${channelName}' channel`); | ||
} | ||
|
||
const event = new CustomEvent(channelName, { detail: payload }); | ||
this.eventTarget.dispatchEvent(event); | ||
|
||
this.logger.log( | ||
`event send to '${channelName}' channel`, | ||
); | ||
} | ||
|
||
subscribe<P>(channelName: string, listener: Listener<P>) { | ||
const eventListener = (ev: Event) => { | ||
const { detail: payload } = ev as CustomEvent; | ||
return listener(payload); | ||
}; | ||
this.eventTarget.addEventListener(channelName, eventListener); | ||
|
||
const listeners = this.listenersRegistered.get(channelName) ?? []; | ||
this.listenersRegistered.set(channelName, [...listeners, eventListener]); | ||
|
||
this.logger.log( | ||
`event listener subscribed to '${channelName}' channel`, | ||
); | ||
} | ||
|
||
unsubscribe(channelName?: string) { | ||
this.logger.log( | ||
`cleaning up event listeners for '${channelName ?? 'all'}' channel`, | ||
); | ||
|
||
if (channelName) { | ||
return this.deleteChannel(channelName); | ||
} | ||
|
||
for (const channel of this.listenersRegistered.keys()) { | ||
this.deleteChannel(channel); | ||
} | ||
} | ||
|
||
private deleteChannel(channelName: string) { | ||
const listeners = this.listenersRegistered.get(channelName) ?? []; | ||
|
||
listeners.map((listener) => | ||
this.eventTarget.removeEventListener(channelName, listener) | ||
); | ||
|
||
this.listenersRegistered.delete(channelName); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './decorator.ts'; | ||
export * from './events.ts'; | ||
export * from './module.ts'; |
Oops, something went wrong.