@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,409 @@
1
+ /**
2
+ * Operation handler registry.
3
+ *
4
+ * This module provides registration and lookup of operation handlers.
5
+ * Handlers must be registered before workflows using them can be executed.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { createOperationRegistry } from '@sprqvntrs/workflows';
10
+ *
11
+ * const registry = createOperationRegistry();
12
+ *
13
+ * registry.register('gather.data', async (context) => {
14
+ * const data = await fetchData(context.previousResults.url);
15
+ * return { status: 'completed', data: { fetchedData: data } };
16
+ * });
17
+ *
18
+ * const handler = registry.get('gather.data');
19
+ * ```
20
+ */
21
+
22
+ import type { OperationHandler, OperationContext, OperationResult } from '../types';
23
+
24
+ // =============================================================================
25
+ // Types
26
+ // =============================================================================
27
+
28
+ /**
29
+ * Operation registry interface.
30
+ *
31
+ * Provides methods for registering and retrieving operation handlers.
32
+ */
33
+ export interface OperationRegistry {
34
+ /**
35
+ * Registers an operation handler.
36
+ *
37
+ * @param type - Operation type identifier
38
+ * @param handler - Handler function
39
+ * @throws Error if handler already registered
40
+ */
41
+ register: <TContext = Record<string, unknown>, TResult = Record<string, unknown>>(
42
+ type: string,
43
+ handler: OperationHandler<TContext, TResult>,
44
+ ) => void;
45
+
46
+ /**
47
+ * Registers multiple handlers at once.
48
+ *
49
+ * @param handlers - Map of type to handler
50
+ */
51
+ registerMany: (handlers: Record<string, OperationHandler>) => void;
52
+
53
+ /**
54
+ * Gets a handler by type.
55
+ *
56
+ * @param type - Operation type
57
+ * @returns Handler or undefined
58
+ */
59
+ get: (type: string) => OperationHandler | undefined;
60
+
61
+ /**
62
+ * Gets a handler by type, throwing if not found.
63
+ *
64
+ * @param type - Operation type
65
+ * @returns Handler
66
+ * @throws Error if handler not found
67
+ */
68
+ getOrThrow: (type: string) => OperationHandler;
69
+
70
+ /**
71
+ * Checks if a handler is registered.
72
+ *
73
+ * @param type - Operation type
74
+ * @returns True if handler exists
75
+ */
76
+ has: (type: string) => boolean;
77
+
78
+ /**
79
+ * Gets all registered operation types.
80
+ *
81
+ * @returns Set of operation types
82
+ */
83
+ types: () => Set<string>;
84
+
85
+ /**
86
+ * Unregisters a handler.
87
+ *
88
+ * @param type - Operation type to remove
89
+ * @returns True if handler was removed
90
+ */
91
+ unregister: (type: string) => boolean;
92
+
93
+ /**
94
+ * Clears all registered handlers.
95
+ */
96
+ clear: () => void;
97
+ }
98
+
99
+ // =============================================================================
100
+ // Registry Factory
101
+ // =============================================================================
102
+
103
+ /**
104
+ * Creates a new operation registry.
105
+ *
106
+ * The registry stores operation handlers and provides type-safe lookup.
107
+ *
108
+ * @returns Operation registry instance
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * const registry = createOperationRegistry();
113
+ *
114
+ * // Register individual handlers
115
+ * registry.register('gather.data', async (context) => {
116
+ * const url = context.previousResults.url as string;
117
+ * const data = await fetch(url).then(r => r.json());
118
+ * return { status: 'completed', data: { fetchedData: data } };
119
+ * });
120
+ *
121
+ * // Register multiple handlers at once
122
+ * registry.registerMany({
123
+ * 'analyze.content': analyzeContentHandler,
124
+ * 'analyze.market': analyzeMarketHandler,
125
+ * 'generate.report': generateReportHandler,
126
+ * });
127
+ *
128
+ * // Get handler
129
+ * const handler = registry.getOrThrow('gather.data');
130
+ *
131
+ * // Check available types
132
+ * const types = registry.types();
133
+ * console.log('Registered operations:', Array.from(types));
134
+ * ```
135
+ */
136
+ export function createOperationRegistry(): OperationRegistry {
137
+ const handlers = new Map<string, OperationHandler>();
138
+
139
+ return {
140
+ register<TContext = Record<string, unknown>, TResult = Record<string, unknown>>(
141
+ type: string,
142
+ handler: OperationHandler<TContext, TResult>,
143
+ ): void {
144
+ if (handlers.has(type)) {
145
+ throw new Error(`Operation handler "${type}" is already registered`);
146
+ }
147
+ handlers.set(type, handler as OperationHandler);
148
+ },
149
+
150
+ registerMany(handlersMap: Record<string, OperationHandler>): void {
151
+ // Check for duplicates first
152
+ for (const type of Object.keys(handlersMap)) {
153
+ if (handlers.has(type)) {
154
+ throw new Error(`Operation handler "${type}" is already registered`);
155
+ }
156
+ }
157
+
158
+ // Register all
159
+ for (const [type, handler] of Object.entries(handlersMap)) {
160
+ handlers.set(type, handler);
161
+ }
162
+ },
163
+
164
+ get(type: string): OperationHandler | undefined {
165
+ return handlers.get(type);
166
+ },
167
+
168
+ getOrThrow(type: string): OperationHandler {
169
+ const handler = handlers.get(type);
170
+ if (!handler) {
171
+ throw new Error(`Operation handler "${type}" not found`);
172
+ }
173
+ return handler;
174
+ },
175
+
176
+ has(type: string): boolean {
177
+ return handlers.has(type);
178
+ },
179
+
180
+ types(): Set<string> {
181
+ return new Set(handlers.keys());
182
+ },
183
+
184
+ unregister(type: string): boolean {
185
+ return handlers.delete(type);
186
+ },
187
+
188
+ clear(): void {
189
+ handlers.clear();
190
+ },
191
+ };
192
+ }
193
+
194
+ // =============================================================================
195
+ // Handler Utilities
196
+ // =============================================================================
197
+
198
+ /**
199
+ * Wraps an operation handler with timeout handling.
200
+ *
201
+ * Creates a new handler that will reject after the specified timeout.
202
+ *
203
+ * @param handler - Original handler
204
+ * @param timeoutMs - Timeout in milliseconds
205
+ * @returns Wrapped handler with timeout
206
+ *
207
+ * @example
208
+ * ```typescript
209
+ * const handler = withTimeout(originalHandler, 30000);
210
+ * // Handler will reject after 30 seconds
211
+ * ```
212
+ */
213
+ export function withTimeout<TContext = Record<string, unknown>, TResult = Record<string, unknown>>(
214
+ handler: OperationHandler<TContext, TResult>,
215
+ timeoutMs: number,
216
+ ): OperationHandler<TContext, TResult> {
217
+ return async (context: OperationContext<TContext>): Promise<OperationResult<TResult>> => {
218
+ const timeoutPromise = new Promise<never>((_, reject) => {
219
+ setTimeout(() => {
220
+ reject(new Error(`Operation timed out after ${timeoutMs}ms`));
221
+ }, timeoutMs);
222
+ });
223
+
224
+ return Promise.race([handler(context), timeoutPromise]);
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Wraps an operation handler with retry logic.
230
+ *
231
+ * Creates a new handler that will retry on failure with exponential backoff.
232
+ *
233
+ * @param handler - Original handler
234
+ * @param options - Retry options
235
+ * @returns Wrapped handler with retry
236
+ *
237
+ * @example
238
+ * ```typescript
239
+ * const handler = withRetry(originalHandler, {
240
+ * maxAttempts: 3,
241
+ * baseDelayMs: 1000,
242
+ * backoffMultiplier: 2,
243
+ * });
244
+ * ```
245
+ */
246
+ export function withRetry<TContext = Record<string, unknown>, TResult = Record<string, unknown>>(
247
+ handler: OperationHandler<TContext, TResult>,
248
+ options: {
249
+ /**
250
+ * Maximum number of attempts.
251
+ * @default 3
252
+ */
253
+ maxAttempts?: number;
254
+ /**
255
+ * Base delay between retries in milliseconds.
256
+ * @default 1000
257
+ */
258
+ baseDelayMs?: number;
259
+ /**
260
+ * Multiplier for exponential backoff.
261
+ * @default 2
262
+ */
263
+ backoffMultiplier?: number;
264
+ /**
265
+ * Function to determine if error should be retried.
266
+ * @default () => true
267
+ */
268
+ shouldRetry?: (error: Error) => boolean;
269
+ } = {},
270
+ ): OperationHandler<TContext, TResult> {
271
+ const { maxAttempts = 3, baseDelayMs = 1000, backoffMultiplier = 2, shouldRetry = () => true } = options;
272
+
273
+ return async (context: OperationContext<TContext>): Promise<OperationResult<TResult>> => {
274
+ let lastError: Error | undefined;
275
+
276
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
277
+ try {
278
+ const result = await handler(context);
279
+ return result;
280
+ } catch (error) {
281
+ lastError = error instanceof Error ? error : new Error(String(error));
282
+
283
+ if (attempt === maxAttempts || !shouldRetry(lastError)) {
284
+ return {
285
+ status: 'failed',
286
+ reason: lastError.message,
287
+ };
288
+ }
289
+
290
+ // Exponential backoff
291
+ const delay = baseDelayMs * Math.pow(backoffMultiplier, attempt - 1);
292
+ await new Promise((resolve) => setTimeout(resolve, delay));
293
+ }
294
+ }
295
+
296
+ return {
297
+ status: 'failed',
298
+ reason: lastError?.message ?? 'Unknown error',
299
+ };
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Wraps an operation handler to catch exceptions and convert to failed result.
305
+ *
306
+ * Ensures handler never throws, returning a failed result instead.
307
+ *
308
+ * @param handler - Original handler
309
+ * @returns Safe handler that never throws
310
+ *
311
+ * @example
312
+ * ```typescript
313
+ * const handler = withErrorBoundary(originalHandler);
314
+ * // Exceptions are caught and converted to { status: 'failed', reason: '...' }
315
+ * ```
316
+ */
317
+ export function withErrorBoundary<TContext = Record<string, unknown>, TResult = Record<string, unknown>>(
318
+ handler: OperationHandler<TContext, TResult>,
319
+ ): OperationHandler<TContext, TResult> {
320
+ return async (context: OperationContext<TContext>): Promise<OperationResult<TResult>> => {
321
+ try {
322
+ return await handler(context);
323
+ } catch (error) {
324
+ const message = error instanceof Error ? error.message : String(error);
325
+ return {
326
+ status: 'failed',
327
+ reason: message,
328
+ };
329
+ }
330
+ };
331
+ }
332
+
333
+ /**
334
+ * Combines multiple handler wrappers.
335
+ *
336
+ * Applies wrappers from right to left (innermost to outermost).
337
+ *
338
+ * @param handler - Original handler
339
+ * @param wrappers - Array of wrapper functions
340
+ * @returns Handler with all wrappers applied
341
+ *
342
+ * @example
343
+ * ```typescript
344
+ * const handler = compose(
345
+ * originalHandler,
346
+ * (h) => withErrorBoundary(h),
347
+ * (h) => withTimeout(h, 30000),
348
+ * (h) => withRetry(h, { maxAttempts: 3 }),
349
+ * );
350
+ * ```
351
+ */
352
+ export function compose<TContext = Record<string, unknown>, TResult = Record<string, unknown>>(
353
+ handler: OperationHandler<TContext, TResult>,
354
+ ...wrappers: Array<(h: OperationHandler<TContext, TResult>) => OperationHandler<TContext, TResult>>
355
+ ): OperationHandler<TContext, TResult> {
356
+ return wrappers.reduceRight((h, wrapper) => wrapper(h), handler);
357
+ }
358
+
359
+ // =============================================================================
360
+ // Helper Types
361
+ // =============================================================================
362
+
363
+ /**
364
+ * Type helper for defining operation handlers with typed context.
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * interface GatherContext {
369
+ * url: string;
370
+ * userId: string;
371
+ * }
372
+ *
373
+ * interface GatherResult {
374
+ * data: unknown;
375
+ * fetchedAt: string;
376
+ * }
377
+ *
378
+ * const gatherHandler: TypedOperationHandler<GatherContext, GatherResult> = async (ctx) => {
379
+ * // ctx.previousResults.url is typed as string
380
+ * return {
381
+ * status: 'completed',
382
+ * data: { data: {}, fetchedAt: new Date().toISOString() },
383
+ * };
384
+ * };
385
+ * ```
386
+ */
387
+ export type TypedOperationHandler<TContext, TResult> = OperationHandler<TContext, TResult>;
388
+
389
+ /**
390
+ * Creates a typed handler with proper inference.
391
+ *
392
+ * Helper function for better TypeScript inference when creating handlers.
393
+ *
394
+ * @param handler - Handler function
395
+ * @returns The same handler with proper typing
396
+ *
397
+ * @example
398
+ * ```typescript
399
+ * const gatherHandler = defineHandler<{ url: string }, { data: unknown }>(async (ctx) => {
400
+ * const response = await fetch(ctx.previousResults.url);
401
+ * return { status: 'completed', data: { data: await response.json() } };
402
+ * });
403
+ * ```
404
+ */
405
+ export function defineHandler<TContext = Record<string, unknown>, TResult = Record<string, unknown>>(
406
+ handler: OperationHandler<TContext, TResult>,
407
+ ): OperationHandler<TContext, TResult> {
408
+ return handler;
409
+ }