summaryrefslogtreecommitdiffhomepage
path: root/.research/03-backend-stack-comparison.md
blob: c00300ad302f9493461578ddd2684d5117c97943 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# 03 — Backend Stack Comparison: Python vs Bun/TypeScript vs Hybrid

> Question: *Should we switch to a different backend stack?*

## Verdict: Stay on Bun + TypeScript (the dispatch stack).

The only serious argument for switching is that yt-dlp is written in Python and
offers a cleaner in-process API. That argument does **not** outweigh the cost of
abandoning the shared-language, shared-contract, shared-toolchain architecture
that is the entire point of mirroring dispatch.

## Detailed comparison

### Option 1: Bun + TypeScript, shell out to yt-dlp binary ✅ RECOMMENDED

**Stack**: Bun, TypeScript (strict), Biome, Vitest, `bun:sqlite`. Identical to
`dispatch-backend`. Drive yt-dlp via `youtube-dl-exec` or a custom `Bun.spawn`
wrapper (see doc 01).

| Pros | Cons |
|---|---|
| Mirrors dispatch exactly — shared Biome/Vitest/tsc config, muscle memory | Progress comes from parsing stdout JSON lines (trivial, see doc 01) |
| Shared TypeScript contracts flow `file:` backend → frontend (dispatch's core value) | Must manage/symlink the yt-dlp binary (youtube-dl-exec handles this) |
| One language, one LSP, one formatter across both repos | — |
| Bun is very fast; `bun:sqlite` is built in | — |
| yt-dlp standalone binary needs **no Python at runtime** | — |
| Async streaming (`AsyncIterable<DownloadEvent>`) fits the pure-reducer model | — |
| Pure download state-machine is unit-testable with zero mocks | — |

### Option 2: Python (FastAPI + `import yt_dlp`) ❌ REJECTED

**Stack**: FastAPI, yt-dlp as a library, SQLite, Pydantic. Frontend still
Svelte/TS.

| Pros | Cons |
|---|---|
| `progress_hooks` gives clean in-process callbacks (no stdout parsing) | **Breaks the dispatch contract model** — can't share TS types with the Svelte frontend without a codegen step (we'd have to build + maintain a JSON Schema → TS pipeline) |
| yt-dlp is native — no subprocess, no binary management | Second language + toolchain (pip, venv, ruff/mypy, pytest) to learn/maintain |
| Pydantic models are nice | No `bun:sqlite` equivalent built-in; more setup |
| | Dispatch's `file:` contract dep pattern (`"@dispatch/wire": "file:../backend/packages/wire"`) is impossible — Python packages can't be consumed by npm |
| | Two deploy artifacts (Python server + static frontend) instead of one ecosystem |
| | The user's environment/muscle-memory is dispatch (Bun). Context-switching cost. |

**The contract problem is decisive.** Dispatch's frontend consumes the backend's
typed contracts as pinned `file:` deps:
```json
"@dispatch/wire": "file:../dispatch-backend/packages/wire"
```
This is what makes `lsp references` and type-safe API calls work across the repo
boundary. A Python backend cannot provide this. We'd either duplicate types
manually (drift risk) or build a codegen pipeline (maintenance burden) — both
defeat the purpose of mirroring dispatch.

### Option 3: Hybrid (Bun backend + Python sidecar) ❌ REJECTED

Bun backend handles API/WS/DB/contracts; a tiny Python service (or even a
stdin/stdout pipe) runs yt-dlp as a library for the "clean progress_hooks" win.

| Pros | Cons |
|---|---|
| Best of both: clean Python progress hooks + TS contracts | **Over-engineered for a personal tool.** Two processes, IPC protocol, two dep trees, two deploy targets. |
| | The "win" (stdout parsing vs callbacks) is ~20 lines of code difference. Not worth a second runtime. |
| | Debugging across the process boundary is harder. |

### What about the `youtube-dl-exec` Python postinstall check?

`youtube-dl-exec`'s postinstall script checks for `python3` to run its install
logic. This is the *only* Python touchpoint in the recommended stack, and it's:
- Build-time only (postinstall), not runtime.
- Skippable: `YOUTUBE_DL_SKIP_PYTHON_CHECK=true npm install`.
- Or avoid entirely: use a custom `Bun.spawn` wrapper and manage the binary via a
  `bin/install-yt-dlp` script (download the standalone binary from GitHub releases).

So even this doesn't justify a Python backend.

## When you *would* reconsider Python

- If this were a high-throughput multi-user service (yt-dlp's GIL / threading
  matters under concurrency) — it's not, it's a personal Walkman music tool.
- If we needed yt-dlp's Python-only features (custom extractors, plugin
  development in Python) — we don't.
- If the stdout progress parsing proved flaky in practice — it won't;
  `--progress-template` is stable, documented output.

## Recommendation

**Option 1.** Build the backend in Bun + TypeScript, identical stack to
`dispatch-backend`. Wrap yt-dlp behind a typed `Downloader` contract. The
~20 lines of stdout-JSON-line parsing is a small price for keeping the entire
dispatch methodology intact: shared contracts, shared toolchain, pure-core
testability, `file:` dep wiring.