motia 0.6.3-beta.130-601955 → 0.6.3-beta.130-426307
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/dist/cjs/cli.js +16 -40
- package/dist/cjs/create/index.d.ts +2 -2
- package/dist/cjs/create/index.js +3 -5
- package/dist/cjs/create/interactive.d.ts +2 -0
- package/dist/cjs/create/interactive.js +28 -19
- package/dist/cjs/create/pull-rules.d.ts +7 -0
- package/dist/cjs/create/pull-rules.js +28 -0
- package/dist/cjs/create/setup-template.js +5 -3
- package/dist/cjs/create/templates/index.js +1 -1
- package/dist/cjs/create/templates/index.ts +1 -1
- package/dist/cjs/cursor-rules/dot-files/.claude/CLAUDE.md +467 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/README.md +97 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/agents/code-reviewer.md +153 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/agents/debugger.md +259 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/agents/test-runner.md +268 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/add-authentication.md +491 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/ai-ml-patterns.md +748 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/authentication.md +515 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/backend-types.md +719 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/build-api.md +407 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/claude-workflows.md +1032 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/complete-backend.md +345 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/create-api.md +96 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/data-processing.md +977 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/integrate-ai.md +852 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/javascript-patterns.md +678 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/multi-language-workflow.md +756 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/multi-language.md +141 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/process-background-jobs.md +587 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/commands/process-events.md +89 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/hooks/pre-commit.sh +84 -0
- package/dist/cjs/cursor-rules/dot-files/.claude/settings.json +37 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/ai-agent-patterns.mdc +725 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/api-design-patterns.mdc +740 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/api-steps.mdc +230 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/architecture.mdc +189 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/authentication-patterns.mdc +620 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/background-job-patterns.mdc +628 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/complete-application-patterns.mdc +433 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/complete-backend-generator.mdc +415 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/cron-steps.mdc +257 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/event-steps.mdc +504 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/instructions.mdc +15 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/multi-language-workflows.mdc +1059 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/noop-steps.mdc +57 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/production-deployment.mdc +668 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/realtime-streaming.mdc +656 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/state-management.mdc +371 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/steps.mdc +373 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/testing.mdc +329 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/typescript.mdc +409 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/ui-steps.mdc +90 -0
- package/dist/cjs/cursor-rules/dot-files/.cursor/rules/workflow-patterns.mdc +938 -0
- package/dist/cjs/cursor-rules/dot-files/AGENTS.md +397 -0
- package/dist/cjs/cursor-rules/dot-files/README.md +58 -0
- package/dist/esm/cli.js +16 -40
- package/dist/esm/create/index.d.ts +2 -2
- package/dist/esm/create/index.js +3 -5
- package/dist/esm/create/interactive.d.ts +2 -0
- package/dist/esm/create/interactive.js +28 -19
- package/dist/esm/create/pull-rules.d.ts +7 -0
- package/dist/esm/create/pull-rules.js +21 -0
- package/dist/esm/create/setup-template.js +5 -3
- package/dist/esm/create/templates/index.js +1 -1
- package/dist/esm/create/templates/index.ts +1 -1
- package/dist/esm/cursor-rules/dot-files/.claude/CLAUDE.md +467 -0
- package/dist/esm/cursor-rules/dot-files/.claude/README.md +97 -0
- package/dist/esm/cursor-rules/dot-files/.claude/agents/code-reviewer.md +153 -0
- package/dist/esm/cursor-rules/dot-files/.claude/agents/debugger.md +259 -0
- package/dist/esm/cursor-rules/dot-files/.claude/agents/test-runner.md +268 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/add-authentication.md +491 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/ai-ml-patterns.md +748 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/authentication.md +515 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/backend-types.md +719 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/build-api.md +407 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/claude-workflows.md +1032 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/complete-backend.md +345 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/create-api.md +96 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/data-processing.md +977 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/integrate-ai.md +852 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/javascript-patterns.md +678 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/multi-language-workflow.md +756 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/multi-language.md +141 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/process-background-jobs.md +587 -0
- package/dist/esm/cursor-rules/dot-files/.claude/commands/process-events.md +89 -0
- package/dist/esm/cursor-rules/dot-files/.claude/hooks/pre-commit.sh +84 -0
- package/dist/esm/cursor-rules/dot-files/.claude/settings.json +37 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/ai-agent-patterns.mdc +725 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/api-design-patterns.mdc +740 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/api-steps.mdc +230 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/architecture.mdc +189 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/authentication-patterns.mdc +620 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/background-job-patterns.mdc +628 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/complete-application-patterns.mdc +433 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/complete-backend-generator.mdc +415 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/cron-steps.mdc +257 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/event-steps.mdc +504 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/instructions.mdc +15 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/multi-language-workflows.mdc +1059 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/noop-steps.mdc +57 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/production-deployment.mdc +668 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/realtime-streaming.mdc +656 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/state-management.mdc +371 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/steps.mdc +373 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/testing.mdc +329 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/typescript.mdc +409 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/ui-steps.mdc +90 -0
- package/dist/esm/cursor-rules/dot-files/.cursor/rules/workflow-patterns.mdc +938 -0
- package/dist/esm/cursor-rules/dot-files/AGENTS.md +397 -0
- package/dist/esm/cursor-rules/dot-files/README.md +58 -0
- package/dist/types/create/index.d.ts +2 -2
- package/dist/types/create/interactive.d.ts +2 -0
- package/dist/types/create/pull-rules.d.ts +7 -0
- package/package.json +4 -4
- package/dist/cjs/cursor-rules/index.d.ts +0 -8
- package/dist/cjs/cursor-rules/index.js +0 -269
- package/dist/esm/cursor-rules/index.d.ts +0 -8
- package/dist/esm/cursor-rules/index.js +0 -263
- package/dist/types/cursor-rules/index.d.ts +0 -8
- /package/dist/cjs/create/templates/{typescript → nodejs}/motia-workbench.json +0 -0
- /package/dist/cjs/create/templates/{typescript → nodejs}/services/pet-store.ts.txt +0 -0
- /package/dist/cjs/create/templates/{typescript → nodejs}/services/types.ts.txt +0 -0
- /package/dist/cjs/create/templates/{typescript → nodejs}/steps/api.step.ts-features.json.txt +0 -0
- /package/dist/cjs/create/templates/{typescript → nodejs}/steps/api.step.ts.txt +0 -0
- /package/dist/cjs/create/templates/{typescript → nodejs}/steps/notification.step.ts.txt +0 -0
- /package/dist/cjs/create/templates/{typescript → nodejs}/steps/process-food-order.step.ts-features.json.txt +0 -0
- /package/dist/cjs/create/templates/{typescript → nodejs}/steps/process-food-order.step.ts.txt +0 -0
- /package/dist/cjs/create/templates/{typescript → nodejs}/steps/state-audit-cron.step.ts-features.json.txt +0 -0
- /package/dist/cjs/create/templates/{typescript → nodejs}/steps/state-audit-cron.step.ts.txt +0 -0
- /package/dist/cjs/create/templates/{typescript → nodejs}/tutorial.tsx.txt +0 -0
- /package/dist/esm/create/templates/{typescript → nodejs}/motia-workbench.json +0 -0
- /package/dist/esm/create/templates/{typescript → nodejs}/services/pet-store.ts.txt +0 -0
- /package/dist/esm/create/templates/{typescript → nodejs}/services/types.ts.txt +0 -0
- /package/dist/esm/create/templates/{typescript → nodejs}/steps/api.step.ts-features.json.txt +0 -0
- /package/dist/esm/create/templates/{typescript → nodejs}/steps/api.step.ts.txt +0 -0
- /package/dist/esm/create/templates/{typescript → nodejs}/steps/notification.step.ts.txt +0 -0
- /package/dist/esm/create/templates/{typescript → nodejs}/steps/process-food-order.step.ts-features.json.txt +0 -0
- /package/dist/esm/create/templates/{typescript → nodejs}/steps/process-food-order.step.ts.txt +0 -0
- /package/dist/esm/create/templates/{typescript → nodejs}/steps/state-audit-cron.step.ts-features.json.txt +0 -0
- /package/dist/esm/create/templates/{typescript → nodejs}/steps/state-audit-cron.step.ts.txt +0 -0
- /package/dist/esm/create/templates/{typescript → nodejs}/tutorial.tsx.txt +0 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Background job patterns for handling asynchronous tasks, scheduled jobs, and long-running processes
|
|
3
|
+
globs:
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
# Background Job Patterns
|
|
7
|
+
|
|
8
|
+
Implement robust background processing systems for asynchronous tasks, scheduled jobs, and long-running processes.
|
|
9
|
+
|
|
10
|
+
## Event-Driven Job Processing
|
|
11
|
+
|
|
12
|
+
### Job Queue System
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// steps/jobs/job-processor.step.ts
|
|
16
|
+
import { EventConfig, Handlers } from 'motia'
|
|
17
|
+
import { z } from 'zod'
|
|
18
|
+
|
|
19
|
+
export const config: EventConfig = {
|
|
20
|
+
type: 'event',
|
|
21
|
+
name: 'JobProcessor',
|
|
22
|
+
description: 'Process background jobs from the job queue',
|
|
23
|
+
subscribes: ['job.enqueued', 'job.retry'],
|
|
24
|
+
emits: ['job.started', 'job.completed', 'job.failed', 'job.retry.scheduled'],
|
|
25
|
+
input: z.object({
|
|
26
|
+
jobId: z.string(),
|
|
27
|
+
type: z.string(),
|
|
28
|
+
payload: z.record(z.any()),
|
|
29
|
+
priority: z.enum(['low', 'medium', 'high']).default('medium'),
|
|
30
|
+
attempts: z.number().default(0),
|
|
31
|
+
maxAttempts: z.number().default(3),
|
|
32
|
+
delayUntil: z.string().optional(),
|
|
33
|
+
createdBy: z.string().optional()
|
|
34
|
+
}),
|
|
35
|
+
flows: ['job-processing']
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const handler: Handlers['JobProcessor'] = async (input, { emit, logger, state }) => {
|
|
39
|
+
const { jobId, type, payload, attempts, maxAttempts, createdBy } = input
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Update job status to processing
|
|
43
|
+
await state.set('jobs', jobId, {
|
|
44
|
+
...input,
|
|
45
|
+
status: 'processing',
|
|
46
|
+
startedAt: new Date().toISOString(),
|
|
47
|
+
attempts: attempts + 1
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
await emit({
|
|
51
|
+
topic: 'job.started',
|
|
52
|
+
data: { jobId, type, attempt: attempts + 1 }
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Process job based on type
|
|
56
|
+
const result = await processJob(type, payload, { state, logger })
|
|
57
|
+
|
|
58
|
+
// Mark job as completed
|
|
59
|
+
await state.set('jobs', jobId, {
|
|
60
|
+
...input,
|
|
61
|
+
status: 'completed',
|
|
62
|
+
result,
|
|
63
|
+
completedAt: new Date().toISOString(),
|
|
64
|
+
attempts: attempts + 1
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
await emit({
|
|
68
|
+
topic: 'job.completed',
|
|
69
|
+
data: { jobId, type, result, duration: calculateDuration(input) }
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
logger.info('Job completed successfully', { jobId, type, attempts: attempts + 1 })
|
|
73
|
+
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const currentAttempts = attempts + 1
|
|
76
|
+
logger.error('Job processing failed', {
|
|
77
|
+
error: error.message,
|
|
78
|
+
jobId,
|
|
79
|
+
type,
|
|
80
|
+
attempts: currentAttempts
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Check if we should retry
|
|
84
|
+
if (currentAttempts < maxAttempts && isRetryableError(error)) {
|
|
85
|
+
const delay = calculateRetryDelay(currentAttempts)
|
|
86
|
+
const retryAt = new Date(Date.now() + delay).toISOString()
|
|
87
|
+
|
|
88
|
+
await state.set('jobs', jobId, {
|
|
89
|
+
...input,
|
|
90
|
+
status: 'failed',
|
|
91
|
+
error: error.message,
|
|
92
|
+
attempts: currentAttempts,
|
|
93
|
+
retryAt,
|
|
94
|
+
lastFailedAt: new Date().toISOString()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
await emit({
|
|
98
|
+
topic: 'job.retry.scheduled',
|
|
99
|
+
data: {
|
|
100
|
+
jobId,
|
|
101
|
+
type,
|
|
102
|
+
attempt: currentAttempts,
|
|
103
|
+
retryAt,
|
|
104
|
+
delay
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
} else {
|
|
108
|
+
// Max attempts reached or non-retryable error
|
|
109
|
+
await state.set('jobs', jobId, {
|
|
110
|
+
...input,
|
|
111
|
+
status: 'failed',
|
|
112
|
+
error: error.message,
|
|
113
|
+
attempts: currentAttempts,
|
|
114
|
+
failedAt: new Date().toISOString()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
await emit({
|
|
118
|
+
topic: 'job.failed',
|
|
119
|
+
data: {
|
|
120
|
+
jobId,
|
|
121
|
+
type,
|
|
122
|
+
error: error.message,
|
|
123
|
+
finalAttempt: currentAttempts,
|
|
124
|
+
createdBy
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function processJob(type: string, payload: any, context: any) {
|
|
132
|
+
switch (type) {
|
|
133
|
+
case 'email':
|
|
134
|
+
return await processEmailJob(payload, context)
|
|
135
|
+
case 'image_processing':
|
|
136
|
+
return await processImageJob(payload, context)
|
|
137
|
+
case 'data_export':
|
|
138
|
+
return await processDataExportJob(payload, context)
|
|
139
|
+
case 'report_generation':
|
|
140
|
+
return await processReportJob(payload, context)
|
|
141
|
+
default:
|
|
142
|
+
throw new Error(`Unknown job type: ${type}`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isRetryableError(error: any): boolean {
|
|
147
|
+
// Define which errors should be retried
|
|
148
|
+
const retryableErrors = [
|
|
149
|
+
'NETWORK_ERROR',
|
|
150
|
+
'TIMEOUT',
|
|
151
|
+
'RATE_LIMIT',
|
|
152
|
+
'TEMPORARY_FAILURE'
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
return retryableErrors.some(type =>
|
|
156
|
+
error.message.includes(type) || error.code === type
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function calculateRetryDelay(attempt: number): number {
|
|
161
|
+
// Exponential backoff with jitter
|
|
162
|
+
const baseDelay = 1000 // 1 second
|
|
163
|
+
const maxDelay = 300000 // 5 minutes
|
|
164
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay)
|
|
165
|
+
|
|
166
|
+
// Add jitter (±25%)
|
|
167
|
+
const jitter = delay * 0.25 * (Math.random() - 0.5)
|
|
168
|
+
return Math.round(delay + jitter)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Job Scheduler
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// steps/jobs/job-scheduler.step.ts
|
|
176
|
+
import { CronConfig, Handlers } from 'motia'
|
|
177
|
+
|
|
178
|
+
export const config: CronConfig = {
|
|
179
|
+
type: 'cron',
|
|
180
|
+
name: 'JobScheduler',
|
|
181
|
+
description: 'Process scheduled and delayed jobs',
|
|
182
|
+
cron: '*/10 * * * * *', // Every 10 seconds
|
|
183
|
+
emits: ['job.enqueued'],
|
|
184
|
+
flows: ['job-scheduling']
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const handler: Handlers['JobScheduler'] = async ({ emit, logger, state }) => {
|
|
188
|
+
try {
|
|
189
|
+
const now = new Date().toISOString()
|
|
190
|
+
|
|
191
|
+
// Get jobs that are ready to be processed
|
|
192
|
+
const readyJobs = await getJobsReadyForProcessing(now, state)
|
|
193
|
+
|
|
194
|
+
for (const job of readyJobs) {
|
|
195
|
+
// Emit job for processing
|
|
196
|
+
await emit({
|
|
197
|
+
topic: 'job.enqueued',
|
|
198
|
+
data: job
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// Update job status
|
|
202
|
+
await state.set('jobs', job.jobId, {
|
|
203
|
+
...job,
|
|
204
|
+
status: 'queued',
|
|
205
|
+
queuedAt: now
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (readyJobs.length > 0) {
|
|
210
|
+
logger.info('Jobs scheduled for processing', { count: readyJobs.length })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Clean up old completed jobs
|
|
214
|
+
await cleanupOldJobs(state, logger)
|
|
215
|
+
|
|
216
|
+
} catch (error) {
|
|
217
|
+
logger.error('Job scheduler failed', { error: error.message })
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function getJobsReadyForProcessing(now: string, state: any) {
|
|
222
|
+
// Get all jobs that are scheduled or need retry
|
|
223
|
+
const allJobs = await state.getGroup('jobs') || {}
|
|
224
|
+
|
|
225
|
+
return Object.values(allJobs).filter((job: any) => {
|
|
226
|
+
return (job.status === 'scheduled' && job.delayUntil && job.delayUntil <= now) ||
|
|
227
|
+
(job.status === 'failed' && job.retryAt && job.retryAt <= now)
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function cleanupOldJobs(state: any, logger: any) {
|
|
232
|
+
const cutoffDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
|
233
|
+
const allJobs = await state.getGroup('jobs') || {}
|
|
234
|
+
|
|
235
|
+
let cleanedCount = 0
|
|
236
|
+
for (const [jobId, job] of Object.entries(allJobs)) {
|
|
237
|
+
if ((job as any).status === 'completed' && (job as any).completedAt < cutoffDate) {
|
|
238
|
+
await state.delete('jobs', jobId)
|
|
239
|
+
cleanedCount++
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (cleanedCount > 0) {
|
|
244
|
+
logger.info('Cleaned up old completed jobs', { count: cleanedCount })
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Specific Job Types
|
|
250
|
+
|
|
251
|
+
### Email Processing Job
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// steps/jobs/email-processor.step.ts
|
|
255
|
+
import { EventConfig, Handlers } from 'motia'
|
|
256
|
+
import { z } from 'zod'
|
|
257
|
+
|
|
258
|
+
export const config: EventConfig = {
|
|
259
|
+
type: 'event',
|
|
260
|
+
name: 'EmailProcessor',
|
|
261
|
+
description: 'Process email sending jobs',
|
|
262
|
+
subscribes: ['email.send.requested'],
|
|
263
|
+
emits: ['job.enqueued'],
|
|
264
|
+
input: z.object({
|
|
265
|
+
to: z.union([z.string().email(), z.array(z.string().email())]),
|
|
266
|
+
subject: z.string(),
|
|
267
|
+
templateId: z.string().optional(),
|
|
268
|
+
htmlContent: z.string().optional(),
|
|
269
|
+
textContent: z.string().optional(),
|
|
270
|
+
variables: z.record(z.any()).optional(),
|
|
271
|
+
attachments: z.array(z.object({
|
|
272
|
+
filename: z.string(),
|
|
273
|
+
content: z.string(), // base64 encoded
|
|
274
|
+
contentType: z.string()
|
|
275
|
+
})).optional(),
|
|
276
|
+
priority: z.enum(['low', 'medium', 'high']).default('medium'),
|
|
277
|
+
sendAt: z.string().optional(), // ISO timestamp for scheduled sending
|
|
278
|
+
userId: z.string().optional()
|
|
279
|
+
}),
|
|
280
|
+
flows: ['email-processing']
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export const handler: Handlers['EmailProcessor'] = async (input, { emit, logger }) => {
|
|
284
|
+
try {
|
|
285
|
+
// Create email job
|
|
286
|
+
const jobId = crypto.randomUUID()
|
|
287
|
+
const emailJob = {
|
|
288
|
+
jobId,
|
|
289
|
+
type: 'email',
|
|
290
|
+
payload: input,
|
|
291
|
+
priority: input.priority,
|
|
292
|
+
maxAttempts: 3,
|
|
293
|
+
delayUntil: input.sendAt || new Date().toISOString(),
|
|
294
|
+
createdBy: input.userId
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Enqueue the job
|
|
298
|
+
await emit({
|
|
299
|
+
topic: 'job.enqueued',
|
|
300
|
+
data: emailJob
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
logger.info('Email job created', {
|
|
304
|
+
jobId,
|
|
305
|
+
recipients: Array.isArray(input.to) ? input.to.length : 1,
|
|
306
|
+
scheduled: !!input.sendAt
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
} catch (error) {
|
|
310
|
+
logger.error('Failed to create email job', { error: error.message })
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function processEmailJob(payload: any, context: any) {
|
|
315
|
+
const { to, subject, templateId, htmlContent, textContent, variables, attachments } = payload
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
let finalHtmlContent = htmlContent
|
|
319
|
+
let finalTextContent = textContent
|
|
320
|
+
|
|
321
|
+
// Process template if provided
|
|
322
|
+
if (templateId) {
|
|
323
|
+
const template = await context.state.get('email-templates', templateId)
|
|
324
|
+
if (template) {
|
|
325
|
+
finalHtmlContent = processTemplate(template.html, variables || {})
|
|
326
|
+
finalTextContent = processTemplate(template.text, variables || {})
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Send email(s)
|
|
331
|
+
const recipients = Array.isArray(to) ? to : [to]
|
|
332
|
+
const results = []
|
|
333
|
+
|
|
334
|
+
for (const recipient of recipients) {
|
|
335
|
+
const result = await sendEmail({
|
|
336
|
+
to: recipient,
|
|
337
|
+
subject,
|
|
338
|
+
html: finalHtmlContent,
|
|
339
|
+
text: finalTextContent,
|
|
340
|
+
attachments
|
|
341
|
+
})
|
|
342
|
+
results.push({ recipient, messageId: result.messageId })
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
sent: results.length,
|
|
347
|
+
results,
|
|
348
|
+
timestamp: new Date().toISOString()
|
|
349
|
+
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw new Error(`Email sending failed: ${error.message}`)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function processTemplate(template: string, variables: Record<string, any>): string {
|
|
356
|
+
// Simple template variable substitution
|
|
357
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
358
|
+
return variables[key] || match
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function sendEmail(emailData: any) {
|
|
363
|
+
// Email service integration (SendGrid, SES, etc.)
|
|
364
|
+
return { messageId: crypto.randomUUID() }
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Data Processing Job
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
// steps/jobs/data-processor.step.ts
|
|
372
|
+
import { EventConfig, Handlers } from 'motia'
|
|
373
|
+
import { z } from 'zod'
|
|
374
|
+
|
|
375
|
+
export const config: EventConfig = {
|
|
376
|
+
type: 'event',
|
|
377
|
+
name: 'DataProcessor',
|
|
378
|
+
description: 'Process large datasets in the background',
|
|
379
|
+
subscribes: ['data.processing.requested'],
|
|
380
|
+
emits: ['job.enqueued'],
|
|
381
|
+
input: z.object({
|
|
382
|
+
dataSource: z.string(), // file path, database query, etc.
|
|
383
|
+
processingType: z.enum(['transform', 'analyze', 'export', 'import']),
|
|
384
|
+
batchSize: z.number().default(1000),
|
|
385
|
+
outputDestination: z.string(),
|
|
386
|
+
filters: z.record(z.any()).optional(),
|
|
387
|
+
transformations: z.array(z.record(z.any())).optional(),
|
|
388
|
+
userId: z.string(),
|
|
389
|
+
priority: z.enum(['low', 'medium', 'high']).default('medium')
|
|
390
|
+
}),
|
|
391
|
+
flows: ['data-processing']
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export const handler: Handlers['DataProcessor'] = async (input, { emit, logger }) => {
|
|
395
|
+
try {
|
|
396
|
+
const jobId = crypto.randomUUID()
|
|
397
|
+
|
|
398
|
+
// Create data processing job
|
|
399
|
+
const dataJob = {
|
|
400
|
+
jobId,
|
|
401
|
+
type: 'data_processing',
|
|
402
|
+
payload: input,
|
|
403
|
+
priority: input.priority,
|
|
404
|
+
maxAttempts: 2, // Data jobs typically shouldn't be retried many times
|
|
405
|
+
createdBy: input.userId
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
await emit({
|
|
409
|
+
topic: 'job.enqueued',
|
|
410
|
+
data: dataJob
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
logger.info('Data processing job created', {
|
|
414
|
+
jobId,
|
|
415
|
+
type: input.processingType,
|
|
416
|
+
source: input.dataSource
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
} catch (error) {
|
|
420
|
+
logger.error('Failed to create data processing job', { error: error.message })
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function processDataProcessingJob(payload: any, context: any) {
|
|
425
|
+
const { dataSource, processingType, batchSize, outputDestination, filters, transformations } = payload
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
// Initialize processing
|
|
429
|
+
const processingStats = {
|
|
430
|
+
startTime: new Date().toISOString(),
|
|
431
|
+
totalRecords: 0,
|
|
432
|
+
processedRecords: 0,
|
|
433
|
+
errorRecords: 0,
|
|
434
|
+
batches: 0
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Get data source
|
|
438
|
+
const dataIterator = await createDataIterator(dataSource, batchSize, filters)
|
|
439
|
+
|
|
440
|
+
// Process data in batches
|
|
441
|
+
for await (const batch of dataIterator) {
|
|
442
|
+
processingStats.batches++
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
const processedBatch = await processBatch(batch, processingType, transformations)
|
|
446
|
+
await writeOutput(processedBatch, outputDestination)
|
|
447
|
+
|
|
448
|
+
processingStats.processedRecords += batch.length
|
|
449
|
+
} catch (error) {
|
|
450
|
+
context.logger.error('Batch processing failed', {
|
|
451
|
+
batch: processingStats.batches,
|
|
452
|
+
error: error.message
|
|
453
|
+
})
|
|
454
|
+
processingStats.errorRecords += batch.length
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
processingStats.totalRecords += batch.length
|
|
458
|
+
|
|
459
|
+
// Update progress periodically
|
|
460
|
+
if (processingStats.batches % 10 === 0) {
|
|
461
|
+
await updateProcessingProgress(processingStats, context.state)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
processingStats.endTime = new Date().toISOString()
|
|
466
|
+
processingStats.duration = new Date(processingStats.endTime).getTime() -
|
|
467
|
+
new Date(processingStats.startTime).getTime()
|
|
468
|
+
|
|
469
|
+
return processingStats
|
|
470
|
+
} catch (error) {
|
|
471
|
+
throw new Error(`Data processing failed: ${error.message}`)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function* createDataIterator(source: string, batchSize: number, filters: any) {
|
|
476
|
+
// Implementation would depend on data source type
|
|
477
|
+
// This is a simplified example
|
|
478
|
+
const totalRecords = 10000 // Would be determined from actual source
|
|
479
|
+
|
|
480
|
+
for (let offset = 0; offset < totalRecords; offset += batchSize) {
|
|
481
|
+
const batch = await fetchDataBatch(source, offset, batchSize, filters)
|
|
482
|
+
if (batch.length === 0) break
|
|
483
|
+
yield batch
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function processBatch(batch: any[], type: string, transformations: any[]) {
|
|
488
|
+
switch (type) {
|
|
489
|
+
case 'transform':
|
|
490
|
+
return applyTransformations(batch, transformations || [])
|
|
491
|
+
case 'analyze':
|
|
492
|
+
return analyzeBatch(batch)
|
|
493
|
+
default:
|
|
494
|
+
return batch
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## Long-Running Process Management
|
|
500
|
+
|
|
501
|
+
### Process Monitor
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
// steps/jobs/process-monitor.step.ts
|
|
505
|
+
import { CronConfig, Handlers } from 'motia'
|
|
506
|
+
|
|
507
|
+
export const config: CronConfig = {
|
|
508
|
+
type: 'cron',
|
|
509
|
+
name: 'ProcessMonitor',
|
|
510
|
+
description: 'Monitor long-running processes and handle timeouts',
|
|
511
|
+
cron: '*/30 * * * * *', // Every 30 seconds
|
|
512
|
+
emits: ['process.timeout', 'process.status.updated'],
|
|
513
|
+
flows: ['process-monitoring']
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export const handler: Handlers['ProcessMonitor'] = async ({ emit, logger, state }) => {
|
|
517
|
+
try {
|
|
518
|
+
const now = Date.now()
|
|
519
|
+
const timeoutThreshold = 30 * 60 * 1000 // 30 minutes
|
|
520
|
+
|
|
521
|
+
// Get all running processes
|
|
522
|
+
const runningJobs = await getRunningJobs(state)
|
|
523
|
+
|
|
524
|
+
for (const job of runningJobs) {
|
|
525
|
+
const startTime = new Date(job.startedAt).getTime()
|
|
526
|
+
const runningTime = now - startTime
|
|
527
|
+
|
|
528
|
+
// Check for timeout
|
|
529
|
+
if (runningTime > timeoutThreshold) {
|
|
530
|
+
await handleJobTimeout(job, runningTime, { emit, state, logger })
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Update process status
|
|
534
|
+
await updateJobStatus(job, runningTime, { emit, state })
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Generate process health report
|
|
538
|
+
await generateProcessHealthReport(runningJobs, { emit, logger })
|
|
539
|
+
|
|
540
|
+
} catch (error) {
|
|
541
|
+
logger.error('Process monitoring failed', { error: error.message })
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function getRunningJobs(state: any) {
|
|
546
|
+
const allJobs = await state.getGroup('jobs') || {}
|
|
547
|
+
return Object.values(allJobs).filter((job: any) =>
|
|
548
|
+
job.status === 'processing' && job.startedAt
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function handleJobTimeout(job: any, runningTime: number, context: any) {
|
|
553
|
+
const { emit, state, logger } = context
|
|
554
|
+
|
|
555
|
+
// Mark job as timed out
|
|
556
|
+
await state.set('jobs', job.jobId, {
|
|
557
|
+
...job,
|
|
558
|
+
status: 'timeout',
|
|
559
|
+
timedOutAt: new Date().toISOString(),
|
|
560
|
+
runningTime
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
await emit({
|
|
564
|
+
topic: 'process.timeout',
|
|
565
|
+
data: {
|
|
566
|
+
jobId: job.jobId,
|
|
567
|
+
type: job.type,
|
|
568
|
+
runningTime,
|
|
569
|
+
startedAt: job.startedAt
|
|
570
|
+
}
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
logger.warn('Job timed out', {
|
|
574
|
+
jobId: job.jobId,
|
|
575
|
+
type: job.type,
|
|
576
|
+
runningTime
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function generateProcessHealthReport(runningJobs: any[], context: any) {
|
|
581
|
+
const report = {
|
|
582
|
+
totalRunning: runningJobs.length,
|
|
583
|
+
byType: {},
|
|
584
|
+
averageRuntime: 0,
|
|
585
|
+
longestRunning: null,
|
|
586
|
+
timestamp: new Date().toISOString()
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const now = Date.now()
|
|
590
|
+
let totalRuntime = 0
|
|
591
|
+
|
|
592
|
+
for (const job of runningJobs) {
|
|
593
|
+
const runtime = now - new Date(job.startedAt).getTime()
|
|
594
|
+
totalRuntime += runtime
|
|
595
|
+
|
|
596
|
+
// Group by type
|
|
597
|
+
if (!report.byType[job.type]) {
|
|
598
|
+
report.byType[job.type] = { count: 0, totalRuntime: 0 }
|
|
599
|
+
}
|
|
600
|
+
report.byType[job.type].count++
|
|
601
|
+
report.byType[job.type].totalRuntime += runtime
|
|
602
|
+
|
|
603
|
+
// Track longest running
|
|
604
|
+
if (!report.longestRunning || runtime > (now - new Date(report.longestRunning.startedAt).getTime())) {
|
|
605
|
+
report.longestRunning = { ...job, runtime }
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (runningJobs.length > 0) {
|
|
610
|
+
report.averageRuntime = totalRuntime / runningJobs.length
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
await context.emit({
|
|
614
|
+
topic: 'process.status.updated',
|
|
615
|
+
data: report
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
This background job system provides:
|
|
621
|
+
- Event-driven job processing with retry logic
|
|
622
|
+
- Job scheduling and delayed execution
|
|
623
|
+
- Specialized processors for different job types
|
|
624
|
+
- Progress tracking and monitoring
|
|
625
|
+
- Timeout handling and process health monitoring
|
|
626
|
+
- Batch processing capabilities
|
|
627
|
+
- Error handling and failure recovery
|
|
628
|
+
- Job cleanup and maintenance
|