@donkeylabs/server 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.
@@ -0,0 +1,320 @@
1
+ /**
2
+ * HTTP Error System
3
+ *
4
+ * Provides throwable HTTP errors that are automatically caught by the server
5
+ * and converted to proper HTTP responses with status codes and error bodies.
6
+ */
7
+
8
+ /** Base class for all HTTP errors */
9
+ export class HttpError extends Error {
10
+ /** HTTP status code */
11
+ readonly status: number;
12
+
13
+ /** Error code for client identification (e.g., "BAD_REQUEST", "USER_NOT_FOUND") */
14
+ readonly code: string;
15
+
16
+ /** Additional error details/metadata */
17
+ readonly details?: Record<string, any>;
18
+
19
+ /** Original error that caused this error */
20
+ override readonly cause?: Error;
21
+
22
+ constructor(
23
+ status: number,
24
+ code: string,
25
+ message: string,
26
+ details?: Record<string, any>,
27
+ cause?: Error
28
+ ) {
29
+ super(message);
30
+ this.name = "HttpError";
31
+ this.status = status;
32
+ this.code = code;
33
+ this.details = details;
34
+ this.cause = cause;
35
+
36
+ // Maintain proper stack trace in V8 environments
37
+ if (Error.captureStackTrace) {
38
+ Error.captureStackTrace(this, this.constructor);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Convert to JSON response body
44
+ */
45
+ toJSON(): Record<string, any> {
46
+ const body: Record<string, any> = {
47
+ error: this.code,
48
+ message: this.message,
49
+ };
50
+
51
+ if (this.details && Object.keys(this.details).length > 0) {
52
+ body.details = this.details;
53
+ }
54
+
55
+ return body;
56
+ }
57
+ }
58
+
59
+ /** 400 Bad Request */
60
+ export class BadRequestError extends HttpError {
61
+ constructor(message: string = "Bad Request", details?: Record<string, any>, cause?: Error) {
62
+ super(400, "BAD_REQUEST", message, details, cause);
63
+ this.name = "BadRequestError";
64
+ }
65
+ }
66
+
67
+ /** 401 Unauthorized */
68
+ export class UnauthorizedError extends HttpError {
69
+ constructor(message: string = "Unauthorized", details?: Record<string, any>, cause?: Error) {
70
+ super(401, "UNAUTHORIZED", message, details, cause);
71
+ this.name = "UnauthorizedError";
72
+ }
73
+ }
74
+
75
+ /** 403 Forbidden */
76
+ export class ForbiddenError extends HttpError {
77
+ constructor(message: string = "Forbidden", details?: Record<string, any>, cause?: Error) {
78
+ super(403, "FORBIDDEN", message, details, cause);
79
+ this.name = "ForbiddenError";
80
+ }
81
+ }
82
+
83
+ /** 404 Not Found */
84
+ export class NotFoundError extends HttpError {
85
+ constructor(message: string = "Not Found", details?: Record<string, any>, cause?: Error) {
86
+ super(404, "NOT_FOUND", message, details, cause);
87
+ this.name = "NotFoundError";
88
+ }
89
+ }
90
+
91
+ /** 405 Method Not Allowed */
92
+ export class MethodNotAllowedError extends HttpError {
93
+ constructor(message: string = "Method Not Allowed", details?: Record<string, any>, cause?: Error) {
94
+ super(405, "METHOD_NOT_ALLOWED", message, details, cause);
95
+ this.name = "MethodNotAllowedError";
96
+ }
97
+ }
98
+
99
+ /** 409 Conflict */
100
+ export class ConflictError extends HttpError {
101
+ constructor(message: string = "Conflict", details?: Record<string, any>, cause?: Error) {
102
+ super(409, "CONFLICT", message, details, cause);
103
+ this.name = "ConflictError";
104
+ }
105
+ }
106
+
107
+ /** 410 Gone */
108
+ export class GoneError extends HttpError {
109
+ constructor(message: string = "Gone", details?: Record<string, any>, cause?: Error) {
110
+ super(410, "GONE", message, details, cause);
111
+ this.name = "GoneError";
112
+ }
113
+ }
114
+
115
+ /** 422 Unprocessable Entity */
116
+ export class UnprocessableEntityError extends HttpError {
117
+ constructor(message: string = "Unprocessable Entity", details?: Record<string, any>, cause?: Error) {
118
+ super(422, "UNPROCESSABLE_ENTITY", message, details, cause);
119
+ this.name = "UnprocessableEntityError";
120
+ }
121
+ }
122
+
123
+ /** 429 Too Many Requests */
124
+ export class TooManyRequestsError extends HttpError {
125
+ constructor(message: string = "Too Many Requests", details?: Record<string, any>, cause?: Error) {
126
+ super(429, "TOO_MANY_REQUESTS", message, details, cause);
127
+ this.name = "TooManyRequestsError";
128
+ }
129
+ }
130
+
131
+ /** 500 Internal Server Error */
132
+ export class InternalServerError extends HttpError {
133
+ constructor(message: string = "Internal Server Error", details?: Record<string, any>, cause?: Error) {
134
+ super(500, "INTERNAL_SERVER_ERROR", message, details, cause);
135
+ this.name = "InternalServerError";
136
+ }
137
+ }
138
+
139
+ /** 501 Not Implemented */
140
+ export class NotImplementedError extends HttpError {
141
+ constructor(message: string = "Not Implemented", details?: Record<string, any>, cause?: Error) {
142
+ super(501, "NOT_IMPLEMENTED", message, details, cause);
143
+ this.name = "NotImplementedError";
144
+ }
145
+ }
146
+
147
+ /** 502 Bad Gateway */
148
+ export class BadGatewayError extends HttpError {
149
+ constructor(message: string = "Bad Gateway", details?: Record<string, any>, cause?: Error) {
150
+ super(502, "BAD_GATEWAY", message, details, cause);
151
+ this.name = "BadGatewayError";
152
+ }
153
+ }
154
+
155
+ /** 503 Service Unavailable */
156
+ export class ServiceUnavailableError extends HttpError {
157
+ constructor(message: string = "Service Unavailable", details?: Record<string, any>, cause?: Error) {
158
+ super(503, "SERVICE_UNAVAILABLE", message, details, cause);
159
+ this.name = "ServiceUnavailableError";
160
+ }
161
+ }
162
+
163
+ /** 504 Gateway Timeout */
164
+ export class GatewayTimeoutError extends HttpError {
165
+ constructor(message: string = "Gateway Timeout", details?: Record<string, any>, cause?: Error) {
166
+ super(504, "GATEWAY_TIMEOUT", message, details, cause);
167
+ this.name = "GatewayTimeoutError";
168
+ }
169
+ }
170
+
171
+ /** Factory function signature for creating errors */
172
+ export type ErrorFactory<T extends HttpError = HttpError> = (
173
+ message?: string,
174
+ details?: Record<string, any>,
175
+ cause?: Error
176
+ ) => T;
177
+
178
+ /** Base error factories available on ctx.errors */
179
+ export interface BaseErrorFactories {
180
+ BadRequest: ErrorFactory<BadRequestError>;
181
+ Unauthorized: ErrorFactory<UnauthorizedError>;
182
+ Forbidden: ErrorFactory<ForbiddenError>;
183
+ NotFound: ErrorFactory<NotFoundError>;
184
+ MethodNotAllowed: ErrorFactory<MethodNotAllowedError>;
185
+ Conflict: ErrorFactory<ConflictError>;
186
+ Gone: ErrorFactory<GoneError>;
187
+ UnprocessableEntity: ErrorFactory<UnprocessableEntityError>;
188
+ TooManyRequests: ErrorFactory<TooManyRequestsError>;
189
+ InternalServer: ErrorFactory<InternalServerError>;
190
+ NotImplemented: ErrorFactory<NotImplementedError>;
191
+ BadGateway: ErrorFactory<BadGatewayError>;
192
+ ServiceUnavailable: ErrorFactory<ServiceUnavailableError>;
193
+ GatewayTimeout: ErrorFactory<GatewayTimeoutError>;
194
+ }
195
+
196
+ /** Extended error factories (augmented by plugins and user) */
197
+ export interface ErrorFactories extends BaseErrorFactories {}
198
+
199
+ /** Definition for a custom error type */
200
+ export interface CustomErrorDefinition {
201
+ /** HTTP status code */
202
+ status: number;
203
+ /** Error code (e.g., "PAYMENT_REQUIRED", "USER_SUSPENDED") */
204
+ code: string;
205
+ /** Default message if none provided */
206
+ defaultMessage?: string;
207
+ }
208
+
209
+ /** Registry of custom error definitions */
210
+ export type CustomErrorRegistry = Record<string, CustomErrorDefinition>;
211
+
212
+ export interface ErrorsConfig {
213
+ /** Include stack traces in error responses (default: false in production) */
214
+ includeStackTrace?: boolean;
215
+ /** Custom error definitions to register */
216
+ customErrors?: CustomErrorRegistry;
217
+ }
218
+
219
+ export interface Errors extends ErrorFactories {
220
+ /** Create a custom HTTP error */
221
+ custom(
222
+ status: number,
223
+ code: string,
224
+ message: string,
225
+ details?: Record<string, any>,
226
+ cause?: Error
227
+ ): HttpError;
228
+
229
+ /** Check if an error is an HttpError */
230
+ isHttpError(error: unknown): error is HttpError;
231
+
232
+ /** Register a custom error type */
233
+ register<K extends string>(
234
+ name: K,
235
+ definition: CustomErrorDefinition
236
+ ): void;
237
+ }
238
+
239
+ /** Create the errors service */
240
+ export function createErrors(config: ErrorsConfig = {}): Errors {
241
+ const customErrors = new Map<string, CustomErrorDefinition>(
242
+ Object.entries(config.customErrors || {})
243
+ );
244
+
245
+ // Base error factories
246
+ const errors: Errors = {
247
+ // Standard HTTP errors
248
+ BadRequest: (message, details, cause) =>
249
+ new BadRequestError(message, details, cause),
250
+ Unauthorized: (message, details, cause) =>
251
+ new UnauthorizedError(message, details, cause),
252
+ Forbidden: (message, details, cause) =>
253
+ new ForbiddenError(message, details, cause),
254
+ NotFound: (message, details, cause) =>
255
+ new NotFoundError(message, details, cause),
256
+ MethodNotAllowed: (message, details, cause) =>
257
+ new MethodNotAllowedError(message, details, cause),
258
+ Conflict: (message, details, cause) =>
259
+ new ConflictError(message, details, cause),
260
+ Gone: (message, details, cause) =>
261
+ new GoneError(message, details, cause),
262
+ UnprocessableEntity: (message, details, cause) =>
263
+ new UnprocessableEntityError(message, details, cause),
264
+ TooManyRequests: (message, details, cause) =>
265
+ new TooManyRequestsError(message, details, cause),
266
+ InternalServer: (message, details, cause) =>
267
+ new InternalServerError(message, details, cause),
268
+ NotImplemented: (message, details, cause) =>
269
+ new NotImplementedError(message, details, cause),
270
+ BadGateway: (message, details, cause) =>
271
+ new BadGatewayError(message, details, cause),
272
+ ServiceUnavailable: (message, details, cause) =>
273
+ new ServiceUnavailableError(message, details, cause),
274
+ GatewayTimeout: (message, details, cause) =>
275
+ new GatewayTimeoutError(message, details, cause),
276
+
277
+ // Utility methods
278
+ custom: (status, code, message, details, cause) =>
279
+ new HttpError(status, code, message, details, cause),
280
+
281
+ isHttpError: (error): error is HttpError =>
282
+ error instanceof HttpError,
283
+
284
+ register: (name, definition) => {
285
+ customErrors.set(name, definition);
286
+ // Add factory method dynamically
287
+ (errors as any)[name] = (
288
+ message?: string,
289
+ details?: Record<string, any>,
290
+ cause?: Error
291
+ ) =>
292
+ new HttpError(
293
+ definition.status,
294
+ definition.code,
295
+ message || definition.defaultMessage || definition.code,
296
+ details,
297
+ cause
298
+ );
299
+ },
300
+ };
301
+
302
+ // Register any custom errors from config
303
+ for (const [name, definition] of customErrors) {
304
+ errors.register(name, definition);
305
+ }
306
+
307
+ return errors;
308
+ }
309
+
310
+ /** Create a BadRequestError with validation details (useful for Zod validation errors) */
311
+ export function createValidationError(
312
+ issues: Array<{ path: (string | number)[]; message: string }>
313
+ ): BadRequestError {
314
+ return new BadRequestError("Validation Failed", {
315
+ issues: issues.map((issue) => ({
316
+ path: issue.path,
317
+ message: issue.message,
318
+ })),
319
+ });
320
+ }
@@ -0,0 +1,163 @@
1
+ // Core Events Service
2
+ // Pub/sub async event queue
3
+
4
+ export interface EventHandler<T = any> {
5
+ (data: T): void | Promise<void>;
6
+ }
7
+
8
+ export interface Subscription {
9
+ unsubscribe(): void;
10
+ }
11
+
12
+ export interface EventRecord {
13
+ event: string;
14
+ data: any;
15
+ timestamp: Date;
16
+ }
17
+
18
+ export interface EventAdapter {
19
+ publish(event: string, data: any): Promise<void>;
20
+ getHistory(event: string, limit?: number): Promise<EventRecord[]>;
21
+ }
22
+
23
+ export interface EventsConfig {
24
+ adapter?: EventAdapter;
25
+ maxHistorySize?: number;
26
+ }
27
+
28
+ export interface Events {
29
+ emit<T = any>(event: string, data: T): Promise<void>;
30
+ on<T = any>(event: string, handler: EventHandler<T>): Subscription;
31
+ once<T = any>(event: string, handler: EventHandler<T>): Subscription;
32
+ off(event: string, handler?: EventHandler): void;
33
+ getHistory(event: string, limit?: number): Promise<EventRecord[]>;
34
+ }
35
+
36
+ // In-memory event adapter with history
37
+ export class MemoryEventAdapter implements EventAdapter {
38
+ private history: EventRecord[] = [];
39
+ private maxHistorySize: number;
40
+
41
+ constructor(maxHistorySize: number = 1000) {
42
+ this.maxHistorySize = maxHistorySize;
43
+ }
44
+
45
+ async publish(event: string, data: any): Promise<void> {
46
+ const record: EventRecord = {
47
+ event,
48
+ data,
49
+ timestamp: new Date(),
50
+ };
51
+
52
+ this.history.push(record);
53
+
54
+ // Trim history if needed
55
+ while (this.history.length > this.maxHistorySize) {
56
+ this.history.shift();
57
+ }
58
+ }
59
+
60
+ async getHistory(event: string, limit: number = 100): Promise<EventRecord[]> {
61
+ return this.history
62
+ .filter(r => r.event === event || event === "*")
63
+ .slice(-limit);
64
+ }
65
+ }
66
+
67
+ class EventsImpl implements Events {
68
+ private handlers = new Map<string, Set<EventHandler>>();
69
+ private adapter: EventAdapter;
70
+
71
+ constructor(config: EventsConfig = {}) {
72
+ this.adapter = config.adapter ?? new MemoryEventAdapter(config.maxHistorySize);
73
+ }
74
+
75
+ async emit<T = any>(event: string, data: T): Promise<void> {
76
+ // Store in adapter (for history/persistence)
77
+ await this.adapter.publish(event, data);
78
+
79
+ // Notify handlers
80
+ const eventHandlers = this.handlers.get(event);
81
+ if (eventHandlers) {
82
+ const promises: Promise<void>[] = [];
83
+ for (const handler of eventHandlers) {
84
+ try {
85
+ const result = handler(data);
86
+ if (result instanceof Promise) {
87
+ promises.push(result.catch(err => {
88
+ console.error(`[Events] Handler error for "${event}":`, err);
89
+ }));
90
+ }
91
+ } catch (err) {
92
+ console.error(`[Events] Handler error for "${event}":`, err);
93
+ }
94
+ }
95
+ // Wait for all async handlers
96
+ await Promise.all(promises);
97
+ }
98
+
99
+ // Also notify pattern handlers (e.g., "user.*" matches "user.created")
100
+ for (const [pattern, handlers] of this.handlers.entries()) {
101
+ if (pattern.includes("*") && this.matchPattern(event, pattern)) {
102
+ for (const handler of handlers) {
103
+ try {
104
+ const result = handler(data);
105
+ if (result instanceof Promise) {
106
+ await result.catch(err => {
107
+ console.error(`[Events] Pattern handler error for "${pattern}":`, err);
108
+ });
109
+ }
110
+ } catch (err) {
111
+ console.error(`[Events] Pattern handler error for "${pattern}":`, err);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ on<T = any>(event: string, handler: EventHandler<T>): Subscription {
119
+ if (!this.handlers.has(event)) {
120
+ this.handlers.set(event, new Set());
121
+ }
122
+ this.handlers.get(event)!.add(handler as EventHandler);
123
+
124
+ return {
125
+ unsubscribe: () => {
126
+ this.handlers.get(event)?.delete(handler as EventHandler);
127
+ },
128
+ };
129
+ }
130
+
131
+ once<T = any>(event: string, handler: EventHandler<T>): Subscription {
132
+ const wrappedHandler: EventHandler<T> = async (data) => {
133
+ this.handlers.get(event)?.delete(wrappedHandler as EventHandler);
134
+ await handler(data);
135
+ };
136
+
137
+ return this.on(event, wrappedHandler);
138
+ }
139
+
140
+ off(event: string, handler?: EventHandler): void {
141
+ if (handler) {
142
+ this.handlers.get(event)?.delete(handler);
143
+ } else {
144
+ this.handlers.delete(event);
145
+ }
146
+ }
147
+
148
+ async getHistory(event: string, limit?: number): Promise<EventRecord[]> {
149
+ return this.adapter.getHistory(event, limit);
150
+ }
151
+
152
+ private matchPattern(event: string, pattern: string): boolean {
153
+ // Convert glob pattern to regex (e.g., "user.*" -> /^user\..*$/)
154
+ const regex = new RegExp(
155
+ "^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
156
+ );
157
+ return regex.test(event);
158
+ }
159
+ }
160
+
161
+ export function createEvents(config?: EventsConfig): Events {
162
+ return new EventsImpl(config);
163
+ }
@@ -0,0 +1,94 @@
1
+ // Core Services - Re-export all services
2
+
3
+ export {
4
+ type Logger,
5
+ type LogLevel,
6
+ type LogEntry,
7
+ type LogTransport,
8
+ type LoggerConfig,
9
+ ConsoleTransport,
10
+ createLogger,
11
+ } from "./logger";
12
+
13
+ export {
14
+ type Cache,
15
+ type CacheAdapter,
16
+ type CacheConfig,
17
+ MemoryCacheAdapter,
18
+ createCache,
19
+ } from "./cache";
20
+
21
+ export {
22
+ type Events,
23
+ type EventHandler,
24
+ type Subscription,
25
+ type EventRecord,
26
+ type EventAdapter,
27
+ type EventsConfig,
28
+ MemoryEventAdapter,
29
+ createEvents,
30
+ } from "./events";
31
+
32
+ export {
33
+ type Cron,
34
+ type CronTask,
35
+ type CronConfig,
36
+ createCron,
37
+ } from "./cron";
38
+
39
+ export {
40
+ type Jobs,
41
+ type Job,
42
+ type JobStatus,
43
+ type JobHandler,
44
+ type JobAdapter,
45
+ type JobsConfig,
46
+ MemoryJobAdapter,
47
+ createJobs,
48
+ } from "./jobs";
49
+
50
+ export {
51
+ type SSE,
52
+ type SSEClient,
53
+ type SSEConfig,
54
+ createSSE,
55
+ } from "./sse";
56
+
57
+ export {
58
+ type RateLimiter,
59
+ type RateLimitResult,
60
+ type RateLimitAdapter,
61
+ type RateLimiterConfig,
62
+ MemoryRateLimitAdapter,
63
+ createRateLimiter,
64
+ extractClientIP,
65
+ parseDuration,
66
+ createRateLimitKey,
67
+ } from "./rate-limiter";
68
+
69
+ export {
70
+ type Errors,
71
+ type ErrorsConfig,
72
+ type ErrorFactory,
73
+ type BaseErrorFactories,
74
+ type ErrorFactories,
75
+ type CustomErrorDefinition,
76
+ type CustomErrorRegistry,
77
+ HttpError,
78
+ BadRequestError,
79
+ UnauthorizedError,
80
+ ForbiddenError,
81
+ NotFoundError,
82
+ MethodNotAllowedError,
83
+ ConflictError,
84
+ GoneError,
85
+ UnprocessableEntityError,
86
+ TooManyRequestsError,
87
+ InternalServerError,
88
+ NotImplementedError,
89
+ BadGatewayError,
90
+ ServiceUnavailableError,
91
+ GatewayTimeoutError,
92
+ createErrors,
93
+ createValidationError,
94
+ } from "./errors";