[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
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.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.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.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
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

Related use cases