@buenojs/bueno 0.8.4 → 0.8.6

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.
Files changed (234) hide show
  1. package/README.md +264 -17
  2. package/dist/cli/{index.js → bin.js} +413 -332
  3. package/dist/container/index.js +273 -0
  4. package/dist/context/index.js +219 -0
  5. package/dist/database/index.js +493 -0
  6. package/dist/frontend/index.js +7697 -0
  7. package/dist/graphql/index.js +2156 -0
  8. package/dist/health/index.js +364 -0
  9. package/dist/i18n/index.js +345 -0
  10. package/dist/index.js +9694 -5047
  11. package/dist/jobs/index.js +819 -0
  12. package/dist/lock/index.js +367 -0
  13. package/dist/logger/index.js +281 -0
  14. package/dist/metrics/index.js +289 -0
  15. package/dist/middleware/index.js +77 -0
  16. package/dist/migrations/index.js +571 -0
  17. package/dist/modules/index.js +3411 -0
  18. package/dist/notification/index.js +484 -0
  19. package/dist/observability/index.js +331 -0
  20. package/dist/openapi/index.js +795 -0
  21. package/dist/orm/index.js +1356 -0
  22. package/dist/router/index.js +886 -0
  23. package/dist/rpc/index.js +691 -0
  24. package/dist/schema/index.js +400 -0
  25. package/dist/telemetry/index.js +595 -0
  26. package/dist/template/index.js +640 -0
  27. package/dist/templates/index.js +640 -0
  28. package/dist/testing/index.js +1111 -0
  29. package/dist/types/index.js +60 -0
  30. package/llms.txt +231 -0
  31. package/package.json +125 -27
  32. package/src/cache/index.ts +2 -1
  33. package/src/cli/ARCHITECTURE.md +3 -3
  34. package/src/cli/bin.ts +2 -2
  35. package/src/cli/commands/build.ts +183 -165
  36. package/src/cli/commands/dev.ts +96 -89
  37. package/src/cli/commands/generate.ts +142 -111
  38. package/src/cli/commands/help.ts +20 -16
  39. package/src/cli/commands/index.ts +3 -6
  40. package/src/cli/commands/migration.ts +124 -105
  41. package/src/cli/commands/new.ts +294 -232
  42. package/src/cli/commands/start.ts +81 -79
  43. package/src/cli/core/args.ts +68 -50
  44. package/src/cli/core/console.ts +89 -95
  45. package/src/cli/core/index.ts +4 -4
  46. package/src/cli/core/prompt.ts +65 -62
  47. package/src/cli/core/spinner.ts +23 -20
  48. package/src/cli/index.ts +46 -38
  49. package/src/cli/templates/database/index.ts +37 -18
  50. package/src/cli/templates/database/mysql.ts +3 -3
  51. package/src/cli/templates/database/none.ts +2 -2
  52. package/src/cli/templates/database/postgresql.ts +3 -3
  53. package/src/cli/templates/database/sqlite.ts +3 -3
  54. package/src/cli/templates/deploy.ts +29 -26
  55. package/src/cli/templates/docker.ts +41 -30
  56. package/src/cli/templates/frontend/index.ts +33 -15
  57. package/src/cli/templates/frontend/none.ts +2 -2
  58. package/src/cli/templates/frontend/react.ts +18 -18
  59. package/src/cli/templates/frontend/solid.ts +15 -15
  60. package/src/cli/templates/frontend/svelte.ts +17 -17
  61. package/src/cli/templates/frontend/vue.ts +15 -15
  62. package/src/cli/templates/generators/index.ts +29 -29
  63. package/src/cli/templates/generators/types.ts +21 -21
  64. package/src/cli/templates/index.ts +6 -6
  65. package/src/cli/templates/project/api.ts +37 -36
  66. package/src/cli/templates/project/default.ts +25 -25
  67. package/src/cli/templates/project/fullstack.ts +28 -26
  68. package/src/cli/templates/project/index.ts +55 -16
  69. package/src/cli/templates/project/minimal.ts +17 -12
  70. package/src/cli/templates/project/types.ts +10 -5
  71. package/src/cli/templates/project/website.ts +15 -15
  72. package/src/cli/utils/fs.ts +55 -41
  73. package/src/cli/utils/index.ts +3 -3
  74. package/src/cli/utils/strings.ts +47 -33
  75. package/src/cli/utils/version.ts +14 -8
  76. package/src/config/env-validation.ts +100 -0
  77. package/src/config/env.ts +169 -41
  78. package/src/config/index.ts +28 -20
  79. package/src/config/loader.ts +25 -16
  80. package/src/config/merge.ts +21 -10
  81. package/src/config/types.ts +566 -25
  82. package/src/config/validation.ts +215 -7
  83. package/src/container/forward-ref.ts +22 -22
  84. package/src/container/index.ts +34 -12
  85. package/src/context/index.ts +11 -1
  86. package/src/database/index.ts +7 -190
  87. package/src/database/orm/builder.ts +457 -0
  88. package/src/database/orm/casts/index.ts +130 -0
  89. package/src/database/orm/casts/types.ts +25 -0
  90. package/src/database/orm/compiler.ts +304 -0
  91. package/src/database/orm/hooks/index.ts +114 -0
  92. package/src/database/orm/index.ts +61 -0
  93. package/src/database/orm/model-registry.ts +59 -0
  94. package/src/database/orm/model.ts +821 -0
  95. package/src/database/orm/relationships/base.ts +146 -0
  96. package/src/database/orm/relationships/belongs-to-many.ts +179 -0
  97. package/src/database/orm/relationships/belongs-to.ts +56 -0
  98. package/src/database/orm/relationships/has-many.ts +45 -0
  99. package/src/database/orm/relationships/has-one.ts +41 -0
  100. package/src/database/orm/relationships/index.ts +11 -0
  101. package/src/database/orm/scopes/index.ts +55 -0
  102. package/src/events/__tests__/event-system.test.ts +235 -0
  103. package/src/events/config.ts +238 -0
  104. package/src/events/example-usage.ts +185 -0
  105. package/src/events/index.ts +278 -0
  106. package/src/events/manager.ts +385 -0
  107. package/src/events/registry.ts +182 -0
  108. package/src/events/types.ts +124 -0
  109. package/src/frontend/api-routes.ts +65 -23
  110. package/src/frontend/bundler.ts +76 -34
  111. package/src/frontend/console-client.ts +2 -2
  112. package/src/frontend/console-stream.ts +94 -38
  113. package/src/frontend/dev-server.ts +94 -46
  114. package/src/frontend/file-router.ts +61 -19
  115. package/src/frontend/frameworks/index.ts +37 -10
  116. package/src/frontend/frameworks/react.ts +10 -8
  117. package/src/frontend/frameworks/solid.ts +11 -9
  118. package/src/frontend/frameworks/svelte.ts +15 -9
  119. package/src/frontend/frameworks/vue.ts +13 -11
  120. package/src/frontend/hmr-client.ts +12 -10
  121. package/src/frontend/hmr.ts +146 -103
  122. package/src/frontend/index.ts +14 -5
  123. package/src/frontend/islands.ts +41 -22
  124. package/src/frontend/isr.ts +59 -37
  125. package/src/frontend/layout.ts +36 -21
  126. package/src/frontend/ssr/react.ts +74 -27
  127. package/src/frontend/ssr/solid.ts +54 -20
  128. package/src/frontend/ssr/svelte.ts +48 -14
  129. package/src/frontend/ssr/vue.ts +50 -18
  130. package/src/frontend/ssr.ts +83 -39
  131. package/src/frontend/types.ts +91 -56
  132. package/src/graphql/built-in-engine.ts +598 -0
  133. package/src/graphql/context-builder.ts +110 -0
  134. package/src/graphql/decorators.ts +358 -0
  135. package/src/graphql/execution-pipeline.ts +227 -0
  136. package/src/graphql/graphql-module.ts +563 -0
  137. package/src/graphql/index.ts +101 -0
  138. package/src/graphql/metadata.ts +237 -0
  139. package/src/graphql/schema-builder.ts +319 -0
  140. package/src/graphql/subscription-handler.ts +283 -0
  141. package/src/graphql/types.ts +324 -0
  142. package/src/health/index.ts +21 -9
  143. package/src/i18n/engine.ts +305 -0
  144. package/src/i18n/index.ts +38 -0
  145. package/src/i18n/loader.ts +218 -0
  146. package/src/i18n/middleware.ts +164 -0
  147. package/src/i18n/negotiator.ts +162 -0
  148. package/src/i18n/types.ts +158 -0
  149. package/src/index.ts +182 -27
  150. package/src/jobs/drivers/memory.ts +315 -0
  151. package/src/jobs/drivers/redis.ts +459 -0
  152. package/src/jobs/index.ts +30 -0
  153. package/src/jobs/queue.ts +281 -0
  154. package/src/jobs/types.ts +295 -0
  155. package/src/jobs/worker.ts +380 -0
  156. package/src/logger/index.ts +1 -3
  157. package/src/logger/transports/index.ts +62 -22
  158. package/src/metrics/index.ts +25 -16
  159. package/src/migrations/index.ts +9 -0
  160. package/src/modules/filters.ts +13 -17
  161. package/src/modules/guards.ts +49 -26
  162. package/src/modules/index.ts +457 -299
  163. package/src/modules/interceptors.ts +58 -20
  164. package/src/modules/lazy.ts +11 -19
  165. package/src/modules/lifecycle.ts +15 -7
  166. package/src/modules/metadata.ts +15 -5
  167. package/src/modules/pipes.ts +94 -72
  168. package/src/notification/channels/base.ts +68 -0
  169. package/src/notification/channels/email.ts +105 -0
  170. package/src/notification/channels/push.ts +104 -0
  171. package/src/notification/channels/sms.ts +105 -0
  172. package/src/notification/channels/whatsapp.ts +104 -0
  173. package/src/notification/index.ts +48 -0
  174. package/src/notification/service.ts +354 -0
  175. package/src/notification/types.ts +344 -0
  176. package/src/observability/__tests__/observability.test.ts +483 -0
  177. package/src/observability/breadcrumbs.ts +114 -0
  178. package/src/observability/index.ts +136 -0
  179. package/src/observability/interceptor.ts +85 -0
  180. package/src/observability/service.ts +303 -0
  181. package/src/observability/trace.ts +37 -0
  182. package/src/observability/types.ts +196 -0
  183. package/src/openapi/__tests__/decorators.test.ts +335 -0
  184. package/src/openapi/__tests__/document-builder.test.ts +285 -0
  185. package/src/openapi/__tests__/route-scanner.test.ts +334 -0
  186. package/src/openapi/__tests__/schema-generator.test.ts +275 -0
  187. package/src/openapi/decorators.ts +328 -0
  188. package/src/openapi/document-builder.ts +274 -0
  189. package/src/openapi/index.ts +112 -0
  190. package/src/openapi/metadata.ts +112 -0
  191. package/src/openapi/route-scanner.ts +289 -0
  192. package/src/openapi/schema-generator.ts +256 -0
  193. package/src/openapi/swagger-module.ts +166 -0
  194. package/src/openapi/types.ts +398 -0
  195. package/src/orm/index.ts +10 -0
  196. package/src/rpc/index.ts +3 -1
  197. package/src/schema/index.ts +9 -0
  198. package/src/security/index.ts +15 -6
  199. package/src/ssg/index.ts +9 -8
  200. package/src/telemetry/index.ts +76 -22
  201. package/src/template/index.ts +7 -0
  202. package/src/templates/engine.ts +224 -0
  203. package/src/templates/index.ts +9 -0
  204. package/src/templates/loader.ts +331 -0
  205. package/src/templates/renderers/markdown.ts +212 -0
  206. package/src/templates/renderers/simple.ts +269 -0
  207. package/src/templates/types.ts +154 -0
  208. package/src/testing/index.ts +100 -27
  209. package/src/types/optional-deps.d.ts +347 -187
  210. package/src/validation/index.ts +92 -2
  211. package/src/validation/schemas.ts +536 -0
  212. package/tests/integration/cli.test.ts +19 -19
  213. package/tests/integration/fullstack.test.ts +4 -4
  214. package/tests/unit/cli.test.ts +1 -1
  215. package/tests/unit/database.test.ts +2 -72
  216. package/tests/unit/env-validation.test.ts +166 -0
  217. package/tests/unit/events.test.ts +910 -0
  218. package/tests/unit/graphql.test.ts +991 -0
  219. package/tests/unit/i18n.test.ts +455 -0
  220. package/tests/unit/jobs.test.ts +493 -0
  221. package/tests/unit/notification.test.ts +988 -0
  222. package/tests/unit/observability.test.ts +453 -0
  223. package/tests/unit/orm/builder.test.ts +323 -0
  224. package/tests/unit/orm/casts.test.ts +179 -0
  225. package/tests/unit/orm/compiler.test.ts +220 -0
  226. package/tests/unit/orm/eager-loading.test.ts +285 -0
  227. package/tests/unit/orm/hooks.test.ts +191 -0
  228. package/tests/unit/orm/model.test.ts +373 -0
  229. package/tests/unit/orm/relationships.test.ts +303 -0
  230. package/tests/unit/orm/scopes.test.ts +74 -0
  231. package/tests/unit/templates-simple.test.ts +53 -0
  232. package/tests/unit/templates.test.ts +454 -0
  233. package/tests/unit/validation.test.ts +18 -24
  234. package/tsconfig.json +11 -3
@@ -0,0 +1,278 @@
1
+ export * from "./types";
2
+ export * from "./manager";
3
+ export * from "./registry";
4
+
5
+ // Event creation utilities
6
+ export function createEvent<T = Record<string, any>>(
7
+ name: string,
8
+ data: T,
9
+ options: EventOptions = {},
10
+ ): Event {
11
+ return {
12
+ id: options.id || generateEventId(),
13
+ name,
14
+ timestamp: options.timestamp || new Date(),
15
+ data,
16
+ context: options.context,
17
+ };
18
+ }
19
+
20
+ export function createEventContext(
21
+ context: Partial<EventContext> = {},
22
+ ): EventContext {
23
+ return {
24
+ userId: context.userId,
25
+ sessionId: context.sessionId,
26
+ requestId: context.requestId,
27
+ ipAddress: context.ipAddress,
28
+ userAgent: context.userAgent,
29
+ };
30
+ }
31
+
32
+ // Event listener utilities
33
+ export function createEventListener(
34
+ name: string,
35
+ handler: EventHandler,
36
+ options: EventListenerOptions = {},
37
+ ): EventListener {
38
+ return {
39
+ id: generateListenerId(),
40
+ name,
41
+ handler,
42
+ priority: options.priority || 0,
43
+ once: options.once || false,
44
+ filter: options.filter,
45
+ };
46
+ }
47
+
48
+ // Event filter utilities
49
+ export function createEventFilter(
50
+ predicate: (event: Event) => boolean,
51
+ ): EventFilter {
52
+ return predicate;
53
+ }
54
+
55
+ // Event middleware utilities
56
+ export function createEventMiddleware(
57
+ handler: (event: Event, next: () => Promise<void>) => Promise<void>,
58
+ ): EventMiddleware {
59
+ return handler;
60
+ }
61
+
62
+ // Event category utilities
63
+ export function createEventCategory(
64
+ name: string,
65
+ description: string,
66
+ events: string[] = [],
67
+ ): EventCategory {
68
+ return { name, description, events };
69
+ }
70
+
71
+ // Helper functions
72
+ function generateEventId(): string {
73
+ return `eid_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
74
+ }
75
+
76
+ function generateListenerId(): string {
77
+ return `lid_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
78
+ }
79
+
80
+ // Event validation utilities
81
+ export function validateEvent(event: Event): boolean {
82
+ if (!event || typeof event !== "object") return false;
83
+ if (typeof event.id !== "string" || !event.id) return false;
84
+ if (
85
+ typeof event.name !== "string" ||
86
+ !event.name ||
87
+ event.name.trim() === ""
88
+ ) {
89
+ throw new Error("Event name must be a non-empty string");
90
+ }
91
+ if (!(event.timestamp instanceof Date)) return false;
92
+ if (typeof event.data !== "object") return false;
93
+ return true;
94
+ }
95
+
96
+ export function validateEventListener(listener: EventListener): boolean {
97
+ if (!listener || typeof listener !== "object") return false;
98
+ if (typeof listener.id !== "string" || !listener.id) return false;
99
+ if (typeof listener.name !== "string" || !listener.name) return false;
100
+ if (typeof listener.handler !== "function") return false;
101
+ if (listener.priority !== undefined && typeof listener.priority !== "number")
102
+ return false;
103
+ if (listener.once !== undefined && typeof listener.once !== "boolean")
104
+ return false;
105
+ if (listener.filter !== undefined && typeof listener.filter !== "function")
106
+ return false;
107
+ return true;
108
+ }
109
+
110
+ // Event serialization utilities
111
+ export function serializeEvent(event: Event): string {
112
+ return JSON.stringify({
113
+ ...event,
114
+ timestamp: event.timestamp.toISOString(),
115
+ });
116
+ }
117
+
118
+ export function deserializeEvent(serializedEvent: string): Event {
119
+ const eventData = JSON.parse(serializedEvent);
120
+ return {
121
+ ...eventData,
122
+ timestamp: new Date(eventData.timestamp),
123
+ };
124
+ }
125
+
126
+ // Event timing utilities
127
+ export function measureEventProcessingTime(
128
+ event: Event,
129
+ callback: () => Promise<void>,
130
+ ): Promise<number> {
131
+ const startTime = Date.now();
132
+ return callback().then(() => Date.now() - startTime);
133
+ }
134
+
135
+ // Event batching utilities
136
+ export function batchEvents(events: Event[], batchSize: number): Event[][] {
137
+ const batches: Event[][] = [];
138
+ for (let i = 0; i < events.length; i += batchSize) {
139
+ batches.push(events.slice(i, i + batchSize));
140
+ }
141
+ return batches;
142
+ }
143
+
144
+ // Event throttling utilities
145
+ export function createEventThrottler(
146
+ limit: number,
147
+ windowMs: number,
148
+ ): (event: Event) => boolean {
149
+ const events: Event[] = [];
150
+
151
+ return (event: Event) => {
152
+ const now = Date.now();
153
+ const windowStart = now - windowMs;
154
+
155
+ // Remove old events
156
+ while (events.length > 0 && events[0].timestamp.getTime() < windowStart) {
157
+ events.shift();
158
+ }
159
+
160
+ if (events.length < limit) {
161
+ events.push(event);
162
+ return true;
163
+ }
164
+
165
+ return false;
166
+ };
167
+ }
168
+
169
+ // Event debouncing utilities
170
+ export function createEventDebouncer(
171
+ waitMs: number,
172
+ emitFn: (event: Event) => Promise<void>,
173
+ ): (event: Event) => Promise<void> {
174
+ let timeout: NodeJS.Timeout;
175
+ let lastEvent: Event | null = null;
176
+
177
+ return (event: Event) => {
178
+ lastEvent = event;
179
+
180
+ return new Promise((resolve) => {
181
+ clearTimeout(timeout);
182
+ timeout = setTimeout(async () => {
183
+ if (lastEvent) {
184
+ await emitFn(lastEvent);
185
+ }
186
+ resolve();
187
+ }, waitMs);
188
+ });
189
+ };
190
+ }
191
+
192
+ // Event priority utilities
193
+ export function sortListenersByPriority(
194
+ listeners: EventListener[],
195
+ ): EventListener[] {
196
+ return listeners.sort((a, b) => (b.priority || 0) - (a.priority || 0));
197
+ }
198
+
199
+ // Event context utilities
200
+ export function mergeEventContexts(
201
+ baseContext: EventContext,
202
+ additionalContext: Partial<EventContext>,
203
+ ): EventContext {
204
+ return {
205
+ ...baseContext,
206
+ ...additionalContext,
207
+ };
208
+ }
209
+
210
+ // Event error handling utilities
211
+ export function createEventError(
212
+ event: Event,
213
+ message: string,
214
+ originalError?: Error,
215
+ ): EventError {
216
+ const error = new Error(message) as EventError;
217
+ error.event = event;
218
+ error.originalError = originalError;
219
+ return error;
220
+ }
221
+
222
+ // Event statistics utilities
223
+ export function calculateEventThroughput(
224
+ events: Event[],
225
+ timeWindowMs: number,
226
+ ): number {
227
+ const now = Date.now();
228
+ const windowStart = now - timeWindowMs;
229
+ const eventsInWindow = events.filter(
230
+ (event) => event.timestamp.getTime() >= windowStart,
231
+ );
232
+ return eventsInWindow.length / (timeWindowMs / 1000);
233
+ }
234
+
235
+ // Event filtering utilities
236
+ export function createCategoryFilter(category: string): EventFilter {
237
+ return (event: Event) => event.context?.category === category;
238
+ }
239
+
240
+ export function createNameFilter(name: string | RegExp): EventFilter {
241
+ return (event: Event) => {
242
+ if (typeof name === "string") {
243
+ return event.name === name;
244
+ }
245
+ return name.test(event.name);
246
+ };
247
+ }
248
+
249
+ // Event transformation utilities
250
+ export function transformEventData<T, U>(
251
+ event: Event,
252
+ transformer: (data: T) => U,
253
+ ): Event {
254
+ return {
255
+ ...event,
256
+ data: transformer(event.data as T),
257
+ };
258
+ }
259
+
260
+ // Event cloning utilities
261
+ export function cloneEvent(event: Event): Event {
262
+ return {
263
+ ...event,
264
+ timestamp: new Date(event.timestamp.getTime()),
265
+ };
266
+ }
267
+
268
+ // Event comparison utilities
269
+ export function areEventsEqual(event1: Event, event2: Event): boolean {
270
+ if (!event1 || !event2) return false;
271
+ return (
272
+ event1.id === event2.id &&
273
+ event1.name === event2.name &&
274
+ event1.timestamp.getTime() === event2.timestamp.getTime() &&
275
+ JSON.stringify(event1.data) === JSON.stringify(event2.data) &&
276
+ JSON.stringify(event1.context) === JSON.stringify(event2.context)
277
+ );
278
+ }
@@ -0,0 +1,385 @@
1
+ import {
2
+ type Event,
3
+ type EventError,
4
+ type EventFilter,
5
+ type EventHandler,
6
+ type EventListener,
7
+ type EventListenerOptions,
8
+ type EventManager,
9
+ EventManagerConfig,
10
+ type EventManagerOptions,
11
+ type EventManagerState,
12
+ type EventMiddleware,
13
+ EventOptions,
14
+ EventStats,
15
+ } from "./types";
16
+
17
+ export class EventManagerImpl implements EventManager {
18
+ private state: EventManagerState;
19
+
20
+ constructor(options: EventManagerOptions = {}) {
21
+ this.state = {
22
+ listeners: new Map(),
23
+ filters: [],
24
+ middleware: [],
25
+ categories: [],
26
+ stats: {
27
+ totalEvents: 0,
28
+ eventsPerSecond: 0,
29
+ listenersCount: 0,
30
+ errorsCount: 0,
31
+ averageProcessingTime: 0,
32
+ },
33
+ config: {
34
+ maxListeners: options.maxListeners || 100,
35
+ eventCategories: options.eventCategories || [],
36
+ middleware: options.middleware || [],
37
+ },
38
+ };
39
+ }
40
+
41
+ async emit(event: Event): Promise<void> {
42
+ // Validate event input - throw for null/undefined
43
+ if (!event || typeof event !== "object") {
44
+ throw new Error("Event must be a valid object");
45
+ }
46
+
47
+ if (
48
+ !event.name ||
49
+ typeof event.name !== "string" ||
50
+ event.name.trim() === ""
51
+ ) {
52
+ this.handleError(
53
+ event,
54
+ new Error("Event name must be a non-empty string"),
55
+ );
56
+ return;
57
+ }
58
+
59
+ if (!event.timestamp || !(event.timestamp instanceof Date)) {
60
+ this.handleError(
61
+ event,
62
+ new Error("Event timestamp must be a valid Date object"),
63
+ );
64
+ return;
65
+ }
66
+
67
+ if (!event.data || typeof event.data !== "object") {
68
+ this.handleError(event, new Error("Event data must be a valid object"));
69
+ return;
70
+ }
71
+
72
+ this.state.stats.totalEvents++;
73
+ const startTime = Date.now();
74
+
75
+ try {
76
+ // Apply middleware - if it fails, stop processing
77
+ const middlewareSuccess = await this.applyMiddleware(event);
78
+ if (!middlewareSuccess) {
79
+ return; // Middleware failed, don't process handlers
80
+ }
81
+
82
+ // Filter event
83
+ if (!this.shouldProcessEvent(event)) {
84
+ return;
85
+ }
86
+
87
+ // Get listeners for this event
88
+ const listeners = this.getListeners(event.name);
89
+
90
+ // Process listeners
91
+ await Promise.all(
92
+ listeners.map((listener) => this.processListener(event, listener)),
93
+ );
94
+
95
+ // Update stats
96
+ const processingTime = Date.now() - startTime;
97
+ this.updateStats(processingTime);
98
+ } catch (error) {
99
+ this.handleError(event, error as Error);
100
+ // Swallow error - don't rethrow
101
+ }
102
+ }
103
+
104
+ private async applyMiddleware(event: Event): Promise<boolean> {
105
+ const middleware = [...this.state.middleware];
106
+ let index = 0;
107
+ let middlewareFailed = false;
108
+
109
+ const next = async (): Promise<void> => {
110
+ if (index < middleware.length && !middlewareFailed) {
111
+ const currentMiddleware = middleware[index];
112
+ index++;
113
+ try {
114
+ await currentMiddleware(event, next);
115
+ } catch (error) {
116
+ middlewareFailed = true;
117
+ this.handleError(event, error as Error);
118
+ // Stop processing middleware but don't rethrow
119
+ return;
120
+ }
121
+ }
122
+ };
123
+
124
+ await next();
125
+ return !middlewareFailed; // Return false if middleware failed
126
+ }
127
+
128
+ emitSync(event: Event): void {
129
+ this.emit(event).catch((error) => {
130
+ console.error("Error in synchronous event emission:", error);
131
+ });
132
+ }
133
+
134
+ on(
135
+ event: string,
136
+ listener: EventHandler,
137
+ options: EventListenerOptions = {},
138
+ ): () => void {
139
+ // Validate listener input
140
+ if (!listener || typeof listener !== "function") {
141
+ throw new Error("Listener must be a valid function");
142
+ }
143
+
144
+ // Validate event name
145
+ if (!event || typeof event !== "string" || event.trim() === "") {
146
+ throw new Error("Event name must be a non-empty string");
147
+ }
148
+
149
+ const listenerId = this.generateListenerId();
150
+ const eventListener: EventListener = {
151
+ id: listenerId,
152
+ name: event,
153
+ handler: listener,
154
+ priority: options.priority || 0,
155
+ once: options.once || false,
156
+ filter: options.filter,
157
+ };
158
+
159
+ this.addListener(eventListener);
160
+ return () => this.removeListener(eventListener);
161
+ }
162
+
163
+ once(event: string, listener: EventHandler): () => void {
164
+ return this.on(event, listener, { once: true });
165
+ }
166
+
167
+ off(event: string, listener: EventHandler): void {
168
+ const listeners = this.state.listeners.get(event) || [];
169
+ const index = listeners.findIndex((l) => l.handler === listener);
170
+
171
+ if (index !== -1) {
172
+ listeners.splice(index, 1);
173
+ this.state.listeners.set(event, listeners);
174
+ this.state.stats.listenersCount = this.getTotalListeners();
175
+ }
176
+ }
177
+
178
+ addListener(listener: EventListener): void {
179
+ const listeners = this.state.listeners.get(listener.name) || [];
180
+ listeners.push(listener);
181
+ this.state.listeners.set(listener.name, listeners);
182
+ this.state.stats.listenersCount = this.getTotalListeners();
183
+ }
184
+
185
+ removeListener(listener: EventListener): void {
186
+ const listeners = this.state.listeners.get(listener.name) || [];
187
+ const index = listeners.findIndex((l) => l.id === listener.id);
188
+
189
+ if (index !== -1) {
190
+ listeners.splice(index, 1);
191
+ this.state.listeners.set(listener.name, listeners);
192
+ this.state.stats.listenersCount = this.getTotalListeners();
193
+ }
194
+ }
195
+
196
+ addFilter(filter: EventFilter): void {
197
+ this.state.filters.push(filter);
198
+ }
199
+
200
+ removeFilter(filter: EventFilter): void {
201
+ const index = this.state.filters.indexOf(filter);
202
+ if (index !== -1) {
203
+ this.state.filters.splice(index, 1);
204
+ }
205
+ }
206
+
207
+ addMiddleware(middleware: EventMiddleware): void {
208
+ this.state.middleware.push(middleware);
209
+ }
210
+
211
+ removeMiddleware(middleware: EventMiddleware): void {
212
+ const index = this.state.middleware.indexOf(middleware);
213
+ if (index !== -1) {
214
+ this.state.middleware.splice(index, 1);
215
+ }
216
+ }
217
+
218
+ getListeners(event: string): EventListener[] {
219
+ return this.state.listeners.get(event) || [];
220
+ }
221
+
222
+ hasListeners(event: string): boolean {
223
+ return this.getListeners(event).length > 0;
224
+ }
225
+
226
+ clearListeners(event?: string): void {
227
+ if (event) {
228
+ this.state.listeners.delete(event);
229
+ } else {
230
+ this.state.listeners.clear();
231
+ }
232
+ this.state.stats.listenersCount = this.getTotalListeners();
233
+ }
234
+
235
+ getEventCategories(): EventCategory[] {
236
+ return [...this.state.categories];
237
+ }
238
+
239
+ registerEventCategory(category: EventCategory): void {
240
+ if (!this.state.categories.find((c) => c.name === category.name)) {
241
+ this.state.categories.push(category);
242
+ }
243
+ }
244
+
245
+ getEventsByCategory(categoryName: string): Event[] {
246
+ return this.state.listeners
247
+ .values()
248
+ .flatMap((listeners) =>
249
+ listeners.filter((listener) => listener.name.startsWith(categoryName)),
250
+ )
251
+ .map((listener) => listener.handler);
252
+ }
253
+
254
+ private shouldProcessEvent(event: Event): boolean {
255
+ try {
256
+ return this.state.filters.every((filter) => filter(event));
257
+ } catch (error) {
258
+ this.handleError(event, error as Error);
259
+ return false;
260
+ }
261
+ }
262
+
263
+ private async processListener(
264
+ event: Event,
265
+ listener: EventListener,
266
+ ): Promise<void> {
267
+ try {
268
+ // Apply listener-level filter if present
269
+ if (listener.filter) {
270
+ const shouldProcess = listener.filter(event);
271
+ if (!shouldProcess) {
272
+ return;
273
+ }
274
+ }
275
+
276
+ await listener.handler(event);
277
+ if (listener.once) {
278
+ this.removeListener(listener);
279
+ }
280
+ } catch (error) {
281
+ this.handleError(event, error as Error, listener);
282
+ }
283
+ }
284
+
285
+ private handleError(
286
+ event: Event,
287
+ error: Error,
288
+ listener?: EventListener,
289
+ ): void {
290
+ this.state.stats.errorsCount++;
291
+
292
+ const eventError: EventError = {
293
+ ...error,
294
+ event,
295
+ originalError: error,
296
+ };
297
+
298
+ // Log errors for debugging purposes (skip in test environment)
299
+ // Check for common test environment indicators
300
+ const isTestEnv =
301
+ process.env.NODE_ENV === "test" ||
302
+ typeof (globalThis as any).jest !== "undefined" ||
303
+ (typeof (globalThis as any).describe !== "undefined" &&
304
+ typeof (globalThis as any).it !== "undefined");
305
+
306
+ if (!isTestEnv) {
307
+ console.error("Event error:", eventError);
308
+ }
309
+
310
+ // If listener is provided, remove it if it's a one-time listener that failed
311
+ if (listener && listener.once) {
312
+ this.removeListener(listener);
313
+ }
314
+ }
315
+
316
+ // Event throttling utilities
317
+ createEventThrottler(
318
+ maxEvents: number,
319
+ timeWindow: number,
320
+ ): (event: Event) => boolean {
321
+ let eventCount = 0;
322
+ let lastResetTime = Date.now();
323
+
324
+ return (event: Event) => {
325
+ const currentTime = Date.now();
326
+ if (currentTime - lastResetTime > timeWindow) {
327
+ eventCount = 0;
328
+ lastResetTime = currentTime;
329
+ }
330
+
331
+ if (eventCount < maxEvents) {
332
+ eventCount++;
333
+ return true;
334
+ }
335
+
336
+ return false;
337
+ };
338
+ }
339
+
340
+ // Event debouncing utilities
341
+ createEventDebouncer(waitTime: number): (event: Event) => Promise<void> {
342
+ let timeout: NodeJS.Timeout | null = null;
343
+ let lastEvent: Event | null = null;
344
+
345
+ return (event: Event) => {
346
+ lastEvent = event;
347
+ return new Promise((resolve) => {
348
+ if (timeout) {
349
+ clearTimeout(timeout);
350
+ }
351
+ timeout = setTimeout(async () => {
352
+ if (lastEvent) {
353
+ await this.emit(lastEvent);
354
+ lastEvent = null;
355
+ timeout = null;
356
+ }
357
+ resolve();
358
+ }, waitTime);
359
+ });
360
+ };
361
+ }
362
+
363
+ private updateStats(processingTime: number): void {
364
+ const stats = this.state.stats;
365
+ stats.averageProcessingTime =
366
+ (stats.averageProcessingTime + processingTime) / 2;
367
+ }
368
+
369
+ private getTotalListeners(): number {
370
+ return Array.from(this.state.listeners.values()).reduce(
371
+ (total, listeners) => total + listeners.length,
372
+ 0,
373
+ );
374
+ }
375
+
376
+ private generateListenerId(): string {
377
+ return `lid_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
378
+ }
379
+ }
380
+
381
+ export function createEventManager(
382
+ options?: EventManagerOptions,
383
+ ): EventManager {
384
+ return new EventManagerImpl(options);
385
+ }