Created
June 10, 2026 09:15
-
-
Save flox1an/5c25a119ed0e1ca2ce38e79877a0ab97 to your computer and use it in GitHub Desktop.
Architecture review — nostube — 2026-06-10 (8 deepening opportunities)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Architecture review — nostube</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script type="module"> | |
| import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"; | |
| mermaid.initialize({ startOnLoad: true, theme: "neutral", securityLevel: "loose" }); | |
| </script> | |
| <style> | |
| .seam { stroke-dasharray: 4 4; } | |
| .leak { stroke: #dc2626; } | |
| .deep { background: linear-gradient(135deg, #0f172a, #1e293b); } | |
| .shallow-band { | |
| background-image: repeating-linear-gradient(135deg,#f1f5f9,#f1f5f9 6px,#e2e8f0 6px,#e2e8f0 12px); | |
| } | |
| .label { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .02em; } | |
| .badge { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } | |
| .grid-divider > * + * { border-left: 1px solid rgb(226 232 240); } | |
| .arrow path { stroke: #475569; stroke-width: 1.4; fill: none; } | |
| .arrow .head { fill: #475569; stroke: none; } | |
| .red.arrow path, .red.arrow .head { stroke: #dc2626; fill: #dc2626; } | |
| .module-box { background: white; border: 1px solid rgb(203 213 225); border-radius: 6px; } | |
| .module-box.dim { background: rgb(248 250 252); color: rgb(100 116 139); } | |
| .module-box.deep { | |
| background: linear-gradient(135deg, #0f172a, #1e293b); | |
| border-color: #0f172a; color: rgb(241 245 249); | |
| } | |
| .module-box.deep .label-sub { color: rgb(148 163 184); } | |
| details > summary { cursor: pointer; } | |
| details[open] > summary .summary-caret { transform: rotate(90deg); } | |
| .summary-caret { transition: transform .15s ease; display: inline-block; } | |
| </style> | |
| </head> | |
| <body class="bg-stone-50 text-slate-900 font-sans antialiased"> | |
| <main class="max-w-6xl mx-auto px-6 py-14 space-y-16"> | |
| <!-- ============================== HEADER ============================== --> | |
| <header class="space-y-6"> | |
| <p class="text-xs uppercase tracking-[0.18em] text-slate-500">Architecture review · 2026-06-10</p> | |
| <h1 class="font-serif text-5xl tracking-tight text-slate-900"> | |
| nostube | |
| <span class="block text-2xl text-slate-500 font-sans font-normal mt-2"> | |
| Eight deepening opportunities — surfaces and seams. | |
| </span> | |
| </h1> | |
| <div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs label text-slate-700 pt-4"> | |
| <div class="border border-slate-200 bg-white rounded px-3 py-2"> | |
| <span class="inline-block w-3 h-3 align-middle mr-2 border border-slate-400 bg-white"></span>module | |
| </div> | |
| <div class="border border-slate-200 bg-white rounded px-3 py-2"> | |
| <span class="inline-block w-3 h-3 align-middle mr-2 deep border border-slate-900"></span>deep module | |
| </div> | |
| <div class="border border-slate-200 bg-white rounded px-3 py-2"> | |
| <svg width="20" height="10" class="inline-block align-middle mr-1"><line class="seam" x1="0" y1="5" x2="20" y2="5" stroke="#475569" stroke-width="1.4"/></svg>seam | |
| </div> | |
| <div class="border border-slate-200 bg-white rounded px-3 py-2"> | |
| <svg width="20" height="10" class="inline-block align-middle mr-1"><line x1="0" y1="5" x2="20" y2="5" stroke="#dc2626" stroke-width="1.6"/></svg>leakage | |
| </div> | |
| </div> | |
| <p class="text-sm text-slate-600 max-w-3xl pt-2"> | |
| Eight candidates, ranked. Each card pairs a current shape with the proposed deepening. | |
| Glossary is from <span class="label">LANGUAGE.md</span>; domain nouns come from | |
| <span class="label">CONTEXT.md</span>. No interfaces yet — pick the candidate you want to grill. | |
| </p> | |
| </header> | |
| <!-- ============================== CANDIDATES ============================== --> | |
| <section id="candidates" class="space-y-14"> | |
| <!-- ---------------------------------------------------------------- --> | |
| <!-- CARD 1: DVM Transcode session --> | |
| <!-- ---------------------------------------------------------------- --> | |
| <article id="c1" class="bg-white border border-slate-200 rounded-lg p-8 space-y-6 shadow-sm"> | |
| <header class="space-y-2"> | |
| <div class="flex items-center gap-3 flex-wrap"> | |
| <span class="badge text-xs px-2 py-1 rounded bg-emerald-100 text-emerald-900 uppercase tracking-wider">Strong</span> | |
| <span class="badge text-xs px-2 py-1 rounded bg-slate-100 text-slate-700 uppercase tracking-wider">in-process</span> | |
| <span class="badge text-xs px-2 py-1 rounded bg-rose-100 text-rose-900 uppercase tracking-wider">no tests</span> | |
| </div> | |
| <h2 class="font-serif text-3xl text-slate-900">1. Collapse the DVM transcode session</h2> | |
| <p class="text-slate-600 text-sm"> | |
| Two implementations of the same kind 5207 / 6207 / 7000 protocol — one in a hook, | |
| one in the upload provider. Same function names, same logic, ~1300 LOC each. | |
| </p> | |
| </header> | |
| <div class="label text-xs text-slate-600 space-y-0.5"> | |
| <div>src/hooks/useDvmTranscode.ts <span class="text-slate-400">— 1323 LOC</span></div> | |
| <div>src/providers/upload/UploadManagerProvider.tsx <span class="text-slate-400">— collectBids / approveBid / subscribeToDvmResponses / processResolution duplicated</span></div> | |
| <div>src/hooks/useDvmTranscodeManager.ts <span class="text-slate-400">— shallow adapter (250 LOC)</span></div> | |
| <div>src/lib/dvm-utils.ts <span class="text-slate-400">— pure parsers, OK</span></div> | |
| </div> | |
| <!-- diagram: mermaid --> | |
| <div class="grid md:grid-cols-2 gap-6 grid-divider"> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">before — two parallel implementations</p> | |
| <div class="rounded border border-slate-200 bg-slate-50 p-3"> | |
| <pre class="mermaid"> | |
| flowchart LR | |
| classDef shallow fill:#fff,stroke:#94a3b8,stroke-dasharray:3 3 | |
| classDef leak stroke:#dc2626,stroke-width:2px | |
| classDef relay fill:#f1f5f9,stroke:#cbd5e1 | |
| Alert[DvmTranscodeAlert]:::shallow | |
| Dialog[UploadDialog] | |
| Hook["useDvmTranscode<br/>(1323 LOC)"] | |
| Mgr["useDvmTranscodeManager<br/>(250 LOC adapter)"]:::shallow | |
| Prov["UploadManagerProvider<br/>(1695 LOC)"] | |
| Relays[(Nostr relays:<br/>5207/6207/7000)]:::relay | |
| Alert --> Mgr --> Prov | |
| Dialog --> Hook | |
| Hook -- "collectBids<br/>approveBid<br/>subscribeToDvmResponses<br/>processResolution" --> Relays | |
| Prov -- "collectBids<br/>approveBid<br/>subscribeToDvmResponses<br/>processResolution" --> Relays | |
| Hook -.-> Prov | |
| class Hook,Prov leak | |
| </pre> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">after — one DVM transcode session</p> | |
| <div class="rounded border border-slate-200 bg-slate-50 p-3"> | |
| <pre class="mermaid"> | |
| flowchart LR | |
| classDef deep fill:#0f172a,color:#f1f5f9,stroke:#0f172a | |
| classDef relay fill:#f1f5f9,stroke:#cbd5e1 | |
| Alert[DvmTranscodeAlert] | |
| Dialog[UploadDialog] | |
| Prov["UploadManagerProvider<br/>(task lifecycle only)"] | |
| Session["dvm-transcode-session.ts<br/>start · cancel · observe"]:::deep | |
| Relays[(Nostr relays)]:::relay | |
| Alert --> Prov | |
| Dialog --> Prov | |
| Prov --> Session | |
| Session --> Relays | |
| </pre> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Problem</h3> | |
| <p class="text-sm text-slate-700"> | |
| The DVM transcode protocol (bid collection, approval, encrypted feedback parsing, | |
| multi-resolution queue, 12-h resume window) is implemented twice. Any kind-7000 format | |
| change must land in two places. Neither path has tests. | |
| </p> | |
| </div> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Solution</h3> | |
| <p class="text-sm text-slate-700"> | |
| Extract a <span class="label">DvmTranscodeSession</span> module behind a small interface | |
| (start, cancel, observe progress). Provider and hook both delegate. The session is a plain | |
| module — testable without React — that owns request building, subscription, parsing, and | |
| the multi-resolution queue. | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500 mb-2">Wins</h3> | |
| <ul class="grid sm:grid-cols-2 gap-x-6 gap-y-1 text-sm text-slate-700 list-disc list-inside"> | |
| <li>locality: one place for DVM protocol changes</li> | |
| <li>leverage: testable session, no React harness</li> | |
| <li>delete ~1100 LOC of duplicate logic</li> | |
| <li>interface shrinks from 11-field hook to 3 verbs</li> | |
| <li>useDvmTranscodeManager collapses to inline usage</li> | |
| <li>useDvmTranscode retires once provider wraps session</li> | |
| </ul> | |
| </div> | |
| </article> | |
| <!-- ---------------------------------------------------------------- --> | |
| <!-- CARD 2: Playback URL ladder --> | |
| <!-- ---------------------------------------------------------------- --> | |
| <article id="c2" class="bg-white border border-slate-200 rounded-lg p-8 space-y-6 shadow-sm"> | |
| <header class="space-y-2"> | |
| <div class="flex items-center gap-3 flex-wrap"> | |
| <span class="badge text-xs px-2 py-1 rounded bg-emerald-100 text-emerald-900 uppercase tracking-wider">Strong</span> | |
| <span class="badge text-xs px-2 py-1 rounded bg-slate-100 text-slate-700 uppercase tracking-wider">in-process</span> | |
| <span class="badge text-xs px-2 py-1 rounded bg-rose-100 text-rose-900 uppercase tracking-wider">no tests</span> | |
| </div> | |
| <h2 class="font-serif text-3xl text-slate-900">2. Deepen the playback URL ladder</h2> | |
| <p class="text-slate-600 text-sm"> | |
| For one Video Event, the URL list is generated three times in three modules | |
| and reasoned about with three different policies (codec filter, HEAD validation, HLS retry). | |
| Nothing remembers what already failed. | |
| </p> | |
| </header> | |
| <div class="label text-xs text-slate-600 space-y-0.5"> | |
| <div>src/lib/media-url-generator.ts <span class="text-slate-400">— generateMediaUrls (called 3×)</span></div> | |
| <div>src/hooks/useMediaUrls.ts <span class="text-slate-400">— 8 options · 7 status fields · 430 LOC</span></div> | |
| <div>src/lib/hls-blossom-loader.ts <span class="text-slate-400">— rebuilds list per HLS segment</span></div> | |
| <div>src/utils/video-event.ts <span class="text-slate-400">— calls it during processEvent</span></div> | |
| <div>src/lib/url-discovery.ts <span class="text-slate-400">— kind 1063 discovery, fires late</span></div> | |
| </div> | |
| <!-- diagram: hand-built mass + side-by-side --> | |
| <div class="grid md:grid-cols-2 gap-6 grid-divider"> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">before — three regenerations</p> | |
| <div class="border border-slate-200 rounded p-4 bg-slate-50 relative" style="min-height:340px"> | |
| <svg viewBox="0 0 360 320" class="w-full"> | |
| <!-- VideoEvent source --> | |
| <rect x="130" y="6" width="100" height="34" fill="#ffffff" stroke="#94a3b8"/> | |
| <text x="180" y="28" font-size="11" text-anchor="middle" class="label">Video Event</text> | |
| <!-- Three boxes --> | |
| <g> | |
| <rect x="6" y="76" width="100" height="80" fill="#ffffff" stroke="#94a3b8"/> | |
| <text x="56" y="96" font-size="10" text-anchor="middle" class="label">processEvent</text> | |
| <rect x="14" y="106" width="84" height="40" fill="#fef3c7" stroke="#f59e0b"/> | |
| <text x="56" y="124" font-size="9" text-anchor="middle" class="label">generateMediaUrls</text> | |
| <text x="56" y="138" font-size="8" text-anchor="middle" fill="#92400e">codec filter</text> | |
| </g> | |
| <g> | |
| <rect x="130" y="76" width="100" height="80" fill="#ffffff" stroke="#94a3b8"/> | |
| <text x="180" y="96" font-size="10" text-anchor="middle" class="label">useMediaUrls</text> | |
| <rect x="138" y="106" width="84" height="40" fill="#fef3c7" stroke="#f59e0b"/> | |
| <text x="180" y="124" font-size="9" text-anchor="middle" class="label">generateMediaUrls</text> | |
| <text x="180" y="138" font-size="8" text-anchor="middle" fill="#92400e">+ HEAD validate</text> | |
| </g> | |
| <g> | |
| <rect x="254" y="76" width="100" height="80" fill="#ffffff" stroke="#94a3b8"/> | |
| <text x="304" y="96" font-size="10" text-anchor="middle" class="label">hls loader</text> | |
| <rect x="262" y="106" width="84" height="40" fill="#fef3c7" stroke="#f59e0b"/> | |
| <text x="304" y="124" font-size="9" text-anchor="middle" class="label">generateMediaUrls</text> | |
| <text x="304" y="138" font-size="8" text-anchor="middle" fill="#92400e">per HLS segment</text> | |
| </g> | |
| <!-- arrows from event to each --> | |
| <line x1="180" y1="40" x2="56" y2="76" stroke="#94a3b8"/> | |
| <line x1="180" y1="40" x2="180" y2="76" stroke="#94a3b8"/> | |
| <line x1="180" y1="40" x2="304" y2="76" stroke="#94a3b8"/> | |
| <!-- late discovery --> | |
| <rect x="60" y="200" width="240" height="32" fill="#fff" stroke="#dc2626" stroke-dasharray="4 4"/> | |
| <text x="180" y="220" font-size="10" text-anchor="middle" fill="#dc2626" class="label">kind 1063 discovery — fires after first failure</text> | |
| <!-- player --> | |
| <rect x="120" y="262" width="120" height="34" fill="#ffffff" stroke="#94a3b8"/> | |
| <text x="180" y="284" font-size="10" text-anchor="middle" class="label">VideoPlayer</text> | |
| <line x1="56" y1="156" x2="170" y2="262" stroke="#94a3b8"/> | |
| <line x1="180" y1="156" x2="180" y2="262" stroke="#94a3b8"/> | |
| <line x1="304" y1="156" x2="190" y2="262" stroke="#94a3b8"/> | |
| </svg> | |
| <p class="text-xs text-slate-500 pt-1"> | |
| generateMediaUrls called 3× per video · codec filter, validation, HLS retry are three different policies on the same list. | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">after — one ladder, built once</p> | |
| <div class="border border-slate-200 rounded p-4 bg-slate-50 relative" style="min-height:340px"> | |
| <svg viewBox="0 0 360 320" class="w-full"> | |
| <rect x="130" y="6" width="100" height="34" fill="#ffffff" stroke="#94a3b8"/> | |
| <text x="180" y="28" font-size="11" text-anchor="middle" class="label">Video Event</text> | |
| <!-- One deep ladder --> | |
| <rect x="50" y="68" width="260" height="170" fill="#0f172a" stroke="#0f172a" rx="4"/> | |
| <text x="180" y="92" font-size="13" text-anchor="middle" fill="#f1f5f9" class="label">PlaybackUrlLadder</text> | |
| <text x="180" y="108" font-size="9" text-anchor="middle" fill="#94a3b8">build · discover · validate · score</text> | |
| <!-- internal grid --> | |
| <g fill="#1e293b" stroke="#334155"> | |
| <rect x="64" y="120" width="110" height="22"/> | |
| <rect x="186" y="120" width="110" height="22"/> | |
| <rect x="64" y="148" width="110" height="22"/> | |
| <rect x="186" y="148" width="110" height="22"/> | |
| <rect x="64" y="176" width="232" height="22"/> | |
| </g> | |
| <g fill="#cbd5e1" font-size="9" class="label"> | |
| <text x="119" y="135" text-anchor="middle">mirrors</text> | |
| <text x="241" y="135" text-anchor="middle">proxies</text> | |
| <text x="119" y="163" text-anchor="middle">codec filter</text> | |
| <text x="241" y="163" text-anchor="middle">kind 1063</text> | |
| <text x="180" y="191" text-anchor="middle">tried / failed memory</text> | |
| </g> | |
| <!-- single interface --> | |
| <rect x="50" y="220" width="260" height="18" fill="#1e293b" stroke="#0f172a"/> | |
| <text x="180" y="233" font-size="9" text-anchor="middle" fill="#a7f3d0" class="label">currentUrl · tryNext() · onError()</text> | |
| <line x1="180" y1="40" x2="180" y2="68" stroke="#94a3b8"/> | |
| <rect x="120" y="262" width="120" height="34" fill="#ffffff" stroke="#94a3b8"/> | |
| <text x="180" y="284" font-size="10" text-anchor="middle" class="label">VideoPlayer + HLS loader</text> | |
| <line x1="180" y1="238" x2="180" y2="262" stroke="#94a3b8"/> | |
| </svg> | |
| <p class="text-xs text-slate-500 pt-1"> | |
| One module owns the ladder. Player and HLS loader share its memory of what's been tried. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Problem</h3> | |
| <p class="text-sm text-slate-700"> | |
| Three call sites independently build the failover list. Codec filtering happens in | |
| <span class="label">processEvent</span> but is invisible to HLS retry. Discovery is async | |
| and the result arrives after the first attempt already chose a URL. A failed URL is forgotten | |
| on next render. | |
| </p> | |
| </div> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Solution</h3> | |
| <p class="text-sm text-slate-700"> | |
| A <span class="label">PlaybackUrlLadder</span> per Video Event. Built once, owns the | |
| ordered list, the codec filter, the kind 1063 discovery, the tried / failed set. The | |
| player and the HLS loader consume the same instance. | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500 mb-2">Wins</h3> | |
| <ul class="grid sm:grid-cols-2 gap-x-6 gap-y-1 text-sm text-slate-700 list-disc list-inside"> | |
| <li>locality: URL policy in one module</li> | |
| <li>leverage: HLS loader shares the ladder, not a copy</li> | |
| <li>useMediaUrls interface shrinks from 15 fields to 3</li> | |
| <li>codec filter and discovery merge into ranking</li> | |
| <li>failed URLs survive across renders</li> | |
| <li>testable: ladder is a plain module</li> | |
| </ul> | |
| </div> | |
| <div class="border-l-4 border-amber-400 bg-amber-50 px-4 py-2 text-sm text-amber-900"> | |
| Touches ADR-0003 ground: the ladder MAY expose an Observable for HLS consumption but | |
| should default to a plain interface — keep RxJS at the Applesauce edge. | |
| </div> | |
| </article> | |
| <!-- ---------------------------------------------------------------- --> | |
| <!-- CARD 3: Relay configuration --> | |
| <!-- ---------------------------------------------------------------- --> | |
| <article id="c3" class="bg-white border border-slate-200 rounded-lg p-8 space-y-6 shadow-sm"> | |
| <header class="space-y-2"> | |
| <div class="flex items-center gap-3 flex-wrap"> | |
| <span class="badge text-xs px-2 py-1 rounded bg-emerald-100 text-emerald-900 uppercase tracking-wider">Strong</span> | |
| <span class="badge text-xs px-2 py-1 rounded bg-slate-100 text-slate-700 uppercase tracking-wider">in-process</span> | |
| </div> | |
| <h2 class="font-serif text-3xl text-slate-900">3. Unify the relay configuration module</h2> | |
| <p class="text-slate-600 text-sm"> | |
| Five hooks answer one question — which relays do we talk to right now? — across 37 callers, | |
| each with a slightly different priority order. | |
| </p> | |
| </header> | |
| <div class="label text-xs text-slate-600 space-y-0.5"> | |
| <div>src/hooks/useReadRelays.ts · useWriteRelays.ts · useStableRelays.ts · useContextRelays.ts · useUserRelays.ts</div> | |
| <div>src/contexts/UserRelaysContext.tsx · src/contexts/AppContext.ts</div> | |
| <div class="text-slate-400">37 files import one of the five hooks.</div> | |
| </div> | |
| <div class="grid md:grid-cols-2 gap-6 grid-divider"> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">before — five names for one concern</p> | |
| <div class="rounded border border-slate-200 bg-slate-50 p-3"> | |
| <pre class="mermaid"> | |
| flowchart LR | |
| classDef thin fill:#fff,stroke:#94a3b8,stroke-dasharray:3 3 | |
| classDef store fill:#f1f5f9,stroke:#cbd5e1 | |
| Cfg[(AppContext.config)]:::store | |
| Nip65[(UserRelaysContext<br/>NIP-65)]:::store | |
| Seen[(seenRelays<br/>nip19 / preset)]:::store | |
| R[useReadRelays]:::thin | |
| W[useWriteRelays]:::thin | |
| S[useStableRelays]:::thin | |
| C[useContextRelays]:::thin | |
| U[useUserRelays]:::thin | |
| R --> Cfg | |
| S --> R | |
| W --> Cfg | |
| W --> Nip65 | |
| U --> Nip65 | |
| C --> Nip65 | |
| C --> Seen | |
| C --> Cfg | |
| Callers["37 callers<br/>(pages, hooks, lib)"] | |
| Callers --> R | |
| Callers --> W | |
| Callers --> S | |
| Callers --> C | |
| Callers --> U | |
| </pre> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">after — one module, named modes</p> | |
| <div class="rounded border border-slate-200 bg-slate-50 p-3"> | |
| <pre class="mermaid"> | |
| flowchart LR | |
| classDef deep fill:#0f172a,color:#f1f5f9,stroke:#0f172a | |
| classDef store fill:#f1f5f9,stroke:#cbd5e1 | |
| Cfg[(AppContext.config)]:::store | |
| Nip65[(UserRelaysContext)]:::store | |
| Seen[(seenRelays)]:::store | |
| RC["relay-configuration<br/>read · write · context · discovery"]:::deep | |
| RC --> Cfg | |
| RC --> Nip65 | |
| RC --> Seen | |
| Callers["37 callers"] | |
| Callers --> RC | |
| </pre> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Problem</h3> | |
| <p class="text-sm text-slate-700"> | |
| Priority order across config / NIP-65 / seenRelays is implicit and inconsistent. | |
| <span class="label">useStableRelays</span> exists only to re-memoize | |
| <span class="label">useReadRelays</span>. <span class="label">useSelectedPreset</span> | |
| is a literal re-export. | |
| </p> | |
| </div> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Solution</h3> | |
| <p class="text-sm text-slate-700"> | |
| One <span class="label">relay-configuration</span> module exposing four named | |
| selection modes (<span class="label">read</span>, <span class="label">write</span>, | |
| <span class="label">context(opts)</span>, <span class="label">discovery(opts)</span>). | |
| Memoization moves inside; the three sources are merged in one place with a stated | |
| priority order. | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500 mb-2">Wins</h3> | |
| <ul class="grid sm:grid-cols-2 gap-x-6 gap-y-1 text-sm text-slate-700 list-disc list-inside"> | |
| <li>locality: priority order in one place</li> | |
| <li>leverage: one interface, 37 call sites</li> | |
| <li>useStableRelays / useFollowedAuthors / useSelectedPreset retire</li> | |
| <li>testable without React</li> | |
| <li>renames stop rippling across hook layers</li> | |
| </ul> | |
| </div> | |
| </article> | |
| <!-- ---------------------------------------------------------------- --> | |
| <!-- CARD 4: Transcoding Tier policy --> | |
| <!-- ---------------------------------------------------------------- --> | |
| <article id="c4" class="bg-white border border-slate-200 rounded-lg p-8 space-y-6 shadow-sm"> | |
| <header class="space-y-2"> | |
| <div class="flex items-center gap-3 flex-wrap"> | |
| <span class="badge text-xs px-2 py-1 rounded bg-emerald-100 text-emerald-900 uppercase tracking-wider">Strong</span> | |
| <span class="badge text-xs px-2 py-1 rounded bg-slate-100 text-slate-700 uppercase tracking-wider">in-process</span> | |
| </div> | |
| <h2 class="font-serif text-3xl text-slate-900">4. Centralise the Transcoding Tier policy</h2> | |
| <p class="text-slate-600 text-sm"> | |
| <span class="label">Transcoding Tier</span> is a named domain concept (browser → | |
| DVM → fallback). The decision is nowhere — it's split across an assess function, a UI | |
| click, and a provider entry point. There is no fallback. | |
| </p> | |
| </header> | |
| <div class="label text-xs text-slate-600 space-y-0.5"> | |
| <div>src/lib/video-transcode.ts <span class="text-slate-400">— assessTranscodeNeed (browser tier only)</span></div> | |
| <div>src/components/VideoUpload.tsx <span class="text-slate-400">— UI decides when to offer DVM</span></div> | |
| <div>src/providers/upload/UploadManagerProvider.tsx <span class="text-slate-400">— startTranscode entry point</span></div> | |
| <div>src/hooks/useDvmTracker.ts <span class="text-slate-400">— DVM availability signal, consumed nowhere central</span></div> | |
| </div> | |
| <div class="grid md:grid-cols-2 gap-6 grid-divider"> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">before — implicit policy</p> | |
| <div class="rounded border border-slate-200 bg-slate-50 p-3"> | |
| <pre class="mermaid"> | |
| flowchart TB | |
| classDef leak stroke:#dc2626,stroke-width:2px | |
| meta[(source meta)] | |
| assess["assessTranscodeNeed<br/>(none/bitrate/full)"] | |
| UI[VideoUpload UI] | |
| click([user clicks DVM]) | |
| prov[UploadManagerProvider.startTranscode] | |
| fallback{{no automatic fallback}}:::leak | |
| meta --> assess | |
| assess --> UI | |
| UI -.tier 1.-> browser["browser job"] | |
| UI -.tier 2.-> click | |
| click --> prov | |
| prov --> dvm["DVM session"] | |
| dvm -.error.-> fallback | |
| </pre> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">after — explicit policy</p> | |
| <div class="rounded border border-slate-200 bg-slate-50 p-3"> | |
| <pre class="mermaid"> | |
| flowchart TB | |
| classDef deep fill:#0f172a,color:#f1f5f9,stroke:#0f172a | |
| meta[(source meta)] | |
| capability[(browser capability)] | |
| dvmAvail[(DVM availability)] | |
| policy["transcoding-tier-policy<br/>chooseTier()"]:::deep | |
| meta --> policy | |
| capability --> policy | |
| dvmAvail --> policy | |
| policy --> B[browser] | |
| policy --> D[DVM] | |
| policy --> F[fallback] | |
| prov[UploadManagerProvider] --> policy | |
| </pre> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Problem</h3> | |
| <p class="text-sm text-slate-700"> | |
| CONTEXT.md describes a three-tier pipeline; the code implements two of the tiers and | |
| lets the user click between them. A failed DVM doesn't try anything else. The provider | |
| hardcodes one entry point per tier instead of consulting a policy. | |
| </p> | |
| </div> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Solution</h3> | |
| <p class="text-sm text-slate-700"> | |
| A <span class="label">transcoding-tier-policy</span> module that takes | |
| source meta, browser capability, and DVM availability and returns the chosen tier | |
| (with reasons). The provider asks the policy, then delegates. Fallback becomes a real | |
| returnable tier. | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500 mb-2">Wins</h3> | |
| <ul class="grid sm:grid-cols-2 gap-x-6 gap-y-1 text-sm text-slate-700 list-disc list-inside"> | |
| <li>locality: domain term ↔ module name</li> | |
| <li>leverage: one decision feeds three executors</li> | |
| <li>fallback tier becomes addressable</li> | |
| <li>policy is pure — testable as a table</li> | |
| <li>UI consults policy, doesn't decide</li> | |
| </ul> | |
| </div> | |
| </article> | |
| <!-- ---------------------------------------------------------------- --> | |
| <!-- CARD 5: UploadManagerProvider split --> | |
| <!-- ---------------------------------------------------------------- --> | |
| <article id="c5" class="bg-white border border-slate-200 rounded-lg p-8 space-y-6 shadow-sm"> | |
| <header class="space-y-2"> | |
| <div class="flex items-center gap-3 flex-wrap"> | |
| <span class="badge text-xs px-2 py-1 rounded bg-amber-100 text-amber-900 uppercase tracking-wider">Worth exploring</span> | |
| <span class="badge text-xs px-2 py-1 rounded bg-slate-100 text-slate-700 uppercase tracking-wider">in-process</span> | |
| </div> | |
| <h2 class="font-serif text-3xl text-slate-900">5. Split the upload manager into focused modules</h2> | |
| <p class="text-slate-600 text-sm"> | |
| 1695 lines doing task lifecycle <em>and</em> Draft CRUD <em>and</em> DVM bidding | |
| <em>and</em> browser-transcode job storage. Provider reaches into the browser-transcode storage | |
| module's internal shape. | |
| </p> | |
| </header> | |
| <div class="label text-xs text-slate-600 space-y-0.5"> | |
| <div>src/providers/upload/UploadManagerProvider.tsx <span class="text-slate-400">— 1695 LOC</span></div> | |
| <div>src/lib/browser-transcode-upload-manager.ts <span class="text-slate-400">— 961 LOC, coupled via getBrowserTranscodeJob / updateBrowserTranscodeJob</span></div> | |
| <div>src/lib/browser-transcode-job-storage.ts <span class="text-slate-400">— private shape leaked into provider</span></div> | |
| <div>src/hooks/useDraftPersistence.ts <span class="text-slate-400">— wrapped here, not delegated</span></div> | |
| </div> | |
| <!-- hand-drawn boxes --> | |
| <div class="grid md:grid-cols-2 gap-6 grid-divider"> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">before — one fat box, four slots</p> | |
| <div class="border border-slate-200 rounded p-4 bg-slate-50"> | |
| <svg viewBox="0 0 360 280" class="w-full"> | |
| <rect x="20" y="20" width="320" height="220" fill="#fff" stroke="#0f172a" stroke-width="1.5"/> | |
| <text x="180" y="40" font-size="11" text-anchor="middle" class="label">UploadManagerProvider — 1695 LOC</text> | |
| <g fill="#f8fafc" stroke="#cbd5e1"> | |
| <rect x="36" y="60" width="142" height="58"/> | |
| <rect x="186" y="60" width="142" height="58"/> | |
| <rect x="36" y="126" width="142" height="58"/> | |
| <rect x="186" y="126" width="142" height="58"/> | |
| </g> | |
| <g font-size="10" class="label" fill="#0f172a"> | |
| <text x="107" y="84" text-anchor="middle">task lifecycle</text> | |
| <text x="257" y="84" text-anchor="middle">draft CRUD</text> | |
| <text x="107" y="150" text-anchor="middle">DVM bidding</text> | |
| <text x="257" y="150" text-anchor="middle">job subscriptions</text> | |
| </g> | |
| <g font-size="8" fill="#64748b"> | |
| <text x="107" y="100" text-anchor="middle">tasks: Map<id,Task></text> | |
| <text x="257" y="100" text-anchor="middle">via useDraftPersistence</text> | |
| <text x="107" y="166" text-anchor="middle">collectBids, processResolution</text> | |
| <text x="257" y="166" text-anchor="middle">jobsRef, storage updates</text> | |
| </g> | |
| <!-- leak into browser-transcode storage --> | |
| <rect x="80" y="200" width="200" height="28" fill="#fff" stroke="#dc2626" stroke-dasharray="4 4"/> | |
| <text x="180" y="218" font-size="9" text-anchor="middle" fill="#dc2626" class="label">reaches into browser-transcode-job-storage internals</text> | |
| </svg> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">after — one orchestrator, four modules</p> | |
| <div class="border border-slate-200 rounded p-4 bg-slate-50"> | |
| <svg viewBox="0 0 360 280" class="w-full"> | |
| <rect x="120" y="14" width="120" height="32" fill="#0f172a" stroke="#0f172a" rx="3"/> | |
| <text x="180" y="34" font-size="10" text-anchor="middle" fill="#f1f5f9" class="label">UploadOrchestrator</text> | |
| <!-- four child modules --> | |
| <g> | |
| <rect x="20" y="80" width="150" height="62" fill="#0f172a" stroke="#0f172a"/> | |
| <text x="95" y="100" font-size="10" text-anchor="middle" fill="#f1f5f9" class="label">TaskRegistry</text> | |
| <text x="95" y="118" font-size="8" text-anchor="middle" fill="#94a3b8">id · status · phase</text> | |
| <text x="95" y="132" font-size="8" text-anchor="middle" fill="#94a3b8">add · update · drop</text> | |
| </g> | |
| <g> | |
| <rect x="190" y="80" width="150" height="62" fill="#0f172a" stroke="#0f172a"/> | |
| <text x="265" y="100" font-size="10" text-anchor="middle" fill="#f1f5f9" class="label">DvmTranscodeSession</text> | |
| <text x="265" y="118" font-size="8" text-anchor="middle" fill="#94a3b8">start · cancel · observe</text> | |
| <text x="265" y="132" font-size="8" text-anchor="middle" fill="#94a3b8">(see card 1)</text> | |
| </g> | |
| <g> | |
| <rect x="20" y="160" width="150" height="62" fill="#0f172a" stroke="#0f172a"/> | |
| <text x="95" y="180" font-size="10" text-anchor="middle" fill="#f1f5f9" class="label">BrowserTranscodeJob</text> | |
| <text x="95" y="198" font-size="8" text-anchor="middle" fill="#94a3b8">queue · transcode · upload</text> | |
| <text x="95" y="212" font-size="8" text-anchor="middle" fill="#94a3b8">owns its storage</text> | |
| </g> | |
| <g> | |
| <rect x="190" y="160" width="150" height="62" fill="#0f172a" stroke="#0f172a"/> | |
| <text x="265" y="180" font-size="10" text-anchor="middle" fill="#f1f5f9" class="label">DraftStore</text> | |
| <text x="265" y="198" font-size="8" text-anchor="middle" fill="#94a3b8">UploadDraft adapter</text> | |
| <text x="265" y="212" font-size="8" text-anchor="middle" fill="#94a3b8">over useDraftPersistence</text> | |
| </g> | |
| <line x1="180" y1="46" x2="95" y2="80" stroke="#475569"/> | |
| <line x1="180" y1="46" x2="265" y2="80" stroke="#475569"/> | |
| <line x1="180" y1="46" x2="95" y2="160" stroke="#475569"/> | |
| <line x1="180" y1="46" x2="265" y2="160" stroke="#475569"/> | |
| <rect x="80" y="244" width="200" height="22" fill="#fff" stroke="#94a3b8"/> | |
| <text x="180" y="259" font-size="9" text-anchor="middle" class="label">UploadManagerContext (small)</text> | |
| </svg> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Problem</h3> | |
| <p class="text-sm text-slate-700"> | |
| Every change risks the other three concerns. Task state and browser-transcode storage hold | |
| overlapping facts about the same job (race conditions on reload). Adding a context field | |
| breaks every consumer. | |
| </p> | |
| </div> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Solution</h3> | |
| <p class="text-sm text-slate-700"> | |
| Provider becomes a small orchestrator over four deep modules: TaskRegistry, | |
| DvmTranscodeSession (from card 1), BrowserTranscodeJob (owns its storage), | |
| DraftStore. Context surface narrows to the orchestrator's verbs. | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500 mb-2">Wins</h3> | |
| <ul class="grid sm:grid-cols-2 gap-x-6 gap-y-1 text-sm text-slate-700 list-disc list-inside"> | |
| <li>locality: each concern has one home</li> | |
| <li>leverage: each module testable alone</li> | |
| <li>storage leak closes inside its module</li> | |
| <li>one source of truth for job state</li> | |
| <li>context surface shrinks</li> | |
| </ul> | |
| </div> | |
| </article> | |
| <!-- ---------------------------------------------------------------- --> | |
| <!-- CARD 6: Persistent cache --> | |
| <!-- ---------------------------------------------------------------- --> | |
| <article id="c6" class="bg-white border border-slate-200 rounded-lg p-8 space-y-6 shadow-sm"> | |
| <header class="space-y-2"> | |
| <div class="flex items-center gap-3 flex-wrap"> | |
| <span class="badge text-xs px-2 py-1 rounded bg-amber-100 text-amber-900 uppercase tracking-wider">Worth exploring</span> | |
| <span class="badge text-xs px-2 py-1 rounded bg-slate-100 text-slate-700 uppercase tracking-wider">local-substitutable</span> | |
| </div> | |
| <h2 class="font-serif text-3xl text-slate-900">6. One persistent-cache module behind the storage seam</h2> | |
| <p class="text-slate-600 text-sm"> | |
| Thirteen modules each open <span class="label">localStorage</span> or | |
| <span class="label">IndexedDB</span> directly with their own TTL, key naming, and cleanup. | |
| Quota and observability are uniformly absent. | |
| </p> | |
| </header> | |
| <div class="label text-xs text-slate-600 space-y-0.5"> | |
| <div>src/lib/preset-storage.ts · notification-storage.ts · trust-score-db.ts · play-position-storage.ts</div> | |
| <div>src/lib/draft-persistence-storage.ts · browser-transcode-job-storage.ts · upload-manager-storage.ts</div> | |
| <div>src/lib/dismissed-alerts.ts · replaced-events.ts · hls-failover-debug.ts</div> | |
| <div>src/hooks/useEventStats.ts · useEventZaps.ts · useUploadNotifications.ts · useVideoPlayPosition.ts</div> | |
| <div>src/contexts/WalletContext.tsx · useAccountPersistence.ts</div> | |
| </div> | |
| <!-- bar diagram before vs after --> | |
| <div class="grid md:grid-cols-2 gap-6 grid-divider"> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">before — thirteen tiny caches</p> | |
| <div class="border border-slate-200 rounded p-4 bg-slate-50"> | |
| <svg viewBox="0 0 380 280" class="w-full"> | |
| <!-- 13 little bars labelled with their TTL --> | |
| <g font-size="8" class="label" fill="#0f172a"> | |
| <!-- row 1 --> | |
| <rect x="10" y="20" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="35" y="46" text-anchor="middle">preset</text><text x="35" y="76" text-anchor="middle" fill="#64748b">1h</text> | |
| <rect x="70" y="20" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="95" y="46" text-anchor="middle">notifs</text><text x="95" y="76" text-anchor="middle" fill="#64748b">7d</text> | |
| <rect x="130" y="20" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="155" y="46" text-anchor="middle">trust</text><text x="155" y="76" text-anchor="middle" fill="#64748b">24h</text> | |
| <rect x="190" y="20" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="215" y="46" text-anchor="middle">stats</text><text x="215" y="76" text-anchor="middle" fill="#64748b">1h</text> | |
| <rect x="250" y="20" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="275" y="46" text-anchor="middle">zaps</text><text x="275" y="76" text-anchor="middle" fill="#64748b">1h</text> | |
| <rect x="310" y="20" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="335" y="46" text-anchor="middle">playpos</text><text x="335" y="76" text-anchor="middle" fill="#64748b">∞</text> | |
| <!-- row 2 --> | |
| <rect x="10" y="100" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="35" y="126" text-anchor="middle">drafts</text><text x="35" y="156" text-anchor="middle" fill="#64748b">∞</text> | |
| <rect x="70" y="100" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="95" y="126" text-anchor="middle">bxJob</text><text x="95" y="156" text-anchor="middle" fill="#64748b">∞</text> | |
| <rect x="130" y="100" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="155" y="126" text-anchor="middle">upMgr</text><text x="155" y="156" text-anchor="middle" fill="#64748b">∞</text> | |
| <rect x="190" y="100" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="215" y="126" text-anchor="middle">alerts</text><text x="215" y="156" text-anchor="middle" fill="#64748b">∞</text> | |
| <rect x="250" y="100" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="275" y="126" text-anchor="middle">replaced</text><text x="275" y="156" text-anchor="middle" fill="#64748b">∞</text> | |
| <rect x="310" y="100" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="335" y="126" text-anchor="middle">debug</text><text x="335" y="156" text-anchor="middle" fill="#64748b">∞</text> | |
| <!-- row 3 --> | |
| <rect x="10" y="180" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="35" y="206" text-anchor="middle">wallet</text><text x="35" y="236" text-anchor="middle" fill="#64748b">∞</text> | |
| <rect x="70" y="180" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="95" y="206" text-anchor="middle">accts</text><text x="95" y="236" text-anchor="middle" fill="#64748b">∞</text> | |
| <rect x="130" y="180" width="50" height="40" fill="#fff" stroke="#94a3b8"/><text x="155" y="206" text-anchor="middle">upNotif</text><text x="155" y="236" text-anchor="middle" fill="#64748b">∞</text> | |
| </g> | |
| <text x="190" y="262" text-anchor="middle" font-size="10" class="label" fill="#dc2626"> | |
| six different TTLs · no shared cleanup · no quota signal | |
| </text> | |
| </svg> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">after — one cache, many adapters</p> | |
| <div class="border border-slate-200 rounded p-4 bg-slate-50"> | |
| <svg viewBox="0 0 380 280" class="w-full"> | |
| <rect x="30" y="60" width="320" height="120" fill="#0f172a" rx="4"/> | |
| <text x="190" y="86" font-size="13" text-anchor="middle" fill="#f1f5f9" class="label">PersistentCache<T></text> | |
| <text x="190" y="104" font-size="9" text-anchor="middle" fill="#94a3b8">localStorage / IndexedDB backends</text> | |
| <g fill="#1e293b" stroke="#334155"> | |
| <rect x="50" y="120" width="80" height="22"/> | |
| <rect x="146" y="120" width="80" height="22"/> | |
| <rect x="242" y="120" width="80" height="22"/> | |
| <rect x="50" y="148" width="80" height="22"/> | |
| <rect x="146" y="148" width="80" height="22"/> | |
| <rect x="242" y="148" width="80" height="22"/> | |
| </g> | |
| <g fill="#cbd5e1" font-size="9" class="label"> | |
| <text x="90" y="135" text-anchor="middle">TTL policy</text> | |
| <text x="186" y="135" text-anchor="middle">prune sweep</text> | |
| <text x="282" y="135" text-anchor="middle">quota signal</text> | |
| <text x="90" y="163" text-anchor="middle">stale-while-revalidate</text> | |
| <text x="186" y="163" text-anchor="middle">key namespace</text> | |
| <text x="282" y="163" text-anchor="middle">size report</text> | |
| </g> | |
| <!-- adapters strip --> | |
| <g fill="#fff" stroke="#cbd5e1"> | |
| <rect x="10" y="208" width="60" height="22"/> | |
| <rect x="76" y="208" width="60" height="22"/> | |
| <rect x="142" y="208" width="60" height="22"/> | |
| <rect x="208" y="208" width="60" height="22"/> | |
| <rect x="274" y="208" width="60" height="22"/> | |
| </g> | |
| <g fill="#0f172a" font-size="9" class="label"> | |
| <text x="40" y="223" text-anchor="middle">preset</text> | |
| <text x="106" y="223" text-anchor="middle">trust</text> | |
| <text x="172" y="223" text-anchor="middle">notif</text> | |
| <text x="238" y="223" text-anchor="middle">drafts</text> | |
| <text x="304" y="223" text-anchor="middle">…</text> | |
| </g> | |
| <text x="190" y="252" font-size="9" text-anchor="middle" fill="#475569">thirteen thin adapters use one cache</text> | |
| </svg> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Problem</h3> | |
| <p class="text-sm text-slate-700"> | |
| Each cache reinvents TTL handling, quota recovery, and cleanup. There's no signal when | |
| storage fills. Two adapters apply two adapters — that's a real seam: the trust-score | |
| cache rides IndexedDB; preset cache rides localStorage. Today they share nothing. | |
| </p> | |
| </div> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Solution</h3> | |
| <p class="text-sm text-slate-700"> | |
| One <span class="label">PersistentCache<T></span> module with pluggable backends | |
| (localStorage, IndexedDB) and named TTL policies. Existing storage files retire into | |
| small adapter constants: | |
| <span class="label">presetCache = new PersistentCache({ ttl: '1h' })</span>. | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500 mb-2">Wins</h3> | |
| <ul class="grid sm:grid-cols-2 gap-x-6 gap-y-1 text-sm text-slate-700 list-disc list-inside"> | |
| <li>locality: TTL / quota / sweep in one module</li> | |
| <li>leverage: thirteen adapters, one cache</li> | |
| <li>two real adapters justify the seam (localStorage + IndexedDB)</li> | |
| <li>quota observability for free</li> | |
| <li>cache-clear command (today: 14 places) becomes one call</li> | |
| </ul> | |
| </div> | |
| </article> | |
| <!-- ---------------------------------------------------------------- --> | |
| <!-- CARD 7: Remove shallow wrappers --> | |
| <!-- ---------------------------------------------------------------- --> | |
| <article id="c7" class="bg-white border border-slate-200 rounded-lg p-8 space-y-6 shadow-sm"> | |
| <header class="space-y-2"> | |
| <div class="flex items-center gap-3 flex-wrap"> | |
| <span class="badge text-xs px-2 py-1 rounded bg-amber-100 text-amber-900 uppercase tracking-wider">Worth exploring</span> | |
| <span class="badge text-xs px-2 py-1 rounded bg-slate-100 text-slate-700 uppercase tracking-wider">in-process</span> | |
| </div> | |
| <h2 class="font-serif text-3xl text-slate-900">7. Delete the shallow wrapper hooks</h2> | |
| <p class="text-slate-600 text-sm"> | |
| Eight hooks pass the deletion test cleanly — deleting each one concentrates complexity | |
| at the call site rather than scattering it across other modules. | |
| </p> | |
| </header> | |
| <div class="label text-xs text-slate-600 space-y-0.5"> | |
| <div>useFileUpload.ts (60) · useVideoFileUpload.ts (140) · useDvmTranscodeManager.ts (250)</div> | |
| <div>useStableRelays.ts (15) · useFollowedAuthors.ts (17) · useSelectedPreset.ts (re-export)</div> | |
| <div>upload-media.ts (40) · video-publishing-workflow.ts (150)</div> | |
| </div> | |
| <!-- cross-section --> | |
| <div class="grid md:grid-cols-2 gap-6 grid-divider"> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">before — five thin layers</p> | |
| <div class="border border-slate-200 rounded p-4 bg-slate-50"> | |
| <div class="space-y-1.5"> | |
| <div class="shallow-band h-7 border border-slate-300 flex items-center px-3 text-xs label text-slate-600">component</div> | |
| <div class="shallow-band h-7 border border-slate-300 flex items-center px-3 text-xs label text-slate-600">useDvmTranscodeManager · adapter</div> | |
| <div class="shallow-band h-7 border border-slate-300 flex items-center px-3 text-xs label text-slate-600">useVideoFileUpload · orchestrator</div> | |
| <div class="shallow-band h-7 border border-slate-300 flex items-center px-3 text-xs label text-slate-600">useFileUpload · 1-line wrapper</div> | |
| <div class="shallow-band h-7 border border-slate-300 flex items-center px-3 text-xs label text-slate-600">upload-media · normaliser</div> | |
| <div class="h-14 bg-white border border-slate-300 flex items-center px-3 text-xs label text-slate-700">blossom-upload (deep)</div> | |
| </div> | |
| <p class="text-xs text-slate-500 pt-3"> | |
| Each thin band adds <5 lines of value. Bugs hide between layers. | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">after — one thick band</p> | |
| <div class="border border-slate-200 rounded p-4 bg-slate-50"> | |
| <div class="space-y-1.5"> | |
| <div class="h-9 bg-white border border-slate-300 flex items-center px-3 text-xs label text-slate-700">component (status enum stays here)</div> | |
| <div class="h-32 deep flex items-center justify-center px-3 text-sm label text-slate-100 rounded-sm">blossom-upload · the deep one</div> | |
| </div> | |
| <p class="text-xs text-slate-500 pt-3"> | |
| Deletion test passes: each retired hook collapses into the caller; no new copy of | |
| blossom-upload appears. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Problem</h3> | |
| <p class="text-sm text-slate-700"> | |
| These hooks reshape one return value or expose a status enum nobody else needs. The | |
| interface is nearly as complex as the implementation. Worse — each is an extra place to | |
| read when chasing a bug across the upload path. | |
| </p> | |
| </div> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Solution</h3> | |
| <p class="text-sm text-slate-700"> | |
| Inline them at the call sites. Retire the modules. Where status enums genuinely help, | |
| lift them into the deep module's return type so all callers benefit. | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500 mb-2">Wins</h3> | |
| <ul class="grid sm:grid-cols-2 gap-x-6 gap-y-1 text-sm text-slate-700 list-disc list-inside"> | |
| <li>locality: bugs concentrate in blossom-upload</li> | |
| <li>leverage: one interface, all upload callers</li> | |
| <li>delete ~700 LOC of indirection</li> | |
| <li>shorter stack traces during debugging</li> | |
| <li>unblocks renaming the deep module</li> | |
| </ul> | |
| </div> | |
| </article> | |
| <!-- ---------------------------------------------------------------- --> | |
| <!-- CARD 8: AuthorPage decomposition --> | |
| <!-- ---------------------------------------------------------------- --> | |
| <article id="c8" class="bg-white border border-slate-200 rounded-lg p-8 space-y-6 shadow-sm"> | |
| <header class="space-y-2"> | |
| <div class="flex items-center gap-3 flex-wrap"> | |
| <span class="badge text-xs px-2 py-1 rounded bg-slate-200 text-slate-700 uppercase tracking-wider">Speculative</span> | |
| <span class="badge text-xs px-2 py-1 rounded bg-slate-100 text-slate-700 uppercase tracking-wider">in-process</span> | |
| </div> | |
| <h2 class="font-serif text-3xl text-slate-900">8. Carve deep modules out of AuthorPage</h2> | |
| <p class="text-slate-600 text-sm"> | |
| 1378 lines, seven internal concerns. Not classic shallowness — it's the inverse: a wide | |
| implementation behind a thin route interface, with no locality for any one concern. | |
| </p> | |
| </header> | |
| <div class="label text-xs text-slate-600 space-y-0.5"> | |
| <div>src/pages/AuthorPage.tsx <span class="text-slate-400">— 1378 LOC, 7 sections</span></div> | |
| </div> | |
| <!-- call-graph collapse: before tree, after one box with faded internals --> | |
| <div class="grid md:grid-cols-2 gap-6 grid-divider"> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">before — seven concerns, one file</p> | |
| <div class="border border-slate-200 rounded p-4 bg-slate-50"> | |
| <svg viewBox="0 0 360 280" class="w-full"> | |
| <rect x="120" y="10" width="120" height="32" fill="#fff" stroke="#0f172a"/> | |
| <text x="180" y="30" font-size="11" text-anchor="middle" class="label">AuthorPage.tsx</text> | |
| <g fill="#fff" stroke="#94a3b8"> | |
| <rect x="10" y="80" width="90" height="40"/> | |
| <rect x="110" y="80" width="90" height="40"/> | |
| <rect x="210" y="80" width="90" height="40"/> | |
| <rect x="10" y="140" width="90" height="40"/> | |
| <rect x="110" y="140" width="90" height="40"/> | |
| <rect x="210" y="140" width="90" height="40"/> | |
| <rect x="110" y="200" width="90" height="40"/> | |
| </g> | |
| <g font-size="9" class="label" fill="#0f172a"> | |
| <text x="55" y="104" text-anchor="middle">AuthorBanner</text> | |
| <text x="155" y="104" text-anchor="middle">AuthorProfile</text> | |
| <text x="255" y="104" text-anchor="middle">EditProfileDialog</text> | |
| <text x="55" y="164" text-anchor="middle">PinnedVideos</text> | |
| <text x="155" y="164" text-anchor="middle">TabNavigation</text> | |
| <text x="255" y="164" text-anchor="middle">VideoListing</text> | |
| <text x="155" y="224" text-anchor="middle">FollowSet bridge</text> | |
| </g> | |
| <g stroke="#94a3b8"> | |
| <line x1="180" y1="42" x2="55" y2="80"/> | |
| <line x1="180" y1="42" x2="155" y2="80"/> | |
| <line x1="180" y1="42" x2="255" y2="80"/> | |
| <line x1="180" y1="42" x2="55" y2="140"/> | |
| <line x1="180" y1="42" x2="155" y2="140"/> | |
| <line x1="180" y1="42" x2="255" y2="140"/> | |
| <line x1="180" y1="42" x2="155" y2="200"/> | |
| </g> | |
| </svg> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-xs uppercase tracking-wider text-slate-500 mb-2">after — three deep modules + thin page</p> | |
| <div class="border border-slate-200 rounded p-4 bg-slate-50"> | |
| <svg viewBox="0 0 360 280" class="w-full"> | |
| <rect x="120" y="10" width="120" height="32" fill="#fff" stroke="#0f172a"/> | |
| <text x="180" y="30" font-size="11" text-anchor="middle" class="label">AuthorPage (thin)</text> | |
| <g> | |
| <rect x="14" y="78" width="100" height="80" fill="#0f172a"/> | |
| <text x="64" y="100" font-size="10" text-anchor="middle" fill="#f1f5f9" class="label">ProfileCard</text> | |
| <text x="64" y="118" font-size="8" text-anchor="middle" fill="#94a3b8">view · edit · upload</text> | |
| <text x="64" y="132" font-size="8" text-anchor="middle" fill="#94a3b8">banner + avatar</text> | |
| </g> | |
| <g> | |
| <rect x="130" y="78" width="100" height="80" fill="#0f172a"/> | |
| <text x="180" y="100" font-size="10" text-anchor="middle" fill="#f1f5f9" class="label">PinnedVideos</text> | |
| <text x="180" y="118" font-size="8" text-anchor="middle" fill="#94a3b8">pin · unpin · list</text> | |
| <text x="180" y="132" font-size="8" text-anchor="middle" fill="#94a3b8">NIP-51 list event</text> | |
| </g> | |
| <g> | |
| <rect x="246" y="78" width="100" height="80" fill="#0f172a"/> | |
| <text x="296" y="100" font-size="10" text-anchor="middle" fill="#f1f5f9" class="label">AuthorTabs</text> | |
| <text x="296" y="118" font-size="8" text-anchor="middle" fill="#94a3b8">videos · shorts</text> | |
| <text x="296" y="132" font-size="8" text-anchor="middle" fill="#94a3b8">playlists · liked</text> | |
| </g> | |
| <line x1="180" y1="42" x2="64" y2="78" stroke="#475569"/> | |
| <line x1="180" y1="42" x2="180" y2="78" stroke="#475569"/> | |
| <line x1="180" y1="42" x2="296" y2="78" stroke="#475569"/> | |
| <rect x="20" y="200" width="320" height="58" fill="#fff" stroke="#94a3b8" stroke-dasharray="3 3"/> | |
| <text x="180" y="222" font-size="10" text-anchor="middle" class="label" fill="#475569">EmbedAppProvider · settings flows</text> | |
| <text x="180" y="240" font-size="9" text-anchor="middle" fill="#64748b">reuse ProfileCard outside AuthorPage</text> | |
| </svg> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Problem</h3> | |
| <p class="text-sm text-slate-700"> | |
| A pin-management bug forces a 1378-line scroll. Nothing about ProfileCard is page-specific. | |
| The shape protects nothing — there is no locality per concern, no shared invariant being held. | |
| </p> | |
| </div> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500">Solution</h3> | |
| <p class="text-sm text-slate-700"> | |
| Carve three deep modules — <span class="label">ProfileCard</span>, | |
| <span class="label">PinnedVideos</span>, <span class="label">AuthorTabs</span> — each | |
| with a small interface. AuthorPage becomes a thin compositor. | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <h3 class="text-xs uppercase tracking-wider text-slate-500 mb-2">Wins</h3> | |
| <ul class="grid sm:grid-cols-2 gap-x-6 gap-y-1 text-sm text-slate-700 list-disc list-inside"> | |
| <li>locality: each concern testable alone</li> | |
| <li>leverage: ProfileCard reusable in embed + settings</li> | |
| <li>1378 LOC → ~4 files of ~10–15 KB</li> | |
| <li>tab routing becomes a config not a switch</li> | |
| <li>NIP-51 pin handling earns a real interface</li> | |
| </ul> | |
| </div> | |
| <p class="text-xs text-slate-500 italic"> | |
| Marked Speculative — the wins are real but smaller than 1–6, and the | |
| risk of cosmetic-only churn is higher. Worth a grilling pass before commitment. | |
| </p> | |
| </article> | |
| </section> | |
| <!-- ============================== TOP RECOMMENDATION ============================== --> | |
| <section id="top-recommendation" class="bg-slate-900 text-slate-100 rounded-xl p-10 space-y-5 shadow-lg"> | |
| <p class="text-xs uppercase tracking-[0.18em] text-emerald-300">Top recommendation</p> | |
| <h2 class="font-serif text-4xl tracking-tight"> | |
| Start with <a href="#c1" class="underline decoration-emerald-400 underline-offset-4">card 1 — collapse the DVM transcode session</a>. | |
| </h2> | |
| <div class="grid md:grid-cols-3 gap-6 text-sm text-slate-200"> | |
| <p> | |
| ~1100 LOC of literal duplication retired across two of the largest files in the repo. | |
| The same kind-7000 parser, the same multi-resolution queue, the same 12-hour resume window — | |
| written twice today, written once tomorrow. | |
| </p> | |
| <p> | |
| The DVM protocol path has <em>zero</em> tests. Once it lives as a plain module, it gets a | |
| test surface — the interface is the test surface — without dragging React into the harness. | |
| </p> | |
| <p> | |
| It clears the runway for card 5: once DVM logic leaves UploadManagerProvider, | |
| the provider's split becomes mechanical rather than a 1700-line untangle. | |
| Cards 2, 3, 4, 6 are independent and can land in any order after. | |
| </p> | |
| </div> | |
| <div class="pt-2 text-xs label text-slate-400"> | |
| Suggested order: 1 → 4 → 5 → 3 → 2 → 6 → 7 → 8. | |
| </div> | |
| </section> | |
| <footer class="pt-6 text-xs label text-slate-500 text-center"> | |
| Generated 2026-06-10 · vocabulary from <span class="text-slate-700">LANGUAGE.md</span> · | |
| domain nouns from <span class="text-slate-700">CONTEXT.md</span> | |
| </footer> | |
| </main> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment