@donkeylabs/server 0.5.0 → 0.6.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/docs/external-jobs.md +131 -11
- package/docs/router.md +93 -0
- package/examples/external-jobs/python/donkeylabs_job.py +366 -0
- package/examples/external-jobs/shell/donkeylabs-job.sh +264 -0
- package/examples/external-jobs/shell/example-job.sh +47 -0
- package/package.json +3 -2
- package/src/client/base.ts +6 -4
- package/src/core/external-job-socket.ts +142 -21
- package/src/core/index.ts +29 -0
- package/src/core/job-adapter-sqlite.ts +287 -0
- package/src/core/jobs.ts +36 -3
- package/src/core/process-adapter-sqlite.ts +282 -0
- package/src/core/process-socket.ts +521 -0
- package/src/core/processes.ts +758 -0
- package/src/core.ts +75 -4
- package/src/harness.ts +3 -0
- package/src/index.ts +12 -0
- package/src/server.ts +32 -3
package/src/core.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { sql, type Kysely } from "kysely";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { z } from "zod";
|
|
@@ -11,6 +11,7 @@ import type { SSE } from "./core/sse";
|
|
|
11
11
|
import type { RateLimiter } from "./core/rate-limiter";
|
|
12
12
|
import type { Errors, CustomErrorRegistry } from "./core/errors";
|
|
13
13
|
import type { Workflows } from "./core/workflows";
|
|
14
|
+
import type { Processes } from "./core/processes";
|
|
14
15
|
|
|
15
16
|
export interface PluginRegistry {}
|
|
16
17
|
|
|
@@ -54,6 +55,7 @@ export interface CoreServices {
|
|
|
54
55
|
rateLimiter: RateLimiter;
|
|
55
56
|
errors: Errors;
|
|
56
57
|
workflows: Workflows;
|
|
58
|
+
processes: Processes;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
@@ -355,8 +357,53 @@ export class PluginManager {
|
|
|
355
357
|
this.plugins.set(plugin.name, plugin);
|
|
356
358
|
}
|
|
357
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Ensures the migrations tracking table exists.
|
|
362
|
+
* This table tracks which migrations have been applied for each plugin.
|
|
363
|
+
*/
|
|
364
|
+
private async ensureMigrationsTable(): Promise<void> {
|
|
365
|
+
await this.core.db.schema
|
|
366
|
+
.createTable("__donkeylabs_migrations__")
|
|
367
|
+
.ifNotExists()
|
|
368
|
+
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
|
|
369
|
+
.addColumn("plugin_name", "text", (col) => col.notNull())
|
|
370
|
+
.addColumn("migration_name", "text", (col) => col.notNull())
|
|
371
|
+
.addColumn("executed_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`))
|
|
372
|
+
.execute();
|
|
373
|
+
|
|
374
|
+
// Create unique index for plugin_name + migration_name (if not exists)
|
|
375
|
+
// Using raw SQL since Kysely doesn't have ifNotExists for indexes
|
|
376
|
+
await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_migrations_unique
|
|
377
|
+
ON __donkeylabs_migrations__(plugin_name, migration_name)`.execute(this.core.db);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Checks if a migration has already been applied for a specific plugin.
|
|
382
|
+
*/
|
|
383
|
+
private async isMigrationApplied(pluginName: string, migrationName: string): Promise<boolean> {
|
|
384
|
+
const result = await sql<{ count: number }>`
|
|
385
|
+
SELECT COUNT(*) as count FROM __donkeylabs_migrations__
|
|
386
|
+
WHERE plugin_name = ${pluginName} AND migration_name = ${migrationName}
|
|
387
|
+
`.execute(this.core.db);
|
|
388
|
+
return (result.rows[0]?.count ?? 0) > 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Records that a migration has been applied for a specific plugin.
|
|
393
|
+
*/
|
|
394
|
+
private async recordMigration(pluginName: string, migrationName: string): Promise<void> {
|
|
395
|
+
await sql`
|
|
396
|
+
INSERT INTO __donkeylabs_migrations__ (plugin_name, migration_name)
|
|
397
|
+
VALUES (${pluginName}, ${migrationName})
|
|
398
|
+
`.execute(this.core.db);
|
|
399
|
+
}
|
|
400
|
+
|
|
358
401
|
async migrate(): Promise<void> {
|
|
359
402
|
console.log("Running migrations (File-System Based)...");
|
|
403
|
+
|
|
404
|
+
// Ensure the migrations tracking table exists
|
|
405
|
+
await this.ensureMigrationsTable();
|
|
406
|
+
|
|
360
407
|
const sortedPlugins = this.resolveOrder();
|
|
361
408
|
|
|
362
409
|
for (const plugin of sortedPlugins) {
|
|
@@ -392,22 +439,46 @@ export class PluginManager {
|
|
|
392
439
|
console.log(`[Migration] checking plugin: ${pluginName} at ${migrationDir}`);
|
|
393
440
|
|
|
394
441
|
for (const file of migrationFiles.sort()) {
|
|
442
|
+
// Check if this migration has already been applied
|
|
443
|
+
const isApplied = await this.isMigrationApplied(pluginName, file);
|
|
444
|
+
if (isApplied) {
|
|
445
|
+
console.log(` - Skipping (already applied): ${file}`);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
395
449
|
console.log(` - Executing migration: ${file}`);
|
|
396
450
|
const migrationPath = join(migrationDir, file);
|
|
397
|
-
|
|
451
|
+
|
|
452
|
+
let migration;
|
|
453
|
+
try {
|
|
454
|
+
migration = await import(migrationPath);
|
|
455
|
+
} catch (importError) {
|
|
456
|
+
const err = importError instanceof Error ? importError : new Error(String(importError));
|
|
457
|
+
throw new Error(`Failed to import migration ${file}: ${err.message}`);
|
|
458
|
+
}
|
|
398
459
|
|
|
399
460
|
if (migration.up) {
|
|
400
461
|
try {
|
|
401
462
|
await migration.up(this.core.db);
|
|
463
|
+
// Record successful migration
|
|
464
|
+
await this.recordMigration(pluginName, file);
|
|
402
465
|
console.log(` Success`);
|
|
403
466
|
} catch (e) {
|
|
404
467
|
console.error(` Failed to run ${file}:`, e);
|
|
468
|
+
throw e; // Stop on migration failure - don't continue with inconsistent state
|
|
405
469
|
}
|
|
406
470
|
}
|
|
407
471
|
}
|
|
408
472
|
}
|
|
409
|
-
} catch {
|
|
410
|
-
//
|
|
473
|
+
} catch (e) {
|
|
474
|
+
// Re-throw migration execution errors (they've already been logged)
|
|
475
|
+
// Only silently catch directory read errors (ENOENT)
|
|
476
|
+
const isDirectoryError = e instanceof Error &&
|
|
477
|
+
((e as NodeJS.ErrnoException).code === 'ENOENT' ||
|
|
478
|
+
(e as NodeJS.ErrnoException).code === 'ENOTDIR');
|
|
479
|
+
if (!isDirectoryError) {
|
|
480
|
+
throw e;
|
|
481
|
+
}
|
|
411
482
|
}
|
|
412
483
|
}
|
|
413
484
|
}
|
package/src/harness.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
createRateLimiter,
|
|
13
13
|
createErrors,
|
|
14
14
|
createWorkflows,
|
|
15
|
+
createProcesses,
|
|
15
16
|
} from "./core/index";
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -36,6 +37,7 @@ export async function createTestHarness(targetPlugin: Plugin, dependencies: Plug
|
|
|
36
37
|
const rateLimiter = createRateLimiter();
|
|
37
38
|
const errors = createErrors();
|
|
38
39
|
const workflows = createWorkflows({ events, jobs, sse });
|
|
40
|
+
const processes = createProcesses({ events, autoRecoverOrphans: false });
|
|
39
41
|
|
|
40
42
|
const core: CoreServices = {
|
|
41
43
|
db,
|
|
@@ -49,6 +51,7 @@ export async function createTestHarness(targetPlugin: Plugin, dependencies: Plug
|
|
|
49
51
|
rateLimiter,
|
|
50
52
|
errors,
|
|
51
53
|
workflows,
|
|
54
|
+
processes,
|
|
52
55
|
};
|
|
53
56
|
|
|
54
57
|
const manager = new PluginManager(core);
|
package/src/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ export {
|
|
|
45
45
|
type InferHandlers,
|
|
46
46
|
type InferMiddleware,
|
|
47
47
|
type InferDependencies,
|
|
48
|
+
type EventSchemas,
|
|
48
49
|
} from "./core";
|
|
49
50
|
|
|
50
51
|
// Middleware
|
|
@@ -72,3 +73,14 @@ export function defineConfig(config: DonkeylabsConfig): DonkeylabsConfig {
|
|
|
72
73
|
|
|
73
74
|
// Re-export HttpError for custom error creation
|
|
74
75
|
export { HttpError } from "./core/errors";
|
|
76
|
+
|
|
77
|
+
// Workflows (step functions)
|
|
78
|
+
export {
|
|
79
|
+
workflow,
|
|
80
|
+
WorkflowBuilder,
|
|
81
|
+
type WorkflowDefinition,
|
|
82
|
+
type WorkflowInstance,
|
|
83
|
+
type WorkflowStatus,
|
|
84
|
+
type WorkflowContext,
|
|
85
|
+
type Workflows,
|
|
86
|
+
} from "./core/workflows";
|
package/src/server.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
createRateLimiter,
|
|
16
16
|
createErrors,
|
|
17
17
|
createWorkflows,
|
|
18
|
+
createProcesses,
|
|
18
19
|
extractClientIP,
|
|
19
20
|
HttpError,
|
|
20
21
|
type LoggerConfig,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
type RateLimiterConfig,
|
|
27
28
|
type ErrorsConfig,
|
|
28
29
|
type WorkflowsConfig,
|
|
30
|
+
type ProcessesConfig,
|
|
29
31
|
} from "./core/index";
|
|
30
32
|
import { zodSchemaToTs } from "./generator/zod-to-ts";
|
|
31
33
|
|
|
@@ -60,6 +62,7 @@ export interface ServerConfig {
|
|
|
60
62
|
rateLimiter?: RateLimiterConfig;
|
|
61
63
|
errors?: ErrorsConfig;
|
|
62
64
|
workflows?: WorkflowsConfig;
|
|
65
|
+
processes?: ProcessesConfig;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
export class AppServer {
|
|
@@ -88,6 +91,10 @@ export class AppServer {
|
|
|
88
91
|
jobs,
|
|
89
92
|
sse,
|
|
90
93
|
});
|
|
94
|
+
const processes = createProcesses({
|
|
95
|
+
...options.processes,
|
|
96
|
+
events,
|
|
97
|
+
});
|
|
91
98
|
|
|
92
99
|
this.coreServices = {
|
|
93
100
|
db: options.db,
|
|
@@ -101,6 +108,7 @@ export class AppServer {
|
|
|
101
108
|
rateLimiter,
|
|
102
109
|
errors,
|
|
103
110
|
workflows,
|
|
111
|
+
processes,
|
|
104
112
|
};
|
|
105
113
|
|
|
106
114
|
this.manager = new PluginManager(this.coreServices);
|
|
@@ -226,12 +234,19 @@ export class AppServer {
|
|
|
226
234
|
outputSource?: string;
|
|
227
235
|
}> = [];
|
|
228
236
|
|
|
237
|
+
const routesWithoutOutput: string[] = [];
|
|
238
|
+
|
|
229
239
|
for (const router of this.routers) {
|
|
230
240
|
for (const route of router.getRoutes()) {
|
|
231
241
|
const parts = route.name.split(".");
|
|
232
242
|
const routeName = parts[parts.length - 1] || route.name;
|
|
233
243
|
const prefix = parts.slice(0, -1).join(".");
|
|
234
244
|
|
|
245
|
+
// Track typed routes without explicit output schema
|
|
246
|
+
if (route.handler === "typed" && !route.output) {
|
|
247
|
+
routesWithoutOutput.push(route.name);
|
|
248
|
+
}
|
|
249
|
+
|
|
235
250
|
routes.push({
|
|
236
251
|
name: route.name,
|
|
237
252
|
prefix,
|
|
@@ -243,6 +258,17 @@ export class AppServer {
|
|
|
243
258
|
}
|
|
244
259
|
}
|
|
245
260
|
|
|
261
|
+
// Warn about routes missing output schemas
|
|
262
|
+
if (routesWithoutOutput.length > 0) {
|
|
263
|
+
logger.warn(
|
|
264
|
+
`${routesWithoutOutput.length} route(s) missing output schema - output type will be 'void'`,
|
|
265
|
+
{ routes: routesWithoutOutput }
|
|
266
|
+
);
|
|
267
|
+
logger.debug(
|
|
268
|
+
"Tip: Add an 'output' Zod schema to define the return type, or ensure handlers return nothing"
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
246
272
|
// Generate the client code
|
|
247
273
|
const code = this.generateClientCode(routes);
|
|
248
274
|
|
|
@@ -334,7 +360,7 @@ export function createApi(options?: ClientOptions) {
|
|
|
334
360
|
if (r.handler !== "typed") return "";
|
|
335
361
|
const routeNs = toPascalCase(r.routeName);
|
|
336
362
|
const inputType = r.inputSource ?? "Record<string, never>";
|
|
337
|
-
const outputType = r.outputSource ?? "
|
|
363
|
+
const outputType = r.outputSource ?? "void";
|
|
338
364
|
return `${indent}export namespace ${routeNs} {
|
|
339
365
|
${indent} export type Input = Expand<${inputType}>;
|
|
340
366
|
${indent} export type Output = Expand<${outputType}>;
|
|
@@ -458,7 +484,8 @@ ${factoryFunction}
|
|
|
458
484
|
this.coreServices.cron.start();
|
|
459
485
|
this.coreServices.jobs.start();
|
|
460
486
|
await this.coreServices.workflows.resume();
|
|
461
|
-
|
|
487
|
+
this.coreServices.processes.start();
|
|
488
|
+
logger.info("Background services started (cron, jobs, workflows, processes)");
|
|
462
489
|
|
|
463
490
|
for (const router of this.routers) {
|
|
464
491
|
for (const route of router.getRoutes()) {
|
|
@@ -677,7 +704,8 @@ ${factoryFunction}
|
|
|
677
704
|
this.coreServices.cron.start();
|
|
678
705
|
this.coreServices.jobs.start();
|
|
679
706
|
await this.coreServices.workflows.resume();
|
|
680
|
-
|
|
707
|
+
this.coreServices.processes.start();
|
|
708
|
+
logger.info("Background services started (cron, jobs, workflows, processes)");
|
|
681
709
|
|
|
682
710
|
// 4. Build route map
|
|
683
711
|
for (const router of this.routers) {
|
|
@@ -817,6 +845,7 @@ ${factoryFunction}
|
|
|
817
845
|
this.coreServices.sse.shutdown();
|
|
818
846
|
|
|
819
847
|
// Stop background services
|
|
848
|
+
await this.coreServices.processes.shutdown();
|
|
820
849
|
await this.coreServices.workflows.stop();
|
|
821
850
|
await this.coreServices.jobs.stop();
|
|
822
851
|
await this.coreServices.cron.stop();
|