@lithia-js/core 1.0.0-canary.1 → 1.0.0-canary.3

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/src/logger.ts DELETED
@@ -1,66 +0,0 @@
1
- import { blue, bold, green, red, white, yellow } from "@lithia-js/utils";
2
-
3
- export type LogLevel =
4
- | "info"
5
- | "warn"
6
- | "error"
7
- | "debug"
8
- | "success"
9
- | "ready"
10
- | "event"
11
- | "wait";
12
-
13
- function formatMeta(meta: any) {
14
- if (meta === undefined) return "";
15
- try {
16
- if (typeof meta === "string") return meta;
17
- return JSON.stringify(meta);
18
- } catch {
19
- return String(meta);
20
- }
21
- }
22
-
23
- export class Logger {
24
- info(msg: any, meta?: any) {
25
- const m = formatMeta(meta);
26
- console.log(` ${msg}${m ? ` — ${m}` : ""}`);
27
- }
28
-
29
- warn(msg: any, meta?: any) {
30
- const symbol = yellow(bold("○"));
31
- const m = formatMeta(meta);
32
- console.warn(`${symbol} ${msg}${m ? ` — ${m}` : ""}`);
33
- }
34
-
35
- error(msg: any, meta?: any) {
36
- const symbol = red(bold("○"));
37
- const m = formatMeta(meta);
38
- console.error(`${symbol} ${msg}${m ? ` — ${m}` : ""}`);
39
- }
40
-
41
- success(msg: any, meta?: any) {
42
- const symbol = green(bold("▲"));
43
- const m = formatMeta(meta);
44
- console.log(`${symbol} ${msg}${m ? ` — ${m}` : ""}`);
45
- }
46
-
47
- event(msg: any, meta?: any) {
48
- const symbol = blue(bold("▲"));
49
- const m = formatMeta(meta);
50
- console.log(`${symbol} ${msg}${m ? ` — ${m}` : ""}`);
51
- }
52
-
53
- debug(msg: any, meta?: any) {
54
- const symbol = white(bold("»"));
55
- const m = formatMeta(meta);
56
- console.log(`${symbol} ${msg}${m ? ` — ${m}` : ""}`);
57
- }
58
-
59
- wait(msg: any, meta?: any) {
60
- const symbol = white(bold("○"));
61
- const m = formatMeta(meta);
62
- console.log(`${symbol} ${msg}${m ? ` — ${m}` : ""}`);
63
- }
64
- }
65
-
66
- export const logger = new Logger();
@@ -1,45 +0,0 @@
1
- import { pathToFileURL } from "node:url";
2
- import importFresh from "import-fresh";
3
-
4
- /**
5
- * Import a module dynamically, bypassing the cache in development.
6
- * This is useful for hot-reloading routes and configuration files.
7
- *
8
- * @param filePath Absolute path to the file to import
9
- * @param isDevelopment Whether to use cache-busting (dev) or native import (prod)
10
- */
11
- export async function coldImport<T = any>(
12
- filePath: string,
13
- isDevelopment: boolean,
14
- ): Promise<T> {
15
- let mod: any;
16
-
17
- if (isDevelopment) {
18
- // Use import-fresh in development for cache-free imports
19
- // Note: import-fresh uses require() under the hood
20
- mod = importFresh(filePath);
21
- } else {
22
- // Production: use normal dynamic import via file URL
23
- const importUrl = pathToFileURL(filePath).href;
24
- mod = await import(importUrl);
25
- }
26
-
27
- // Normalize CommonJS module structure
28
- // When importing CommonJS via dynamic import or some bundlers, it can return:
29
- // { default: { default: fn, ... } }
30
- // We need to unwrap it to: { default: fn, ... }
31
- if (mod.default && typeof mod.default === "object" && mod.default.default) {
32
- return mod.default as T;
33
- }
34
-
35
- return mod as T;
36
- }
37
-
38
- const AsyncFunction = (async () => {}).constructor;
39
-
40
- /**
41
- * Check if a value is an async function.
42
- */
43
- export function isAsyncFunction(fn: unknown): boolean {
44
- return fn instanceof AsyncFunction;
45
- }
@@ -1,344 +0,0 @@
1
- /**
2
- * Event processor module for Socket.IO event handling.
3
- *
4
- * This module is the core of Lithia's Socket.IO event processing pipeline. It handles:
5
- * - Event handler module loading with cache busting in development
6
- * - Event context initialization for hooks
7
- * - Error handling with logging and digest generation
8
- * - Error event emission for centralized error handling
9
- *
10
- * @module server/event-processor
11
- */
12
-
13
- import { createHash } from "node:crypto";
14
- import type { Event } from "@lithia-js/native";
15
- import { green, red } from "@lithia-js/utils";
16
- import type { Socket } from "socket.io";
17
- import { type EventContext, eventContext } from "../context/event-context";
18
- import { InvalidEventModuleError } from "../errors";
19
- import type { Lithia } from "../lithia";
20
- import { logger } from "../logger";
21
- import { coldImport, isAsyncFunction } from "../module-loader";
22
-
23
- /**
24
- * Event handler function signature.
25
- *
26
- * The primary function exported from event files to handle Socket.IO events.
27
- *
28
- * @param socket - The Socket.IO socket instance that triggered the event
29
- */
30
- export type EventHandler = (socket: Socket) => Promise<void>;
31
-
32
- /**
33
- * Structure of an event module loaded from the file system.
34
- *
35
- * Event files must export a default async function that handles the event.
36
- */
37
- export interface EventModule {
38
- /**
39
- * The default event handler function.
40
- *
41
- * This function is called when the event is triggered by a client.
42
- */
43
- default: (socket: Socket) => Promise<void>;
44
- }
45
-
46
- /**
47
- * Standardized error information for event errors.
48
- *
49
- * Contains details about errors that occur during event processing.
50
- *
51
- * @internal
52
- */
53
- export interface EventErrorInfo {
54
- /** The event name that caused the error. */
55
- eventName: string;
56
- /** Human-readable error message. */
57
- message: string;
58
- /** ISO timestamp of when the error occurred. */
59
- timestamp: string;
60
- /** Short error digest for correlation with logs. */
61
- digest: string;
62
- /** Socket ID that triggered the error. */
63
- socketId: string;
64
- /** Error stack trace (development only). */
65
- stack?: string;
66
- }
67
-
68
- /**
69
- * Event processor for the Lithia Socket.IO pipeline.
70
- *
71
- * This class orchestrates the entire event processing flow from receiving
72
- * a Socket.IO event to executing the handler. It manages:
73
- *
74
- * - **Event context**: Initializes context with event data for hooks
75
- * - **Module loading**: Loads event handlers with cache busting in development
76
- * - **Error handling**: Catches and logs errors with digest generation
77
- * - **Error events**: Emits error events for centralized error handling
78
- * - **Logging**: Logs all events with timing and error details
79
- *
80
- * @remarks
81
- * The processor uses AsyncLocalStorage to provide event context to hooks,
82
- * allowing event handlers to access data without explicit parameters.
83
- *
84
- * In development mode, event modules are reloaded on each event (cache busting).
85
- * In production, modules are cached for better performance.
86
- */
87
- export class EventProcessor {
88
- /**
89
- * Creates a new event processor.
90
- *
91
- * @param lithia - The Lithia application instance
92
- */
93
- constructor(private lithia: Lithia) {}
94
-
95
- /**
96
- * Processes an incoming Socket.IO event through the complete pipeline.
97
- *
98
- * This is the main entry point for event processing. The method executes
99
- * the following steps in order:
100
- *
101
- * 1. Initialize event context with data for hooks
102
- * 2. Load the event handler module
103
- * 3. Execute the event handler
104
- * 4. Log the event with timing
105
- *
106
- * Any errors thrown during this process are caught and handled by
107
- * {@link handleError}, which logs the error and emits an error event.
108
- *
109
- * @param socket - The Socket.IO socket that triggered the event
110
- * @param event - The event definition from the manifest
111
- * @param args - Additional arguments passed with the event
112
- *
113
- * @example
114
- * ```typescript
115
- * const processor = new EventProcessor(lithia);
116
- * await processor.processEvent(socket, event, messageData);
117
- * ```
118
- */
119
- async processEvent(
120
- socket: Socket,
121
- event: Event,
122
- ...args: any[]
123
- ): Promise<void> {
124
- const start = process.hrtime.bigint();
125
-
126
- // Initialize context for hooks
127
- const eventCtx: EventContext = {
128
- data: args[0],
129
- socket,
130
- };
131
-
132
- await this.lithia.runWithContext(async () => {
133
- await eventContext.run(eventCtx, async () => {
134
- try {
135
- // Import event module
136
- const module = await this.importEventModule(event);
137
-
138
- // Execute event handler
139
- await module.default(socket);
140
-
141
- // Log successful event
142
- this.logEvent(event, start);
143
- } catch (err) {
144
- // Handle and log error
145
- await this.handleError(err, event, socket, start);
146
- }
147
- });
148
- });
149
- }
150
-
151
- // ==================== Error Handling ====================
152
-
153
- /**
154
- * Centralized error handler for the event pipeline.
155
- *
156
- * This method handles all errors thrown during event processing:
157
- *
158
- * **Development mode:**
159
- * - Returns detailed error messages to client
160
- * - Includes full error stacks
161
- * - Logs with error digest
162
- *
163
- * **Production mode:**
164
- * - Returns generic error messages to client
165
- * - Includes error digest for log correlation
166
- * - Hides sensitive error details
167
- *
168
- * After logging, emits an 'error' event to the socket so the client
169
- * can handle errors gracefully.
170
- *
171
- * @param err - The error that was thrown
172
- * @param event - The event definition that caused the error
173
- * @param socket - The Socket.IO socket that triggered the event
174
- * @param start - High-resolution timestamp when event processing started
175
- *
176
- * @private
177
- */
178
- private async handleError(
179
- err: unknown,
180
- event: Event,
181
- socket: Socket,
182
- start: bigint,
183
- ): Promise<void> {
184
- const isDevelopment = this.lithia.getEnvironment() === "development";
185
-
186
- // Generate error digest
187
- const digest = this.generateErrorDigest(err);
188
-
189
- // Build error details
190
- const errorMessage =
191
- err instanceof Error ? err.message : "Internal Server Error";
192
- const errorStack = err instanceof Error ? err.stack : undefined;
193
-
194
- // Log error with digest (same format for both environments)
195
- logger.error(
196
- `[Event: ${event.name}] [Digest: ${red(digest)}] ${errorStack || errorMessage}`,
197
- );
198
-
199
- // Build error information for client
200
- const errorInfo: EventErrorInfo = {
201
- eventName: event.name,
202
- message: isDevelopment
203
- ? errorMessage
204
- : "An error occurred while processing the event",
205
- timestamp: new Date().toISOString(),
206
- digest: digest,
207
- socketId: socket.id,
208
- stack: isDevelopment ? errorStack : undefined,
209
- };
210
-
211
- // Emit error event to the client
212
- socket.emit("error", errorInfo);
213
-
214
- // Log the event as failed
215
- this.logEvent(event, start, true);
216
- }
217
-
218
- // ==================== Logging & Diagnostics ====================
219
-
220
- /**
221
- * Logs a completed event with timing information.
222
- *
223
- * The log includes:
224
- * - Event name
225
- * - Socket ID
226
- * - Processing duration in milliseconds
227
- * - Success or error status
228
- *
229
- * Respects the `logging.events` configuration flag. Critical errors
230
- * are always logged regardless of the flag.
231
- *
232
- * @param event - The event that was processed
233
- * @param socket - The socket that triggered the event
234
- * @param start - High-resolution timestamp when processing started
235
- * @param isError - Whether the event resulted in an error
236
- *
237
- * @private
238
- */
239
- private logEvent(event: Event, start: bigint, isError = false): void {
240
- // Always log errors, otherwise respect the logging.events flag
241
- const shouldLog = isError || this.lithia.options.logging?.events !== false;
242
-
243
- if (!shouldLog) return;
244
-
245
- const end = process.hrtime.bigint();
246
- const duration = Number(end - start) / 1_000_000;
247
- const durationStr = `${duration.toFixed(2)}ms`;
248
-
249
- // Use same color scheme as requests for consistency
250
- const statusStr = isError ? red("ERROR") : green("OK");
251
-
252
- logger.info(`[${statusStr}] EVENT ${event.name} - ${durationStr}`);
253
- }
254
-
255
- /**
256
- * Generates a short hexadecimal digest for error correlation.
257
- *
258
- * The digest is used to correlate server-side error logs with client-facing
259
- * error responses. This allows developers to find the detailed error in logs
260
- * using the digest shown to the client.
261
- *
262
- * The digest is generated from:
263
- * - Error message and stack
264
- * - Current timestamp
265
- * - Random factor for uniqueness
266
- *
267
- * @param err - The error to generate a digest for
268
- * @returns An 8-character hexadecimal digest
269
- *
270
- * @private
271
- */
272
- private generateErrorDigest(err: unknown): string {
273
- // Create a unique digest based on error message, timestamp, and random factor
274
- const errorString =
275
- err instanceof Error ? `${err.message}${err.stack}` : String(err);
276
-
277
- const hash = createHash("sha256")
278
- .update(`${errorString}${Date.now()}${Math.random()}`)
279
- .digest("hex");
280
-
281
- // Return first 8 characters (similar to request-processor)
282
- return hash.substring(0, 8);
283
- }
284
-
285
- // ==================== Module Loading ====================
286
-
287
- /**
288
- * Imports an event module from the file system.
289
- *
290
- * In **development mode**, uses cache-busting to reload the module on each
291
- * event, enabling hot reloading without server restart.
292
- *
293
- * In **production mode**, uses standard dynamic imports with caching for
294
- * better performance.
295
- *
296
- * The method also validates the module structure:
297
- * - Must have a default export
298
- * - Default export must be a function
299
- * - Default export must be async
300
- *
301
- * @param event - The event whose module should be imported
302
- * @returns The loaded and validated event module
303
- * @throws {InvalidEventModuleError} If the module structure is invalid
304
- * @throws {Error} If the module fails to load
305
- *
306
- * @private
307
- */
308
- private async importEventModule(event: Event): Promise<EventModule> {
309
- try {
310
- const isDevelopment = this.lithia.getEnvironment() === "development";
311
-
312
- const mod = await coldImport<EventModule>(event.filePath, isDevelopment);
313
-
314
- if (!mod.default) {
315
- throw new InvalidEventModuleError(
316
- event.filePath,
317
- "Missing default export",
318
- );
319
- }
320
-
321
- if (typeof mod.default !== "function") {
322
- throw new InvalidEventModuleError(
323
- event.filePath,
324
- "Default export is not a function",
325
- );
326
- }
327
-
328
- if (!isAsyncFunction(mod.default)) {
329
- throw new InvalidEventModuleError(
330
- event.filePath,
331
- "Default export is not an async function",
332
- );
333
- }
334
-
335
- return mod;
336
- } catch (err) {
337
- if (err instanceof InvalidEventModuleError) {
338
- throw err;
339
- }
340
-
341
- throw new Error(`Failed to import event: ${event.name}`);
342
- }
343
- }
344
- }