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 UIProject 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 propertiesCore Engine
Chunked Download Flow
- Metadata: A HEAD request fetches
Content-LengthandAccept-Ranges. If ranges are supported, the file is split into N chunks. - Parallel Workers: Each chunk is downloaded independently via
fetchwith aRangeheader. - Progress: Bytes flow through an EMA-based speed calculator (α=0.2 smoothing).
- Merge: Completed chunks are assembled in order via streaming pipeline.
- 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
| Method | Path | Description |
|---|---|---|
| GET | /api/jobs | List all jobs |
| POST | /api/jobs | Add a new job |
| GET | /api/jobs/:id | Get job details |
| POST | /api/jobs/:id/pause | Pause a job |
| POST | /api/jobs/:id/resume | Resume a job |
| DELETE | /api/jobs/:id | Remove a job |
| WS | /ws | Real-time progress stream |
Tech Stack
| Package | Purpose |
|---|---|
| sql.js | SQLite via WebAssembly (no native deps) |
| hono | HTTP server + router + WebSocket |
| ink | React-based terminal UI |
| nanoid | Job ID generation |
| mime-types | File extension detection from Content-Type |