[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Long-Running Task Tracker

Build a Long-Running Task Tracker

Build a long-running task tracker with progress updates, cancellation support, step-by-step status, ETA calculation, and notification on completion for async operations.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Nadia leads backend at a 20-person company. Several operations take 5-30 minutes: data imports (50K rows), report generation, bulk email sends, video transcoding. Users click "Import" and see a spinner for 20 minutes with no feedback. If they close the tab, they don't know if it finished. There's no way to cancel a running import. Failed tasks show a generic error with no detail about which step failed. They need a task tracker: submit long tasks, poll progress, see step-by-step status, cancel running tasks, get ETA, and receive notification on completion.

Step 1: Build the Task Tracker

typescript
import { pool } from "../db";
import { Redis } from "ioredis";
import { randomBytes } from "node:crypto";
const redis = new Redis(process.env.REDIS_URL!);

interface Task { id: string; type: string; userId: string; status: "queued" | "running" | "completed" | "failed" | "cancelled"; progress: number; currentStep: string; steps: Array<{ name: string; status: string; startedAt: string | null; completedAt: string | null; details: string }>; eta: string | null; result: any; error: string | null; startedAt: string; completedAt: string | null; }

// Submit a long-running task
export async function submitTask(params: { type: string; userId: string; steps: string[]; payload: any }): Promise<Task> {
  const id = `task-${randomBytes(8).toString("hex")}`;
  const task: Task = {
    id, type: params.type, userId: params.userId, status: "queued", progress: 0, currentStep: params.steps[0],
    steps: params.steps.map((name) => ({ name, status: "pending", startedAt: null, completedAt: null, details: "" })),
    eta: null, result: null, error: null, startedAt: new Date().toISOString(), completedAt: null,
  };
  await redis.setex(`task:${id}`, 86400, JSON.stringify(task));
  await redis.rpush("task:queue", JSON.stringify({ taskId: id, ...params }));
  await pool.query(`INSERT INTO long_tasks (id, type, user_id, status, payload, started_at) VALUES ($1, $2, $3, 'queued', $4, NOW())`, [id, params.type, params.userId, JSON.stringify(params.payload)]);
  return task;
}

// Update task progress (called by worker)
export async function updateProgress(taskId: string, update: { step: string; progress: number; details?: string; status?: string }): Promise<void> {
  const data = await redis.get(`task:${taskId}`);
  if (!data) return;
  const task: Task = JSON.parse(data);

  // Check cancellation
  if (await redis.exists(`task:cancel:${taskId}`)) { task.status = "cancelled"; task.completedAt = new Date().toISOString(); await redis.setex(`task:${taskId}`, 86400, JSON.stringify(task)); return; }

  task.progress = update.progress;
  task.currentStep = update.step;
  const stepIdx = task.steps.findIndex((s) => s.name === update.step);
  if (stepIdx >= 0) {
    task.steps[stepIdx].status = update.status || "running";
    if (!task.steps[stepIdx].startedAt) task.steps[stepIdx].startedAt = new Date().toISOString();
    if (update.details) task.steps[stepIdx].details = update.details;
    if (update.status === "completed") task.steps[stepIdx].completedAt = new Date().toISOString();
  }
  task.status = "running";

  // Calculate ETA
  const elapsed = Date.now() - new Date(task.startedAt).getTime();
  if (task.progress > 0) {
    const totalEstimated = elapsed / (task.progress / 100);
    const remaining = totalEstimated - elapsed;
    task.eta = new Date(Date.now() + remaining).toISOString();
  }

  await redis.setex(`task:${taskId}`, 86400, JSON.stringify(task));
}

// Complete task
export async function completeTask(taskId: string, result: any): Promise<void> {
  const data = await redis.get(`task:${taskId}`);
  if (!data) return;
  const task: Task = JSON.parse(data);
  task.status = "completed"; task.progress = 100; task.result = result; task.completedAt = new Date().toISOString();
  for (const step of task.steps) { if (step.status !== "completed") { step.status = "completed"; step.completedAt = new Date().toISOString(); }}
  await redis.setex(`task:${taskId}`, 86400, JSON.stringify(task));
  await pool.query("UPDATE long_tasks SET status = 'completed', result = $2, completed_at = NOW() WHERE id = $1", [taskId, JSON.stringify(result)]);
  await redis.rpush("notification:queue", JSON.stringify({ type: "task_completed", taskId, userId: task.userId, taskType: task.type }));
}

// Fail task
export async function failTask(taskId: string, error: string): Promise<void> {
  const data = await redis.get(`task:${taskId}`);
  if (!data) return;
  const task: Task = JSON.parse(data);
  task.status = "failed"; task.error = error; task.completedAt = new Date().toISOString();
  await redis.setex(`task:${taskId}`, 86400, JSON.stringify(task));
  await pool.query("UPDATE long_tasks SET status = 'failed', error = $2, completed_at = NOW() WHERE id = $1", [taskId, error]);
  await redis.rpush("notification:queue", JSON.stringify({ type: "task_failed", taskId, userId: task.userId, error }));
}

// Cancel task
export async function cancelTask(taskId: string): Promise<void> {
  await redis.setex(`task:cancel:${taskId}`, 3600, "1");
}

// Get task status (for polling)
export async function getTaskStatus(taskId: string): Promise<Task | null> {
  const data = await redis.get(`task:${taskId}`);
  return data ? JSON.parse(data) : null;
}

// Get user's tasks
export async function getUserTasks(userId: string): Promise<Task[]> {
  const { rows } = await pool.query("SELECT id FROM long_tasks WHERE user_id = $1 ORDER BY started_at DESC LIMIT 20", [userId]);
  const tasks: Task[] = [];
  for (const row of rows) {
    const data = await redis.get(`task:${row.id}`);
    if (data) tasks.push(JSON.parse(data));
  }
  return tasks;
}

Results

  • "Import: 67% — Processing row 33,500 of 50,000 — ETA: 3 min" — users see real progress instead of spinner; reduced support tickets about "is it stuck?" by 90%
  • Tab-safe — close tab, come back, see task status; notification when done; no lost work
  • Cancellation works — user starts wrong import → cancel button → task stops at next checkpoint; no waiting 20 minutes for wrong data
  • Step-by-step visibility — "Validating ✅ → Importing 🔄 → Indexing ⏳" — user knows exactly what's happening and which step failed
  • Notification on completion — push notification when 30-minute report is done; user doesn't poll manually; goes back to other work