All files / web/src/db/schema background-tasks.ts

100% Statements 159/159
100% Branches 5/5
100% Functions 1/1
100% Lines 159/159

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 1602x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 41x 41x 41x 41x 41x 41x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 39x 39x 2x 2x 2x 2x 37x 37x 37x 37x 37x 37x 37x 2x 2x 2x 37x 37x 37x 37x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x  
import { createId } from '@paralleldrive/cuid2'
import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core'
import { relations } from 'drizzle-orm'
 
/**
 * Background tasks table - stores long-running task metadata and results
 *
 * Used for tasks like TensorFlow training, worksheet parsing, worksheet generation,
 * and flowchart embedding. Persists task state to survive page reloads and enables
 * event replay.
 */
export const backgroundTasks = sqliteTable(
  'background_tasks',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => createId()),
 
    /** Task type - determines which handler processes it */
    type: text('type').notNull(), // 'vision-training' | 'worksheet-parse' | 'worksheet-generate' | 'flowchart-embed' | 'demo'
 
    /** Current status of the task */
    status: text('status').notNull().default('pending'), // 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
 
    /** JSON input for the task (task-specific parameters) */
    input: text('input', { mode: 'json' }),
 
    /** JSON result when completed (task-specific output) */
    output: text('output', { mode: 'json' }),
 
    /** Error message if task failed */
    error: text('error'),
 
    /** Progress percentage (0-100) */
    progress: integer('progress').default(0),
 
    /** Human-readable progress message */
    progressMessage: text('progress_message'),
 
    /** When the task was created */
    createdAt: integer('created_at', { mode: 'timestamp' })
      .notNull()
      .$defaultFn(() => new Date()),
 
    /** When the task started running */
    startedAt: integer('started_at', { mode: 'timestamp' }),
 
    /** When the task completed (success or failure) */
    completedAt: integer('completed_at', { mode: 'timestamp' }),
 
    /** Optional user ID association */
    userId: text('user_id'),
 
    /** Runner ID - identifies which pod/process owns this task */
    runnerId: text('runner_id'),
 
    /** Last heartbeat timestamp - updated periodically while task is running */
    lastHeartbeat: integer('last_heartbeat', { mode: 'timestamp' }),
 
    /** Optional parent task ID for orchestrated subtasks */
    parentTaskId: text('parent_task_id'),
  },
  (table) => ({
    statusIdx: index('background_tasks_status_idx').on(table.status),
    typeIdx: index('background_tasks_type_idx').on(table.type),
    userIdx: index('background_tasks_user_idx').on(table.userId),
    createdAtIdx: index('background_tasks_created_at_idx').on(table.createdAt),
    parentTaskIdx: index('background_tasks_parent_task_idx').on(table.parentTaskId),
  })
)
 
/**
 * Background task events table - stores events for replay
 *
 * When a client reconnects or subscribes to a running task, all events
 * are replayed in order to bring them up to date.
 */
export const backgroundTaskEvents = sqliteTable(
  'background_task_events',
  {
    id: integer('id').primaryKey({ autoIncrement: true }),
 
    /** Foreign key to the parent task */
    taskId: text('task_id')
      .notNull()
      .references(() => backgroundTasks.id, { onDelete: 'cascade' }),
 
    /** Type of event (see src/lib/tasks/events.ts for per-task-type definitions) */
    eventType: text('event_type').notNull(),
 
    /** JSON payload for the event (event-specific data) */
    payload: text('payload', { mode: 'json' }),
 
    /** When the event was emitted */
    createdAt: integer('created_at', { mode: 'timestamp' })
      .notNull()
      .$defaultFn(() => new Date()),
  },
  (table) => ({
    taskIdIdx: index('background_task_events_task_id_idx').on(table.taskId),
  })
)
 
// Relations
export const backgroundTasksRelations = relations(backgroundTasks, ({ one, many }) => ({
  events: many(backgroundTaskEvents),
  parentTask: one(backgroundTasks, {
    fields: [backgroundTasks.parentTaskId],
    references: [backgroundTasks.id],
    relationName: 'parentChild',
  }),
  childTasks: many(backgroundTasks, { relationName: 'parentChild' }),
}))
 
export const backgroundTaskEventsRelations = relations(backgroundTaskEvents, ({ one }) => ({
  task: one(backgroundTasks, {
    fields: [backgroundTaskEvents.taskId],
    references: [backgroundTasks.id],
  }),
}))
 
// Types
export type BackgroundTask = typeof backgroundTasks.$inferSelect
export type NewBackgroundTask = typeof backgroundTasks.$inferInsert
export type BackgroundTaskEvent = typeof backgroundTaskEvents.$inferSelect
export type NewBackgroundTaskEvent = typeof backgroundTaskEvents.$inferInsert
 
export type TaskType =
  | 'vision-training'
  | 'worksheet-parse'
  | 'worksheet-reparse'
  | 'worksheet-generate'
  | 'flowchart-embed'
  | 'flowchart-generate'
  | 'flowchart-refine'
  | 'seed-students'
  | 'audio-generate'
  | 'collected-clip-generate'
  | 'image-generate'
  | 'phi-explore-generate'
  | 'blog-image-generate'
  | 'demo'
  | 'demo-refine'
  | 'session-plan'
  | 'session-song'
  | 'profile-image-generate'
  | 'page-spot-generate'
  | 'postcard-generate'
  | 'postcard-image-generate'
  | 'postcard-review'
  | 'postcard-thumbnail-generate'
  | 'moment-cull'
export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
/**
 * Event types are defined per task type in `src/lib/tasks/events.ts`.
 * The DB column `event_type` stores the string discriminant from those unions.
 * Lifecycle events (started, progress, completed, failed, cancelled) are
 * emitted by the task manager; domain events are emitted by handlers.
 */