@donkeylabs/server 2.0.21 → 2.0.23

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
@@ -1,7 +1,7 @@
1
1
  import { sql, type Kysely } from "kysely";
2
2
  import { readdir } from "node:fs/promises";
3
- import { join, dirname } from "node:path";
4
- import { fileURLToPath } from "node:url";
3
+ import { join, dirname, resolve } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import type { z } from "zod";
6
6
  import type { Logger } from "./core/logger";
7
7
  import type { Cache } from "./core/cache";
@@ -16,6 +16,33 @@ import type { Processes } from "./core/processes";
16
16
  import type { Audit } from "./core/audit";
17
17
  import type { WebSocketService } from "./core/websocket";
18
18
  import type { Storage } from "./core/storage";
19
+ import type { Logs } from "./core/logs";
20
+
21
+ // ============================================
22
+ // Auto-detect caller module for plugin define()
23
+ // ============================================
24
+
25
+ const CORE_FILE = resolve(fileURLToPath(import.meta.url));
26
+
27
+ /**
28
+ * Walk the call stack to find the file that invoked define().
29
+ * Returns a file:// URL string or undefined if detection fails.
30
+ * Skips frames originating from this file (core.ts).
31
+ */
32
+ function captureCallerUrl(): string | undefined {
33
+ const stack = new Error().stack ?? "";
34
+ for (const line of stack.split("\n").slice(1)) {
35
+ const match = line.match(/at\s+(?:.*?\s+\(?)?([^\s():]+):\d+:\d+/);
36
+ if (match) {
37
+ let filePath = match[1];
38
+ if (filePath.startsWith("file://")) filePath = fileURLToPath(filePath);
39
+ if (filePath.startsWith("native")) continue;
40
+ filePath = resolve(filePath);
41
+ if (filePath !== CORE_FILE) return pathToFileURL(filePath).href;
42
+ }
43
+ }
44
+ return undefined;
45
+ }
19
46
 
20
47
  export interface PluginRegistry {}
21
48
 
@@ -109,6 +136,7 @@ export interface CoreServices {
109
136
  audit: Audit;
110
137
  websocket: WebSocketService;
111
138
  storage: Storage;
139
+ logs: Logs;
112
140
  }
113
141
 
114
142
  /**
@@ -328,7 +356,7 @@ export class PluginBuilder<LocalSchema = {}> {
328
356
  client?: ClientConfig;
329
357
  customErrors?: CustomErrors;
330
358
  } {
331
- return config as any;
359
+ return { ...config, _modulePath: captureCallerUrl() } as any;
332
360
  }
333
361
  }
334
362
 
@@ -384,9 +412,11 @@ export class ConfiguredPluginBuilder<LocalSchema, Config> {
384
412
  client?: ClientConfig;
385
413
  customErrors?: CustomErrors;
386
414
  }> {
415
+ const modulePath = captureCallerUrl();
387
416
  const factory = (config: Config) => ({
388
417
  ...pluginDef,
389
418
  _boundConfig: config,
419
+ _modulePath: modulePath,
390
420
  });
391
421
  return factory as any;
392
422
  }
@@ -423,6 +453,8 @@ export type Plugin = {
423
453
  service: (ctx: any) => any;
424
454
  /** Called after service is created - use for registering crons, events, etc. */
425
455
  init?: (ctx: any, service: any) => void | Promise<void>;
456
+ /** Auto-detected module path where the plugin was defined */
457
+ _modulePath?: string;
426
458
  };
427
459
 
428
460
  export type PluginWithConfig<Config = void> = Plugin & {
@@ -452,6 +484,54 @@ export class PluginManager {
452
484
  return Array.from(this.plugins.values());
453
485
  }
454
486
 
487
+ getPluginNames(): string[] {
488
+ return Array.from(this.plugins.keys());
489
+ }
490
+
491
+ /** Returns { name: modulePath } for plugins that have a captured module path */
492
+ getPluginModulePaths(): Record<string, string> {
493
+ const result: Record<string, string> = {};
494
+ for (const [name, plugin] of this.plugins) {
495
+ if (plugin._modulePath) {
496
+ result[name] = plugin._modulePath;
497
+ }
498
+ }
499
+ return result;
500
+ }
501
+
502
+ /** Returns { name: boundConfig } for configured plugins */
503
+ getPluginConfigs(): Record<string, any> {
504
+ const result: Record<string, any> = {};
505
+ for (const [name, plugin] of this.plugins) {
506
+ if ((plugin as ConfiguredPlugin)._boundConfig !== undefined) {
507
+ result[name] = (plugin as ConfiguredPlugin)._boundConfig;
508
+ }
509
+ }
510
+ return result;
511
+ }
512
+
513
+ /** Returns { name: [...deps] } for plugins with dependencies */
514
+ getPluginDependencies(): Record<string, string[]> {
515
+ const result: Record<string, string[]> = {};
516
+ for (const [name, plugin] of this.plugins) {
517
+ if (plugin.dependencies && plugin.dependencies.length > 0) {
518
+ result[name] = [...plugin.dependencies];
519
+ }
520
+ }
521
+ return result;
522
+ }
523
+
524
+ /** Returns custom error definitions per plugin */
525
+ getPluginCustomErrors(): Record<string, Record<string, any>> {
526
+ const result: Record<string, Record<string, any>> = {};
527
+ for (const [name, plugin] of this.plugins) {
528
+ if (plugin.customErrors && Object.keys(plugin.customErrors).length > 0) {
529
+ result[name] = plugin.customErrors;
530
+ }
531
+ }
532
+ return result;
533
+ }
534
+
455
535
  register(plugin: ConfiguredPlugin): void {
456
536
  if (this.plugins.has(plugin.name)) {
457
537
  throw new Error(`Plugin ${plugin.name} is already registered.`);
package/src/harness.ts CHANGED
@@ -16,9 +16,11 @@ import {
16
16
  createAudit,
17
17
  createWebSocket,
18
18
  createStorage,
19
+ createLogs,
19
20
  KyselyJobAdapter,
20
21
  KyselyWorkflowAdapter,
21
22
  MemoryAuditAdapter,
23
+ MemoryLogsAdapter,
22
24
  } from "./core/index";
23
25
  import { AppServer, type ServerConfig } from "./server";
24
26
  import type { IRouter, RouteDefinition } from "./router";
@@ -68,6 +70,7 @@ export async function createTestHarness(targetPlugin: Plugin, dependencies: Plug
68
70
  const audit = createAudit({ adapter: new MemoryAuditAdapter() });
69
71
  const websocket = createWebSocket();
70
72
  const storage = createStorage(); // Uses memory adapter by default
73
+ const logs = createLogs({ adapter: new MemoryLogsAdapter(), events });
71
74
 
72
75
  const core: CoreServices = {
73
76
  db,
@@ -85,6 +88,7 @@ export async function createTestHarness(targetPlugin: Plugin, dependencies: Plug
85
88
  audit,
86
89
  websocket,
87
90
  storage,
91
+ logs,
88
92
  };
89
93
 
90
94
  const manager = new PluginManager(core);
package/src/index.ts CHANGED
@@ -99,6 +99,16 @@ export {
99
99
  type ErrorFactories,
100
100
  } from "./core/index";
101
101
 
102
+ // Logs (persistent logging)
103
+ export {
104
+ type Logs,
105
+ type LogSource,
106
+ type PersistentLogEntry,
107
+ type LogsQueryFilters,
108
+ type LogsConfig,
109
+ type LogsRetentionConfig,
110
+ } from "./core/logs";
111
+
102
112
  // Workflows (step functions)
103
113
  export {
104
114
  workflow,
package/src/server.ts CHANGED
@@ -19,12 +19,16 @@ import {
19
19
  createAudit,
20
20
  createWebSocket,
21
21
  createStorage,
22
+ createLogs,
22
23
  extractClientIP,
23
24
  HttpError,
24
25
  KyselyJobAdapter,
25
26
  KyselyProcessAdapter,
26
27
  KyselyWorkflowAdapter,
27
28
  KyselyAuditAdapter,
29
+ KyselyLogsAdapter,
30
+ PersistentTransport,
31
+ ConsoleTransport,
28
32
  type LoggerConfig,
29
33
  type CacheConfig,
30
34
  type EventsConfig,
@@ -38,6 +42,7 @@ import {
38
42
  type AuditConfig,
39
43
  type WebSocketConfig,
40
44
  type StorageConfig,
45
+ type LogsConfig,
41
46
  } from "./core/index";
42
47
  import type { AdminConfig } from "./admin";
43
48
  import { zodSchemaToTs } from "./generator/zod-to-ts";
@@ -80,6 +85,7 @@ export interface ServerConfig {
80
85
  audit?: AuditConfig;
81
86
  websocket?: WebSocketConfig;
82
87
  storage?: StorageConfig;
88
+ logs?: LogsConfig;
83
89
  /**
84
90
  * Admin dashboard configuration.
85
91
  * Automatically enabled in dev mode, disabled in production.
@@ -224,23 +230,52 @@ export class AppServer {
224
230
  const useLegacy = options.useLegacyCoreDatabases ?? false;
225
231
 
226
232
  // Initialize core services
227
- const logger = createLogger(options.logger);
233
+ // Order matters: events → logs → logger (with PersistentTransport) → cron/jobs (with logger)
228
234
  const cache = createCache(options.cache);
229
235
  const events = createEvents(options.events);
230
- const cron = createCron(options.cron);
231
236
  const sse = createSSE(options.sse);
232
237
  const rateLimiter = createRateLimiter(options.rateLimiter);
233
238
  const errors = createErrors(options.errors);
234
239
 
240
+ // Create logs service with its own database
241
+ const logsAdapter = options.logs?.adapter ?? new KyselyLogsAdapter({
242
+ dbPath: options.logs?.dbPath,
243
+ });
244
+ const logs = createLogs({
245
+ ...options.logs,
246
+ adapter: logsAdapter,
247
+ events,
248
+ });
249
+
250
+ // Create logger with both console and persistent transports
251
+ const persistentTransport = new PersistentTransport(logs, {
252
+ minLevel: options.logs?.minLevel,
253
+ });
254
+ const loggerTransports = [
255
+ new ConsoleTransport(options.logger?.format ?? "pretty"),
256
+ persistentTransport,
257
+ ];
258
+ const logger = createLogger({
259
+ ...options.logger,
260
+ transports: options.logger?.transports ?? loggerTransports,
261
+ });
262
+
263
+ // Cron with logger for scoped logging
264
+ const cron = createCron({
265
+ ...options.cron,
266
+ logger,
267
+ });
268
+
235
269
  // Create adapters - use Kysely by default, or legacy SQLite if requested
236
270
  const jobAdapter = options.jobs?.adapter ?? (useLegacy ? undefined : new KyselyJobAdapter(options.db));
237
271
  const workflowAdapter = options.workflows?.adapter ?? (useLegacy ? undefined : new KyselyWorkflowAdapter(options.db));
238
272
  const auditAdapter = options.audit?.adapter ?? new KyselyAuditAdapter(options.db);
239
273
 
240
- // Jobs can emit events and use Kysely adapter
274
+ // Jobs can emit events and use Kysely adapter, with logger for scoped logging
241
275
  const jobs = createJobs({
242
276
  ...options.jobs,
243
277
  events,
278
+ logger,
244
279
  adapter: jobAdapter,
245
280
  // Disable built-in persistence when using Kysely adapter
246
281
  persist: useLegacy ? options.jobs?.persist : false,
@@ -256,8 +291,6 @@ export class AppServer {
256
291
  });
257
292
 
258
293
  // Processes - still uses its own adapter pattern but can use Kysely
259
- // Note: ProcessesImpl creates its own SqliteProcessAdapter internally
260
- // For full Kysely support, we need to modify processes.ts
261
294
  const processes = createProcesses({
262
295
  ...options.processes,
263
296
  events,
@@ -287,6 +320,7 @@ export class AppServer {
287
320
  audit,
288
321
  websocket,
289
322
  storage,
323
+ logs,
290
324
  };
291
325
 
292
326
  // Resolve circular dependency: workflows needs core for step handlers
@@ -989,10 +1023,20 @@ ${factoryFunction}
989
1023
  // Pass plugins to workflows so handlers can access ctx.plugins
990
1024
  this.coreServices.workflows.setPlugins(this.manager.getServices());
991
1025
 
1026
+ // Forward plugin metadata so isolated workflows can instantiate plugins locally
1027
+ this.coreServices.workflows.setPluginMetadata({
1028
+ names: this.manager.getPluginNames(),
1029
+ modulePaths: this.manager.getPluginModulePaths(),
1030
+ configs: this.manager.getPluginConfigs(),
1031
+ dependencies: this.manager.getPluginDependencies(),
1032
+ customErrors: this.manager.getPluginCustomErrors(),
1033
+ });
1034
+
992
1035
  this.isInitialized = true;
993
1036
 
994
1037
  this.coreServices.cron.start();
995
1038
  this.coreServices.jobs.start();
1039
+ await this.coreServices.workflows.resolveDbPath();
996
1040
  await this.coreServices.workflows.resume();
997
1041
  this.coreServices.processes.start();
998
1042
  logger.info("Background services started (cron, jobs, workflows, processes)");
@@ -1407,6 +1451,10 @@ ${factoryFunction}
1407
1451
  // Run user shutdown handlers first (in reverse order - LIFO)
1408
1452
  await this.runShutdownHandlers();
1409
1453
 
1454
+ // Flush and stop logs before other services shut down
1455
+ await this.coreServices.logs.flush();
1456
+ this.coreServices.logs.stop();
1457
+
1410
1458
  // Stop SSE (closes all client connections)
1411
1459
  this.coreServices.sse.shutdown();
1412
1460
 
File without changes