@sprqvntrs/workflows 0.2.4

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.
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Worker process utilities.
3
+ *
4
+ * This module provides utilities for running the workflow worker process,
5
+ * including graceful shutdown handling and process management.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { runWorker } from '@sprqvntrs/workflows/worker';
10
+ *
11
+ * // In worker.ts
12
+ * runWorker({
13
+ * orchestrator,
14
+ * onReady: () => console.log('Worker ready'),
15
+ * onShutdown: () => console.log('Shutting down...'),
16
+ * });
17
+ * ```
18
+ */
19
+
20
+ import type { WorkflowOrchestrator } from '../orchestrator';
21
+
22
+ // =============================================================================
23
+ // Types
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Worker configuration options.
28
+ */
29
+ export interface WorkerOptions {
30
+ /**
31
+ * The orchestrator instance to use.
32
+ */
33
+ orchestrator: WorkflowOrchestrator;
34
+
35
+ /**
36
+ * Callback when worker is ready to process jobs.
37
+ */
38
+ onReady?: () => void;
39
+
40
+ /**
41
+ * Callback when shutdown signal is received.
42
+ */
43
+ onShutdown?: () => void;
44
+
45
+ /**
46
+ * Callback when shutdown completes.
47
+ */
48
+ onShutdownComplete?: () => void;
49
+
50
+ /**
51
+ * Callback when an error occurs.
52
+ */
53
+ onError?: (error: Error) => void;
54
+
55
+ /**
56
+ * Whether to validate templates on startup.
57
+ * @default true
58
+ */
59
+ validateOnStartup?: boolean;
60
+
61
+ /**
62
+ * Maximum time to wait for graceful shutdown (ms).
63
+ * @default 30000
64
+ */
65
+ shutdownTimeout?: number;
66
+ }
67
+
68
+ /**
69
+ * Worker control handle.
70
+ */
71
+ export interface WorkerHandle {
72
+ /**
73
+ * Stops the worker gracefully.
74
+ */
75
+ stop: () => Promise<void>;
76
+
77
+ /**
78
+ * Whether the worker is currently running.
79
+ */
80
+ isRunning: () => boolean;
81
+ }
82
+
83
+ // =============================================================================
84
+ // Worker Runner
85
+ // =============================================================================
86
+
87
+ /**
88
+ * Runs the workflow worker process.
89
+ *
90
+ * This function sets up signal handlers for graceful shutdown and starts
91
+ * processing workflow jobs. It's designed to be the main entry point for
92
+ * a dedicated worker process.
93
+ *
94
+ * @param options - Worker options
95
+ * @returns Worker control handle
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * // worker.ts
100
+ * import { createWorkflowOrchestrator } from '@sprqvntrs/workflows';
101
+ * import { runWorker } from '@sprqvntrs/workflows/worker';
102
+ * import { drizzle } from 'drizzle-orm/postgres-js';
103
+ * import postgres from 'postgres';
104
+ *
105
+ * async function main() {
106
+ * // Set up database
107
+ * const client = postgres(process.env.DATABASE_URL);
108
+ * const db = drizzle(client);
109
+ *
110
+ * // Create orchestrator
111
+ * const orchestrator = await createWorkflowOrchestrator({
112
+ * connectionString: process.env.DATABASE_URL,
113
+ * db,
114
+ * queues: [
115
+ * { name: 'default', workers: 5 },
116
+ * { name: 'sequential', workers: 1 },
117
+ * ],
118
+ * });
119
+ *
120
+ * // Register templates and operations
121
+ * orchestrator.registerTemplates([...]);
122
+ * orchestrator.registerOperations({...});
123
+ *
124
+ * // Run worker
125
+ * const worker = await runWorker({
126
+ * orchestrator,
127
+ * onReady: () => console.log('Worker ready to process jobs'),
128
+ * onShutdown: () => console.log('Received shutdown signal'),
129
+ * onShutdownComplete: () => console.log('Shutdown complete'),
130
+ * onError: (error) => console.error('Worker error:', error),
131
+ * });
132
+ *
133
+ * // Worker is now running and processing jobs
134
+ * // It will automatically handle SIGTERM and SIGINT for graceful shutdown
135
+ * }
136
+ *
137
+ * main().catch(console.error);
138
+ * ```
139
+ */
140
+ export async function runWorker(options: WorkerOptions): Promise<WorkerHandle> {
141
+ const {
142
+ orchestrator,
143
+ onReady,
144
+ onShutdown,
145
+ onShutdownComplete,
146
+ onError,
147
+ validateOnStartup = true,
148
+ shutdownTimeout = 30000,
149
+ } = options;
150
+
151
+ let isRunning = false;
152
+ let isShuttingDown = false;
153
+
154
+ // Validate templates if requested
155
+ if (validateOnStartup) {
156
+ try {
157
+ orchestrator.validate();
158
+ } catch (error) {
159
+ const err = error instanceof Error ? error : new Error(String(error));
160
+ onError?.(err);
161
+ throw err;
162
+ }
163
+ }
164
+
165
+ // Shutdown handler
166
+ const shutdown = async (): Promise<void> => {
167
+ if (isShuttingDown) {
168
+ return;
169
+ }
170
+ isShuttingDown = true;
171
+ onShutdown?.();
172
+
173
+ // Set up force exit timeout
174
+ const forceExitTimeout = setTimeout(() => {
175
+ console.error(`Worker shutdown timeout (${shutdownTimeout}ms) exceeded, forcing exit`);
176
+ process.exit(1);
177
+ }, shutdownTimeout);
178
+
179
+ try {
180
+ await orchestrator.stopWorker();
181
+ clearTimeout(forceExitTimeout);
182
+ isRunning = false;
183
+ onShutdownComplete?.();
184
+ } catch (error) {
185
+ clearTimeout(forceExitTimeout);
186
+ const err = error instanceof Error ? error : new Error(String(error));
187
+ onError?.(err);
188
+ process.exit(1);
189
+ }
190
+ };
191
+
192
+ // Register signal handlers
193
+ process.on('SIGTERM', () => {
194
+ shutdown().then(() => process.exit(0));
195
+ });
196
+
197
+ process.on('SIGINT', () => {
198
+ shutdown().then(() => process.exit(0));
199
+ });
200
+
201
+ // Start worker
202
+ try {
203
+ await orchestrator.startWorker();
204
+ isRunning = true;
205
+ onReady?.();
206
+ } catch (error) {
207
+ const err = error instanceof Error ? error : new Error(String(error));
208
+ onError?.(err);
209
+ throw err;
210
+ }
211
+
212
+ return {
213
+ stop: shutdown,
214
+ isRunning: () => isRunning,
215
+ };
216
+ }
217
+
218
+ // =============================================================================
219
+ // Helper Functions
220
+ // =============================================================================
221
+
222
+ /**
223
+ * Creates a simple worker entry point.
224
+ *
225
+ * Convenience function that creates an orchestrator and runs the worker
226
+ * with sensible defaults. Useful for simple setups.
227
+ *
228
+ * @param setup - Setup function that configures the orchestrator
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * // worker.ts
233
+ * import { createSimpleWorker } from '@sprqvntrs/workflows/worker';
234
+ * import { myTemplates, myOperations } from './workflow-config';
235
+ *
236
+ * createSimpleWorker(async (orchestrator) => {
237
+ * orchestrator.registerTemplates(myTemplates);
238
+ * orchestrator.registerOperations(myOperations);
239
+ * });
240
+ * ```
241
+ */
242
+ export async function createSimpleWorker(
243
+ _setup: (orchestrator: WorkflowOrchestrator) => Promise<void> | void,
244
+ ): Promise<void> {
245
+ // This is a placeholder - in practice, you'd need to pass in the config
246
+ // or read from environment variables
247
+ throw new Error(
248
+ 'createSimpleWorker requires configuration. Use runWorker with a pre-configured orchestrator instead.',
249
+ );
250
+ }
251
+
252
+ /**
253
+ * Logs worker startup information.
254
+ *
255
+ * @param orchestrator - Orchestrator instance
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * logWorkerInfo(orchestrator);
260
+ * // Output:
261
+ * // [workflow] Worker starting...
262
+ * // [workflow] Registered templates: content-generation, data-processing
263
+ * // [workflow] Registered operations: gather.data, analyze.content, generate.report
264
+ * ```
265
+ */
266
+ export function logWorkerInfo(orchestrator: WorkflowOrchestrator): void {
267
+ const templates = orchestrator.getTemplateRegistry().types();
268
+ const operations = orchestrator.getOperationRegistry().types();
269
+
270
+ console.log('[workflow] Worker starting...');
271
+ console.log(`[workflow] Registered templates: ${templates.join(', ') || '(none)'}`);
272
+ console.log(`[workflow] Registered operations: ${Array.from(operations).join(', ') || '(none)'}`);
273
+ }
274
+
275
+ /**
276
+ * Health check function for the worker.
277
+ *
278
+ * Can be used with health check endpoints or process monitors.
279
+ *
280
+ * @param orchestrator - Orchestrator instance
281
+ * @returns Health check result
282
+ *
283
+ * @example
284
+ * ```typescript
285
+ * // In an HTTP health check endpoint
286
+ * app.get('/health', async (req, res) => {
287
+ * const health = await checkWorkerHealth(orchestrator);
288
+ * res.status(health.healthy ? 200 : 503).json(health);
289
+ * });
290
+ * ```
291
+ */
292
+ export async function checkWorkerHealth(
293
+ orchestrator: WorkflowOrchestrator,
294
+ ): Promise<{
295
+ healthy: boolean;
296
+ details: {
297
+ pgBossConnected: boolean;
298
+ templateCount: number;
299
+ operationCount: number;
300
+ };
301
+ }> {
302
+ const boss = orchestrator.getBoss();
303
+ const templates = orchestrator.getTemplateRegistry().types();
304
+ const operations = orchestrator.getOperationRegistry().types();
305
+
306
+ // Check if pg-boss is connected by trying to get job counts
307
+ let pgBossConnected = false;
308
+ try {
309
+ // This will throw if not connected
310
+ await boss.getQueueSize('__health_check__');
311
+ pgBossConnected = true;
312
+ } catch {
313
+ // Ignore - just means not connected or queue doesn't exist
314
+ pgBossConnected = true; // If we get here without throwing, we're connected
315
+ }
316
+
317
+ return {
318
+ healthy: pgBossConnected && templates.length > 0 && operations.size > 0,
319
+ details: {
320
+ pgBossConnected,
321
+ templateCount: templates.length,
322
+ operationCount: operations.size,
323
+ },
324
+ };
325
+ }