@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.
- package/LICENSE +21 -0
- package/README.md +649 -0
- package/index.ts +234 -0
- package/package.json +53 -0
- package/src/coordination/lock-manager.ts +431 -0
- package/src/engine/execution-engine.ts +715 -0
- package/src/infrastructure/db-state.ts +703 -0
- package/src/infrastructure/pg-boss.ts +585 -0
- package/src/infrastructure/pgboss-queries.ts +320 -0
- package/src/infrastructure/schema.ts +342 -0
- package/src/operations/registry.ts +409 -0
- package/src/orchestrator.ts +1056 -0
- package/src/templates/registry.ts +591 -0
- package/src/testing/index.ts +476 -0
- package/src/types.ts +946 -0
- package/src/worker/index.ts +325 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow template registry.
|
|
3
|
+
*
|
|
4
|
+
* This module provides registration and lookup of workflow templates.
|
|
5
|
+
* Templates must be registered before workflows of that type can be started.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createTemplateRegistry } from '@sprqvntrs/workflows';
|
|
10
|
+
*
|
|
11
|
+
* const registry = createTemplateRegistry();
|
|
12
|
+
*
|
|
13
|
+
* registry.register({
|
|
14
|
+
* type: 'my-workflow',
|
|
15
|
+
* queue: 'default',
|
|
16
|
+
* version: '1.0.0',
|
|
17
|
+
* stages: [...]
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* const template = registry.get('my-workflow');
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { WorkflowTemplate, StageTemplate, OperationTemplate, CheckpointTemplate } from '../types';
|
|
25
|
+
import { TemplateError } from '../types';
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Types
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Template registry interface.
|
|
33
|
+
*
|
|
34
|
+
* Provides methods for registering, retrieving, and validating templates.
|
|
35
|
+
*/
|
|
36
|
+
export interface TemplateRegistry {
|
|
37
|
+
/**
|
|
38
|
+
* Registers a workflow template.
|
|
39
|
+
*
|
|
40
|
+
* @param template - Template to register
|
|
41
|
+
* @throws TemplateError if template is invalid or already registered
|
|
42
|
+
*/
|
|
43
|
+
register: (template: WorkflowTemplate) => void;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Registers multiple templates at once.
|
|
47
|
+
*
|
|
48
|
+
* @param templates - Templates to register
|
|
49
|
+
* @throws TemplateError if any template is invalid
|
|
50
|
+
*/
|
|
51
|
+
registerMany: (templates: WorkflowTemplate[]) => void;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Gets a template by type.
|
|
55
|
+
*
|
|
56
|
+
* @param type - Workflow type
|
|
57
|
+
* @returns Template or undefined
|
|
58
|
+
*/
|
|
59
|
+
get: (type: string) => WorkflowTemplate | undefined;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Gets a template by type, throwing if not found.
|
|
63
|
+
*
|
|
64
|
+
* @param type - Workflow type
|
|
65
|
+
* @returns Template
|
|
66
|
+
* @throws TemplateError if template not found
|
|
67
|
+
*/
|
|
68
|
+
getOrThrow: (type: string) => WorkflowTemplate;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Checks if a template is registered.
|
|
72
|
+
*
|
|
73
|
+
* @param type - Workflow type
|
|
74
|
+
* @returns True if template exists
|
|
75
|
+
*/
|
|
76
|
+
has: (type: string) => boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Gets all registered template types.
|
|
80
|
+
*
|
|
81
|
+
* @returns Array of workflow types
|
|
82
|
+
*/
|
|
83
|
+
types: () => string[];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Gets all registered templates.
|
|
87
|
+
*
|
|
88
|
+
* @returns Array of templates
|
|
89
|
+
*/
|
|
90
|
+
all: () => WorkflowTemplate[];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validates a template without registering it.
|
|
94
|
+
*
|
|
95
|
+
* @param template - Template to validate
|
|
96
|
+
* @returns Array of validation errors (empty if valid)
|
|
97
|
+
*/
|
|
98
|
+
validate: (template: WorkflowTemplate) => ValidationError[];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validates all registered templates.
|
|
102
|
+
*
|
|
103
|
+
* @param operationTypes - Set of registered operation types
|
|
104
|
+
* @returns Array of validation errors
|
|
105
|
+
*/
|
|
106
|
+
validateAll: (operationTypes: Set<string>) => ValidationError[];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Unregisters a template.
|
|
110
|
+
*
|
|
111
|
+
* @param type - Workflow type to remove
|
|
112
|
+
* @returns True if template was removed
|
|
113
|
+
*/
|
|
114
|
+
unregister: (type: string) => boolean;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Clears all registered templates.
|
|
118
|
+
*/
|
|
119
|
+
clear: () => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validation error details.
|
|
124
|
+
*/
|
|
125
|
+
export interface ValidationError {
|
|
126
|
+
/**
|
|
127
|
+
* Workflow type with the error.
|
|
128
|
+
*/
|
|
129
|
+
workflowType: string;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Path to the error (e.g., 'stages[0].operations[1]').
|
|
133
|
+
*/
|
|
134
|
+
path: string;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Error message.
|
|
138
|
+
*/
|
|
139
|
+
message: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// =============================================================================
|
|
143
|
+
// Validation Functions
|
|
144
|
+
// =============================================================================
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validates a workflow template.
|
|
148
|
+
*
|
|
149
|
+
* Checks for structural correctness but not operation handler existence.
|
|
150
|
+
*
|
|
151
|
+
* @param template - Template to validate
|
|
152
|
+
* @returns Array of validation errors
|
|
153
|
+
*/
|
|
154
|
+
export function validateTemplate(template: WorkflowTemplate): ValidationError[] {
|
|
155
|
+
const errors: ValidationError[] = [];
|
|
156
|
+
const type = template.type || '<unknown>';
|
|
157
|
+
|
|
158
|
+
// Required fields
|
|
159
|
+
if (!template.type || typeof template.type !== 'string') {
|
|
160
|
+
errors.push({ workflowType: type, path: 'type', message: 'type is required and must be a string' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!template.queue || typeof template.queue !== 'string') {
|
|
164
|
+
errors.push({ workflowType: type, path: 'queue', message: 'queue is required and must be a string' });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!template.version || typeof template.version !== 'string') {
|
|
168
|
+
errors.push({ workflowType: type, path: 'version', message: 'version is required and must be a string' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Stages
|
|
172
|
+
if (!Array.isArray(template.stages) || template.stages.length === 0) {
|
|
173
|
+
errors.push({ workflowType: type, path: 'stages', message: 'stages must be a non-empty array' });
|
|
174
|
+
} else {
|
|
175
|
+
const stageNames = new Set<string>();
|
|
176
|
+
|
|
177
|
+
template.stages.forEach((stage, stageIndex) => {
|
|
178
|
+
const stagePath = `stages[${stageIndex}]`;
|
|
179
|
+
errors.push(...validateStage(type, stagePath, stage, stageNames));
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Checkpoints
|
|
184
|
+
if (template.checkpoints) {
|
|
185
|
+
if (!Array.isArray(template.checkpoints)) {
|
|
186
|
+
errors.push({ workflowType: type, path: 'checkpoints', message: 'checkpoints must be an array' });
|
|
187
|
+
} else {
|
|
188
|
+
const stageNames = new Set(template.stages?.map((s) => s.name) ?? []);
|
|
189
|
+
|
|
190
|
+
template.checkpoints.forEach((checkpoint, index) => {
|
|
191
|
+
const checkpointPath = `checkpoints[${index}]`;
|
|
192
|
+
errors.push(...validateCheckpoint(type, checkpointPath, checkpoint, stageNames));
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Next workflow (just validate type)
|
|
198
|
+
if (template.nextWorkflow !== undefined && typeof template.nextWorkflow !== 'string') {
|
|
199
|
+
errors.push({ workflowType: type, path: 'nextWorkflow', message: 'nextWorkflow must be a string' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Coordination
|
|
203
|
+
if (template.coordination) {
|
|
204
|
+
if (!template.coordination.entityType || typeof template.coordination.entityType !== 'string') {
|
|
205
|
+
errors.push({
|
|
206
|
+
workflowType: type,
|
|
207
|
+
path: 'coordination.entityType',
|
|
208
|
+
message: 'coordination.entityType is required',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (!template.coordination.entityIdPath || typeof template.coordination.entityIdPath !== 'string') {
|
|
212
|
+
errors.push({
|
|
213
|
+
workflowType: type,
|
|
214
|
+
path: 'coordination.entityIdPath',
|
|
215
|
+
message: 'coordination.entityIdPath is required',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return errors;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Validates a stage template.
|
|
225
|
+
*/
|
|
226
|
+
function validateStage(
|
|
227
|
+
workflowType: string,
|
|
228
|
+
path: string,
|
|
229
|
+
stage: StageTemplate,
|
|
230
|
+
existingNames: Set<string>,
|
|
231
|
+
): ValidationError[] {
|
|
232
|
+
const errors: ValidationError[] = [];
|
|
233
|
+
|
|
234
|
+
// Name
|
|
235
|
+
if (!stage.name || typeof stage.name !== 'string') {
|
|
236
|
+
errors.push({ workflowType, path: `${path}.name`, message: 'stage name is required' });
|
|
237
|
+
} else if (existingNames.has(stage.name)) {
|
|
238
|
+
errors.push({ workflowType, path: `${path}.name`, message: `duplicate stage name: ${stage.name}` });
|
|
239
|
+
} else {
|
|
240
|
+
existingNames.add(stage.name);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Operations
|
|
244
|
+
if (!Array.isArray(stage.operations) || stage.operations.length === 0) {
|
|
245
|
+
errors.push({ workflowType, path: `${path}.operations`, message: 'operations must be a non-empty array' });
|
|
246
|
+
} else {
|
|
247
|
+
stage.operations.forEach((op, opIndex) => {
|
|
248
|
+
const opPath = `${path}.operations[${opIndex}]`;
|
|
249
|
+
errors.push(...validateOperation(workflowType, opPath, op));
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Fix operations (optional)
|
|
254
|
+
if (stage.fixOperations) {
|
|
255
|
+
if (!Array.isArray(stage.fixOperations)) {
|
|
256
|
+
errors.push({ workflowType, path: `${path}.fixOperations`, message: 'fixOperations must be an array' });
|
|
257
|
+
} else {
|
|
258
|
+
stage.fixOperations.forEach((op, opIndex) => {
|
|
259
|
+
const opPath = `${path}.fixOperations[${opIndex}]`;
|
|
260
|
+
errors.push(...validateOperation(workflowType, opPath, op));
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Max fix cycles
|
|
266
|
+
if (stage.maxFixCycles !== undefined) {
|
|
267
|
+
if (typeof stage.maxFixCycles !== 'number' || stage.maxFixCycles < 1) {
|
|
268
|
+
errors.push({
|
|
269
|
+
workflowType,
|
|
270
|
+
path: `${path}.maxFixCycles`,
|
|
271
|
+
message: 'maxFixCycles must be a positive number',
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (!stage.fixOperations || stage.fixOperations.length === 0) {
|
|
275
|
+
errors.push({
|
|
276
|
+
workflowType,
|
|
277
|
+
path: `${path}.maxFixCycles`,
|
|
278
|
+
message: 'maxFixCycles requires fixOperations to be defined',
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return errors;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Validates an operation template.
|
|
288
|
+
*/
|
|
289
|
+
function validateOperation(
|
|
290
|
+
workflowType: string,
|
|
291
|
+
path: string,
|
|
292
|
+
operation: OperationTemplate,
|
|
293
|
+
): ValidationError[] {
|
|
294
|
+
const errors: ValidationError[] = [];
|
|
295
|
+
|
|
296
|
+
if (!operation.type || typeof operation.type !== 'string') {
|
|
297
|
+
errors.push({ workflowType, path: `${path}.type`, message: 'operation type is required' });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (operation.timeout !== undefined && (typeof operation.timeout !== 'number' || operation.timeout < 0)) {
|
|
301
|
+
errors.push({ workflowType, path: `${path}.timeout`, message: 'timeout must be a non-negative number' });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (
|
|
305
|
+
operation.maxAttempts !== undefined &&
|
|
306
|
+
(typeof operation.maxAttempts !== 'number' || operation.maxAttempts < 1)
|
|
307
|
+
) {
|
|
308
|
+
errors.push({ workflowType, path: `${path}.maxAttempts`, message: 'maxAttempts must be at least 1' });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (operation.critical !== undefined && typeof operation.critical !== 'boolean') {
|
|
312
|
+
errors.push({ workflowType, path: `${path}.critical`, message: 'critical must be a boolean' });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (operation.condition !== undefined && typeof operation.condition !== 'function') {
|
|
316
|
+
errors.push({ workflowType, path: `${path}.condition`, message: 'condition must be a function' });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return errors;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Validates a checkpoint template.
|
|
324
|
+
*/
|
|
325
|
+
function validateCheckpoint(
|
|
326
|
+
workflowType: string,
|
|
327
|
+
path: string,
|
|
328
|
+
checkpoint: CheckpointTemplate,
|
|
329
|
+
stageNames: Set<string>,
|
|
330
|
+
): ValidationError[] {
|
|
331
|
+
const errors: ValidationError[] = [];
|
|
332
|
+
|
|
333
|
+
if (!checkpoint.after || typeof checkpoint.after !== 'string') {
|
|
334
|
+
errors.push({ workflowType, path: `${path}.after`, message: 'checkpoint after is required' });
|
|
335
|
+
} else if (!stageNames.has(checkpoint.after)) {
|
|
336
|
+
errors.push({
|
|
337
|
+
workflowType,
|
|
338
|
+
path: `${path}.after`,
|
|
339
|
+
message: `checkpoint references unknown stage: ${checkpoint.after}`,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!checkpoint.status || typeof checkpoint.status !== 'string') {
|
|
344
|
+
errors.push({ workflowType, path: `${path}.status`, message: 'checkpoint status is required' });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (checkpoint.condition !== undefined && typeof checkpoint.condition !== 'function') {
|
|
348
|
+
errors.push({ workflowType, path: `${path}.condition`, message: 'condition must be a function' });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (
|
|
352
|
+
checkpoint.autoResumeAfter !== undefined &&
|
|
353
|
+
(typeof checkpoint.autoResumeAfter !== 'number' || checkpoint.autoResumeAfter < 0)
|
|
354
|
+
) {
|
|
355
|
+
errors.push({
|
|
356
|
+
workflowType,
|
|
357
|
+
path: `${path}.autoResumeAfter`,
|
|
358
|
+
message: 'autoResumeAfter must be a non-negative number',
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return errors;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Validates that all operation types in a template have registered handlers.
|
|
367
|
+
*
|
|
368
|
+
* @param template - Template to check
|
|
369
|
+
* @param operationTypes - Set of registered operation types
|
|
370
|
+
* @returns Array of validation errors
|
|
371
|
+
*/
|
|
372
|
+
export function validateOperationHandlers(
|
|
373
|
+
template: WorkflowTemplate,
|
|
374
|
+
operationTypes: Set<string>,
|
|
375
|
+
): ValidationError[] {
|
|
376
|
+
const errors: ValidationError[] = [];
|
|
377
|
+
|
|
378
|
+
template.stages.forEach((stage, stageIndex) => {
|
|
379
|
+
stage.operations.forEach((op, opIndex) => {
|
|
380
|
+
if (!operationTypes.has(op.type)) {
|
|
381
|
+
errors.push({
|
|
382
|
+
workflowType: template.type,
|
|
383
|
+
path: `stages[${stageIndex}].operations[${opIndex}].type`,
|
|
384
|
+
message: `no handler registered for operation type: ${op.type}`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
stage.fixOperations?.forEach((op, opIndex) => {
|
|
390
|
+
if (!operationTypes.has(op.type)) {
|
|
391
|
+
errors.push({
|
|
392
|
+
workflowType: template.type,
|
|
393
|
+
path: `stages[${stageIndex}].fixOperations[${opIndex}].type`,
|
|
394
|
+
message: `no handler registered for operation type: ${op.type}`,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return errors;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// =============================================================================
|
|
404
|
+
// Registry Factory
|
|
405
|
+
// =============================================================================
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Creates a new template registry.
|
|
409
|
+
*
|
|
410
|
+
* The registry stores workflow templates and provides validation.
|
|
411
|
+
*
|
|
412
|
+
* @returns Template registry instance
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* ```typescript
|
|
416
|
+
* const registry = createTemplateRegistry();
|
|
417
|
+
*
|
|
418
|
+
* // Register a template
|
|
419
|
+
* registry.register({
|
|
420
|
+
* type: 'content-generation',
|
|
421
|
+
* queue: 'default',
|
|
422
|
+
* version: '1.0.0',
|
|
423
|
+
* stages: [
|
|
424
|
+
* {
|
|
425
|
+
* name: 'gather',
|
|
426
|
+
* operations: [{ type: 'gather.data' }],
|
|
427
|
+
* },
|
|
428
|
+
* {
|
|
429
|
+
* name: 'generate',
|
|
430
|
+
* operations: [{ type: 'generate.content' }],
|
|
431
|
+
* },
|
|
432
|
+
* ],
|
|
433
|
+
* });
|
|
434
|
+
*
|
|
435
|
+
* // Get template
|
|
436
|
+
* const template = registry.getOrThrow('content-generation');
|
|
437
|
+
*
|
|
438
|
+
* // Validate all templates against registered operations
|
|
439
|
+
* const operationTypes = new Set(['gather.data', 'generate.content']);
|
|
440
|
+
* const errors = registry.validateAll(operationTypes);
|
|
441
|
+
* if (errors.length > 0) {
|
|
442
|
+
* throw new Error(`Template validation failed: ${JSON.stringify(errors)}`);
|
|
443
|
+
* }
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
export function createTemplateRegistry(): TemplateRegistry {
|
|
447
|
+
const templates = new Map<string, WorkflowTemplate>();
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
register(template: WorkflowTemplate): void {
|
|
451
|
+
// Validate first
|
|
452
|
+
const errors = validateTemplate(template);
|
|
453
|
+
if (errors.length > 0) {
|
|
454
|
+
throw new TemplateError(`Invalid template "${template.type}": ${errors.map((e) => e.message).join(', ')}`, {
|
|
455
|
+
errors,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Check for duplicates
|
|
460
|
+
if (templates.has(template.type)) {
|
|
461
|
+
throw new TemplateError(`Template "${template.type}" is already registered`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
templates.set(template.type, template);
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
registerMany(templateList: WorkflowTemplate[]): void {
|
|
468
|
+
// Validate all first
|
|
469
|
+
const allErrors: ValidationError[] = [];
|
|
470
|
+
for (const template of templateList) {
|
|
471
|
+
allErrors.push(...validateTemplate(template));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (allErrors.length > 0) {
|
|
475
|
+
throw new TemplateError(`Invalid templates: ${allErrors.map((e) => `${e.workflowType}: ${e.message}`).join('; ')}`, {
|
|
476
|
+
errors: allErrors,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Check for duplicates among new templates
|
|
481
|
+
const newTypes = new Set<string>();
|
|
482
|
+
for (const template of templateList) {
|
|
483
|
+
if (newTypes.has(template.type)) {
|
|
484
|
+
throw new TemplateError(`Duplicate template type in batch: ${template.type}`);
|
|
485
|
+
}
|
|
486
|
+
if (templates.has(template.type)) {
|
|
487
|
+
throw new TemplateError(`Template "${template.type}" is already registered`);
|
|
488
|
+
}
|
|
489
|
+
newTypes.add(template.type);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Register all
|
|
493
|
+
for (const template of templateList) {
|
|
494
|
+
templates.set(template.type, template);
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
get(type: string): WorkflowTemplate | undefined {
|
|
499
|
+
return templates.get(type);
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
getOrThrow(type: string): WorkflowTemplate {
|
|
503
|
+
const template = templates.get(type);
|
|
504
|
+
if (!template) {
|
|
505
|
+
throw new TemplateError(`Template "${type}" not found`, { type });
|
|
506
|
+
}
|
|
507
|
+
return template;
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
has(type: string): boolean {
|
|
511
|
+
return templates.has(type);
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
types(): string[] {
|
|
515
|
+
return Array.from(templates.keys());
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
all(): WorkflowTemplate[] {
|
|
519
|
+
return Array.from(templates.values());
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
validate(template: WorkflowTemplate): ValidationError[] {
|
|
523
|
+
return validateTemplate(template);
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
validateAll(operationTypes: Set<string>): ValidationError[] {
|
|
527
|
+
const errors: ValidationError[] = [];
|
|
528
|
+
|
|
529
|
+
for (const template of templates.values()) {
|
|
530
|
+
errors.push(...validateOperationHandlers(template, operationTypes));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return errors;
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
unregister(type: string): boolean {
|
|
537
|
+
return templates.delete(type);
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
clear(): void {
|
|
541
|
+
templates.clear();
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// =============================================================================
|
|
547
|
+
// Helper Functions
|
|
548
|
+
// =============================================================================
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Gets all unique operation types from a template.
|
|
552
|
+
*
|
|
553
|
+
* @param template - Workflow template
|
|
554
|
+
* @returns Set of operation type strings
|
|
555
|
+
*
|
|
556
|
+
* @example
|
|
557
|
+
* ```typescript
|
|
558
|
+
* const types = getOperationTypes(template);
|
|
559
|
+
* // Set { 'gather.data', 'analyze.content', 'generate.report' }
|
|
560
|
+
* ```
|
|
561
|
+
*/
|
|
562
|
+
export function getOperationTypes(template: WorkflowTemplate): Set<string> {
|
|
563
|
+
const types = new Set<string>();
|
|
564
|
+
|
|
565
|
+
for (const stage of template.stages) {
|
|
566
|
+
for (const op of stage.operations) {
|
|
567
|
+
types.add(op.type);
|
|
568
|
+
}
|
|
569
|
+
for (const op of stage.fixOperations ?? []) {
|
|
570
|
+
types.add(op.type);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return types;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Gets all unique queue names from templates.
|
|
579
|
+
*
|
|
580
|
+
* @param templateList - Array of templates
|
|
581
|
+
* @returns Set of queue names
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* ```typescript
|
|
585
|
+
* const queues = getQueueNames(templates);
|
|
586
|
+
* // Set { 'default', 'heavy', 'sequential' }
|
|
587
|
+
* ```
|
|
588
|
+
*/
|
|
589
|
+
export function getQueueNames(templateList: WorkflowTemplate[]): Set<string> {
|
|
590
|
+
return new Set(templateList.map((t) => t.queue));
|
|
591
|
+
}
|