@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.
- package/README.md +15 -0
- package/cli/commands/generate.ts +461 -0
- package/cli/commands/init.ts +287 -0
- package/cli/commands/interactive.ts +223 -0
- package/cli/commands/plugin.ts +192 -0
- package/cli/donkeylabs +100 -0
- package/cli/index.ts +100 -0
- package/mcp/donkeylabs-mcp +3238 -0
- package/mcp/server.ts +3238 -0
- package/package.json +74 -0
- package/src/client/base.ts +481 -0
- package/src/client/index.ts +150 -0
- package/src/core/cache.ts +183 -0
- package/src/core/cron.ts +255 -0
- package/src/core/errors.ts +320 -0
- package/src/core/events.ts +163 -0
- package/src/core/index.ts +94 -0
- package/src/core/jobs.ts +334 -0
- package/src/core/logger.ts +131 -0
- package/src/core/rate-limiter.ts +193 -0
- package/src/core/sse.ts +210 -0
- package/src/core.ts +428 -0
- package/src/handlers.ts +87 -0
- package/src/harness.ts +70 -0
- package/src/index.ts +38 -0
- package/src/middleware.ts +34 -0
- package/src/registry.ts +13 -0
- package/src/router.ts +155 -0
- package/src/server.ts +233 -0
- package/templates/init/donkeylabs.config.ts.template +14 -0
- package/templates/init/index.ts.template +41 -0
- package/templates/plugin/index.ts.template +25 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Core Cache Service
|
|
2
|
+
// Key-value store with TTL, in-memory by default
|
|
3
|
+
|
|
4
|
+
export interface CacheAdapter {
|
|
5
|
+
get<T>(key: string): Promise<T | null>;
|
|
6
|
+
set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
|
|
7
|
+
delete(key: string): Promise<boolean>;
|
|
8
|
+
has(key: string): Promise<boolean>;
|
|
9
|
+
clear(): Promise<void>;
|
|
10
|
+
keys(pattern?: string): Promise<string[]>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CacheConfig {
|
|
14
|
+
adapter?: CacheAdapter;
|
|
15
|
+
defaultTtlMs?: number; // Default: 5 minutes
|
|
16
|
+
maxSize?: number; // Default: 1000 items (LRU eviction)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Cache {
|
|
20
|
+
get<T>(key: string): Promise<T | null>;
|
|
21
|
+
set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
|
|
22
|
+
delete(key: string): Promise<boolean>;
|
|
23
|
+
has(key: string): Promise<boolean>;
|
|
24
|
+
clear(): Promise<void>;
|
|
25
|
+
keys(pattern?: string): Promise<string[]>;
|
|
26
|
+
getOrSet<T>(key: string, factory: () => Promise<T>, ttlMs?: number): Promise<T>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CacheEntry<T> {
|
|
30
|
+
value: T;
|
|
31
|
+
expiresAt: number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// In-memory cache adapter with LRU eviction
|
|
35
|
+
export class MemoryCacheAdapter implements CacheAdapter {
|
|
36
|
+
private cache = new Map<string, CacheEntry<any>>();
|
|
37
|
+
private accessOrder: string[] = [];
|
|
38
|
+
private maxSize: number;
|
|
39
|
+
|
|
40
|
+
constructor(maxSize: number = 1000) {
|
|
41
|
+
this.maxSize = maxSize;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private isExpired(entry: CacheEntry<any>): boolean {
|
|
45
|
+
return entry.expiresAt !== null && Date.now() > entry.expiresAt;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private updateAccessOrder(key: string): void {
|
|
49
|
+
const index = this.accessOrder.indexOf(key);
|
|
50
|
+
if (index > -1) {
|
|
51
|
+
this.accessOrder.splice(index, 1);
|
|
52
|
+
}
|
|
53
|
+
this.accessOrder.push(key);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private evictIfNeeded(): void {
|
|
57
|
+
while (this.cache.size >= this.maxSize && this.accessOrder.length > 0) {
|
|
58
|
+
const oldest = this.accessOrder.shift();
|
|
59
|
+
if (oldest) {
|
|
60
|
+
this.cache.delete(oldest);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async get<T>(key: string): Promise<T | null> {
|
|
66
|
+
const entry = this.cache.get(key);
|
|
67
|
+
if (!entry) return null;
|
|
68
|
+
|
|
69
|
+
if (this.isExpired(entry)) {
|
|
70
|
+
this.cache.delete(key);
|
|
71
|
+
const index = this.accessOrder.indexOf(key);
|
|
72
|
+
if (index > -1) this.accessOrder.splice(index, 1);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.updateAccessOrder(key);
|
|
77
|
+
return entry.value as T;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async set<T>(key: string, value: T, ttlMs?: number): Promise<void> {
|
|
81
|
+
this.evictIfNeeded();
|
|
82
|
+
|
|
83
|
+
const entry: CacheEntry<T> = {
|
|
84
|
+
value,
|
|
85
|
+
expiresAt: ttlMs ? Date.now() + ttlMs : null,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
this.cache.set(key, entry);
|
|
89
|
+
this.updateAccessOrder(key);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async delete(key: string): Promise<boolean> {
|
|
93
|
+
const existed = this.cache.has(key);
|
|
94
|
+
this.cache.delete(key);
|
|
95
|
+
const index = this.accessOrder.indexOf(key);
|
|
96
|
+
if (index > -1) this.accessOrder.splice(index, 1);
|
|
97
|
+
return existed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async has(key: string): Promise<boolean> {
|
|
101
|
+
const entry = this.cache.get(key);
|
|
102
|
+
if (!entry) return false;
|
|
103
|
+
|
|
104
|
+
if (this.isExpired(entry)) {
|
|
105
|
+
this.cache.delete(key);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async clear(): Promise<void> {
|
|
113
|
+
this.cache.clear();
|
|
114
|
+
this.accessOrder = [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async keys(pattern?: string): Promise<string[]> {
|
|
118
|
+
const allKeys: string[] = [];
|
|
119
|
+
|
|
120
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
121
|
+
if (!this.isExpired(entry)) {
|
|
122
|
+
if (!pattern || this.matchPattern(key, pattern)) {
|
|
123
|
+
allKeys.push(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return allKeys;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private matchPattern(key: string, pattern: string): boolean {
|
|
132
|
+
// Simple glob pattern matching (* as wildcard)
|
|
133
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
134
|
+
return regex.test(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
class CacheImpl implements Cache {
|
|
139
|
+
private adapter: CacheAdapter;
|
|
140
|
+
private defaultTtlMs: number;
|
|
141
|
+
|
|
142
|
+
constructor(config: CacheConfig = {}) {
|
|
143
|
+
this.adapter = config.adapter ?? new MemoryCacheAdapter(config.maxSize);
|
|
144
|
+
this.defaultTtlMs = config.defaultTtlMs ?? 5 * 60 * 1000; // 5 minutes
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async get<T>(key: string): Promise<T | null> {
|
|
148
|
+
return this.adapter.get<T>(key);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async set<T>(key: string, value: T, ttlMs?: number): Promise<void> {
|
|
152
|
+
return this.adapter.set(key, value, ttlMs ?? this.defaultTtlMs);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async delete(key: string): Promise<boolean> {
|
|
156
|
+
return this.adapter.delete(key);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async has(key: string): Promise<boolean> {
|
|
160
|
+
return this.adapter.has(key);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async clear(): Promise<void> {
|
|
164
|
+
return this.adapter.clear();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async keys(pattern?: string): Promise<string[]> {
|
|
168
|
+
return this.adapter.keys(pattern);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async getOrSet<T>(key: string, factory: () => Promise<T>, ttlMs?: number): Promise<T> {
|
|
172
|
+
const existing = await this.get<T>(key);
|
|
173
|
+
if (existing !== null) return existing;
|
|
174
|
+
|
|
175
|
+
const value = await factory();
|
|
176
|
+
await this.set(key, value, ttlMs);
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createCache(config?: CacheConfig): Cache {
|
|
182
|
+
return new CacheImpl(config);
|
|
183
|
+
}
|
package/src/core/cron.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Core Cron Service
|
|
2
|
+
// Schedule recurring tasks with cron expressions
|
|
3
|
+
|
|
4
|
+
export interface CronTask {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
expression: string;
|
|
8
|
+
handler: () => void | Promise<void>;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
lastRun?: Date;
|
|
11
|
+
nextRun?: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CronConfig {
|
|
15
|
+
timezone?: string; // For future use
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Cron {
|
|
19
|
+
schedule(
|
|
20
|
+
expression: string,
|
|
21
|
+
handler: () => void | Promise<void>,
|
|
22
|
+
options?: { name?: string; enabled?: boolean }
|
|
23
|
+
): string;
|
|
24
|
+
unschedule(taskId: string): boolean;
|
|
25
|
+
pause(taskId: string): void;
|
|
26
|
+
resume(taskId: string): void;
|
|
27
|
+
list(): CronTask[];
|
|
28
|
+
get(taskId: string): CronTask | undefined;
|
|
29
|
+
trigger(taskId: string): Promise<void>;
|
|
30
|
+
start(): void;
|
|
31
|
+
stop(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Simple cron expression parser
|
|
35
|
+
// Supports: * (any), specific values, ranges (1-5), steps (*/5)
|
|
36
|
+
// Format: second minute hour dayOfMonth month dayOfWeek
|
|
37
|
+
// Also supports 5-field format (minute hour dayOfMonth month dayOfWeek)
|
|
38
|
+
class CronExpression {
|
|
39
|
+
private fields: [number[], number[], number[], number[], number[], number[]];
|
|
40
|
+
|
|
41
|
+
constructor(expression: string) {
|
|
42
|
+
const parts = expression.trim().split(/\s+/);
|
|
43
|
+
|
|
44
|
+
// Support both 5-field and 6-field cron
|
|
45
|
+
if (parts.length === 5) {
|
|
46
|
+
// minute hour dayOfMonth month dayOfWeek
|
|
47
|
+
this.fields = [
|
|
48
|
+
[0], // seconds (always 0)
|
|
49
|
+
this.parseField(parts[0]!, 0, 59), // minutes
|
|
50
|
+
this.parseField(parts[1]!, 0, 23), // hours
|
|
51
|
+
this.parseField(parts[2]!, 1, 31), // day of month
|
|
52
|
+
this.parseField(parts[3]!, 1, 12), // month
|
|
53
|
+
this.parseField(parts[4]!, 0, 6), // day of week
|
|
54
|
+
];
|
|
55
|
+
} else if (parts.length === 6) {
|
|
56
|
+
// second minute hour dayOfMonth month dayOfWeek
|
|
57
|
+
this.fields = [
|
|
58
|
+
this.parseField(parts[0]!, 0, 59), // seconds
|
|
59
|
+
this.parseField(parts[1]!, 0, 59), // minutes
|
|
60
|
+
this.parseField(parts[2]!, 0, 23), // hours
|
|
61
|
+
this.parseField(parts[3]!, 1, 31), // day of month
|
|
62
|
+
this.parseField(parts[4]!, 1, 12), // month
|
|
63
|
+
this.parseField(parts[5]!, 0, 6), // day of week
|
|
64
|
+
];
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error(`Invalid cron expression: ${expression}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private parseField(field: string, min: number, max: number): number[] {
|
|
71
|
+
const values: Set<number> = new Set();
|
|
72
|
+
|
|
73
|
+
for (const part of field.split(",")) {
|
|
74
|
+
if (part === "*") {
|
|
75
|
+
for (let i = min; i <= max; i++) values.add(i);
|
|
76
|
+
} else if (part.includes("/")) {
|
|
77
|
+
const [range, stepStr] = part.split("/");
|
|
78
|
+
const step = parseInt(stepStr!, 10);
|
|
79
|
+
const start = range === "*" ? min : parseInt(range!, 10);
|
|
80
|
+
for (let i = start; i <= max; i += step) values.add(i);
|
|
81
|
+
} else if (part.includes("-")) {
|
|
82
|
+
const [startStr, endStr] = part.split("-");
|
|
83
|
+
const start = parseInt(startStr!, 10);
|
|
84
|
+
const end = parseInt(endStr!, 10);
|
|
85
|
+
for (let i = start; i <= end; i++) values.add(i);
|
|
86
|
+
} else {
|
|
87
|
+
values.add(parseInt(part, 10));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Array.from(values).sort((a, b) => a - b);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
matches(date: Date): boolean {
|
|
95
|
+
const second = date.getSeconds();
|
|
96
|
+
const minute = date.getMinutes();
|
|
97
|
+
const hour = date.getHours();
|
|
98
|
+
const dayOfMonth = date.getDate();
|
|
99
|
+
const month = date.getMonth() + 1;
|
|
100
|
+
const dayOfWeek = date.getDay();
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
this.fields[0].includes(second) &&
|
|
104
|
+
this.fields[1].includes(minute) &&
|
|
105
|
+
this.fields[2].includes(hour) &&
|
|
106
|
+
this.fields[3].includes(dayOfMonth) &&
|
|
107
|
+
this.fields[4].includes(month) &&
|
|
108
|
+
this.fields[5].includes(dayOfWeek)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getNextRun(from: Date = new Date()): Date {
|
|
113
|
+
const next = new Date(from);
|
|
114
|
+
next.setMilliseconds(0);
|
|
115
|
+
next.setSeconds(next.getSeconds() + 1);
|
|
116
|
+
|
|
117
|
+
// Search up to 1 year ahead
|
|
118
|
+
const maxIterations = 366 * 24 * 60 * 60;
|
|
119
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
120
|
+
if (this.matches(next)) {
|
|
121
|
+
return next;
|
|
122
|
+
}
|
|
123
|
+
next.setSeconds(next.getSeconds() + 1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
throw new Error("Could not find next run time within 1 year");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface InternalCronTask extends CronTask {
|
|
131
|
+
_cronExpr: CronExpression;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
class CronImpl implements Cron {
|
|
135
|
+
private tasks = new Map<string, InternalCronTask>();
|
|
136
|
+
private running = false;
|
|
137
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
138
|
+
private taskCounter = 0;
|
|
139
|
+
|
|
140
|
+
constructor(_config: CronConfig = {}) {
|
|
141
|
+
// timezone handling for future use
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
schedule(
|
|
145
|
+
expression: string,
|
|
146
|
+
handler: () => void | Promise<void>,
|
|
147
|
+
options: { name?: string; enabled?: boolean } = {}
|
|
148
|
+
): string {
|
|
149
|
+
const id = `cron_${++this.taskCounter}_${Date.now()}`;
|
|
150
|
+
const cronExpr = new CronExpression(expression);
|
|
151
|
+
|
|
152
|
+
const task: InternalCronTask = {
|
|
153
|
+
id,
|
|
154
|
+
name: options.name ?? id,
|
|
155
|
+
expression,
|
|
156
|
+
handler,
|
|
157
|
+
enabled: options.enabled ?? true,
|
|
158
|
+
nextRun: cronExpr.getNextRun(),
|
|
159
|
+
_cronExpr: cronExpr,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
this.tasks.set(id, task);
|
|
163
|
+
|
|
164
|
+
return id;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
unschedule(taskId: string): boolean {
|
|
168
|
+
return this.tasks.delete(taskId);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
pause(taskId: string): void {
|
|
172
|
+
const task = this.tasks.get(taskId);
|
|
173
|
+
if (task) task.enabled = false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
resume(taskId: string): void {
|
|
177
|
+
const task = this.tasks.get(taskId);
|
|
178
|
+
if (task) {
|
|
179
|
+
task.enabled = true;
|
|
180
|
+
const cronExpr = new CronExpression(task.expression);
|
|
181
|
+
task.nextRun = cronExpr.getNextRun();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
list(): CronTask[] {
|
|
186
|
+
return Array.from(this.tasks.values()).map(t => ({
|
|
187
|
+
id: t.id,
|
|
188
|
+
name: t.name,
|
|
189
|
+
expression: t.expression,
|
|
190
|
+
handler: t.handler,
|
|
191
|
+
enabled: t.enabled,
|
|
192
|
+
lastRun: t.lastRun,
|
|
193
|
+
nextRun: t.nextRun,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
get(taskId: string): CronTask | undefined {
|
|
198
|
+
const task = this.tasks.get(taskId);
|
|
199
|
+
if (!task) return undefined;
|
|
200
|
+
return {
|
|
201
|
+
id: task.id,
|
|
202
|
+
name: task.name,
|
|
203
|
+
expression: task.expression,
|
|
204
|
+
handler: task.handler,
|
|
205
|
+
enabled: task.enabled,
|
|
206
|
+
lastRun: task.lastRun,
|
|
207
|
+
nextRun: task.nextRun,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async trigger(taskId: string): Promise<void> {
|
|
212
|
+
const task = this.tasks.get(taskId);
|
|
213
|
+
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
214
|
+
|
|
215
|
+
task.lastRun = new Date();
|
|
216
|
+
await task.handler();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
start(): void {
|
|
220
|
+
if (this.running) return;
|
|
221
|
+
this.running = true;
|
|
222
|
+
|
|
223
|
+
// Check every second
|
|
224
|
+
this.timer = setInterval(() => {
|
|
225
|
+
const now = new Date();
|
|
226
|
+
|
|
227
|
+
for (const task of this.tasks.values()) {
|
|
228
|
+
if (!task.enabled) continue;
|
|
229
|
+
|
|
230
|
+
const cronExpr = new CronExpression(task.expression);
|
|
231
|
+
if (cronExpr.matches(now)) {
|
|
232
|
+
task.lastRun = now;
|
|
233
|
+
task.nextRun = cronExpr.getNextRun(now);
|
|
234
|
+
|
|
235
|
+
// Execute handler (fire and forget, but log errors)
|
|
236
|
+
Promise.resolve(task.handler()).catch(err => {
|
|
237
|
+
console.error(`[Cron] Task "${task.name}" failed:`, err);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}, 1000);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async stop(): Promise<void> {
|
|
245
|
+
this.running = false;
|
|
246
|
+
if (this.timer) {
|
|
247
|
+
clearInterval(this.timer);
|
|
248
|
+
this.timer = null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function createCron(config?: CronConfig): Cron {
|
|
254
|
+
return new CronImpl(config);
|
|
255
|
+
}
|