@donkeylabs/server 2.0.7 → 2.0.10
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/lifecycle-hooks.md +16 -16
- package/docs/processes.md +93 -0
- package/package.json +13 -3
- package/src/admin/dashboard.ts +717 -0
- package/src/admin/index.ts +85 -0
- package/src/admin/routes.ts +573 -0
- package/src/admin/styles.ts +422 -0
- package/src/core/index.ts +25 -0
- package/src/core/job-adapter-kysely.ts +22 -1
- package/src/core/job-adapter-sqlite.ts +22 -1
- package/src/core/jobs.ts +37 -0
- package/src/core/process-client.ts +121 -0
- package/src/core/processes.ts +67 -0
- package/src/core/storage-adapter-local.ts +403 -0
- package/src/core/storage-adapter-s3.ts +409 -0
- package/src/core/storage.ts +543 -0
- package/src/core/websocket.ts +13 -3
- package/src/core/workflow-adapter-kysely.ts +22 -1
- package/src/core/workflows.ts +37 -0
- package/src/core.ts +10 -1
- package/src/harness.ts +3 -0
- package/src/index.ts +19 -0
- package/src/process-client.ts +7 -0
- package/src/server.ts +71 -31
package/src/core.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { Workflows } from "./core/workflows";
|
|
|
15
15
|
import type { Processes } from "./core/processes";
|
|
16
16
|
import type { Audit } from "./core/audit";
|
|
17
17
|
import type { WebSocketService } from "./core/websocket";
|
|
18
|
+
import type { Storage } from "./core/storage";
|
|
18
19
|
|
|
19
20
|
export interface PluginRegistry {}
|
|
20
21
|
|
|
@@ -107,6 +108,7 @@ export interface CoreServices {
|
|
|
107
108
|
processes: Processes;
|
|
108
109
|
audit: Audit;
|
|
109
110
|
websocket: WebSocketService;
|
|
111
|
+
storage: Storage;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
/**
|
|
@@ -147,13 +149,20 @@ export interface GlobalContext {
|
|
|
147
149
|
/** Application config */
|
|
148
150
|
config: Record<string, any>;
|
|
149
151
|
/** Custom user-registered services - typed via ServiceRegistry augmentation */
|
|
150
|
-
services: ServiceRegistry
|
|
152
|
+
services: ServiceRegistry;
|
|
151
153
|
/** Client IP address */
|
|
152
154
|
ip: string;
|
|
153
155
|
/** Unique request ID */
|
|
154
156
|
requestId: string;
|
|
155
157
|
/** Authenticated user (set by auth middleware) */
|
|
156
158
|
user?: any;
|
|
159
|
+
/**
|
|
160
|
+
* Abort signal for the request.
|
|
161
|
+
* Fires when client disconnects (closes connection, navigates away, aborts fetch).
|
|
162
|
+
* Use this to clean up resources in streaming/SSE handlers.
|
|
163
|
+
* Only available in HTTP context, undefined in direct calls (SSR).
|
|
164
|
+
*/
|
|
165
|
+
signal?: AbortSignal;
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
export class PluginContext<Deps = any, Schema = any, Config = void> {
|
package/src/harness.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
createProcesses,
|
|
16
16
|
createAudit,
|
|
17
17
|
createWebSocket,
|
|
18
|
+
createStorage,
|
|
18
19
|
KyselyJobAdapter,
|
|
19
20
|
KyselyWorkflowAdapter,
|
|
20
21
|
MemoryAuditAdapter,
|
|
@@ -63,6 +64,7 @@ export async function createTestHarness(targetPlugin: Plugin, dependencies: Plug
|
|
|
63
64
|
// Use in-memory adapter for audit in tests
|
|
64
65
|
const audit = createAudit({ adapter: new MemoryAuditAdapter() });
|
|
65
66
|
const websocket = createWebSocket();
|
|
67
|
+
const storage = createStorage(); // Uses memory adapter by default
|
|
66
68
|
|
|
67
69
|
const core: CoreServices = {
|
|
68
70
|
db,
|
|
@@ -79,6 +81,7 @@ export async function createTestHarness(targetPlugin: Plugin, dependencies: Plug
|
|
|
79
81
|
processes,
|
|
80
82
|
audit,
|
|
81
83
|
websocket,
|
|
84
|
+
storage,
|
|
82
85
|
};
|
|
83
86
|
|
|
84
87
|
const manager = new PluginManager(core);
|
package/src/index.ts
CHANGED
|
@@ -102,5 +102,24 @@ export {
|
|
|
102
102
|
type Workflows,
|
|
103
103
|
} from "./core/workflows";
|
|
104
104
|
|
|
105
|
+
// Processes (managed subprocesses)
|
|
106
|
+
export {
|
|
107
|
+
type Processes,
|
|
108
|
+
type ProcessDefinition,
|
|
109
|
+
type ProcessStats,
|
|
110
|
+
type ManagedProcess,
|
|
111
|
+
type ProcessConfig,
|
|
112
|
+
type ProcessStatus,
|
|
113
|
+
type SpawnOptions,
|
|
114
|
+
} from "./core/processes";
|
|
115
|
+
|
|
116
|
+
// Admin Dashboard
|
|
117
|
+
export {
|
|
118
|
+
type AdminConfig,
|
|
119
|
+
isAdminEnabled,
|
|
120
|
+
createAdmin,
|
|
121
|
+
createAdminRouter,
|
|
122
|
+
} from "./admin";
|
|
123
|
+
|
|
105
124
|
// Test Harness - for plugin testing
|
|
106
125
|
export { createTestHarness } from "./harness";
|
package/src/process-client.ts
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* const client = await ProcessClient.connect();
|
|
9
9
|
* client.emit("progress", { percent: 50 });
|
|
10
|
+
*
|
|
11
|
+
* // With stats emission
|
|
12
|
+
* const client = await ProcessClient.connect({
|
|
13
|
+
* stats: { enabled: true, interval: 2000 }
|
|
14
|
+
* });
|
|
10
15
|
* ```
|
|
11
16
|
*/
|
|
12
17
|
|
|
@@ -14,6 +19,8 @@ export {
|
|
|
14
19
|
ProcessClient,
|
|
15
20
|
type ProcessClient as ProcessClientType,
|
|
16
21
|
type ProcessClientConfig,
|
|
22
|
+
type StatsConfig,
|
|
23
|
+
type ProcessStats,
|
|
17
24
|
connect,
|
|
18
25
|
createProcessClient,
|
|
19
26
|
} from "./core/process-client";
|
package/src/server.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
createProcesses,
|
|
19
19
|
createAudit,
|
|
20
20
|
createWebSocket,
|
|
21
|
+
createStorage,
|
|
21
22
|
extractClientIP,
|
|
22
23
|
HttpError,
|
|
23
24
|
KyselyJobAdapter,
|
|
@@ -36,7 +37,9 @@ import {
|
|
|
36
37
|
type ProcessesConfig,
|
|
37
38
|
type AuditConfig,
|
|
38
39
|
type WebSocketConfig,
|
|
40
|
+
type StorageConfig,
|
|
39
41
|
} from "./core/index";
|
|
42
|
+
import type { AdminConfig } from "./admin";
|
|
40
43
|
import { zodSchemaToTs } from "./generator/zod-to-ts";
|
|
41
44
|
|
|
42
45
|
export interface TypeGenerationConfig {
|
|
@@ -76,6 +79,12 @@ export interface ServerConfig {
|
|
|
76
79
|
processes?: ProcessesConfig;
|
|
77
80
|
audit?: AuditConfig;
|
|
78
81
|
websocket?: WebSocketConfig;
|
|
82
|
+
storage?: StorageConfig;
|
|
83
|
+
/**
|
|
84
|
+
* Admin dashboard configuration.
|
|
85
|
+
* Automatically enabled in dev mode, disabled in production.
|
|
86
|
+
*/
|
|
87
|
+
admin?: AdminConfig;
|
|
79
88
|
/**
|
|
80
89
|
* Use legacy separate databases for core services.
|
|
81
90
|
* Set to true to keep using .donkeylabs/*.db files instead of shared DB.
|
|
@@ -125,9 +134,10 @@ export interface HookContext {
|
|
|
125
134
|
export type OnReadyHandler = (ctx: HookContext) => void | Promise<void>;
|
|
126
135
|
|
|
127
136
|
/**
|
|
128
|
-
* Handler for onShutdown hook - called when server is shutting down
|
|
137
|
+
* Handler for onShutdown hook - called when server is shutting down.
|
|
138
|
+
* Receives the same context as onReady for cleanup operations.
|
|
129
139
|
*/
|
|
130
|
-
export type OnShutdownHandler = () => void | Promise<void>;
|
|
140
|
+
export type OnShutdownHandler = (ctx: HookContext) => void | Promise<void>;
|
|
131
141
|
|
|
132
142
|
/**
|
|
133
143
|
* Handler for onError hook - called when an unhandled error occurs
|
|
@@ -195,6 +205,7 @@ export class AppServer {
|
|
|
195
205
|
private shutdownHandlers: OnShutdownHandler[] = [];
|
|
196
206
|
private errorHandlers: OnErrorHandler[] = [];
|
|
197
207
|
private isShuttingDown = false;
|
|
208
|
+
private generateModeSetup = false;
|
|
198
209
|
|
|
199
210
|
// Custom services registry
|
|
200
211
|
private serviceFactories = new Map<string, ServiceFactory<any>>();
|
|
@@ -255,6 +266,7 @@ export class AppServer {
|
|
|
255
266
|
adapter: auditAdapter,
|
|
256
267
|
});
|
|
257
268
|
const websocket = createWebSocket(options.websocket);
|
|
269
|
+
const storage = createStorage(options.storage);
|
|
258
270
|
|
|
259
271
|
this.coreServices = {
|
|
260
272
|
db: options.db,
|
|
@@ -271,6 +283,7 @@ export class AppServer {
|
|
|
271
283
|
processes,
|
|
272
284
|
audit,
|
|
273
285
|
websocket,
|
|
286
|
+
storage,
|
|
274
287
|
};
|
|
275
288
|
|
|
276
289
|
// Resolve circular dependency: workflows needs core for step handlers
|
|
@@ -366,10 +379,10 @@ export class AppServer {
|
|
|
366
379
|
*
|
|
367
380
|
* @example
|
|
368
381
|
* ```ts
|
|
369
|
-
* server.onShutdown(async () => {
|
|
370
|
-
* await redis.quit();
|
|
371
|
-
* await externalApi.disconnect();
|
|
372
|
-
*
|
|
382
|
+
* server.onShutdown(async (ctx) => {
|
|
383
|
+
* await ctx.services.redis.quit();
|
|
384
|
+
* await ctx.services.externalApi.disconnect();
|
|
385
|
+
* ctx.core.logger.info("Cleanup complete");
|
|
373
386
|
* });
|
|
374
387
|
* ```
|
|
375
388
|
*/
|
|
@@ -453,10 +466,11 @@ export class AppServer {
|
|
|
453
466
|
* Run all registered shutdown handlers (internal helper).
|
|
454
467
|
*/
|
|
455
468
|
private async runShutdownHandlers(): Promise<void> {
|
|
469
|
+
const ctx = this.getHookContext();
|
|
456
470
|
// Run shutdown handlers in reverse order (LIFO)
|
|
457
471
|
for (const handler of [...this.shutdownHandlers].reverse()) {
|
|
458
472
|
try {
|
|
459
|
-
await handler();
|
|
473
|
+
await handler(ctx);
|
|
460
474
|
} catch (error) {
|
|
461
475
|
this.coreServices.logger.error("Error in onShutdown handler", { error });
|
|
462
476
|
}
|
|
@@ -485,6 +499,18 @@ export class AppServer {
|
|
|
485
499
|
*/
|
|
486
500
|
use(router: IRouter): this {
|
|
487
501
|
this.routers.push(router);
|
|
502
|
+
|
|
503
|
+
// Set up generate mode handler on first use() call
|
|
504
|
+
// This handles SvelteKit-style entry files that don't call start()
|
|
505
|
+
if (!this.generateModeSetup && process.env.DONKEYLABS_GENERATE === "1") {
|
|
506
|
+
this.generateModeSetup = true;
|
|
507
|
+
// Use beforeExit to output routes after module finishes loading
|
|
508
|
+
process.on("beforeExit", () => {
|
|
509
|
+
this.outputRoutesForGeneration();
|
|
510
|
+
process.exit(0);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
488
514
|
return this;
|
|
489
515
|
}
|
|
490
516
|
|
|
@@ -566,15 +592,8 @@ export class AppServer {
|
|
|
566
592
|
|
|
567
593
|
/**
|
|
568
594
|
* Handle CLI type generation mode.
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
* Otherwise, does nothing.
|
|
572
|
-
*
|
|
573
|
-
* @example
|
|
574
|
-
* ```ts
|
|
575
|
-
* server.use(routes);
|
|
576
|
-
* server.handleGenerateMode(); // Add this line at the end
|
|
577
|
-
* ```
|
|
595
|
+
* @deprecated No longer needed - this is now handled automatically in start() and initialize().
|
|
596
|
+
* You can safely remove any calls to this method.
|
|
578
597
|
*/
|
|
579
598
|
handleGenerateMode(): void {
|
|
580
599
|
if (process.env.DONKEYLABS_GENERATE === "1") {
|
|
@@ -702,17 +721,17 @@ export class AppServer {
|
|
|
702
721
|
): string {
|
|
703
722
|
const baseImport =
|
|
704
723
|
this.typeGenConfig?.baseImport ??
|
|
705
|
-
'import {
|
|
706
|
-
const baseClass = this.typeGenConfig?.baseClass ?? "
|
|
724
|
+
'import { ApiClientBase, type ApiClientOptions, type RequestOptions } from "@donkeylabs/server/client";';
|
|
725
|
+
const baseClass = this.typeGenConfig?.baseClass ?? "ApiClientBase";
|
|
707
726
|
const constructorSignature =
|
|
708
|
-
this.typeGenConfig?.constructorSignature ?? "options?:
|
|
727
|
+
this.typeGenConfig?.constructorSignature ?? "baseUrl: string, options?: ApiClientOptions";
|
|
709
728
|
const constructorBody =
|
|
710
|
-
this.typeGenConfig?.constructorBody ?? "super(options);";
|
|
729
|
+
this.typeGenConfig?.constructorBody ?? "super(baseUrl, options);";
|
|
711
730
|
const defaultFactory = `/**
|
|
712
731
|
* Create an API client instance
|
|
713
732
|
*/
|
|
714
|
-
export function
|
|
715
|
-
return new ApiClient(options);
|
|
733
|
+
export function createApiClient(baseUrl: string, options?: ApiClientOptions) {
|
|
734
|
+
return new ApiClient(baseUrl, options);
|
|
716
735
|
}`;
|
|
717
736
|
const factoryFunction = this.typeGenConfig?.factoryFunction ?? defaultFactory;
|
|
718
737
|
|
|
@@ -864,16 +883,19 @@ ${indent}export type ${routeNs} = { Input: ${routeNs}.Input; Events: ${routeNs}.
|
|
|
864
883
|
// rootNode children are top-level namespaces (api, health) -> Top Level Class Properties
|
|
865
884
|
const methodBlocks: string[] = [generateMethodBlock(rootNode, " ", "", true)];
|
|
866
885
|
|
|
867
|
-
// Check
|
|
886
|
+
// Check what additional imports we need (SSE types not in base import)
|
|
868
887
|
const hasSSERoutes = routes.some(r => r.handler === "sse");
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
888
|
+
|
|
889
|
+
const additionalImports: string[] = [];
|
|
890
|
+
if (hasSSERoutes) {
|
|
891
|
+
additionalImports.push('import { type SSEOptions, type SSESubscription } from "@donkeylabs/server/client";');
|
|
892
|
+
}
|
|
893
|
+
const extraImports = additionalImports.length > 0 ? '\n' + additionalImports.join('\n') : "";
|
|
872
894
|
|
|
873
895
|
return `// Auto-generated by @donkeylabs/server
|
|
874
896
|
// DO NOT EDIT MANUALLY
|
|
875
897
|
|
|
876
|
-
${baseImport}${
|
|
898
|
+
${baseImport}${extraImports}
|
|
877
899
|
|
|
878
900
|
// Utility type that forces TypeScript to expand types on hover
|
|
879
901
|
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
|
@@ -923,6 +945,12 @@ ${factoryFunction}
|
|
|
923
945
|
* Used by adapters (e.g., SvelteKit) that manage their own HTTP server.
|
|
924
946
|
*/
|
|
925
947
|
async initialize(): Promise<void> {
|
|
948
|
+
// Handle CLI type generation mode - exit early before any initialization
|
|
949
|
+
if (process.env.DONKEYLABS_GENERATE === "1") {
|
|
950
|
+
this.outputRoutesForGeneration();
|
|
951
|
+
process.exit(0);
|
|
952
|
+
}
|
|
953
|
+
|
|
926
954
|
const { logger } = this.coreServices;
|
|
927
955
|
|
|
928
956
|
// Auto-generate types in dev mode if configured
|
|
@@ -1004,6 +1032,7 @@ ${factoryFunction}
|
|
|
1004
1032
|
services: this.serviceRegistry,
|
|
1005
1033
|
ip,
|
|
1006
1034
|
requestId: crypto.randomUUID(),
|
|
1035
|
+
signal: req.signal,
|
|
1007
1036
|
};
|
|
1008
1037
|
|
|
1009
1038
|
// Get middleware stack
|
|
@@ -1139,12 +1168,19 @@ ${factoryFunction}
|
|
|
1139
1168
|
/**
|
|
1140
1169
|
* Start the server.
|
|
1141
1170
|
* This will:
|
|
1142
|
-
* 1.
|
|
1143
|
-
* 2.
|
|
1144
|
-
* 3.
|
|
1145
|
-
* 4. Start
|
|
1171
|
+
* 1. Check for CLI generate mode (DONKEYLABS_GENERATE=1)
|
|
1172
|
+
* 2. Run all plugin migrations
|
|
1173
|
+
* 3. Initialize all plugins in dependency order
|
|
1174
|
+
* 4. Start cron and jobs services
|
|
1175
|
+
* 5. Start the HTTP server
|
|
1146
1176
|
*/
|
|
1147
1177
|
async start() {
|
|
1178
|
+
// Handle CLI type generation mode - exit early before any initialization
|
|
1179
|
+
if (process.env.DONKEYLABS_GENERATE === "1") {
|
|
1180
|
+
this.outputRoutesForGeneration();
|
|
1181
|
+
process.exit(0);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1148
1184
|
const { logger } = this.coreServices;
|
|
1149
1185
|
|
|
1150
1186
|
// Auto-generate types in dev mode if configured
|
|
@@ -1229,6 +1265,7 @@ ${factoryFunction}
|
|
|
1229
1265
|
services: this.serviceRegistry,
|
|
1230
1266
|
ip,
|
|
1231
1267
|
requestId: crypto.randomUUID(),
|
|
1268
|
+
signal: req.signal,
|
|
1232
1269
|
};
|
|
1233
1270
|
|
|
1234
1271
|
// Get middleware stack for this route
|
|
@@ -1367,6 +1404,9 @@ ${factoryFunction}
|
|
|
1367
1404
|
// Stop audit service (cleanup timers)
|
|
1368
1405
|
this.coreServices.audit.stop();
|
|
1369
1406
|
|
|
1407
|
+
// Stop storage (cleanup connections)
|
|
1408
|
+
this.coreServices.storage.stop();
|
|
1409
|
+
|
|
1370
1410
|
logger.info("Server shutdown complete");
|
|
1371
1411
|
}
|
|
1372
1412
|
|