@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/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 & Record<string, any>;
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";
@@ -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
- * console.log("Cleanup complete");
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
- * Call this at the end of your server entry file after registering all routes.
570
- * If DONKEYLABS_GENERATE=1 is set, outputs route metadata and exits.
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 { UnifiedApiClientBase, type ClientOptions } from "@donkeylabs/adapter-sveltekit/client";';
706
- const baseClass = this.typeGenConfig?.baseClass ?? "UnifiedApiClientBase";
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?: ClientOptions";
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 createApi(options?: ClientOptions) {
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 if we have any SSE routes to know if we need SSE type imports
886
+ // Check what additional imports we need (SSE types not in base import)
868
887
  const hasSSERoutes = routes.some(r => r.handler === "sse");
869
- const sseImports = hasSSERoutes
870
- ? '\nimport { type SSEOptions, type SSESubscription } from "@donkeylabs/server/client";'
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}${sseImports}
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. Run all plugin migrations
1143
- * 2. Initialize all plugins in dependency order
1144
- * 3. Start cron and jobs services
1145
- * 4. Start the HTTP server
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