@donkeylabs/server 2.0.37 → 2.2.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.
@@ -20,6 +20,7 @@ import {
20
20
  KyselyWorkflowAdapter,
21
21
  MemoryAuditAdapter,
22
22
  MemoryLogsAdapter,
23
+ createHealth,
23
24
  } from "./index";
24
25
  import { PluginManager, type CoreServices, type ConfiguredPlugin } from "../core";
25
26
 
@@ -96,6 +97,7 @@ export async function bootstrapSubprocess(
96
97
  const audit = createAudit({ adapter: auditAdapter });
97
98
  const websocket = createWebSocket();
98
99
  const storage = createStorage();
100
+ const health = createHealth({ dbCheck: false });
99
101
 
100
102
  const core: CoreServices = {
101
103
  db,
@@ -114,6 +116,7 @@ export async function bootstrapSubprocess(
114
116
  websocket,
115
117
  storage,
116
118
  logs,
119
+ health,
117
120
  };
118
121
 
119
122
  workflows.setCore(core);
@@ -226,6 +226,7 @@ function createIpcEventBridge(socket: Socket, instanceId: string): StateMachineE
226
226
  type: "progress",
227
227
  instanceId: id,
228
228
  timestamp: Date.now(),
229
+ stepName: currentStep,
229
230
  progress,
230
231
  completedSteps: completed,
231
232
  totalSteps: total,
@@ -751,12 +751,13 @@ export class WorkflowStateMachine {
751
751
 
752
752
  this.events.onStepCompleted(instanceId, stepName, output, nextStep);
753
753
 
754
- // Calculate progress
754
+ // Calculate progress — re-fetch after persist to get accurate count
755
+ const updated = await this.adapter.getInstance(instanceId);
755
756
  const totalSteps = definition.steps.size;
756
- const completedSteps = Object.values(instance.stepResults).filter(
757
- (r) => r.status === "completed",
758
- ).length + 1; // +1 for current step
759
- const progress = Math.round((completedSteps / totalSteps) * 100);
757
+ const completedSteps = updated
758
+ ? Object.values(updated.stepResults).filter((r) => r.status === "completed").length
759
+ : 1;
760
+ const progress = Math.min(100, Math.round((completedSteps / totalSteps) * 100));
760
761
 
761
762
  this.events.onProgress(instanceId, progress, stepName, completedSteps, totalSteps);
762
763
  }
@@ -1762,20 +1762,21 @@ class WorkflowsImpl implements Workflows {
1762
1762
  await this.emitEvent("workflow.progress", {
1763
1763
  instanceId,
1764
1764
  progress: event.progress,
1765
+ currentStep: event.stepName,
1765
1766
  completedSteps: event.completedSteps,
1766
1767
  totalSteps: event.totalSteps,
1767
1768
  });
1768
1769
  if (this.sse) {
1769
1770
  this.sse.broadcast(`workflow:${instanceId}`, "progress", {
1770
1771
  progress: event.progress,
1772
+ currentStep: event.stepName,
1771
1773
  completedSteps: event.completedSteps,
1772
1774
  totalSteps: event.totalSteps,
1773
1775
  });
1774
1776
  this.sse.broadcast("workflows:all", "workflow.progress", {
1775
1777
  instanceId,
1776
1778
  progress: event.progress,
1777
- completedSteps: event.completedSteps,
1778
- totalSteps: event.totalSteps,
1779
+ currentStep: event.stepName,
1779
1780
  });
1780
1781
  }
1781
1782
  break;
package/src/core.ts CHANGED
@@ -17,6 +17,7 @@ import type { Audit } from "./core/audit";
17
17
  import type { WebSocketService } from "./core/websocket";
18
18
  import type { Storage } from "./core/storage";
19
19
  import type { Logs } from "./core/logs";
20
+ import type { Health } from "./core/health";
20
21
 
21
22
  // ============================================
22
23
  // Auto-detect caller module for plugin define()
@@ -137,6 +138,7 @@ export interface CoreServices {
137
138
  websocket: WebSocketService;
138
139
  storage: Storage;
139
140
  logs: Logs;
141
+ health: Health;
140
142
  }
141
143
 
142
144
  /**
@@ -182,6 +184,8 @@ export interface GlobalContext {
182
184
  ip: string;
183
185
  /** Unique request ID */
184
186
  requestId: string;
187
+ /** Trace ID for distributed tracing (from X-Request-Id/X-Trace-Id header, or defaults to requestId) */
188
+ traceId: string;
185
189
  /** Authenticated user (set by auth middleware) */
186
190
  user?: any;
187
191
  /**
@@ -431,7 +435,11 @@ export type InferService<T> = UnwrapPluginFactory<T> extends { _service: infer S
431
435
  : never;
432
436
  export type InferSchema<T> = UnwrapPluginFactory<T> extends { _schema: infer S } ? S : never;
433
437
  export type InferHandlers<T> = UnwrapPluginFactory<T> extends { handlers?: infer H } ? H : {};
434
- export type InferMiddleware<T> = UnwrapPluginFactory<T> extends { middleware?: (ctx: any) => infer M } ? M : {};
438
+ export type InferMiddleware<T> = UnwrapPluginFactory<T> extends {
439
+ middleware?: (ctx: any, service: any) => infer M;
440
+ }
441
+ ? M
442
+ : {};
435
443
  export type InferDependencies<T> = UnwrapPluginFactory<T> extends { _dependencies: infer D } ? D : readonly [];
436
444
  export type InferConfig<T> = T extends (config: infer C) => any ? C : void;
437
445
  export type InferEvents<T> = UnwrapPluginFactory<T> extends { events?: infer E } ? E : {};
@@ -19,6 +19,10 @@ export interface RouteInfo {
19
19
  outputSource?: string;
20
20
  /** SSE event schemas (for sse handler) */
21
21
  eventsSource?: Record<string, string>;
22
+ /** API version (semver) if from a versioned router */
23
+ version?: string;
24
+ /** Whether this route version is deprecated */
25
+ deprecated?: boolean;
22
26
  }
23
27
 
24
28
  export interface EventInfo {
package/src/harness.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  createWebSocket,
18
18
  createStorage,
19
19
  createLogs,
20
+ createHealth,
20
21
  KyselyJobAdapter,
21
22
  KyselyWorkflowAdapter,
22
23
  MemoryAuditAdapter,
@@ -71,6 +72,7 @@ export async function createTestHarness(targetPlugin: Plugin, dependencies: Plug
71
72
  const websocket = createWebSocket();
72
73
  const storage = createStorage(); // Uses memory adapter by default
73
74
  const logs = createLogs({ adapter: new MemoryLogsAdapter(), events });
75
+ const health = createHealth({ dbCheck: false }); // No DB check in unit tests
74
76
 
75
77
  const core: CoreServices = {
76
78
  db,
@@ -89,6 +91,7 @@ export async function createTestHarness(targetPlugin: Plugin, dependencies: Plug
89
91
  websocket,
90
92
  storage,
91
93
  logs,
94
+ health,
92
95
  };
93
96
 
94
97
  const manager = new PluginManager(core);
@@ -158,28 +161,37 @@ export class TestApiClient extends ApiClientBase {
158
161
  * const user = await client.call("users.create", { name: "Test", email: "test@example.com" });
159
162
  * ```
160
163
  */
161
- async call<TOutput = any>(route: string, input: any = {}): Promise<TOutput> {
164
+ async call<TOutput = any>(
165
+ route: string,
166
+ input: any = {},
167
+ options?: { version?: string }
168
+ ): Promise<TOutput> {
162
169
  const routeDef = this.routeMap.get(route);
163
170
  if (!routeDef) {
164
171
  throw new Error(`Route not found: ${route}. Available routes: ${[...this.routeMap.keys()].join(", ")}`);
165
172
  }
166
173
 
174
+ const versionHeaders: Record<string, string> = {};
175
+ if (options?.version) {
176
+ versionHeaders["X-API-Version"] = options.version;
177
+ }
178
+
167
179
  // Handle different handler types
168
180
  if (routeDef.handler === "typed" || routeDef.handler === "formData") {
169
- return this.request(route, input);
181
+ return this.request(route, input, { headers: versionHeaders });
170
182
  } else if (routeDef.handler === "stream" || routeDef.handler === "html") {
171
183
  const response = await this.rawRequest(route, {
172
184
  method: "POST",
173
- headers: { "Content-Type": "application/json" },
185
+ headers: { "Content-Type": "application/json", ...versionHeaders },
174
186
  body: JSON.stringify(input),
175
187
  });
176
188
  return response as any;
177
189
  } else if (routeDef.handler === "raw") {
178
- const response = await this.rawRequest(route);
190
+ const response = await this.rawRequest(route, { headers: versionHeaders });
179
191
  return response as any;
180
192
  }
181
193
 
182
- return this.request(route, input);
194
+ return this.request(route, input, { headers: versionHeaders });
183
195
  }
184
196
 
185
197
  /**
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  export {
5
5
  AppServer,
6
6
  type ServerConfig,
7
+ type ShutdownConfig,
7
8
  // Lifecycle hooks
8
9
  type HookContext,
9
10
  type OnReadyHandler,
@@ -91,14 +92,34 @@ export function defineConfig(config: DonkeylabsConfig): DonkeylabsConfig {
91
92
  // Re-export HttpError for custom error creation
92
93
  export { HttpError } from "./core/errors";
93
94
 
95
+ // Versioning
96
+ export {
97
+ parseSemVer,
98
+ type SemVer,
99
+ type VersioningConfig,
100
+ type DeprecationInfo,
101
+ type RouterOptions,
102
+ } from "./versioning";
103
+
94
104
  // Core services types
95
105
  export {
96
106
  type Logger,
97
107
  type LogLevel,
98
108
  type ErrorFactory,
99
109
  type ErrorFactories,
110
+ type EventMetadata,
100
111
  } from "./core/index";
101
112
 
113
+ // Health checks
114
+ export {
115
+ type Health,
116
+ type HealthCheck,
117
+ type HealthCheckResult,
118
+ type HealthConfig,
119
+ type HealthResponse,
120
+ type HealthStatus,
121
+ } from "./core/health";
122
+
102
123
  // Logs (persistent logging)
103
124
  export {
104
125
  type Logs,
package/src/router.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { z } from "zod";
3
3
  import type { GlobalContext, PluginHandlerRegistry } from "./core";
4
4
  import type { MiddlewareDefinition } from "./middleware";
5
+ import type { RouterOptions, DeprecationInfo } from "./versioning";
5
6
 
6
7
  export type ServerContext = GlobalContext;
7
8
 
@@ -255,6 +256,8 @@ export interface IRouter {
255
256
  getRoutes(): RouteDefinition<any, any, any>[];
256
257
  getMetadata(): RouteMetadata[];
257
258
  getPrefix(): string;
259
+ getVersion(): string | undefined;
260
+ getDeprecation(): DeprecationInfo | undefined;
258
261
  }
259
262
 
260
263
  export class Router implements IRouter {
@@ -262,9 +265,13 @@ export class Router implements IRouter {
262
265
  private childRouters: IRouter[] = [];
263
266
  private prefix: string;
264
267
  private _middlewareStack: MiddlewareDefinition[] = [];
268
+ private _version?: string;
269
+ private _deprecated?: DeprecationInfo;
265
270
 
266
- constructor(prefix: string = "") {
271
+ constructor(prefix: string = "", options?: RouterOptions) {
267
272
  this.prefix = prefix;
273
+ this._version = options?.version;
274
+ this._deprecated = options?.deprecated;
268
275
  }
269
276
 
270
277
  route(name: string): IRouteBuilder<this> {
@@ -277,6 +284,11 @@ export class Router implements IRouter {
277
284
  const fullPrefix = this.prefix ? `${this.prefix}.${prefixOrRouter}` : prefixOrRouter;
278
285
  const childRouter = new Router(fullPrefix);
279
286
  childRouter._middlewareStack = [...this._middlewareStack];
287
+ // Inherit parent version unless child overrides
288
+ if (this._version && !childRouter._version) {
289
+ childRouter._version = this._version;
290
+ childRouter._deprecated = this._deprecated;
291
+ }
280
292
  // Track child router so its routes are included in getRoutes()
281
293
  this.childRouters.push(childRouter);
282
294
  return childRouter;
@@ -355,6 +367,14 @@ export class Router implements IRouter {
355
367
  getPrefix(): string {
356
368
  return this.prefix;
357
369
  }
370
+
371
+ getVersion(): string | undefined {
372
+ return this._version;
373
+ }
374
+
375
+ getDeprecation(): DeprecationInfo | undefined {
376
+ return this._deprecated;
377
+ }
358
378
  }
359
379
 
360
380
  /** Creates a Proxy that intercepts middleware method calls and adds them to the router's middleware stack */
@@ -372,7 +392,7 @@ function createMiddlewareBuilderProxy<TRouter extends Router>(router: TRouter):
372
392
  });
373
393
  }
374
394
 
375
- export const createRouter = (prefix?: string): IRouter => new Router(prefix);
395
+ export const createRouter = (prefix?: string, options?: RouterOptions): IRouter => new Router(prefix, options);
376
396
 
377
397
  /**
378
398
  * Define a route with type inference for input/output schemas.