@umituz/web-cloudflare 1.0.1
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 +621 -0
- package/package.json +87 -0
- package/src/config/patterns.ts +469 -0
- package/src/config/types.ts +648 -0
- package/src/domain/entities/analytics.entity.ts +47 -0
- package/src/domain/entities/d1.entity.ts +37 -0
- package/src/domain/entities/image.entity.ts +48 -0
- package/src/domain/entities/index.ts +11 -0
- package/src/domain/entities/kv.entity.ts +34 -0
- package/src/domain/entities/r2.entity.ts +55 -0
- package/src/domain/entities/worker.entity.ts +35 -0
- package/src/domain/index.ts +7 -0
- package/src/domain/interfaces/index.ts +6 -0
- package/src/domain/interfaces/services.interface.ts +82 -0
- package/src/index.ts +53 -0
- package/src/infrastructure/constants/index.ts +13 -0
- package/src/infrastructure/domain/ai-gateway.entity.ts +169 -0
- package/src/infrastructure/domain/workflows.entity.ts +108 -0
- package/src/infrastructure/middleware/index.ts +405 -0
- package/src/infrastructure/router/index.ts +549 -0
- package/src/infrastructure/services/ai-gateway/index.ts +416 -0
- package/src/infrastructure/services/analytics/analytics.service.ts +189 -0
- package/src/infrastructure/services/analytics/index.ts +7 -0
- package/src/infrastructure/services/d1/d1.service.ts +191 -0
- package/src/infrastructure/services/d1/index.ts +7 -0
- package/src/infrastructure/services/images/images.service.ts +227 -0
- package/src/infrastructure/services/images/index.ts +7 -0
- package/src/infrastructure/services/kv/index.ts +7 -0
- package/src/infrastructure/services/kv/kv.service.ts +116 -0
- package/src/infrastructure/services/r2/index.ts +7 -0
- package/src/infrastructure/services/r2/r2.service.ts +164 -0
- package/src/infrastructure/services/workers/index.ts +7 -0
- package/src/infrastructure/services/workers/workers.service.ts +164 -0
- package/src/infrastructure/services/workflows/index.ts +437 -0
- package/src/infrastructure/utils/helpers.ts +732 -0
- package/src/infrastructure/utils/index.ts +6 -0
- package/src/infrastructure/utils/utils.util.ts +150 -0
- package/src/presentation/hooks/cloudflare.hooks.ts +314 -0
- package/src/presentation/hooks/index.ts +6 -0
- package/src/worker.example.ts +41 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Workflows Service
|
|
3
|
+
* @description Service for orchestrating long-running, retryable operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
WorkflowDefinition,
|
|
8
|
+
WorkflowExecution,
|
|
9
|
+
WorkflowStep,
|
|
10
|
+
WorkflowInstanceState,
|
|
11
|
+
MediaProcessingWorkflow,
|
|
12
|
+
AIGenerationWorkflow,
|
|
13
|
+
BatchOperationWorkflow,
|
|
14
|
+
} from '../../domain/workflows.entity';
|
|
15
|
+
|
|
16
|
+
export interface WorkflowServiceConfig {
|
|
17
|
+
KV?: KVNamespace;
|
|
18
|
+
D1?: D1Database;
|
|
19
|
+
maxExecutionTime?: number; // seconds
|
|
20
|
+
defaultRetries?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class WorkflowService {
|
|
24
|
+
private kv?: KVNamespace;
|
|
25
|
+
private d1?: D1Database;
|
|
26
|
+
private maxExecutionTime: number;
|
|
27
|
+
private defaultRetries: number;
|
|
28
|
+
|
|
29
|
+
constructor(config: WorkflowServiceConfig = {}) {
|
|
30
|
+
this.kv = config.KV;
|
|
31
|
+
this.d1 = config.D1;
|
|
32
|
+
this.maxExecutionTime = config.maxExecutionTime || 300; // 5 minutes default
|
|
33
|
+
this.defaultRetries = config.defaultRetries || 3;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a new workflow definition
|
|
38
|
+
*/
|
|
39
|
+
async createWorkflow(definition: WorkflowDefinition): Promise<void> {
|
|
40
|
+
const key = `workflow:${definition.id}`;
|
|
41
|
+
if (this.kv) {
|
|
42
|
+
await this.kv.put(key, JSON.stringify(definition));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get workflow definition
|
|
48
|
+
*/
|
|
49
|
+
async getWorkflow(workflowId: string): Promise<WorkflowDefinition | null> {
|
|
50
|
+
if (this.kv) {
|
|
51
|
+
const data = await this.kv.get(`workflow:${workflowId}`);
|
|
52
|
+
return data ? JSON.parse(data) : null;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start a workflow execution
|
|
59
|
+
*/
|
|
60
|
+
async startExecution(
|
|
61
|
+
workflowId: string,
|
|
62
|
+
inputs: Record<string, unknown>
|
|
63
|
+
): Promise<WorkflowExecution> {
|
|
64
|
+
const workflow = await this.getWorkflow(workflowId);
|
|
65
|
+
if (!workflow) {
|
|
66
|
+
throw new Error(`Workflow ${workflowId} not found`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const execution: WorkflowExecution = {
|
|
70
|
+
id: this.generateId(),
|
|
71
|
+
workflowId,
|
|
72
|
+
status: 'pending',
|
|
73
|
+
completedSteps: [],
|
|
74
|
+
failedSteps: [],
|
|
75
|
+
inputs,
|
|
76
|
+
startedAt: Date.now(),
|
|
77
|
+
retryCount: 0,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
await this.saveExecution(execution);
|
|
81
|
+
await this.executeWorkflow(execution, workflow);
|
|
82
|
+
|
|
83
|
+
return execution;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Execute workflow steps
|
|
88
|
+
*/
|
|
89
|
+
private async executeWorkflow(
|
|
90
|
+
execution: WorkflowExecution,
|
|
91
|
+
workflow: WorkflowDefinition
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
execution.status = 'running';
|
|
94
|
+
await this.saveExecution(execution);
|
|
95
|
+
|
|
96
|
+
const results: Record<string, unknown> = {};
|
|
97
|
+
const stepStatus: Record<string, 'completed' | 'failed'> = {};
|
|
98
|
+
|
|
99
|
+
// Execute steps in order, respecting dependencies
|
|
100
|
+
for (const step of workflow.steps) {
|
|
101
|
+
// Check if dependencies are met
|
|
102
|
+
if (step.dependencies) {
|
|
103
|
+
const depsMet = step.dependencies.every(
|
|
104
|
+
(dep) => stepStatus[dep] === 'completed'
|
|
105
|
+
);
|
|
106
|
+
if (!depsMet) {
|
|
107
|
+
continue; // Skip for now, will retry later
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
// Get step state for retries
|
|
113
|
+
const state = await this.getStepState(execution.id, step.id);
|
|
114
|
+
const stepInputs = state?.data || { ...results, ...step.inputs };
|
|
115
|
+
|
|
116
|
+
// Execute step
|
|
117
|
+
const stepResult = await this.executeStep(step, stepInputs);
|
|
118
|
+
|
|
119
|
+
// Store result
|
|
120
|
+
results[step.id] = stepResult;
|
|
121
|
+
stepStatus[step.id] = 'completed';
|
|
122
|
+
execution.completedSteps.push(step.id);
|
|
123
|
+
|
|
124
|
+
// Save step state for idempotency
|
|
125
|
+
await this.saveStepState(execution.id, step.id, stepResult);
|
|
126
|
+
|
|
127
|
+
} catch (error) {
|
|
128
|
+
stepStatus[step.id] = 'failed';
|
|
129
|
+
execution.failedSteps.push(step.id);
|
|
130
|
+
execution.error = error instanceof Error ? error.message : String(error);
|
|
131
|
+
|
|
132
|
+
// Check retry policy
|
|
133
|
+
const retryPolicy = step.retryPolicy || workflow.retryConfig;
|
|
134
|
+
if (retryPolicy && execution.retryCount < this.defaultRetries) {
|
|
135
|
+
execution.retryCount++;
|
|
136
|
+
execution.status = 'retrying';
|
|
137
|
+
await this.saveExecution(execution);
|
|
138
|
+
|
|
139
|
+
// Exponential backoff
|
|
140
|
+
const delay = Math.min(
|
|
141
|
+
retryPolicy.initialDelay * Math.pow(retryPolicy.backoffMultiplier, execution.retryCount),
|
|
142
|
+
retryPolicy.maxDelay
|
|
143
|
+
);
|
|
144
|
+
await this.sleep(delay);
|
|
145
|
+
|
|
146
|
+
// Retry this step
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Max retries exceeded, mark as failed
|
|
151
|
+
execution.status = 'failed';
|
|
152
|
+
execution.completedAt = Date.now();
|
|
153
|
+
await this.saveExecution(execution);
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// All steps completed
|
|
159
|
+
execution.status = 'completed';
|
|
160
|
+
execution.outputs = results;
|
|
161
|
+
execution.completedAt = Date.now();
|
|
162
|
+
await this.saveExecution(execution);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Execute a single step
|
|
167
|
+
*/
|
|
168
|
+
private async executeStep(
|
|
169
|
+
step: WorkflowStep,
|
|
170
|
+
inputs: Record<string, unknown>
|
|
171
|
+
): Promise<unknown> {
|
|
172
|
+
// This would call the actual handler function
|
|
173
|
+
// For now, simulate execution
|
|
174
|
+
const handler = this.getHandler(step.handler);
|
|
175
|
+
if (!handler) {
|
|
176
|
+
throw new Error(`Handler ${step.handler} not found`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return handler(inputs);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resume a workflow from a specific step
|
|
184
|
+
*/
|
|
185
|
+
async resumeExecution(
|
|
186
|
+
executionId: string,
|
|
187
|
+
fromStep?: string
|
|
188
|
+
): Promise<WorkflowExecution> {
|
|
189
|
+
const execution = await this.getExecution(executionId);
|
|
190
|
+
if (!execution) {
|
|
191
|
+
throw new Error(`Execution ${executionId} not found`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const workflow = await this.getWorkflow(execution.workflowId);
|
|
195
|
+
if (!workflow) {
|
|
196
|
+
throw new Error(`Workflow ${execution.workflowId} not found`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// If fromStep is specified, reset to that step
|
|
200
|
+
if (fromStep) {
|
|
201
|
+
const stepIndex = workflow.steps.findIndex((s) => s.id === fromStep);
|
|
202
|
+
if (stepIndex === -1) {
|
|
203
|
+
throw new Error(`Step ${fromStep} not found`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Reset completed steps after the resume point
|
|
207
|
+
execution.completedSteps = workflow.steps
|
|
208
|
+
.slice(0, stepIndex)
|
|
209
|
+
.map((s) => s.id);
|
|
210
|
+
execution.failedSteps = [];
|
|
211
|
+
execution.status = 'pending';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await this.executeWorkflow(execution, workflow);
|
|
215
|
+
return execution;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get execution status
|
|
220
|
+
*/
|
|
221
|
+
async getExecution(executionId: string): Promise<WorkflowExecution | null> {
|
|
222
|
+
if (this.kv) {
|
|
223
|
+
const data = await this.kv.get(`execution:${executionId}`);
|
|
224
|
+
return data ? JSON.parse(data) : null;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* List executions for a workflow
|
|
231
|
+
*/
|
|
232
|
+
async listExecutions(workflowId: string): Promise<WorkflowExecution[]> {
|
|
233
|
+
// This would typically use D1 for proper querying
|
|
234
|
+
// For now, return empty array
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Save execution state
|
|
240
|
+
*/
|
|
241
|
+
private async saveExecution(execution: WorkflowExecution): Promise<void> {
|
|
242
|
+
if (this.kv) {
|
|
243
|
+
await this.kv.put(
|
|
244
|
+
`execution:${execution.id}`,
|
|
245
|
+
JSON.stringify(execution),
|
|
246
|
+
{ expirationTtl: 86400 } // 24 hours
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Save step state for idempotency
|
|
253
|
+
*/
|
|
254
|
+
private async saveStepState(
|
|
255
|
+
executionId: string,
|
|
256
|
+
stepId: string,
|
|
257
|
+
data: Record<string, unknown>
|
|
258
|
+
): Promise<void> {
|
|
259
|
+
if (this.kv) {
|
|
260
|
+
const state: WorkflowInstanceState = {
|
|
261
|
+
executionId,
|
|
262
|
+
stepId,
|
|
263
|
+
data,
|
|
264
|
+
timestamp: Date.now(),
|
|
265
|
+
};
|
|
266
|
+
await this.kv.put(
|
|
267
|
+
`step:${executionId}:${stepId}`,
|
|
268
|
+
JSON.stringify(state)
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get step state
|
|
275
|
+
*/
|
|
276
|
+
private async getStepState(
|
|
277
|
+
executionId: string,
|
|
278
|
+
stepId: string
|
|
279
|
+
): Promise<WorkflowInstanceState | null> {
|
|
280
|
+
if (this.kv) {
|
|
281
|
+
const data = await this.kv.get(`step:${executionId}:${stepId}`);
|
|
282
|
+
return data ? JSON.parse(data) : null;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get handler function (mock implementation)
|
|
289
|
+
*/
|
|
290
|
+
private getHandler(name: string): ((inputs: Record<string, unknown>) => Promise<unknown>) | null {
|
|
291
|
+
const handlers: Record<string, (inputs: Record<string, unknown>) => Promise<unknown>> = {
|
|
292
|
+
'media-process': async (inputs) => ({ processed: true, urls: [] }),
|
|
293
|
+
'ai-generate': async (inputs) => ({ result: 'generated text', tokens: 100 }),
|
|
294
|
+
'batch-operation': async (inputs) => ({ successful: 10, failed: 0 }),
|
|
295
|
+
};
|
|
296
|
+
return handlers[name] || null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private generateId(): string {
|
|
300
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private sleep(ms: number): Promise<void> {
|
|
304
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Predefined workflow templates
|
|
309
|
+
export const WORKFLOW_TEMPLATES: Record<string, Partial<WorkflowDefinition>> = {
|
|
310
|
+
'media-processing': {
|
|
311
|
+
name: 'Media Processing Workflow',
|
|
312
|
+
description: 'Process media files with various operations',
|
|
313
|
+
version: '1.0.0',
|
|
314
|
+
steps: [
|
|
315
|
+
{
|
|
316
|
+
id: 'download',
|
|
317
|
+
name: 'Download Media',
|
|
318
|
+
handler: 'media-download',
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: 'transcode',
|
|
322
|
+
name: 'Transcode Media',
|
|
323
|
+
handler: 'media-transcode',
|
|
324
|
+
dependencies: ['download'],
|
|
325
|
+
retryPolicy: {
|
|
326
|
+
maxAttempts: 3,
|
|
327
|
+
backoffMultiplier: 2,
|
|
328
|
+
initialDelay: 1000,
|
|
329
|
+
maxDelay: 10000,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: 'optimize',
|
|
334
|
+
name: 'Optimize Media',
|
|
335
|
+
handler: 'media-optimize',
|
|
336
|
+
dependencies: ['transcode'],
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
id: 'upload',
|
|
340
|
+
name: 'Upload to R2',
|
|
341
|
+
handler: 'r2-upload',
|
|
342
|
+
dependencies: ['optimize'],
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
'ai-generation': {
|
|
348
|
+
name: 'AI Content Generation',
|
|
349
|
+
description: 'Generate content using AI with emotion control',
|
|
350
|
+
version: '1.0.0',
|
|
351
|
+
steps: [
|
|
352
|
+
{
|
|
353
|
+
id: 'generate-script',
|
|
354
|
+
name: 'Generate Script with Workers AI',
|
|
355
|
+
handler: 'workers-ai-generate',
|
|
356
|
+
timeout: 30,
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
id: 'validate-content',
|
|
360
|
+
name: 'Validate Generated Content',
|
|
361
|
+
handler: 'content-validate',
|
|
362
|
+
dependencies: ['generate-script'],
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: 'save-to-database',
|
|
366
|
+
name: 'Save to Database',
|
|
367
|
+
handler: 'd1-insert',
|
|
368
|
+
dependencies: ['validate-content'],
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
'batch-operations': {
|
|
374
|
+
name: 'Batch Operations',
|
|
375
|
+
description: 'Execute operations in batches with parallelism',
|
|
376
|
+
version: '1.0.0',
|
|
377
|
+
steps: [
|
|
378
|
+
{
|
|
379
|
+
id: 'fetch-items',
|
|
380
|
+
name: 'Fetch Items to Process',
|
|
381
|
+
handler: 'batch-fetch',
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
id: 'process-batches',
|
|
385
|
+
name: 'Process in Batches',
|
|
386
|
+
handler: 'batch-process',
|
|
387
|
+
dependencies: ['fetch-items'],
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
id: 'aggregate-results',
|
|
391
|
+
name: 'Aggregate Results',
|
|
392
|
+
handler: 'batch-aggregate',
|
|
393
|
+
dependencies: ['process-batches'],
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
// Voice cloning inspired workflow
|
|
399
|
+
'voice-content-creation': {
|
|
400
|
+
name: 'Voice Content Creation',
|
|
401
|
+
description: 'Create AI-generated audio content with cloned voice',
|
|
402
|
+
version: '1.0.0',
|
|
403
|
+
steps: [
|
|
404
|
+
{
|
|
405
|
+
id: 'clone-voice',
|
|
406
|
+
name: 'Clone Voice from Sample',
|
|
407
|
+
handler: 'fishaudio-clone',
|
|
408
|
+
timeout: 60,
|
|
409
|
+
retryPolicy: {
|
|
410
|
+
maxAttempts: 3,
|
|
411
|
+
backoffMultiplier: 2,
|
|
412
|
+
initialDelay: 2000,
|
|
413
|
+
maxDelay: 15000,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: 'generate-script',
|
|
418
|
+
name: 'Generate Script with Emotion',
|
|
419
|
+
handler: 'workers-ai-script',
|
|
420
|
+
timeout: 30,
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
id: 'generate-tts',
|
|
424
|
+
name: 'Generate TTS with Cloned Voice',
|
|
425
|
+
handler: 'fishaudio-tts',
|
|
426
|
+
dependencies: ['clone-voice', 'generate-script'],
|
|
427
|
+
timeout: 60,
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: 'combine-audio',
|
|
431
|
+
name: 'Combine Audio Tracks',
|
|
432
|
+
handler: 'audio-combine',
|
|
433
|
+
dependencies: ['generate-tts'],
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
};
|