@donkeylabs/server 2.0.18 → 2.0.20

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/workflows.md CHANGED
@@ -305,6 +305,21 @@ interface WorkflowContext {
305
305
 
306
306
  /** Type-safe step result getter */
307
307
  getStepResult<T>(stepName: string): T | undefined;
308
+
309
+ /** Core services (logger, events, cache, jobs, sse, etc.) */
310
+ core: CoreServices;
311
+
312
+ /** Plugin services - access your plugins' service methods */
313
+ plugins: Record<string, any>;
314
+
315
+ /** Custom metadata that persists across steps (read-only snapshot) */
316
+ metadata: Record<string, any>;
317
+
318
+ /** Set a metadata value that persists across workflow steps */
319
+ setMetadata(key: string, value: any): Promise<void>;
320
+
321
+ /** Get a typed metadata value */
322
+ getMetadata<T>(key: string): T | undefined;
308
323
  }
309
324
  ```
310
325
 
@@ -320,11 +335,59 @@ Example usage in step configuration:
320
335
  handler: async (input, ctx) => {
321
336
  // Access any step's output
322
337
  const calcResult = ctx.getStepResult<{ amount: number }>("calculate");
338
+
339
+ // Access plugin services
340
+ const order = await ctx.plugins.orders.getById(input.orderId);
341
+
342
+ // Use core services
343
+ ctx.core.logger.info("Processing order", { orderId: input.orderId });
344
+
323
345
  return { processed: true, amount: calcResult?.amount };
324
346
  },
325
347
  })
326
348
  ```
327
349
 
350
+ ### Cross-Step Metadata
351
+
352
+ Use metadata to share data across workflow steps that isn't part of the normal step output flow:
353
+
354
+ ```typescript
355
+ .task("validate", {
356
+ inputSchema: z.object({ orderId: z.string() }),
357
+ handler: async (input, ctx) => {
358
+ // Store correlation ID for logging/tracing across steps
359
+ await ctx.setMetadata("correlationId", crypto.randomUUID());
360
+
361
+ // Store complex context that multiple steps need
362
+ await ctx.setMetadata("orderContext", {
363
+ customer: await ctx.plugins.customers.getByOrder(input.orderId),
364
+ flags: { expedited: false, giftWrap: false },
365
+ });
366
+
367
+ return { valid: true };
368
+ },
369
+ })
370
+ .task("fulfill", {
371
+ handler: async (input, ctx) => {
372
+ // Read metadata from previous steps
373
+ const correlationId = ctx.getMetadata<string>("correlationId");
374
+ const orderCtx = ctx.getMetadata<{ customer: Customer; flags: object }>("orderContext");
375
+
376
+ ctx.core.logger.info("Fulfilling order", { correlationId, customer: orderCtx?.customer.id });
377
+
378
+ // Update metadata for downstream steps
379
+ await ctx.setMetadata("orderContext", {
380
+ ...orderCtx,
381
+ flags: { ...orderCtx?.flags, fulfilled: true },
382
+ });
383
+
384
+ return { shipped: true };
385
+ },
386
+ })
387
+ ```
388
+
389
+ Metadata is persisted to the database and survives server restarts.
390
+
328
391
  ## Retry Configuration
329
392
 
330
393
  Configure retries at the step level or set defaults for the entire workflow:
@@ -440,6 +503,8 @@ interface WorkflowInstance {
440
503
  output?: any;
441
504
  error?: string;
442
505
  stepResults: Record<string, StepResult>;
506
+ /** Custom metadata that persists across steps */
507
+ metadata?: Record<string, any>;
443
508
  createdAt: Date;
444
509
  startedAt?: Date;
445
510
  completedAt?: Date;
@@ -459,7 +524,28 @@ interface StepResult {
459
524
 
460
525
  ## Persistence
461
526
 
462
- By default, workflows use an in-memory adapter. For production, implement a `WorkflowAdapter`:
527
+ By default, workflows use a **Kysely database adapter** that stores workflow instances in the same database as your application. This provides automatic persistence and restart resilience.
528
+
529
+ The framework automatically creates and manages the `__donkeylabs_workflow_instances__` table with proper migrations.
530
+
531
+ ### Built-in Kysely Adapter
532
+
533
+ The `KyselyWorkflowAdapter` is automatically configured when you provide a database connection:
534
+
535
+ ```typescript
536
+ const server = new AppServer({
537
+ db: createDatabase(), // Workflows will automatically persist to this database
538
+ workflows: {
539
+ pollInterval: 1000, // How often to check job completion
540
+ cleanupDays: 30, // Auto-cleanup completed workflows older than 30 days (0 to disable)
541
+ cleanupInterval: 3600000, // Cleanup check interval (default: 1 hour)
542
+ },
543
+ });
544
+ ```
545
+
546
+ ### Custom Adapter
547
+
548
+ For custom storage backends, implement the `WorkflowAdapter` interface:
463
549
 
464
550
  ```typescript
465
551
  interface WorkflowAdapter {
@@ -469,6 +555,7 @@ interface WorkflowAdapter {
469
555
  deleteInstance(instanceId: string): Promise<boolean>;
470
556
  getInstancesByWorkflow(workflowName: string, status?: WorkflowStatus): Promise<WorkflowInstance[]>;
471
557
  getRunningInstances(): Promise<WorkflowInstance[]>;
558
+ getAllInstances(options?: GetAllWorkflowsOptions): Promise<WorkflowInstance[]>;
472
559
  }
473
560
  ```
474
561
 
@@ -478,8 +565,8 @@ Configure via `ServerConfig`:
478
565
  const server = new AppServer({
479
566
  db: createDatabase(),
480
567
  workflows: {
481
- adapter: new MyDatabaseWorkflowAdapter(db),
482
- pollInterval: 1000, // How often to check job completion
568
+ adapter: new MyCustomWorkflowAdapter(),
569
+ pollInterval: 1000,
483
570
  },
484
571
  });
485
572
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "2.0.18",
3
+ "version": "2.0.20",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
@@ -30,6 +30,10 @@
30
30
  "types": "./src/process-client.ts",
31
31
  "import": "./src/process-client.ts"
32
32
  },
33
+ "./testing": {
34
+ "types": "./src/testing/index.ts",
35
+ "import": "./src/testing/index.ts"
36
+ },
33
37
  "./context": {
34
38
  "types": "./context.d.ts"
35
39
  },
@@ -68,7 +72,10 @@
68
72
  "kysely": "^0.27.0 || ^0.28.0",
69
73
  "zod": "^3.20.0",
70
74
  "@aws-sdk/client-s3": "^3.0.0",
71
- "@aws-sdk/s3-request-presigner": "^3.0.0"
75
+ "@aws-sdk/s3-request-presigner": "^3.0.0",
76
+ "@playwright/test": "^1.40.0",
77
+ "pg": "^8.0.0",
78
+ "mysql2": "^3.0.0"
72
79
  },
73
80
  "peerDependenciesMeta": {
74
81
  "@aws-sdk/client-s3": {
@@ -76,6 +83,15 @@
76
83
  },
77
84
  "@aws-sdk/s3-request-presigner": {
78
85
  "optional": true
86
+ },
87
+ "@playwright/test": {
88
+ "optional": true
89
+ },
90
+ "pg": {
91
+ "optional": true
92
+ },
93
+ "mysql2": {
94
+ "optional": true
79
95
  }
80
96
  },
81
97
  "dependencies": {
@@ -397,6 +397,159 @@ export function createAdminRouter(config: AdminRouteContext) {
397
397
  })
398
398
  );
399
399
 
400
+ // Workflows get route - full instance details with step results
401
+ router.route("workflows.get").typed(
402
+ defineRoute({
403
+ input: z.object({ instanceId: z.string() }),
404
+ output: z.object({
405
+ id: z.string(),
406
+ workflowName: z.string(),
407
+ status: z.string(),
408
+ currentStep: z.string().nullable(),
409
+ input: z.any(),
410
+ output: z.any().nullable(),
411
+ error: z.string().nullable(),
412
+ stepResults: z.record(
413
+ z.object({
414
+ stepName: z.string(),
415
+ status: z.string(),
416
+ input: z.any().optional(),
417
+ output: z.any().optional(),
418
+ error: z.string().optional(),
419
+ startedAt: z.string().nullable(),
420
+ completedAt: z.string().nullable(),
421
+ attempts: z.number(),
422
+ })
423
+ ),
424
+ metadata: z.record(z.any()).nullable(),
425
+ createdAt: z.string(),
426
+ startedAt: z.string().nullable(),
427
+ completedAt: z.string().nullable(),
428
+ parentId: z.string().nullable(),
429
+ branchName: z.string().nullable(),
430
+ }).nullable(),
431
+ handle: async (input, ctx) => {
432
+ if (!checkAuth(ctx)) {
433
+ throw ctx.errors.Forbidden("Unauthorized");
434
+ }
435
+ const instance = await ctx.core.workflows.getInstance(input.instanceId);
436
+ if (!instance) {
437
+ return null;
438
+ }
439
+
440
+ // Transform step results with proper date serialization
441
+ const stepResults: Record<string, any> = {};
442
+ for (const [key, result] of Object.entries(instance.stepResults)) {
443
+ stepResults[key] = {
444
+ stepName: result.stepName,
445
+ status: result.status,
446
+ input: result.input,
447
+ output: result.output,
448
+ error: result.error,
449
+ startedAt: result.startedAt?.toISOString() ?? null,
450
+ completedAt: result.completedAt?.toISOString() ?? null,
451
+ attempts: result.attempts,
452
+ };
453
+ }
454
+
455
+ return {
456
+ id: instance.id,
457
+ workflowName: instance.workflowName,
458
+ status: instance.status,
459
+ currentStep: instance.currentStep ?? null,
460
+ input: instance.input,
461
+ output: instance.output ?? null,
462
+ error: instance.error ?? null,
463
+ stepResults,
464
+ metadata: instance.metadata ?? null,
465
+ createdAt: instance.createdAt.toISOString(),
466
+ startedAt: instance.startedAt?.toISOString() ?? null,
467
+ completedAt: instance.completedAt?.toISOString() ?? null,
468
+ parentId: instance.parentId ?? null,
469
+ branchName: instance.branchName ?? null,
470
+ };
471
+ },
472
+ })
473
+ );
474
+
475
+ // Workflows subscribe route - SSE for real-time workflow events
476
+ // Subscribes to workflow:{instanceId} channel for progress, completed, and failed events
477
+ router.route("workflows.subscribe").sse({
478
+ input: z.object({
479
+ instanceId: z.string(),
480
+ }),
481
+ events: {
482
+ progress: z.object({
483
+ progress: z.number(),
484
+ currentStep: z.string().optional(),
485
+ completedSteps: z.number(),
486
+ totalSteps: z.number(),
487
+ }),
488
+ "step.started": z.object({
489
+ stepName: z.string(),
490
+ }),
491
+ "step.completed": z.object({
492
+ stepName: z.string(),
493
+ output: z.any().optional(),
494
+ }),
495
+ "step.failed": z.object({
496
+ stepName: z.string(),
497
+ error: z.string(),
498
+ }),
499
+ completed: z.object({
500
+ output: z.any().optional(),
501
+ }),
502
+ failed: z.object({
503
+ error: z.string(),
504
+ }),
505
+ },
506
+ handle: (input, ctx) => {
507
+ if (!checkAuth(ctx)) {
508
+ return [];
509
+ }
510
+ // Subscribe to the workflow-specific channel
511
+ return [`workflow:${input.instanceId}`];
512
+ },
513
+ });
514
+
515
+ // Workflows subscribe all route - SSE for all workflow events (useful for dashboard)
516
+ // Subscribes to workflow:* pattern for all workflow updates
517
+ router.route("workflows.subscribeAll").sse({
518
+ input: z.object({
519
+ // Optional filter by workflow name
520
+ workflowName: z.string().optional(),
521
+ }),
522
+ events: {
523
+ "workflow.started": z.object({
524
+ instanceId: z.string(),
525
+ workflowName: z.string(),
526
+ }),
527
+ "workflow.progress": z.object({
528
+ instanceId: z.string(),
529
+ workflowName: z.string(),
530
+ progress: z.number(),
531
+ currentStep: z.string().optional(),
532
+ }),
533
+ "workflow.completed": z.object({
534
+ instanceId: z.string(),
535
+ workflowName: z.string(),
536
+ }),
537
+ "workflow.failed": z.object({
538
+ instanceId: z.string(),
539
+ workflowName: z.string(),
540
+ error: z.string(),
541
+ }),
542
+ },
543
+ handle: (input, ctx) => {
544
+ if (!checkAuth(ctx)) {
545
+ return [];
546
+ }
547
+ // Subscribe to the global workflow events channel
548
+ // Events are emitted via ctx.core.events and need to be bridged to SSE
549
+ return ["workflows:all"];
550
+ },
551
+ });
552
+
400
553
  // Audit list route
401
554
  router.route("audit.list").typed(
402
555
  defineRoute({
package/src/core/cron.ts CHANGED
@@ -35,8 +35,16 @@ export interface Cron {
35
35
  // Supports: * (any), specific values, ranges (1-5), steps (*/5)
36
36
  // Format: second minute hour dayOfMonth month dayOfWeek
37
37
  // Also supports 5-field format (minute hour dayOfMonth month dayOfWeek)
38
+ //
39
+ // Day-of-month/day-of-week semantics:
40
+ // - Standard cron uses OR semantics when both are restricted (not *)
41
+ // - If BOTH day-of-month and day-of-week are explicitly set, job runs if EITHER matches
42
+ // - If one is *, the other is used exclusively
38
43
  class CronExpression {
39
44
  private fields: [number[], number[], number[], number[], number[], number[]];
45
+ // Track if day fields were explicitly set (not *)
46
+ private dayOfMonthRestricted: boolean;
47
+ private dayOfWeekRestricted: boolean;
40
48
 
41
49
  constructor(expression: string) {
42
50
  const parts = expression.trim().split(/\s+/);
@@ -44,6 +52,8 @@ class CronExpression {
44
52
  // Support both 5-field and 6-field cron
45
53
  if (parts.length === 5) {
46
54
  // minute hour dayOfMonth month dayOfWeek
55
+ this.dayOfMonthRestricted = parts[2] !== "*";
56
+ this.dayOfWeekRestricted = parts[4] !== "*";
47
57
  this.fields = [
48
58
  [0], // seconds (always 0)
49
59
  this.parseField(parts[0]!, 0, 59), // minutes
@@ -54,6 +64,8 @@ class CronExpression {
54
64
  ];
55
65
  } else if (parts.length === 6) {
56
66
  // second minute hour dayOfMonth month dayOfWeek
67
+ this.dayOfMonthRestricted = parts[3] !== "*";
68
+ this.dayOfWeekRestricted = parts[5] !== "*";
57
69
  this.fields = [
58
70
  this.parseField(parts[0]!, 0, 59), // seconds
59
71
  this.parseField(parts[1]!, 0, 59), // minutes
@@ -99,31 +111,142 @@ class CronExpression {
99
111
  const month = date.getMonth() + 1;
100
112
  const dayOfWeek = date.getDay();
101
113
 
102
- return (
103
- this.fields[0].includes(second) &&
104
- this.fields[1].includes(minute) &&
105
- this.fields[2].includes(hour) &&
106
- this.fields[3].includes(dayOfMonth) &&
107
- this.fields[4].includes(month) &&
108
- this.fields[5].includes(dayOfWeek)
109
- );
114
+ // Check time fields (always AND)
115
+ if (!this.fields[0].includes(second)) return false;
116
+ if (!this.fields[1].includes(minute)) return false;
117
+ if (!this.fields[2].includes(hour)) return false;
118
+ if (!this.fields[4].includes(month)) return false;
119
+
120
+ // Day matching uses standard cron semantics:
121
+ // - If both day-of-month AND day-of-week are restricted (not *), use OR
122
+ // - Otherwise, use AND (one or both are unrestricted)
123
+ const domMatches = this.fields[3].includes(dayOfMonth);
124
+ const dowMatches = this.fields[5].includes(dayOfWeek);
125
+
126
+ if (this.dayOfMonthRestricted && this.dayOfWeekRestricted) {
127
+ // Both restricted: OR semantics - job runs if EITHER matches
128
+ return domMatches || dowMatches;
129
+ } else {
130
+ // One or both unrestricted: AND semantics
131
+ return domMatches && dowMatches;
132
+ }
110
133
  }
111
134
 
135
+ /**
136
+ * Get the next run time using an optimized jump algorithm.
137
+ * Instead of iterating second-by-second (which could be 31M iterations),
138
+ * this jumps directly to the next valid value for each field.
139
+ */
112
140
  getNextRun(from: Date = new Date()): Date {
113
141
  const next = new Date(from);
114
142
  next.setMilliseconds(0);
115
143
  next.setSeconds(next.getSeconds() + 1);
116
144
 
117
- // Search up to 1 year ahead
118
- const maxIterations = 366 * 24 * 60 * 60;
119
- for (let i = 0; i < maxIterations; i++) {
120
- if (this.matches(next)) {
121
- return next;
145
+ const [seconds, minutes, hours, daysOfMonth, months, daysOfWeek] = this.fields;
146
+
147
+ // Maximum iterations to prevent infinite loops (covers 4 years to handle leap years)
148
+ const maxYearIterations = 4;
149
+ const startYear = next.getFullYear();
150
+
151
+ // Iterate through potential dates (worst case: a few hundred iterations)
152
+ for (let yearOffset = 0; yearOffset <= maxYearIterations; yearOffset++) {
153
+ // Try each valid month
154
+ for (const month of months) {
155
+ const targetMonth = month - 1; // JS months are 0-indexed
156
+
157
+ // Skip months in the past
158
+ if (next.getFullYear() === startYear + yearOffset) {
159
+ if (targetMonth < next.getMonth()) continue;
160
+ }
161
+
162
+ // Set to this month
163
+ if (targetMonth !== next.getMonth() || next.getFullYear() !== startYear + yearOffset) {
164
+ next.setFullYear(startYear + yearOffset, targetMonth, 1);
165
+ next.setHours(0, 0, 0, 0);
166
+ }
167
+
168
+ // Get days in this month
169
+ const daysInMonth = new Date(next.getFullYear(), targetMonth + 1, 0).getDate();
170
+
171
+ // Try each valid day of month
172
+ for (const dayOfMonth of daysOfMonth) {
173
+ if (dayOfMonth > daysInMonth) continue; // Skip invalid days for this month
174
+
175
+ // Check if this day matches day-of-week constraint
176
+ // Use OR semantics if both are restricted, AND otherwise
177
+ const testDate = new Date(next.getFullYear(), targetMonth, dayOfMonth);
178
+ const dayOfWeek = testDate.getDay();
179
+ const domMatches = daysOfMonth.includes(dayOfMonth);
180
+ const dowMatches = daysOfWeek.includes(dayOfWeek);
181
+
182
+ let dayMatches: boolean;
183
+ if (this.dayOfMonthRestricted && this.dayOfWeekRestricted) {
184
+ // Both restricted: OR semantics
185
+ dayMatches = domMatches || dowMatches;
186
+ } else {
187
+ // One or both unrestricted: AND semantics
188
+ dayMatches = domMatches && dowMatches;
189
+ }
190
+ if (!dayMatches) continue;
191
+
192
+ // Skip days in the past
193
+ if (testDate < new Date(from.getFullYear(), from.getMonth(), from.getDate())) continue;
194
+
195
+ // Set to this day
196
+ if (dayOfMonth !== next.getDate()) {
197
+ next.setDate(dayOfMonth);
198
+ next.setHours(0, 0, 0, 0);
199
+ }
200
+
201
+ // Try each valid hour
202
+ for (const hour of hours) {
203
+ // Skip hours in the past for today
204
+ if (next.getFullYear() === from.getFullYear() &&
205
+ next.getMonth() === from.getMonth() &&
206
+ next.getDate() === from.getDate() &&
207
+ hour < from.getHours()) continue;
208
+
209
+ if (hour !== next.getHours()) {
210
+ next.setHours(hour, 0, 0, 0);
211
+ }
212
+
213
+ // Try each valid minute
214
+ for (const minute of minutes) {
215
+ // Skip minutes in the past for this hour
216
+ if (next.getFullYear() === from.getFullYear() &&
217
+ next.getMonth() === from.getMonth() &&
218
+ next.getDate() === from.getDate() &&
219
+ next.getHours() === from.getHours() &&
220
+ minute < from.getMinutes()) continue;
221
+
222
+ if (minute !== next.getMinutes()) {
223
+ next.setMinutes(minute, 0, 0);
224
+ }
225
+
226
+ // Try each valid second
227
+ for (const second of seconds) {
228
+ // Skip seconds in the past for this minute
229
+ if (next.getFullYear() === from.getFullYear() &&
230
+ next.getMonth() === from.getMonth() &&
231
+ next.getDate() === from.getDate() &&
232
+ next.getHours() === from.getHours() &&
233
+ next.getMinutes() === from.getMinutes() &&
234
+ second <= from.getSeconds()) continue;
235
+
236
+ next.setSeconds(second);
237
+
238
+ // Verify the date is still valid (handles edge cases like month rollover)
239
+ if (next > from && this.matches(next)) {
240
+ return next;
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
122
246
  }
123
- next.setSeconds(next.getSeconds() + 1);
124
247
  }
125
248
 
126
- throw new Error("Could not find next run time within 1 year");
249
+ throw new Error("Could not find next run time within 4 years");
127
250
  }
128
251
  }
129
252
 
@@ -220,6 +343,9 @@ class CronImpl implements Cron {
220
343
  if (this.running) return;
221
344
  this.running = true;
222
345
 
346
+ // Catch-up check for missed runs since last shutdown
347
+ this.catchUpMissedRuns();
348
+
223
349
  // Check every second
224
350
  this.timer = setInterval(() => {
225
351
  const now = new Date();
@@ -241,6 +367,49 @@ class CronImpl implements Cron {
241
367
  }, 1000);
242
368
  }
243
369
 
370
+ /**
371
+ * Catch up on missed runs since last shutdown.
372
+ * For each task with a lastRun timestamp, execute any runs that were missed.
373
+ * Limited to 10 catch-up runs per task to avoid flooding.
374
+ */
375
+ private catchUpMissedRuns(): void {
376
+ const now = new Date();
377
+ const maxCatchUp = 10;
378
+
379
+ for (const task of this.tasks.values()) {
380
+ if (!task.enabled || !task.lastRun) continue;
381
+
382
+ try {
383
+ const cronExpr = new CronExpression(task.expression);
384
+ let missedRun = cronExpr.getNextRun(task.lastRun);
385
+ let catchUpCount = 0;
386
+
387
+ // Execute any missed runs (up to limit to avoid flooding)
388
+ while (missedRun && missedRun < now && catchUpCount < maxCatchUp) {
389
+ console.log(`[Cron] Catching up missed run for "${task.name}" at ${missedRun.toISOString()}`);
390
+
391
+ // Execute the handler asynchronously
392
+ Promise.resolve(task.handler()).catch(err => {
393
+ console.error(`[Cron] Catch-up task "${task.name}" failed:`, err);
394
+ });
395
+
396
+ task.lastRun = missedRun;
397
+ missedRun = cronExpr.getNextRun(missedRun);
398
+ catchUpCount++;
399
+ }
400
+
401
+ // Update nextRun to the next scheduled time
402
+ task.nextRun = cronExpr.getNextRun(now);
403
+
404
+ if (catchUpCount > 0) {
405
+ console.log(`[Cron] Caught up ${catchUpCount} missed runs for "${task.name}"`);
406
+ }
407
+ } catch (err) {
408
+ console.error(`[Cron] Error catching up task "${task.name}":`, err);
409
+ }
410
+ }
411
+ }
412
+
244
413
  async stop(): Promise<void> {
245
414
  this.running = false;
246
415
  if (this.timer) {
package/src/core/index.ts CHANGED
@@ -132,6 +132,7 @@ export {
132
132
  export {
133
133
  type Workflows,
134
134
  type WorkflowsConfig,
135
+ type WorkflowRegisterOptions,
135
136
  type WorkflowDefinition,
136
137
  type WorkflowInstance,
137
138
  type WorkflowStatus,
@@ -154,6 +155,30 @@ export {
154
155
  createWorkflows,
155
156
  } from "./workflows";
156
157
 
158
+ export {
159
+ type WorkflowSocketServer,
160
+ type WorkflowSocketServerOptions,
161
+ type WorkflowSocketConfig,
162
+ type WorkflowEvent,
163
+ type WorkflowEventType,
164
+ type ProxyRequest,
165
+ type ProxyResponse,
166
+ type WorkflowMessage,
167
+ createWorkflowSocketServer,
168
+ isWorkflowEvent,
169
+ isProxyRequest,
170
+ parseWorkflowMessage,
171
+ } from "./workflow-socket";
172
+
173
+ export {
174
+ type ProxyConnection,
175
+ WorkflowProxyConnection,
176
+ createPluginProxy,
177
+ createCoreProxy,
178
+ createPluginsProxy,
179
+ createCoreServicesProxy,
180
+ } from "./workflow-proxy";
181
+
157
182
  export {
158
183
  type Processes,
159
184
  type ProcessesConfig,