Llinuxctrl
Architecture Deep Dive

Architecture

Overview

grabr is built with a modular architecture. The core download engine is UI-agnostic — it emits events consumed by the CLI dashboard, WebSocket server, or any programmatic consumer.

URL → chunker → [chunk workers] → merger → final file

               EventEmitter

        CLI Dashboard  |  WebSocket → Browser UI

Project Structure

src/
├── index.ts           # Public library entry point
├── core/              # Download engine (UI-agnostic)
│   ├── downloader.ts  #   Orchestrator (EventEmitter)
│   ├── chunker.ts     #   HEAD request → split into ranges
│   ├── worker.ts      #   Single chunk fetch
│   ├── merger.ts      #   Assembles chunks into final file
│   ├── resume.ts      #   Resume state files
│   ├── config.ts      #   ~/.grabr/config.json
│   └── types.ts       #   Shared types
├── store/
│   ├── db.ts          #   sql.js SQLite setup
│   └── jobs.ts        #   CRUD for download jobs
├── cli/               # Terminal UI (Ink + React)
│   ├── index.tsx      #   CLI entry: arg parsing + routing
│   ├── commands/      #   add, list, pause, resume, remove, clear, ui, daemon
│   └── ui/            #   Ink components (Dashboard, JobRow, ProgressBar)
├── server/            # HTTP server (Hono)
│   └── index.ts       #   REST API + WebSocket + static file serving
└── web/               # Web UI source (vanilla TypeScript)
    ├── main.ts        #   WebSocket client + render loop
    ├── components/    #   JobCard, ProgressRing, Topbar
    └── styles/        #   CSS custom properties

Core Engine

Chunked Download Flow

  1. Metadata: A HEAD request fetches Content-Length and Accept-Ranges. If ranges are supported, the file is split into N chunks.
  2. Parallel Workers: Each chunk is downloaded independently via fetch with a Range header.
  3. Progress: Bytes flow through an EMA-based speed calculator (α=0.2 smoothing).
  4. Merge: Completed chunks are assembled in order via streaming pipeline.
  5. Retry: Failed chunks retry up to 3 times with exponential backoff (2s, 4s, 8s).

Resume

Each job writes a resume file (.grabr/<jobId>.json) tracking per-chunk progress. If interrupted, chunks that completed before the interruption are skipped.

Filename Collision

If a file already exists at the destination, grabr appends a counter:file(1).zip, file(2).zip, etc.

Storage

Uses sql.js — SQLite compiled to WebAssembly. No native compilation required, works on both Node.js and Bun. The database is persisted to .grabr/grabr.db and flushed to disk after every write operation.

CLI Dashboard

Built with Ink (React for terminals). The dashboard connects to the local daemon via WebSocket for live updates, or runs the downloader in-process for standalone mode.

Server + Web UI

The daemon uses Hono for HTTP routing, Bun's native WebSocket for real-time events, and serves a vanilla TypeScript web dashboard with SVG progress rings.

API Endpoints

MethodPathDescription
GET/api/jobsList all jobs
POST/api/jobsAdd a new job
GET/api/jobs/:idGet job details
POST/api/jobs/:id/pausePause a job
POST/api/jobs/:id/resumeResume a job
DELETE/api/jobs/:idRemove a job
WS/wsReal-time progress stream

Tech Stack

PackagePurpose
sql.jsSQLite via WebAssembly (no native deps)
honoHTTP server + router + WebSocket
inkReact-based terminal UI
nanoidJob ID generation
mime-typesFile extension detection from Content-Type