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
grabr/
├── src/
│ ├── index.ts # Public library entry point
│ ├── core/ # Download engine (UI-agnostic)
│ │ ├── downloader.ts
│ │ ├── chunker.ts
│ │ ├── worker.ts
│ │ ├── merger.ts
│ │ ├── resume.ts
│ │ ├── config.ts
│ │ └── types.ts
│ ├── store/
│ │ ├── db.ts # sql.js SQLite setup
│ │ └── jobs.ts
│ ├── cli/ # Terminal UI (Ink + React)
│ │ ├── index.tsx
│ │ ├── commands/
│ │ └── ui/
│ ├── server/ # HTTP server (Hono)
│ │ └── index.ts
│ └── web/ # Web UI source (vanilla TS)
│ ├── main.ts
│ ├── components/
│ └── styles/Core 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 the home directory ~/.grabr/grabr.dband 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 Honofor 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 |
| POST | /api/jobs/pause-all | Pause all active jobs |
| POST | /api/jobs/resume-all | Resume all paused jobs |
| POST | /api/jobs/clear-completed | Clear all completed jobs |
| GET | /api/version | Get daemon version |
| GET | /api/youtube/formats | Get YouTube video formats (requires yt-dlp) |
| 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 |