@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,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
+ }
@@ -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
+ }