@donkeylabs/server 0.3.0 → 0.3.1
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/LICENSE +1 -1
- package/docs/api-client.md +7 -7
- package/docs/cache.md +1 -74
- package/docs/core-services.md +4 -116
- package/docs/cron.md +1 -1
- package/docs/errors.md +2 -2
- package/docs/events.md +3 -98
- package/docs/handlers.md +13 -48
- package/docs/logger.md +3 -58
- package/docs/middleware.md +2 -2
- package/docs/plugins.md +13 -64
- package/docs/project-structure.md +4 -142
- package/docs/rate-limiter.md +4 -136
- package/docs/router.md +6 -14
- package/docs/sse.md +1 -99
- package/docs/sveltekit-adapter.md +420 -0
- package/package.json +6 -6
- package/registry.d.ts +15 -14
- package/src/core/cache.ts +0 -75
- package/src/core/cron.ts +3 -96
- package/src/core/errors.ts +78 -11
- package/src/core/events.ts +1 -47
- package/src/core/index.ts +0 -4
- package/src/core/jobs.ts +0 -112
- package/src/core/logger.ts +12 -79
- package/src/core/rate-limiter.ts +29 -108
- package/src/core/sse.ts +1 -84
- package/src/core.ts +13 -104
- package/src/generator/index.ts +551 -0
- package/src/handlers.ts +14 -110
- package/src/index.ts +19 -23
- package/src/middleware.ts +2 -5
- package/src/registry.ts +4 -0
- package/src/server.ts +354 -337
- package/README.md +0 -254
- package/cli/commands/dev.ts +0 -134
- package/cli/commands/generate.ts +0 -605
- package/cli/commands/init.ts +0 -205
- package/cli/commands/interactive.ts +0 -417
- package/cli/commands/plugin.ts +0 -192
- package/cli/commands/route.ts +0 -195
- package/cli/donkeylabs +0 -2
- package/cli/index.ts +0 -114
- package/docs/svelte-frontend.md +0 -324
- package/docs/testing.md +0 -438
- package/mcp/donkeylabs-mcp +0 -3238
- package/mcp/server.ts +0 -3238
package/src/core/cache.ts
CHANGED
|
@@ -24,17 +24,6 @@ export interface Cache {
|
|
|
24
24
|
clear(): Promise<void>;
|
|
25
25
|
keys(pattern?: string): Promise<string[]>;
|
|
26
26
|
getOrSet<T>(key: string, factory: () => Promise<T>, ttlMs?: number): Promise<T>;
|
|
27
|
-
|
|
28
|
-
/** Create a namespaced cache with automatic key prefixing */
|
|
29
|
-
namespace(prefix: string): NamespacedCache;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** A cache namespace with automatic key prefixing and optional TTL policy */
|
|
33
|
-
export interface NamespacedCache extends Omit<Cache, "namespace"> {
|
|
34
|
-
/** The namespace prefix */
|
|
35
|
-
readonly prefix: string;
|
|
36
|
-
/** Clear only keys in this namespace */
|
|
37
|
-
clearNamespace(): Promise<void>;
|
|
38
27
|
}
|
|
39
28
|
|
|
40
29
|
interface CacheEntry<T> {
|
|
@@ -187,70 +176,6 @@ class CacheImpl implements Cache {
|
|
|
187
176
|
await this.set(key, value, ttlMs);
|
|
188
177
|
return value;
|
|
189
178
|
}
|
|
190
|
-
|
|
191
|
-
namespace(prefix: string): NamespacedCache {
|
|
192
|
-
return new NamespacedCacheImpl(this, prefix);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** Namespaced cache that automatically prefixes all keys */
|
|
197
|
-
class NamespacedCacheImpl implements NamespacedCache {
|
|
198
|
-
readonly prefix: string;
|
|
199
|
-
private parent: Cache;
|
|
200
|
-
|
|
201
|
-
constructor(parent: Cache, prefix: string) {
|
|
202
|
-
this.parent = parent;
|
|
203
|
-
this.prefix = prefix.endsWith(":") ? prefix : `${prefix}:`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
private prefixKey(key: string): string {
|
|
207
|
-
return `${this.prefix}${key}`;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async get<T>(key: string): Promise<T | null> {
|
|
211
|
-
return this.parent.get<T>(this.prefixKey(key));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async set<T>(key: string, value: T, ttlMs?: number): Promise<void> {
|
|
215
|
-
return this.parent.set(this.prefixKey(key), value, ttlMs);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async delete(key: string): Promise<boolean> {
|
|
219
|
-
return this.parent.delete(this.prefixKey(key));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async has(key: string): Promise<boolean> {
|
|
223
|
-
return this.parent.has(this.prefixKey(key));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async clear(): Promise<void> {
|
|
227
|
-
// Clear the entire cache (same as parent)
|
|
228
|
-
return this.parent.clear();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async clearNamespace(): Promise<void> {
|
|
232
|
-
// Clear only keys with this namespace prefix
|
|
233
|
-
const keys = await this.parent.keys(`${this.prefix}*`);
|
|
234
|
-
for (const key of keys) {
|
|
235
|
-
await this.parent.delete(key);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async keys(pattern?: string): Promise<string[]> {
|
|
240
|
-
// Get keys with namespace prefix, then strip the prefix from results
|
|
241
|
-
const fullPattern = pattern ? `${this.prefix}${pattern}` : `${this.prefix}*`;
|
|
242
|
-
const keys = await this.parent.keys(fullPattern);
|
|
243
|
-
return keys.map(k => k.slice(this.prefix.length));
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async getOrSet<T>(key: string, factory: () => Promise<T>, ttlMs?: number): Promise<T> {
|
|
247
|
-
const existing = await this.get<T>(key);
|
|
248
|
-
if (existing !== null) return existing;
|
|
249
|
-
|
|
250
|
-
const value = await factory();
|
|
251
|
-
await this.set(key, value, ttlMs);
|
|
252
|
-
return value;
|
|
253
|
-
}
|
|
254
179
|
}
|
|
255
180
|
|
|
256
181
|
export function createCache(config?: CacheConfig): Cache {
|
package/src/core/cron.ts
CHANGED
|
@@ -9,22 +9,6 @@ export interface CronTask {
|
|
|
9
9
|
enabled: boolean;
|
|
10
10
|
lastRun?: Date;
|
|
11
11
|
nextRun?: Date;
|
|
12
|
-
/** Plugin that registered this task */
|
|
13
|
-
pluginName?: string;
|
|
14
|
-
/** Optional description */
|
|
15
|
-
description?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Declarative cron task definition for plugin registration */
|
|
19
|
-
export interface CronTaskDefinition {
|
|
20
|
-
/** Cron expression (5 or 6 fields) */
|
|
21
|
-
expression: string;
|
|
22
|
-
/** Task handler function */
|
|
23
|
-
handler: () => void | Promise<void>;
|
|
24
|
-
/** Whether task starts enabled (default: true) */
|
|
25
|
-
enabled?: boolean;
|
|
26
|
-
/** Optional description */
|
|
27
|
-
description?: string;
|
|
28
12
|
}
|
|
29
13
|
|
|
30
14
|
export interface CronConfig {
|
|
@@ -32,39 +16,16 @@ export interface CronConfig {
|
|
|
32
16
|
}
|
|
33
17
|
|
|
34
18
|
export interface Cron {
|
|
35
|
-
/** Schedule a cron task (imperative API) */
|
|
36
19
|
schedule(
|
|
37
20
|
expression: string,
|
|
38
21
|
handler: () => void | Promise<void>,
|
|
39
|
-
options?: { name?: string; enabled?: boolean
|
|
22
|
+
options?: { name?: string; enabled?: boolean }
|
|
40
23
|
): string;
|
|
41
|
-
|
|
42
|
-
/** Register a named cron task (declarative API) */
|
|
43
|
-
registerTask(
|
|
44
|
-
name: string,
|
|
45
|
-
definition: CronTaskDefinition,
|
|
46
|
-
pluginName?: string
|
|
47
|
-
): string;
|
|
48
|
-
|
|
49
|
-
/** Register multiple cron tasks (for plugins) */
|
|
50
|
-
registerTasks(
|
|
51
|
-
tasks: Record<string, CronTaskDefinition>,
|
|
52
|
-
pluginName?: string
|
|
53
|
-
): string[];
|
|
54
|
-
|
|
55
24
|
unschedule(taskId: string): boolean;
|
|
56
25
|
pause(taskId: string): void;
|
|
57
26
|
resume(taskId: string): void;
|
|
58
27
|
list(): CronTask[];
|
|
59
|
-
|
|
60
|
-
/** List tasks registered by a specific plugin */
|
|
61
|
-
listByPlugin(pluginName: string): CronTask[];
|
|
62
|
-
|
|
63
28
|
get(taskId: string): CronTask | undefined;
|
|
64
|
-
|
|
65
|
-
/** Get task by name */
|
|
66
|
-
getByName(name: string): CronTask | undefined;
|
|
67
|
-
|
|
68
29
|
trigger(taskId: string): Promise<void>;
|
|
69
30
|
start(): void;
|
|
70
31
|
stop(): Promise<void>;
|
|
@@ -170,12 +131,8 @@ interface InternalCronTask extends CronTask {
|
|
|
170
131
|
_cronExpr: CronExpression;
|
|
171
132
|
}
|
|
172
133
|
|
|
173
|
-
/** Index tasks by name for quick lookup */
|
|
174
|
-
type TaskNameIndex = Map<string, string>; // name -> id
|
|
175
|
-
|
|
176
134
|
class CronImpl implements Cron {
|
|
177
135
|
private tasks = new Map<string, InternalCronTask>();
|
|
178
|
-
private taskNameIndex: TaskNameIndex = new Map();
|
|
179
136
|
private running = false;
|
|
180
137
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
181
138
|
private taskCounter = 0;
|
|
@@ -187,63 +144,27 @@ class CronImpl implements Cron {
|
|
|
187
144
|
schedule(
|
|
188
145
|
expression: string,
|
|
189
146
|
handler: () => void | Promise<void>,
|
|
190
|
-
options: { name?: string; enabled?: boolean
|
|
147
|
+
options: { name?: string; enabled?: boolean } = {}
|
|
191
148
|
): string {
|
|
192
149
|
const id = `cron_${++this.taskCounter}_${Date.now()}`;
|
|
193
150
|
const cronExpr = new CronExpression(expression);
|
|
194
|
-
const name = options.name ?? id;
|
|
195
151
|
|
|
196
152
|
const task: InternalCronTask = {
|
|
197
153
|
id,
|
|
198
|
-
name,
|
|
154
|
+
name: options.name ?? id,
|
|
199
155
|
expression,
|
|
200
156
|
handler,
|
|
201
157
|
enabled: options.enabled ?? true,
|
|
202
158
|
nextRun: cronExpr.getNextRun(),
|
|
203
|
-
pluginName: options.pluginName,
|
|
204
|
-
description: options.description,
|
|
205
159
|
_cronExpr: cronExpr,
|
|
206
160
|
};
|
|
207
161
|
|
|
208
162
|
this.tasks.set(id, task);
|
|
209
|
-
this.taskNameIndex.set(name, id);
|
|
210
163
|
|
|
211
164
|
return id;
|
|
212
165
|
}
|
|
213
166
|
|
|
214
|
-
registerTask(
|
|
215
|
-
name: string,
|
|
216
|
-
definition: CronTaskDefinition,
|
|
217
|
-
pluginName?: string
|
|
218
|
-
): string {
|
|
219
|
-
if (this.taskNameIndex.has(name)) {
|
|
220
|
-
throw new Error(`Cron task "${name}" is already registered`);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return this.schedule(definition.expression, definition.handler, {
|
|
224
|
-
name,
|
|
225
|
-
enabled: definition.enabled,
|
|
226
|
-
pluginName,
|
|
227
|
-
description: definition.description,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
registerTasks(
|
|
232
|
-
tasks: Record<string, CronTaskDefinition>,
|
|
233
|
-
pluginName?: string
|
|
234
|
-
): string[] {
|
|
235
|
-
const ids: string[] = [];
|
|
236
|
-
for (const [name, definition] of Object.entries(tasks)) {
|
|
237
|
-
ids.push(this.registerTask(name, definition, pluginName));
|
|
238
|
-
}
|
|
239
|
-
return ids;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
167
|
unschedule(taskId: string): boolean {
|
|
243
|
-
const task = this.tasks.get(taskId);
|
|
244
|
-
if (task) {
|
|
245
|
-
this.taskNameIndex.delete(task.name);
|
|
246
|
-
}
|
|
247
168
|
return this.tasks.delete(taskId);
|
|
248
169
|
}
|
|
249
170
|
|
|
@@ -270,15 +191,9 @@ class CronImpl implements Cron {
|
|
|
270
191
|
enabled: t.enabled,
|
|
271
192
|
lastRun: t.lastRun,
|
|
272
193
|
nextRun: t.nextRun,
|
|
273
|
-
pluginName: t.pluginName,
|
|
274
|
-
description: t.description,
|
|
275
194
|
}));
|
|
276
195
|
}
|
|
277
196
|
|
|
278
|
-
listByPlugin(pluginName: string): CronTask[] {
|
|
279
|
-
return this.list().filter(task => task.pluginName === pluginName);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
197
|
get(taskId: string): CronTask | undefined {
|
|
283
198
|
const task = this.tasks.get(taskId);
|
|
284
199
|
if (!task) return undefined;
|
|
@@ -290,17 +205,9 @@ class CronImpl implements Cron {
|
|
|
290
205
|
enabled: task.enabled,
|
|
291
206
|
lastRun: task.lastRun,
|
|
292
207
|
nextRun: task.nextRun,
|
|
293
|
-
pluginName: task.pluginName,
|
|
294
|
-
description: task.description,
|
|
295
208
|
};
|
|
296
209
|
}
|
|
297
210
|
|
|
298
|
-
getByName(name: string): CronTask | undefined {
|
|
299
|
-
const id = this.taskNameIndex.get(name);
|
|
300
|
-
if (!id) return undefined;
|
|
301
|
-
return this.get(id);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
211
|
async trigger(taskId: string): Promise<void> {
|
|
305
212
|
const task = this.tasks.get(taskId);
|
|
306
213
|
if (!task) throw new Error(`Task ${taskId} not found`);
|
package/src/core/errors.ts
CHANGED
|
@@ -3,9 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides throwable HTTP errors that are automatically caught by the server
|
|
5
5
|
* and converted to proper HTTP responses with status codes and error bodies.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* throw ctx.errors.BadRequest("Invalid user ID");
|
|
9
|
+
* throw ctx.errors.NotFound("User not found", { userId: 123 });
|
|
10
|
+
* throw ctx.errors.Unauthorized("Please log in");
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
// ==========================================
|
|
14
|
+
// Base HTTP Error
|
|
15
|
+
// ==========================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Base class for all HTTP errors.
|
|
19
|
+
* Thrown errors are caught by the server and converted to HTTP responses.
|
|
20
|
+
*/
|
|
9
21
|
export class HttpError extends Error {
|
|
10
22
|
/** HTTP status code */
|
|
11
23
|
readonly status: number;
|
|
@@ -56,6 +68,10 @@ export class HttpError extends Error {
|
|
|
56
68
|
}
|
|
57
69
|
}
|
|
58
70
|
|
|
71
|
+
// ==========================================
|
|
72
|
+
// Standard HTTP Error Classes
|
|
73
|
+
// ==========================================
|
|
74
|
+
|
|
59
75
|
/** 400 Bad Request */
|
|
60
76
|
export class BadRequestError extends HttpError {
|
|
61
77
|
constructor(message: string = "Bad Request", details?: Record<string, any>, cause?: Error) {
|
|
@@ -168,35 +184,65 @@ export class GatewayTimeoutError extends HttpError {
|
|
|
168
184
|
}
|
|
169
185
|
}
|
|
170
186
|
|
|
171
|
-
|
|
187
|
+
// ==========================================
|
|
188
|
+
// Error Factory Type
|
|
189
|
+
// ==========================================
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Factory function signature for creating errors
|
|
193
|
+
*/
|
|
172
194
|
export type ErrorFactory<T extends HttpError = HttpError> = (
|
|
173
195
|
message?: string,
|
|
174
196
|
details?: Record<string, any>,
|
|
175
197
|
cause?: Error
|
|
176
198
|
) => T;
|
|
177
199
|
|
|
178
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* Base error factories available on ctx.errors
|
|
202
|
+
*/
|
|
179
203
|
export interface BaseErrorFactories {
|
|
204
|
+
/** 400 Bad Request */
|
|
180
205
|
BadRequest: ErrorFactory<BadRequestError>;
|
|
206
|
+
/** 401 Unauthorized */
|
|
181
207
|
Unauthorized: ErrorFactory<UnauthorizedError>;
|
|
208
|
+
/** 403 Forbidden */
|
|
182
209
|
Forbidden: ErrorFactory<ForbiddenError>;
|
|
210
|
+
/** 404 Not Found */
|
|
183
211
|
NotFound: ErrorFactory<NotFoundError>;
|
|
212
|
+
/** 405 Method Not Allowed */
|
|
184
213
|
MethodNotAllowed: ErrorFactory<MethodNotAllowedError>;
|
|
214
|
+
/** 409 Conflict */
|
|
185
215
|
Conflict: ErrorFactory<ConflictError>;
|
|
216
|
+
/** 410 Gone */
|
|
186
217
|
Gone: ErrorFactory<GoneError>;
|
|
218
|
+
/** 422 Unprocessable Entity */
|
|
187
219
|
UnprocessableEntity: ErrorFactory<UnprocessableEntityError>;
|
|
220
|
+
/** 429 Too Many Requests */
|
|
188
221
|
TooManyRequests: ErrorFactory<TooManyRequestsError>;
|
|
222
|
+
/** 500 Internal Server Error */
|
|
189
223
|
InternalServer: ErrorFactory<InternalServerError>;
|
|
224
|
+
/** 501 Not Implemented */
|
|
190
225
|
NotImplemented: ErrorFactory<NotImplementedError>;
|
|
226
|
+
/** 502 Bad Gateway */
|
|
191
227
|
BadGateway: ErrorFactory<BadGatewayError>;
|
|
228
|
+
/** 503 Service Unavailable */
|
|
192
229
|
ServiceUnavailable: ErrorFactory<ServiceUnavailableError>;
|
|
230
|
+
/** 504 Gateway Timeout */
|
|
193
231
|
GatewayTimeout: ErrorFactory<GatewayTimeoutError>;
|
|
194
232
|
}
|
|
195
233
|
|
|
196
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* Extended error factories (augmented by plugins and user)
|
|
236
|
+
*/
|
|
197
237
|
export interface ErrorFactories extends BaseErrorFactories {}
|
|
198
238
|
|
|
199
|
-
|
|
239
|
+
// ==========================================
|
|
240
|
+
// Custom Error Definition
|
|
241
|
+
// ==========================================
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Definition for a custom error type
|
|
245
|
+
*/
|
|
200
246
|
export interface CustomErrorDefinition {
|
|
201
247
|
/** HTTP status code */
|
|
202
248
|
status: number;
|
|
@@ -206,9 +252,15 @@ export interface CustomErrorDefinition {
|
|
|
206
252
|
defaultMessage?: string;
|
|
207
253
|
}
|
|
208
254
|
|
|
209
|
-
/**
|
|
255
|
+
/**
|
|
256
|
+
* Registry of custom error definitions
|
|
257
|
+
*/
|
|
210
258
|
export type CustomErrorRegistry = Record<string, CustomErrorDefinition>;
|
|
211
259
|
|
|
260
|
+
// ==========================================
|
|
261
|
+
// Error Service
|
|
262
|
+
// ==========================================
|
|
263
|
+
|
|
212
264
|
export interface ErrorsConfig {
|
|
213
265
|
/** Include stack traces in error responses (default: false in production) */
|
|
214
266
|
includeStackTrace?: boolean;
|
|
@@ -217,7 +269,9 @@ export interface ErrorsConfig {
|
|
|
217
269
|
}
|
|
218
270
|
|
|
219
271
|
export interface Errors extends ErrorFactories {
|
|
220
|
-
/**
|
|
272
|
+
/**
|
|
273
|
+
* Create a custom HTTP error
|
|
274
|
+
*/
|
|
221
275
|
custom(
|
|
222
276
|
status: number,
|
|
223
277
|
code: string,
|
|
@@ -226,17 +280,23 @@ export interface Errors extends ErrorFactories {
|
|
|
226
280
|
cause?: Error
|
|
227
281
|
): HttpError;
|
|
228
282
|
|
|
229
|
-
/**
|
|
283
|
+
/**
|
|
284
|
+
* Check if an error is an HttpError
|
|
285
|
+
*/
|
|
230
286
|
isHttpError(error: unknown): error is HttpError;
|
|
231
287
|
|
|
232
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* Register a custom error type
|
|
290
|
+
*/
|
|
233
291
|
register<K extends string>(
|
|
234
292
|
name: K,
|
|
235
293
|
definition: CustomErrorDefinition
|
|
236
294
|
): void;
|
|
237
295
|
}
|
|
238
296
|
|
|
239
|
-
/**
|
|
297
|
+
/**
|
|
298
|
+
* Create the errors service
|
|
299
|
+
*/
|
|
240
300
|
export function createErrors(config: ErrorsConfig = {}): Errors {
|
|
241
301
|
const customErrors = new Map<string, CustomErrorDefinition>(
|
|
242
302
|
Object.entries(config.customErrors || {})
|
|
@@ -307,7 +367,14 @@ export function createErrors(config: ErrorsConfig = {}): Errors {
|
|
|
307
367
|
return errors;
|
|
308
368
|
}
|
|
309
369
|
|
|
310
|
-
|
|
370
|
+
// ==========================================
|
|
371
|
+
// Validation Error Helper
|
|
372
|
+
// ==========================================
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Create a BadRequestError with validation details
|
|
376
|
+
* Useful for Zod validation errors
|
|
377
|
+
*/
|
|
311
378
|
export function createValidationError(
|
|
312
379
|
issues: Array<{ path: (string | number)[]; message: string }>
|
|
313
380
|
): BadRequestError {
|
package/src/core/events.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// Core Events Service
|
|
2
|
-
// Pub/sub async event queue
|
|
3
|
-
|
|
4
|
-
import { z } from "zod";
|
|
2
|
+
// Pub/sub async event queue
|
|
5
3
|
|
|
6
4
|
export interface EventHandler<T = any> {
|
|
7
5
|
(data: T): void | Promise<void>;
|
|
@@ -28,15 +26,6 @@ export interface EventsConfig {
|
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
export interface Events {
|
|
31
|
-
// Schema registration (strict - events must be registered before emit)
|
|
32
|
-
register<T>(event: string, schema: z.ZodType<T>): void;
|
|
33
|
-
registerMany(schemas: Record<string, z.ZodType<any>>): void;
|
|
34
|
-
isRegistered(event: string): boolean;
|
|
35
|
-
getSchema(event: string): z.ZodType<any> | undefined;
|
|
36
|
-
/** List all registered event names */
|
|
37
|
-
listRegistered(): string[];
|
|
38
|
-
|
|
39
|
-
// Event operations
|
|
40
29
|
emit<T = any>(event: string, data: T): Promise<void>;
|
|
41
30
|
on<T = any>(event: string, handler: EventHandler<T>): Subscription;
|
|
42
31
|
once<T = any>(event: string, handler: EventHandler<T>): Subscription;
|
|
@@ -78,47 +67,12 @@ export class MemoryEventAdapter implements EventAdapter {
|
|
|
78
67
|
class EventsImpl implements Events {
|
|
79
68
|
private handlers = new Map<string, Set<EventHandler>>();
|
|
80
69
|
private adapter: EventAdapter;
|
|
81
|
-
private schemas = new Map<string, z.ZodType<any>>();
|
|
82
70
|
|
|
83
71
|
constructor(config: EventsConfig = {}) {
|
|
84
72
|
this.adapter = config.adapter ?? new MemoryEventAdapter(config.maxHistorySize);
|
|
85
73
|
}
|
|
86
74
|
|
|
87
|
-
register<T>(event: string, schema: z.ZodType<T>): void {
|
|
88
|
-
this.schemas.set(event, schema);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
registerMany(schemas: Record<string, z.ZodType<any>>): void {
|
|
92
|
-
for (const [name, schema] of Object.entries(schemas)) {
|
|
93
|
-
this.schemas.set(name, schema);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
isRegistered(event: string): boolean {
|
|
98
|
-
return this.schemas.has(event);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
getSchema(event: string): z.ZodType<any> | undefined {
|
|
102
|
-
return this.schemas.get(event);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
listRegistered(): string[] {
|
|
106
|
-
return Array.from(this.schemas.keys());
|
|
107
|
-
}
|
|
108
|
-
|
|
109
75
|
async emit<T = any>(event: string, data: T): Promise<void> {
|
|
110
|
-
// Strict mode: event must be registered
|
|
111
|
-
const schema = this.schemas.get(event);
|
|
112
|
-
if (!schema) {
|
|
113
|
-
throw new Error(`Event '${event}' is not registered. Register it first with events.register() or in plugin events config.`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Validate data against schema
|
|
117
|
-
const result = schema.safeParse(data);
|
|
118
|
-
if (!result.success) {
|
|
119
|
-
throw new Error(`Event '${event}' validation failed: ${result.error.message}`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
76
|
// Store in adapter (for history/persistence)
|
|
123
77
|
await this.adapter.publish(event, data);
|
|
124
78
|
|
package/src/core/index.ts
CHANGED
|
@@ -12,7 +12,6 @@ export {
|
|
|
12
12
|
|
|
13
13
|
export {
|
|
14
14
|
type Cache,
|
|
15
|
-
type NamespacedCache,
|
|
16
15
|
type CacheAdapter,
|
|
17
16
|
type CacheConfig,
|
|
18
17
|
MemoryCacheAdapter,
|
|
@@ -33,7 +32,6 @@ export {
|
|
|
33
32
|
export {
|
|
34
33
|
type Cron,
|
|
35
34
|
type CronTask,
|
|
36
|
-
type CronTaskDefinition,
|
|
37
35
|
type CronConfig,
|
|
38
36
|
createCron,
|
|
39
37
|
} from "./cron";
|
|
@@ -43,8 +41,6 @@ export {
|
|
|
43
41
|
type Job,
|
|
44
42
|
type JobStatus,
|
|
45
43
|
type JobHandler,
|
|
46
|
-
type JobDefinition,
|
|
47
|
-
type RegisteredJob,
|
|
48
44
|
type JobAdapter,
|
|
49
45
|
type JobsConfig,
|
|
50
46
|
MemoryJobAdapter,
|
package/src/core/jobs.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// Background job queue with scheduling
|
|
3
3
|
|
|
4
4
|
import type { Events } from "./events";
|
|
5
|
-
import type { z } from "zod";
|
|
6
5
|
|
|
7
6
|
export type JobStatus = "pending" | "running" | "completed" | "failed" | "scheduled";
|
|
8
7
|
|
|
@@ -43,53 +42,8 @@ export interface JobsConfig {
|
|
|
43
42
|
maxAttempts?: number; // Default retry attempts, default 3
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
export interface JobDefinition<T = any, R = any> {
|
|
47
|
-
/** Zod schema for validating job payloads */
|
|
48
|
-
schema: z.ZodType<T>;
|
|
49
|
-
/** Job handler function */
|
|
50
|
-
handler: JobHandler<T, R>;
|
|
51
|
-
/** Optional description */
|
|
52
|
-
description?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface RegisteredJob {
|
|
56
|
-
name: string;
|
|
57
|
-
schema: z.ZodType<any>;
|
|
58
|
-
handler: JobHandler;
|
|
59
|
-
description?: string;
|
|
60
|
-
/** Plugin that registered this job */
|
|
61
|
-
pluginName?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
45
|
export interface Jobs {
|
|
65
|
-
/** Register a job with handler only (backward compatible, but no payload validation) */
|
|
66
46
|
register<T = any, R = any>(name: string, handler: JobHandler<T, R>): void;
|
|
67
|
-
|
|
68
|
-
/** Register a job with schema validation and handler */
|
|
69
|
-
registerJob<T = any, R = any>(
|
|
70
|
-
name: string,
|
|
71
|
-
definition: JobDefinition<T, R>,
|
|
72
|
-
pluginName?: string
|
|
73
|
-
): void;
|
|
74
|
-
|
|
75
|
-
/** Register multiple jobs at once (for plugins) */
|
|
76
|
-
registerJobs(
|
|
77
|
-
jobs: Record<string, JobDefinition>,
|
|
78
|
-
pluginName?: string
|
|
79
|
-
): void;
|
|
80
|
-
|
|
81
|
-
/** Check if a job is registered */
|
|
82
|
-
isRegistered(name: string): boolean;
|
|
83
|
-
|
|
84
|
-
/** Get registered job info */
|
|
85
|
-
getRegisteredJob(name: string): RegisteredJob | undefined;
|
|
86
|
-
|
|
87
|
-
/** List all registered jobs */
|
|
88
|
-
listRegisteredJobs(): RegisteredJob[];
|
|
89
|
-
|
|
90
|
-
/** List registered jobs by plugin */
|
|
91
|
-
listJobsByPlugin(pluginName: string): RegisteredJob[];
|
|
92
|
-
|
|
93
47
|
enqueue<T = any>(name: string, data: T, options?: { maxAttempts?: number }): Promise<string>;
|
|
94
48
|
schedule<T = any>(name: string, data: T, runAt: Date, options?: { maxAttempts?: number }): Promise<string>;
|
|
95
49
|
get(jobId: string): Promise<Job | null>;
|
|
@@ -162,7 +116,6 @@ class JobsImpl implements Jobs {
|
|
|
162
116
|
private adapter: JobAdapter;
|
|
163
117
|
private events?: Events;
|
|
164
118
|
private handlers = new Map<string, JobHandler>();
|
|
165
|
-
private registeredJobs = new Map<string, RegisteredJob>();
|
|
166
119
|
private running = false;
|
|
167
120
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
168
121
|
private activeJobs = 0;
|
|
@@ -185,67 +138,11 @@ class JobsImpl implements Jobs {
|
|
|
185
138
|
this.handlers.set(name, handler);
|
|
186
139
|
}
|
|
187
140
|
|
|
188
|
-
registerJob<T = any, R = any>(
|
|
189
|
-
name: string,
|
|
190
|
-
definition: JobDefinition<T, R>,
|
|
191
|
-
pluginName?: string
|
|
192
|
-
): void {
|
|
193
|
-
if (this.registeredJobs.has(name)) {
|
|
194
|
-
throw new Error(`Job "${name}" is already registered`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
this.registeredJobs.set(name, {
|
|
198
|
-
name,
|
|
199
|
-
schema: definition.schema,
|
|
200
|
-
handler: definition.handler,
|
|
201
|
-
description: definition.description,
|
|
202
|
-
pluginName,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Also register the handler in the handlers map for backward compatibility
|
|
206
|
-
this.handlers.set(name, definition.handler);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
registerJobs(
|
|
210
|
-
jobs: Record<string, JobDefinition>,
|
|
211
|
-
pluginName?: string
|
|
212
|
-
): void {
|
|
213
|
-
for (const [name, definition] of Object.entries(jobs)) {
|
|
214
|
-
this.registerJob(name, definition, pluginName);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
isRegistered(name: string): boolean {
|
|
219
|
-
return this.registeredJobs.has(name) || this.handlers.has(name);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
getRegisteredJob(name: string): RegisteredJob | undefined {
|
|
223
|
-
return this.registeredJobs.get(name);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
listRegisteredJobs(): RegisteredJob[] {
|
|
227
|
-
return Array.from(this.registeredJobs.values());
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
listJobsByPlugin(pluginName: string): RegisteredJob[] {
|
|
231
|
-
return Array.from(this.registeredJobs.values())
|
|
232
|
-
.filter(job => job.pluginName === pluginName);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
141
|
async enqueue<T = any>(name: string, data: T, options: { maxAttempts?: number } = {}): Promise<string> {
|
|
236
142
|
if (!this.handlers.has(name)) {
|
|
237
143
|
throw new Error(`No handler registered for job "${name}"`);
|
|
238
144
|
}
|
|
239
145
|
|
|
240
|
-
// Validate payload if schema is registered
|
|
241
|
-
const registered = this.registeredJobs.get(name);
|
|
242
|
-
if (registered) {
|
|
243
|
-
const result = registered.schema.safeParse(data);
|
|
244
|
-
if (!result.success) {
|
|
245
|
-
throw new Error(`Job "${name}" payload validation failed: ${result.error.message}`);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
146
|
const job = await this.adapter.create({
|
|
250
147
|
name,
|
|
251
148
|
data,
|
|
@@ -268,15 +165,6 @@ class JobsImpl implements Jobs {
|
|
|
268
165
|
throw new Error(`No handler registered for job "${name}"`);
|
|
269
166
|
}
|
|
270
167
|
|
|
271
|
-
// Validate payload if schema is registered
|
|
272
|
-
const registered = this.registeredJobs.get(name);
|
|
273
|
-
if (registered) {
|
|
274
|
-
const result = registered.schema.safeParse(data);
|
|
275
|
-
if (!result.success) {
|
|
276
|
-
throw new Error(`Job "${name}" payload validation failed: ${result.error.message}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
168
|
const job = await this.adapter.create({
|
|
281
169
|
name,
|
|
282
170
|
data,
|