@smounters/imperium-events 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/event.decorators.d.ts +14 -0
- package/dist/event.decorators.js +19 -0
- package/dist/event.module.d.ts +26 -0
- package/dist/event.module.js +59 -0
- package/dist/event.service.d.ts +31 -0
- package/dist/event.service.js +87 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/package.json +53 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-03-30
|
|
4
|
+
|
|
5
|
+
- Initial release.
|
|
6
|
+
- `@OnEvent(pattern)` method decorator with wildcard support.
|
|
7
|
+
- `EventModule.register({ listeners })` dynamic module.
|
|
8
|
+
- `EventService` with `emit()` and `getHandlers()`.
|
|
9
|
+
- Concurrent handler execution with per-handler error isolation.
|
|
10
|
+
- Zero runtime dependencies.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergio (@smounters)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, 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,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @smounters/imperium-events
|
|
2
|
+
|
|
3
|
+
Typed in-process event emitter for [`@smounters/imperium`](https://www.npmjs.com/package/@smounters/imperium). Decorate methods with `@OnEvent()` and emit from any service. Supports wildcard patterns.
|
|
4
|
+
|
|
5
|
+
Part of the [Imperium monorepo](https://github.com/smounters/imperium).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @smounters/imperium-events
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Injectable, Module } from "@smounters/imperium/decorators";
|
|
17
|
+
import { OnEvent, EventModule, EventService } from "@smounters/imperium-events";
|
|
18
|
+
|
|
19
|
+
// --- Listener ---
|
|
20
|
+
|
|
21
|
+
@Injectable()
|
|
22
|
+
class TradeNotifier {
|
|
23
|
+
@OnEvent("trade.opened")
|
|
24
|
+
async onTradeOpened(payload: { symbol: string; price: number }) {
|
|
25
|
+
// push notification, log, update stats
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@OnEvent("trade.*") // wildcard — matches trade.opened, trade.closed, etc.
|
|
29
|
+
async auditLog(payload: unknown) {
|
|
30
|
+
// write to audit log
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- Emitter ---
|
|
35
|
+
|
|
36
|
+
@Injectable()
|
|
37
|
+
class TradingEngine {
|
|
38
|
+
constructor(private readonly events: EventService) {}
|
|
39
|
+
|
|
40
|
+
async executeTrade(symbol: string, price: number) {
|
|
41
|
+
// ... execute trade logic
|
|
42
|
+
await this.events.emit("trade.opened", { symbol, price });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Module ---
|
|
47
|
+
|
|
48
|
+
@Module({
|
|
49
|
+
imports: [EventModule.register({ listeners: [TradeNotifier] })],
|
|
50
|
+
providers: [TradingEngine],
|
|
51
|
+
})
|
|
52
|
+
class TradingModule {}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## API
|
|
56
|
+
|
|
57
|
+
### `@OnEvent(pattern)`
|
|
58
|
+
|
|
59
|
+
Method decorator. Registers the method as a handler for events matching the pattern.
|
|
60
|
+
|
|
61
|
+
- `"trade.opened"` — exact match
|
|
62
|
+
- `"trade.*"` — wildcard, matches any single segment
|
|
63
|
+
|
|
64
|
+
### `EventModule.register({ listeners })`
|
|
65
|
+
|
|
66
|
+
Dynamic module. Pass providers that contain `@OnEvent()` methods.
|
|
67
|
+
|
|
68
|
+
### `EventService`
|
|
69
|
+
|
|
70
|
+
Injectable service:
|
|
71
|
+
|
|
72
|
+
- `emit(event, payload?)` — emit an event. All matching handlers run concurrently. Errors in individual handlers are caught and logged without blocking others.
|
|
73
|
+
- `getHandlers()` — returns registered handlers (for introspection/testing)
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
export declare const ON_EVENT_KEY: unique symbol;
|
|
3
|
+
export interface EventHandlerMeta {
|
|
4
|
+
pattern: string;
|
|
5
|
+
methodName: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Mark a method to handle events matching the given pattern.
|
|
9
|
+
*
|
|
10
|
+
* Supports exact match (`"trade.opened"`) and wildcard (`"trade.*"`).
|
|
11
|
+
*
|
|
12
|
+
* @param pattern - Event name or wildcard pattern
|
|
13
|
+
*/
|
|
14
|
+
export declare function OnEvent(pattern: string): MethodDecorator;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
export const ON_EVENT_KEY = Symbol("events:handlers");
|
|
3
|
+
/**
|
|
4
|
+
* Mark a method to handle events matching the given pattern.
|
|
5
|
+
*
|
|
6
|
+
* Supports exact match (`"trade.opened"`) and wildcard (`"trade.*"`).
|
|
7
|
+
*
|
|
8
|
+
* @param pattern - Event name or wildcard pattern
|
|
9
|
+
*/
|
|
10
|
+
export function OnEvent(pattern) {
|
|
11
|
+
return (target, propertyKey) => {
|
|
12
|
+
const existing = Reflect.getMetadata(ON_EVENT_KEY, target.constructor) ?? [];
|
|
13
|
+
existing.push({
|
|
14
|
+
pattern,
|
|
15
|
+
methodName: propertyKey,
|
|
16
|
+
});
|
|
17
|
+
Reflect.defineMetadata(ON_EVENT_KEY, existing, target.constructor);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import type { Constructor } from "@smounters/imperium/core";
|
|
3
|
+
import type { DependencyContainer } from "tsyringe";
|
|
4
|
+
import { EventService } from "./event.service.js";
|
|
5
|
+
export interface EventModuleOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Providers that contain @OnEvent() decorated methods.
|
|
8
|
+
* Automatically registered as DI providers and scanned for event handlers.
|
|
9
|
+
*/
|
|
10
|
+
listeners: Constructor[];
|
|
11
|
+
}
|
|
12
|
+
export declare class EventModule {
|
|
13
|
+
static register(options: EventModuleOptions): {
|
|
14
|
+
module: typeof EventModule;
|
|
15
|
+
providers: (Constructor | {
|
|
16
|
+
provide: symbol;
|
|
17
|
+
useValue: Constructor[];
|
|
18
|
+
useFactory?: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
provide: string;
|
|
21
|
+
useFactory: (container: DependencyContainer) => DependencyContainer;
|
|
22
|
+
useValue?: undefined;
|
|
23
|
+
})[];
|
|
24
|
+
exports: (typeof EventService)[];
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
var EventModule_1;
|
|
14
|
+
import "reflect-metadata";
|
|
15
|
+
import { Module, Injectable, Inject } from "@smounters/imperium/decorators";
|
|
16
|
+
import { EventService } from "./event.service.js";
|
|
17
|
+
const EVENT_LISTENERS_TOKEN = Symbol("events:listeners");
|
|
18
|
+
let EventBootstrap = class EventBootstrap {
|
|
19
|
+
constructor(eventService, listeners, container) {
|
|
20
|
+
this.eventService = eventService;
|
|
21
|
+
this.listeners = listeners;
|
|
22
|
+
this.container = container;
|
|
23
|
+
}
|
|
24
|
+
onModuleInit() {
|
|
25
|
+
for (const listener of this.listeners) {
|
|
26
|
+
const instance = this.container.resolve(listener);
|
|
27
|
+
this.eventService.registerListener(instance, listener.name);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
EventBootstrap = __decorate([
|
|
32
|
+
Injectable(),
|
|
33
|
+
__param(1, Inject(EVENT_LISTENERS_TOKEN)),
|
|
34
|
+
__param(2, Inject("events:container")),
|
|
35
|
+
__metadata("design:paramtypes", [EventService, Array, Object])
|
|
36
|
+
], EventBootstrap);
|
|
37
|
+
let EventModule = EventModule_1 = class EventModule {
|
|
38
|
+
static register(options) {
|
|
39
|
+
const eventListeners = options.listeners.filter((l) => EventService.hasEventHandlers(l));
|
|
40
|
+
return {
|
|
41
|
+
module: EventModule_1,
|
|
42
|
+
providers: [
|
|
43
|
+
EventService,
|
|
44
|
+
EventBootstrap,
|
|
45
|
+
...eventListeners,
|
|
46
|
+
{ provide: EVENT_LISTENERS_TOKEN, useValue: eventListeners },
|
|
47
|
+
{
|
|
48
|
+
provide: "events:container",
|
|
49
|
+
useFactory: (container) => container,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
exports: [EventService],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
EventModule = EventModule_1 = __decorate([
|
|
57
|
+
Module({})
|
|
58
|
+
], EventModule);
|
|
59
|
+
export { EventModule };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
export declare class EventService {
|
|
3
|
+
private readonly handlers;
|
|
4
|
+
private logger;
|
|
5
|
+
setLogger(logger: {
|
|
6
|
+
error: (...args: unknown[]) => void;
|
|
7
|
+
}): void;
|
|
8
|
+
/**
|
|
9
|
+
* Register an event listener instance. Scans @OnEvent() metadata
|
|
10
|
+
* and binds methods as handlers.
|
|
11
|
+
*/
|
|
12
|
+
registerListener(instance: object, listenerName: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Check if a class has any @OnEvent() decorated methods.
|
|
15
|
+
*/
|
|
16
|
+
static hasEventHandlers(target: Function): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Emit an event. All matching handlers are called concurrently.
|
|
19
|
+
* Errors in individual handlers are caught and logged — they don't
|
|
20
|
+
* block other handlers or the caller.
|
|
21
|
+
*/
|
|
22
|
+
emit(event: string, payload?: unknown): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Get all registered handlers (for introspection/testing).
|
|
25
|
+
*/
|
|
26
|
+
getHandlers(): readonly {
|
|
27
|
+
pattern: string;
|
|
28
|
+
listenerName: string;
|
|
29
|
+
methodName: string;
|
|
30
|
+
}[];
|
|
31
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import "reflect-metadata";
|
|
8
|
+
import { injectable } from "tsyringe";
|
|
9
|
+
import { ON_EVENT_KEY } from "./event.decorators.js";
|
|
10
|
+
function patternToRegex(pattern) {
|
|
11
|
+
const escaped = pattern
|
|
12
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
13
|
+
.replace(/\*/g, "[^.]+");
|
|
14
|
+
return new RegExp(`^${escaped}$`);
|
|
15
|
+
}
|
|
16
|
+
let EventService = class EventService {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.handlers = [];
|
|
19
|
+
this.logger = console;
|
|
20
|
+
}
|
|
21
|
+
setLogger(logger) {
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Register an event listener instance. Scans @OnEvent() metadata
|
|
26
|
+
* and binds methods as handlers.
|
|
27
|
+
*/
|
|
28
|
+
registerListener(instance, listenerName) {
|
|
29
|
+
const constructor = instance.constructor;
|
|
30
|
+
const metas = Reflect.getMetadata(ON_EVENT_KEY, constructor) ?? [];
|
|
31
|
+
for (const meta of metas) {
|
|
32
|
+
const method = instance[meta.methodName];
|
|
33
|
+
if (typeof method !== "function") {
|
|
34
|
+
throw new Error(`@OnEvent() target ${listenerName}.${meta.methodName} is not a function`);
|
|
35
|
+
}
|
|
36
|
+
this.handlers.push({
|
|
37
|
+
pattern: meta.pattern,
|
|
38
|
+
regex: patternToRegex(meta.pattern),
|
|
39
|
+
callback: method.bind(instance),
|
|
40
|
+
listenerName,
|
|
41
|
+
methodName: meta.methodName,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if a class has any @OnEvent() decorated methods.
|
|
47
|
+
*/
|
|
48
|
+
static hasEventHandlers(target) {
|
|
49
|
+
const metas = Reflect.getMetadata(ON_EVENT_KEY, target);
|
|
50
|
+
return (metas?.length ?? 0) > 0;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Emit an event. All matching handlers are called concurrently.
|
|
54
|
+
* Errors in individual handlers are caught and logged — they don't
|
|
55
|
+
* block other handlers or the caller.
|
|
56
|
+
*/
|
|
57
|
+
async emit(event, payload) {
|
|
58
|
+
const matching = this.handlers.filter((h) => h.regex.test(event));
|
|
59
|
+
if (matching.length === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const promises = matching.map(async (h) => h.callback(payload));
|
|
63
|
+
const results = await Promise.allSettled(promises);
|
|
64
|
+
for (let i = 0; i < results.length; i++) {
|
|
65
|
+
const result = results[i];
|
|
66
|
+
const handler = matching[i];
|
|
67
|
+
if (result?.status === "rejected" && handler) {
|
|
68
|
+
this.logger.error(`[imperium-events] Handler ${handler.listenerName}.${handler.methodName} ` +
|
|
69
|
+
`failed for event "${event}":`, result.reason);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get all registered handlers (for introspection/testing).
|
|
75
|
+
*/
|
|
76
|
+
getHandlers() {
|
|
77
|
+
return this.handlers.map((h) => ({
|
|
78
|
+
pattern: h.pattern,
|
|
79
|
+
listenerName: h.listenerName,
|
|
80
|
+
methodName: h.methodName,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
EventService = __decorate([
|
|
85
|
+
injectable()
|
|
86
|
+
], EventService);
|
|
87
|
+
export { EventService };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { OnEvent } from "./event.decorators.js";
|
|
2
|
+
export type { EventHandlerMeta } from "./event.decorators.js";
|
|
3
|
+
export { EventService } from "./event.service.js";
|
|
4
|
+
export { EventModule } from "./event.module.js";
|
|
5
|
+
export type { EventModuleOptions } from "./event.module.js";
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@smounters/imperium-events",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed event emitter for @smounters/imperium",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"events",
|
|
7
|
+
"event-emitter",
|
|
8
|
+
"imperium",
|
|
9
|
+
"nestjs-like",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"CHANGELOG.md"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/smounters/imperium.git",
|
|
32
|
+
"directory": "packages/imperium-events"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20.0.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@smounters/imperium": "^1.1.0",
|
|
39
|
+
"reflect-metadata": "^0.2.2",
|
|
40
|
+
"tsyringe": "^4.10.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@smounters/imperium": "1.1.2"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.json",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
49
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"clean": "rm -rf dist"
|
|
52
|
+
}
|
|
53
|
+
}
|