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/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +10 -0
- package/dist/function-task.d.ts +319 -0
- package/dist/function-task.d.ts.map +1 -0
- package/dist/function-task.js +286 -0
- package/dist/function-task.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +112 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +510 -0
- package/dist/markdown.js.map +1 -0
- package/dist/project.d.ts +259 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +397 -0
- package/dist/project.js.map +1 -0
- package/dist/queue.d.ts +17 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +347 -0
- package/dist/queue.js.map +1 -0
- package/dist/task.d.ts +69 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +321 -0
- package/dist/task.js.map +1 -0
- package/dist/types.d.ts +292 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/package.json +41 -0
- package/src/index.ts +161 -0
- package/src/markdown.ts +622 -0
- package/src/project.ts +571 -0
- package/src/queue.ts +424 -0
- package/src/task.ts +389 -0
- package/src/types.ts +403 -0
- package/test/markdown.test.ts +550 -0
- package/test/project.test.ts +562 -0
- package/test/queue.test.ts +482 -0
- package/test/task.test.ts +464 -0
- package/tsconfig.json +20 -0
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
|
+
}
|