digital-tasks 2.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/src/queue.ts ADDED
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Task Queue - In-memory task queue implementation
3
+ *
4
+ * Provides task queuing, assignment, and execution management.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ import type {
10
+ AnyTask,
11
+ TaskQueue,
12
+ TaskQueueOptions,
13
+ TaskQuery,
14
+ TaskStatus,
15
+ TaskPriority,
16
+ UpdateTaskOptions,
17
+ WorkerRef,
18
+ TaskQueueStats,
19
+ TaskEvent,
20
+ FunctionDefinition,
21
+ } from './types.js'
22
+
23
+ /**
24
+ * Priority values for sorting
25
+ */
26
+ const priorityOrder: Record<TaskPriority, number> = {
27
+ critical: 5,
28
+ urgent: 4,
29
+ high: 3,
30
+ normal: 2,
31
+ low: 1,
32
+ }
33
+
34
+ /**
35
+ * In-memory task queue implementation
36
+ */
37
+ class InMemoryTaskQueue implements TaskQueue {
38
+ private tasks: Map<string, AnyTask> = new Map()
39
+ private options: TaskQueueOptions
40
+
41
+ constructor(options: TaskQueueOptions = {}) {
42
+ this.options = {
43
+ name: 'default',
44
+ concurrency: 10,
45
+ defaultTimeout: 5 * 60 * 1000, // 5 minutes
46
+ persistent: false,
47
+ ...options,
48
+ }
49
+ }
50
+
51
+ async add(task: AnyTask): Promise<void> {
52
+ // Add created event
53
+ const event: TaskEvent = {
54
+ id: `evt_${Date.now()}`,
55
+ type: 'created',
56
+ timestamp: new Date(),
57
+ message: `Task created: ${task.function.name}`,
58
+ }
59
+
60
+ const taskWithEvent = {
61
+ ...task,
62
+ events: [...(task.events || []), event],
63
+ }
64
+
65
+ this.tasks.set(task.id, taskWithEvent)
66
+ }
67
+
68
+ async get(id: string): Promise<AnyTask | undefined> {
69
+ return this.tasks.get(id)
70
+ }
71
+
72
+ async update(id: string, options: UpdateTaskOptions): Promise<AnyTask | undefined> {
73
+ const task = this.tasks.get(id)
74
+ if (!task) return undefined
75
+
76
+ const events = [...(task.events || [])]
77
+
78
+ // Add event if provided
79
+ if (options.event) {
80
+ events.push({
81
+ ...options.event,
82
+ id: `evt_${Date.now()}`,
83
+ timestamp: new Date(),
84
+ })
85
+ }
86
+
87
+ // Add status change event
88
+ if (options.status && options.status !== task.status) {
89
+ events.push({
90
+ id: `evt_${Date.now()}_status`,
91
+ type: options.status === 'completed' ? 'completed' :
92
+ options.status === 'failed' ? 'failed' :
93
+ options.status === 'in_progress' ? 'started' :
94
+ options.status === 'blocked' ? 'blocked' : 'progress',
95
+ timestamp: new Date(),
96
+ message: `Status changed from ${task.status} to ${options.status}`,
97
+ })
98
+ }
99
+
100
+ // Build progress update if provided
101
+ let progressUpdate = task.progress
102
+ if (options.progress) {
103
+ progressUpdate = {
104
+ percent: options.progress.percent ?? task.progress?.percent ?? 0,
105
+ step: options.progress.step ?? task.progress?.step,
106
+ totalSteps: options.progress.totalSteps ?? task.progress?.totalSteps,
107
+ currentStep: options.progress.currentStep ?? task.progress?.currentStep,
108
+ estimatedTimeRemaining: options.progress.estimatedTimeRemaining ?? task.progress?.estimatedTimeRemaining,
109
+ updatedAt: new Date(),
110
+ }
111
+ }
112
+
113
+ const updated: AnyTask = {
114
+ ...task,
115
+ ...(options.status && { status: options.status }),
116
+ ...(options.priority && { priority: options.priority }),
117
+ ...(options.assignment && { assignment: options.assignment }),
118
+ ...(progressUpdate && { progress: progressUpdate }),
119
+ ...(options.metadata && {
120
+ metadata: { ...task.metadata, ...options.metadata },
121
+ }),
122
+ events,
123
+ }
124
+
125
+ this.tasks.set(id, updated)
126
+ return updated
127
+ }
128
+
129
+ async remove(id: string): Promise<boolean> {
130
+ return this.tasks.delete(id)
131
+ }
132
+
133
+ async query(options: TaskQuery): Promise<AnyTask[]> {
134
+ let results = Array.from(this.tasks.values())
135
+
136
+ // Filter by status
137
+ if (options.status) {
138
+ const statuses = Array.isArray(options.status) ? options.status : [options.status]
139
+ results = results.filter((t) => statuses.includes(t.status))
140
+ }
141
+
142
+ // Filter by priority
143
+ if (options.priority) {
144
+ const priorities = Array.isArray(options.priority) ? options.priority : [options.priority]
145
+ results = results.filter((t) => priorities.includes(t.priority))
146
+ }
147
+
148
+ // Filter by function type
149
+ if (options.functionType) {
150
+ results = results.filter((t) => t.function.type === options.functionType)
151
+ }
152
+
153
+ // Filter by assigned worker
154
+ if (options.assignedTo) {
155
+ results = results.filter((t) => t.assignment?.worker.id === options.assignedTo)
156
+ }
157
+
158
+ // Filter by tags
159
+ if (options.tags && options.tags.length > 0) {
160
+ results = results.filter(
161
+ (t) => t.tags && options.tags!.some((tag) => t.tags!.includes(tag))
162
+ )
163
+ }
164
+
165
+ // Filter by project
166
+ if (options.projectId) {
167
+ results = results.filter((t) => t.projectId === options.projectId)
168
+ }
169
+
170
+ // Filter by parent
171
+ if (options.parentId) {
172
+ results = results.filter((t) => t.parentId === options.parentId)
173
+ }
174
+
175
+ // Text search
176
+ if (options.search) {
177
+ const search = options.search.toLowerCase()
178
+ results = results.filter(
179
+ (t) =>
180
+ t.function.name.toLowerCase().includes(search) ||
181
+ t.function.description?.toLowerCase().includes(search)
182
+ )
183
+ }
184
+
185
+ // Sort
186
+ if (options.sortBy) {
187
+ results.sort((a, b) => {
188
+ let aVal: number
189
+ let bVal: number
190
+
191
+ switch (options.sortBy) {
192
+ case 'createdAt':
193
+ aVal = a.createdAt.getTime()
194
+ bVal = b.createdAt.getTime()
195
+ break
196
+ case 'priority':
197
+ aVal = priorityOrder[a.priority]
198
+ bVal = priorityOrder[b.priority]
199
+ break
200
+ case 'deadline':
201
+ aVal = a.deadline?.getTime() || Infinity
202
+ bVal = b.deadline?.getTime() || Infinity
203
+ break
204
+ case 'status':
205
+ aVal = a.status.charCodeAt(0)
206
+ bVal = b.status.charCodeAt(0)
207
+ break
208
+ default:
209
+ return 0
210
+ }
211
+
212
+ return options.sortOrder === 'desc' ? bVal - aVal : aVal - bVal
213
+ })
214
+ }
215
+
216
+ // Pagination
217
+ const offset = options.offset ?? 0
218
+ const limit = options.limit ?? results.length
219
+ results = results.slice(offset, offset + limit)
220
+
221
+ return results
222
+ }
223
+
224
+ async getNextForWorker(worker: WorkerRef): Promise<AnyTask | undefined> {
225
+ // Get queued tasks sorted by priority, then by deadline
226
+ const queuedTasks = await this.query({
227
+ status: ['pending', 'queued'],
228
+ sortBy: 'priority',
229
+ sortOrder: 'desc',
230
+ })
231
+
232
+ // Find first task the worker can handle
233
+ for (const task of queuedTasks) {
234
+ // Check if worker type is allowed
235
+ if (task.allowedWorkers && !task.allowedWorkers.includes(worker.type) && !task.allowedWorkers.includes('any')) {
236
+ continue
237
+ }
238
+
239
+ // Note: Could check worker skills against task requirements in the future
240
+
241
+ // Check if task is scheduled for later
242
+ if (task.scheduledFor && task.scheduledFor > new Date()) {
243
+ continue
244
+ }
245
+
246
+ // Check dependencies
247
+ if (task.dependencies && task.dependencies.length > 0) {
248
+ const unblockedDeps = task.dependencies.filter(
249
+ (d) => d.type === 'blocked_by' && !d.satisfied
250
+ )
251
+ if (unblockedDeps.length > 0) {
252
+ continue
253
+ }
254
+ }
255
+
256
+ return task
257
+ }
258
+
259
+ return undefined
260
+ }
261
+
262
+ async claim(taskId: string, worker: WorkerRef): Promise<boolean> {
263
+ const task = await this.get(taskId)
264
+ if (!task) return false
265
+
266
+ // Check if already assigned
267
+ if (task.assignment) {
268
+ return false
269
+ }
270
+
271
+ // Update task with assignment
272
+ await this.update(taskId, {
273
+ status: 'assigned',
274
+ assignment: {
275
+ worker,
276
+ assignedAt: new Date(),
277
+ },
278
+ event: {
279
+ type: 'assigned',
280
+ actor: worker,
281
+ message: `Assigned to ${worker.name || worker.id}`,
282
+ },
283
+ })
284
+
285
+ return true
286
+ }
287
+
288
+ async complete(taskId: string, output: unknown): Promise<void> {
289
+ const task = await this.get(taskId)
290
+ if (!task) return
291
+
292
+ await this.update(taskId, {
293
+ status: 'completed',
294
+ event: {
295
+ type: 'completed',
296
+ actor: task.assignment?.worker,
297
+ message: 'Task completed successfully',
298
+ data: { output },
299
+ },
300
+ })
301
+
302
+ // Update task output separately since it's not in UpdateTaskOptions
303
+ const updated = await this.get(taskId)
304
+ if (updated) {
305
+ this.tasks.set(taskId, {
306
+ ...updated,
307
+ output: {
308
+ value: output,
309
+ producedAt: new Date(),
310
+ },
311
+ completedAt: new Date(),
312
+ })
313
+ }
314
+
315
+ // Satisfy dependencies in other tasks
316
+ for (const [, otherTask] of this.tasks) {
317
+ if (otherTask.dependencies) {
318
+ const hasDep = otherTask.dependencies.find(
319
+ (d) => d.taskId === taskId && d.type === 'blocked_by'
320
+ )
321
+ if (hasDep) {
322
+ const updatedDeps = otherTask.dependencies.map((d) =>
323
+ d.taskId === taskId ? { ...d, satisfied: true } : d
324
+ )
325
+ this.tasks.set(otherTask.id, {
326
+ ...otherTask,
327
+ dependencies: updatedDeps,
328
+ })
329
+
330
+ // Unblock if all dependencies satisfied
331
+ const allSatisfied = updatedDeps.filter((d) => d.type === 'blocked_by').every((d) => d.satisfied)
332
+ if (allSatisfied && otherTask.status === 'blocked') {
333
+ await this.update(otherTask.id, {
334
+ status: 'queued',
335
+ event: {
336
+ type: 'unblocked',
337
+ message: 'All dependencies satisfied',
338
+ },
339
+ })
340
+ }
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ async fail(taskId: string, error: string): Promise<void> {
347
+ const task = await this.get(taskId)
348
+ if (!task) return
349
+
350
+ await this.update(taskId, {
351
+ status: 'failed',
352
+ event: {
353
+ type: 'failed',
354
+ actor: task.assignment?.worker,
355
+ message: `Task failed: ${error}`,
356
+ data: { error },
357
+ },
358
+ })
359
+ }
360
+
361
+ async stats(): Promise<TaskQueueStats> {
362
+ const tasks = Array.from(this.tasks.values())
363
+
364
+ const byStatus: Record<TaskStatus, number> = {
365
+ pending: 0,
366
+ queued: 0,
367
+ assigned: 0,
368
+ in_progress: 0,
369
+ blocked: 0,
370
+ review: 0,
371
+ completed: 0,
372
+ failed: 0,
373
+ cancelled: 0,
374
+ }
375
+
376
+ const byPriority: Record<TaskPriority, number> = {
377
+ low: 0,
378
+ normal: 0,
379
+ high: 0,
380
+ urgent: 0,
381
+ critical: 0,
382
+ }
383
+
384
+ let totalWaitTime = 0
385
+ let waitTimeCount = 0
386
+ let totalCompletionTime = 0
387
+ let completionTimeCount = 0
388
+
389
+ for (const task of tasks) {
390
+ byStatus[task.status]++
391
+ byPriority[task.priority]++
392
+
393
+ if (task.startedAt && task.createdAt) {
394
+ totalWaitTime += task.startedAt.getTime() - task.createdAt.getTime()
395
+ waitTimeCount++
396
+ }
397
+
398
+ if (task.completedAt && task.startedAt) {
399
+ totalCompletionTime += task.completedAt.getTime() - task.startedAt.getTime()
400
+ completionTimeCount++
401
+ }
402
+ }
403
+
404
+ return {
405
+ total: tasks.length,
406
+ byStatus,
407
+ byPriority,
408
+ avgWaitTime: waitTimeCount > 0 ? totalWaitTime / waitTimeCount : undefined,
409
+ avgCompletionTime: completionTimeCount > 0 ? totalCompletionTime / completionTimeCount : undefined,
410
+ }
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Global task queue instance
416
+ */
417
+ export const taskQueue: TaskQueue = new InMemoryTaskQueue()
418
+
419
+ /**
420
+ * Create a new task queue instance
421
+ */
422
+ export function createTaskQueue(options?: TaskQueueOptions): TaskQueue {
423
+ return new InMemoryTaskQueue(options)
424
+ }