@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.
- package/README.md +264 -17
- package/dist/cli/{index.js → bin.js} +413 -332
- package/dist/container/index.js +273 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/graphql/index.js +2156 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +9694 -5047
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3411 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +795 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/llms.txt +231 -0
- package/package.json +125 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/ARCHITECTURE.md +3 -3
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +294 -232
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +37 -18
- package/src/cli/templates/database/mysql.ts +3 -3
- package/src/cli/templates/database/none.ts +2 -2
- package/src/cli/templates/database/postgresql.ts +3 -3
- package/src/cli/templates/database/sqlite.ts +3 -3
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +33 -15
- package/src/cli/templates/frontend/none.ts +2 -2
- package/src/cli/templates/frontend/react.ts +18 -18
- package/src/cli/templates/frontend/solid.ts +15 -15
- package/src/cli/templates/frontend/svelte.ts +17 -17
- package/src/cli/templates/frontend/vue.ts +15 -15
- package/src/cli/templates/generators/index.ts +29 -29
- package/src/cli/templates/generators/types.ts +21 -21
- package/src/cli/templates/index.ts +6 -6
- package/src/cli/templates/project/api.ts +37 -36
- package/src/cli/templates/project/default.ts +25 -25
- package/src/cli/templates/project/fullstack.ts +28 -26
- package/src/cli/templates/project/index.ts +55 -16
- package/src/cli/templates/project/minimal.ts +17 -12
- package/src/cli/templates/project/types.ts +10 -5
- package/src/cli/templates/project/website.ts +15 -15
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -3
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +14 -8
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +566 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/graphql/built-in-engine.ts +598 -0
- package/src/graphql/context-builder.ts +110 -0
- package/src/graphql/decorators.ts +358 -0
- package/src/graphql/execution-pipeline.ts +227 -0
- package/src/graphql/graphql-module.ts +563 -0
- package/src/graphql/index.ts +101 -0
- package/src/graphql/metadata.ts +237 -0
- package/src/graphql/schema-builder.ts +319 -0
- package/src/graphql/subscription-handler.ts +283 -0
- package/src/graphql/types.ts +324 -0
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +182 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +457 -299
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/cli.test.ts +19 -19
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/cli.test.ts +1 -1
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/graphql.test.ts +991 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- 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
|
+
}
|