Live product

Torch

torch.directory

Shipped ScrapingClient-side search
Torch screenshot

Torch is a search engine for flashlights: 15,186 catalog entries, 43 comparable columns, four set-logic modes per filter — and no server anywhere in the stack. The whole thing is one 8.9 MB JSON file (1.97 MB gzipped) and one 13,100 × 13,100 pixel WebP sprite sheet on GitHub Pages. Sorting is free because the build precomputes 20 full index permutations of all 15,186 rows, and the Web Worker uses the chosen permutation as its iteration order — so results emerge already sorted and interactive sort is a pointer swap instead of an O(n log n) pass. Search is a hand-rolled fuzzy matcher with a one-error-per-four-characters budget over a per-column index. The service worker precaches the entire ~27 MB payload, so after one visit the whole engine works offline.

Getting the data was the actual work. A from-scratch pipeline discovers and scrapes 51 storefronts — 31 Shopify, 4 WooCommerce, and 16 bespoke crawlers written to each site's platform: Magento <select> dropdown parsing, BigCommerce <dl> spec tables, numeric-ID URL walking, CS-Cart family trees, curl-with-real-UA for Cloudflare-walled vendors — then enriches gaps from five review sites, BudgetLightForum's Discourse API, and the Keepa Amazon API, whose per-call token budget dictated the architecture: discover ASINs free by scraping, spend tokens only on product enrichment, batch at exactly 5 so the cron never stalls waiting for refill. Five purpose-built normalizers collapse the long tail while refusing unsafe merges — 904 LED strings become 401 canonical emitters, and generations, die sizes and HD/HI variants are never merged. A ~1,300-character regex with lookbehinds separates flashlights from replacement o-rings, and keeps the accessories in the dataset as a filterable type rather than deleting them.

The governing rule is written into the top of the enrichment module as a boxed banner: never fabricate, infer, estimate or default a value. Empty is honest; a wrong value is worse than no value. Earlier revisions guessed weight from battery type and lumens from LED — that code was deleted and the banner now guards against reintroducing it. The only permitted derivation is the ANSI FL1 identity relating throw to candela. Every row ships a 0–16 completeness score as a sortable column, and coverage is reported after clearing parsing artifacts, never inflated. Keepa price history is bucketed, gap-filled and compiled into literal SVG sparkline paths at build time — flat lines omitted below 5% variation — surfacing all-time and 90-day lows and flagging the 1,275 entries currently sitting at their historical floor. The entire pipeline runs on an Android phone under Termux, which is why it uses bun:sqlite and a custom Vite shim.

  • 20 precomputed index permutations × 15,186 rows; the worker iterates in sort order so filtering and sorting are the same pass — interactive sort costs nothing (filter-worker.ts:254-279, build-torch-db.ts:446-495).
  • No backend. 15,186 × 43 table served as one static file: 8.9 MB JSON, 1.97 MB gzipped on the wire; GitHub Pages, zero hosting cost.
  • 51 storefront crawlers (31 Shopify, 4 WooCommerce, 16 bespoke: Magento, BigCommerce, CS-Cart, Shift4Shop, UeeShop, Shoplazza, custom PHP) + 5 review sites + BudgetLightForum + Keepa (19,593 ASINs discovered, 10,666 scraped).
  • 4 logic modes per filter — any / all / only / none as real set algebra, plus a showUnknown flag so sparse rows aren't silently dropped (filter-worker.ts:17-86).
  • 13,100 × 13,100 px WebP sprite carries 14,707 product thumbnails in a single request; no vendor CDN hotlinking.
  • Fully offline: the service worker precaches the entire ~27 MB dataset, not just the shell.
  • Normalization that refuses to over-merge: 904 LED strings → 401 canonical, 647 batteries → 94, 106 brand aliases + 37 typo fixes — while never merging LED generations, die sizes or HD/HI.
  • Price intelligence: Keepa history → 24-bucket, gap-filled, build-time SVG sparklines (skipped under 5% variation); 1,275 entries flagged at their all-time low.
  • Runs on a phone: whole pipeline under Termux/Android — bun:sqlite, custom Vite CLI shim, --smol OOM modes.