frogConvert

Architecture

How it works under the hood

frogConvert - How It Works


The Big Picture

flowchart LR
    U([πŸ‘€ User]) -->|uploads file| B[Browser UI]
    B -->|asks: how do I get from A to B?| G[Route Finder\nTraversionGraph]
    G -->|finds a path| C[Conversion Engine\nFormatHandlers]
    C -->|runs tools in sequence| O[Output File]
    O -->|auto-download| U

    B -->|just make it smaller| K[Compression Engine\ncore/compression]
    K -->|same format in, same format out| C

    style U fill:#6ee7b7,stroke:#059669,color:#000
    style O fill:#6ee7b7,stroke:#059669,color:#000
    style B fill:#93c5fd,stroke:#3b82f6,color:#000
    style G fill:#fcd34d,stroke:#d97706,color:#000
    style C fill:#f9a8d4,stroke:#db2777,color:#000
    style K fill:#c4b5fd,stroke:#7c3aed,color:#000

Three subsystems share this page: the conversion pipeline (route finder + handlers), the PDF Workspace (structural PDF editing, no handlers), and the compression engine, which borrows handlers as engines but skips the route finder entirely - there is no path to find when the input and output formats are the same.

Everything stays inside your browser tab. Nothing leaves your computer.


What Happens When You Convert a File

Step-by-step:

flowchart TD
    A[You drop a file onto the page] --> B[Browser detects file type\ne.g. image/jpeg]
    B --> C[You pick an output format\ne.g. PDF]
    C --> D{Is there a direct\nconverter for this?}
    D -- Yes --> E[Run that converter]
    D -- No --> F[Route Finder calculates\na multi-step path\ne.g. JPG β†’ PNG β†’ PDF]
    F --> G[Step 1: Run Converter A\nJPG β†’ PNG]
    G --> H[Step 2: Run Converter B\nPNG β†’ PDF]
    E --> I[Output file ready]
    H --> I
    I --> J[Browser downloads the file]

    subgraph " "
    K[Same Format Picked\ne.g. JPEG β†’ JPEG] --> N[Pass through original bytes\n'No conversion needed']
    N --> I
    end

The Route Finder (TraversionGraph)

Every file format is a node, every handler is a directed edge. The Route Finder runs Dijkstra's algorithm to find the lowest-cost path from the input format to the output format.

flowchart LR
    JPG((JPG)) -->|FFmpeg, cheap| PNG((PNG))
    JPG -->|FFmpeg, cheap| MP4((MP4))
    PNG -->|Pandoc, medium| PDF((PDF))
    MP4 -->|FFmpeg, cheap| MP3((MP3))
    WAV((WAV)) -->|FFmpeg, cheap| MP3

    style PDF fill:#fcd34d,stroke:#d97706,color:#000
    style JPG fill:#93c5fd,stroke:#3b82f6,color:#000

Costs go up when:


Conversion Tools (Handlers)

Each converter is called a handler. A handler knows:

classDiagram
    class FormatHandler {
        +name: string
        +supportedFormats?: FileFormat[]
        +ready: boolean
        +requiresMainThread?: boolean
        +init() Promise~void~
        +doConvert(files, from, to) Promise~FileData[]~
    }

    FormatHandler <|-- FFmpegHandler : audio/video
    FormatHandler <|-- ImageMagickHandler : images
    FormatHandler <|-- PandocHandler : documents
    FormatHandler <|-- CanvasToBlobHandler : browser-only
    FormatHandler <|-- JSONHandler : text formats

Some handlers are pure compute (run in a background thread). Others need browser features like <canvas> or AudioContext and must run on the main thread - that's what requiresMainThread controls.

Handler Examples

Handler What it does Runs where
FFmpeg.ts Audio/video conversion Background worker
ImageMagick.ts Image conversion Background worker
pandoc.ts Documents (PDF, DOCX, MD…) Background worker
canvasToBlob.ts Encodes images using the browser's canvas Main thread
json.ts JSON ↔ other data formats Background worker
font.ts Font file conversion Background worker
libreoffice.ts Office docs β†’ PDF (DOCX, PPTX, XLSX, ODT…) Main thread (native binary or remote API)

PDF Workspace (Editor Mode)

frogConvert ships a second workspace alongside the converter: an in-browser PDF editor. Unlike the conversion pipeline, which originates from the Convert to it! fork, the PDF Workspace is frogConvert-original; it is not present in the upstream project. It is a parallel subsystem and does not route through TraversionGraph or FormatHandlers. If you are extending the converter, ignore it. If you are extending the editor, ignore the handler authoring guide.

App-mode toggle. src/main.ts and src/router.ts maintain an "app mode" state - converter, pdf-editor or compress - that swaps which workspace section is visible in index.html. The converter workspace is #convert-card, the editor is #pdf-workspace, and Compress is #compress-card.

Four operations, each isolated in src/tools/, plus two shared helpers:

File Operation Library
src/tools/pdfMerge.ts Concatenate multiple PDFs into one pdf-lib
src/tools/pdfOrganize.ts Reorder, rotate (Β±90Β°), insert blank pages pdf-lib
src/tools/pdfExtract.ts Extract a page range as a new PDF pdf-lib
src/tools/pdfWatermark.ts Stamp text watermark across selected pages, single or tiled pdf-lib
src/tools/pdfSource.ts (helper) Load a source for editing, refusing an encrypted one pdf-lib
src/tools/pdfThumbnails.ts (helper) Render page previews (lazy, cached) pdfjs-dist

Every operation loads its source through loadEditablePdf, not PDFDocument.load. The flag the tools used to pass, ignoreEncryption: true, suppresses the error on an encrypted PDF without supplying a password: the document loads, reports a correct page count, and its content streams stay encrypted. Copy those pages into a new document and what lands is a page of the right size with nothing on it. Measured on a merge of a password-protected file with a 4-page document: all 5 pages present, pages 2-5 carrying 3,930 / 3,953 / 3,953 / 2,635 characters, page 1 carrying zero, with no error anywhere. Measuring a PDF is a different job and still uses the flag deliberately, since page count reads fine through encryption and core/compression/pdfIntegrity.ts needs exactly that to catch the compression side of the same defect.

Orchestrator. src/components/PdfWorkspace/PdfWorkspace.ts owns the editor UI: tab switching (Merge / Organize / Watermark), drag-and-drop reorder via sortablejs, rotation accumulation, watermark live-preview, and download wiring.

Dependency split. pdf-lib is the write path (creates new PDFs). pdfjs-dist is the render path (only used for thumbnails and the watermark preview). Keep them separate; do not import pdfjs-dist in tool files.

Safari note. pdfjs-dist thumbnail rendering hits Safari JS-engine limits with PDF input. src/tools/pdfThumbnails.ts carries a fallback path; preserve it when refactoring.

Where to put new code. A new conversion (e.g. PDF β†’ CSV) is a new handler under src/handlers/. A new PDF editing operation (e.g. sign) is a new tool under src/tools/ plus a new tab in PdfWorkspace.ts. They are not interchangeable.


Web Workers (Why the Page Doesn't Freeze)

Converting a video can take seconds. If that ran on the browser's main thread, the whole page would lock up.

frogConvert uses Web Workers - background threads that run heavy work without touching the UI:

flowchart TD
    UI[Main Thread\nUI / Page stays responsive] -->|sends file + job| W1[conversion.worker.ts\nRuns doConvert in background]
    UI -->|asks for route| W2[route-search.worker.ts\nRuns Dijkstra in background]
    W1 -->|returns converted file| UI
    W2 -->|returns path| UI

Handlers with requiresMainThread: true are the exception - they need browser APIs that only exist on the main thread, so they run there.


PWA, offline, and external entry points

This section covers how a session starts: install, service-worker cache strategy, and the OS-level entry points (share-target, "Open with…") that bring files into frogConvert. For how a session survives a reload mid-task, see Session persistence below.

frogConvert installs as a Progressive Web App. The service worker (src/pwa/sw.ts, built via vite-plugin-pwa with the injectManifest strategy) precaches the app shell: entry HTMLs, CSS, icons, the hashed Inter subsets, and the JavaScript those HTMLs name. The ~250 lazy handler chunks stay out of it and are runtime-cached as the user encounters them, so a cold install still does not pre-pull 17 MB.

Which JS counts as "the shell" is computed at build time by the collect-shell-chunks plugin in vite.config.js: every entry chunk plus the transitive closure of its static imports, and the workers those chunks name. Dynamic imports are deliberately not walked.

Two things join it that no import graph can reach. The service-worker registration module (virtual:pwa-register) arrives as a dynamic import - it must, because that module does not exist in a desktop build and Rollup could not resolve a static import of it - and it is the thing that shows the "New version available" prompt. Left out of the precache it 404s for exactly the user it exists to serve: after a deploy the shell loads from the precache, this module does not, and the prompt that would move that user onto the new build never appears. The fonts join it for the same reason one level down: the CSS that @font-faces them is precached, so without them a returning user - and any offline user on a cold paint - renders in a fallback face.

Workers count because they are structural, not lazy. new Worker(new URL('/assets/x.js', import.meta.url)) bakes a hashed URL into a chunk exactly the way a <script src> bakes one into HTML, so precaching the chunk without the worker rebuilds the original bug one level down. Two are reachable this way and neither is optional: route-search.worker is constructed during graph init on every page load, and conversion.worker for every conversion. A lazy import emits import("./x.js") and a __vite__mapDeps table instead, which is what keeps the ~250 handler chunks out.

flowchart LR
    Net((Network)) --> SW["Service Worker<br/>src/pwa/sw.ts"]
    SW -->|Precache, versioned + atomic| SH["App shell<br/>entry HTML + CSS + shell JS"]
    SW -->|CacheFirst, 30 entries, 7d| WC["/wasm/ cache<br/>FFmpeg, ImageMagick, etc."]
    SW -->|CacheFirst, 400 entries, no TTL| AC["assets-v2<br/>lazy /assets/ chunks"]
    SW -->|StaleWhileRevalidate| JC["/js/ cache - copied workers"]
    SW -->|StaleWhileRevalidate| DC["/docs/*.md cache"]
    SW -->|POST handler| ST["Share-target replay<br/>CacheStorage"]

    style SW fill:#fcd34d,stroke:#d97706,color:#000
    style SH fill:#86efac,stroke:#16a34a,color:#000

Cache strategy

Path Strategy Why
Entry HTML, CSS, shell JS, icons, fonts Precache - versioned and atomic The HTML and the JS it names must never disagree. See Stale shell recovery for what happened when they did.
/wasm/, *.sf2 CacheFirst, wasm-v1, 30 entries, 7-day TTL, status 200 only WASM blobs are huge and content-stable. Status 200 only because opaque cross-origin responses can't be introspected - caching them would let a transient CDN error look like success.
/assets/ (lazy chunks) CacheFirst, assets-v2, 400 entries, no TTL Content-hashed, so a URL's bytes can never change - revalidating spends a round trip to re-confirm what the hash already guarantees. A TTL here is a scheduled outage: entries expire out from under HTML that still names them, and the refetch 404s once the deploy has moved on. Eviction is LRU only.
/js/ StaleWhileRevalidate, js-runtime-v1 The espeakng worker files, copied unhashed by vite-plugin-static-copy.
/docs/*.md StaleWhileRevalidate, docs-md-v1 Docs serve hot from cache while revalidating.
/index.html (NavigationRoute) Precache Single SPA entry. Denylisted: /api, /.well-known, /docs, /headless.

Every runtime cache above and the precache itself carries rejectHtmlFallback (src/pwa/cachePolicy.ts): a text/html body arriving under a URL that does not name a document is refused rather than stored. Workbox's own cacheability check reads only the status code, so without this a host whose SPA fallback answers a deleted chunk with 200 text/html gets that HTML written under a .js URL - permanently, in the precache's case, since precache entries are never revalidated.

The URL half of that check is load-bearing. Because addPlugins applies the guard to the precache too, and ~125 of the ~165 precache entries are HTML documents, a version of this guard that refused HTML on content type alone made precacheAndRoute reject its install promise with bad-precaching-response. The worker then went from installing straight to redundant and the registration was discarded - so nothing was cached, no update ever rolled out, and every returning user silently kept the worker they already had. That shipped in 3.0.0. If you touch this predicate, the test that catches it is bun run test:shell, which drives a real worker; the unit tests cannot, and did not.

Stale shell recovery

Every asset URL is content-hashed, so a deploy replaces the whole set. A returning user holding cached HTML that names the previous build's chunks requests URLs the server no longer has. Because that failure happens during module evaluation, there is no running app left to report it - the page renders fully and is bound to nothing.

Precaching the shell atomically is the fix for the shell; a lazy chunk can still 404 against a newer deploy. Two handlers cover the remainder, and the split between them is deliberate:

Handler Covers Armed
src/pwa/bootRecovery.js - dependency-free IIFE, inlined into every entry <head> The bundle never loaded at all Boot only. Stands down as soon as the app sets window.__frogShellBooted.
src/pwa/staleShell.ts - listens for Vite's vite:preloadError A lazy chunk failed while the app is running From app start onwards.

The boot handler must not stay armed: it would also catch the <link rel="modulepreload"> tags Vite appends for lazy chunks, and a transient network blip mid-conversion would then purge caches and reload, destroying queued files.

Both purge only the shell-bearing caches and reload once. wasm-v1 (~17 MB of engines at content-stable URLs) and the share-target cache are spared - neither is implicated in a hash mismatch, and clearing the latter would drop files a share is mid-way through handing over. localStorage is untouched, so the format registry, theme and any saved session survive. A shared sessionStorage marker with a 5-minute cooldown stops a genuinely broken deploy becoming a reload loop; the two files keep that key, the cooldown and the cache list in sync, asserted by a test.

Serving is the other half. /assets/, /js/ and /wasm/ must 404 rather than fall through to the SPA rule - configured in both netlify.toml and docker/nginx/default.conf, and covered for other hosts by rejectHtmlFallback above. A build-time assertion in vite.config.js fails the build if any script named by a precached HTML file, any worker named by a precached chunk, or any font named by a precached stylesheet is not itself precached - the invariant the whole arrangement exists to hold. It is checked against the emitted bytes rather than against the closure that produced them, so a change in how Rollup renders these references fails the build instead of quietly precaching a shell with a hole in it.

External file entry points

The PWA registers two OS-level integrations:

Both paths funnel into a single EXTERNAL_FILES_EVENT CustomEvent that src/main.ts listens for. main.ts owns the routing decision (Converter for non-PDF, PDF Editor for .pdf) so the SW and launchQueue consumer stay agnostic to active app mode.

Share-target ordering caveat

The custom share-target fetch listener is installed before Workbox's registerRoute calls. Workbox installs its own fetch listener the first time registerRoute is called; raw addEventListener calls registered later run after it. A multipart POST to / has request.mode === "navigate" and would otherwise be eaten by the precached /index.html NavigationRoute. Ordering is load-bearing.

Update flow

registerType: 'prompt' - the SW never silently skipWaiting(). When a new SW is detected, src/pwa/registerSW.ts shows a dismissable "New version available - Reload now" banner. The user controls when reload happens.

Desktop carve-out

PWA registration is gated on !import.meta.env.VITE_IS_DESKTOP and on protocol !== 'app:'. Electron runs from app:// where a service worker is both useless and a registration footgun, so the manifest and SW are not built into desktop bundles.


Session persistence

This section covers mid-task survival: persisting in-flight work to IndexedDB so the user can close a tab and resume later. The first-launch / entry-point surface is in PWA, offline, and external entry points above.

Both surfaces (Converter, PDF Workspace) persist their state to IndexedDB so the user can close a tab mid-task and resume later.

IDB: frogconvert (v1)
β”œβ”€ sessions      keyPath: sessionId          indexed on: kind, savedAt
└─ fileBytes     keyPath: <sessionId>:<id>   indexed on: sessionId

The factory src/components/persistence/createPersistor.ts takes a per-surface spec (build payload, current file ids, byte resolver, applier) and returns a generic persistor with dirty tracking, debounced flush, and resume detection. The Converter wires through convertPersist.ts; PDF Workspace inlines the factory at PdfWorkspace.ts.

Manifest-last write invariant

A flush computes a byte-diff against lastWrittenIds, writes byte adds, deletes byte removes, and writes the manifest last. A tab kill between byte writes and manifest write leaves a stale manifest pointing only at fileIds whose bytes already landed. There is never a manifest that references unwritten bytes - that would be a broken-session class on next restore. Quota errors pause autosave with a single toast; non-quota errors (missing file, serialization) skip the id and continue.

Resume decision tree

flowchart TD
    A[Page load] --> B[BroadcastChannel handshake\n~150ms probe for live siblings]
    B --> C{sessionStorage has sessionId?}
    C -- no --> D{Most-recent orphan of same kind?}
    D -- yes --> E[Show Resume? popup]
    D -- no --> F[None - start fresh]
    C -- yes --> G{Sibling tab claims this id?}
    G -- yes --> H[Tab clone - mint new id, fall through to orphan]
    G -- no --> I{User already dropped a file?}
    I -- yes --> E
    I -- no --> J[Silent restore]

navigator.webdriver short-circuits all of this so Puppeteer e2e flows never see the Resume popup.


Code Structure at a Glance

frogConvert/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ handlers/           ← Conversion tools (FFmpeg, ImageMagick, etc.)
β”‚   β”œβ”€β”€ core/
β”‚   β”‚   β”œβ”€β”€ FormatHandler/  ← The FormatHandler interface + base classes
β”‚   β”‚   β”œβ”€β”€ TraversionGraph/← Route-finding algorithm (Dijkstra)
β”‚   β”‚   β”œβ”€β”€ CommonFormats/  ← Registry of all MIME types and extensions
β”‚   β”‚   β”œβ”€β”€ compression/    ← Compression engine: dispatch, batching, tiering
β”‚   β”‚   β”‚                     (UI-free - takes a `run` callback, never imports components)
β”‚   β”‚   β”œβ”€β”€ utils/          ← Shared core helpers
β”‚   β”‚   └── index.ts        ← Barrel re-export
β”‚   β”œβ”€β”€ tools/              ← PDF editor ops (merge, organize, extract, watermark, thumbnails)
β”‚   β”œβ”€β”€ pwa/                ← Service worker, registration, share-target, cache controls,
β”‚   β”‚                         stale-shell recovery (staleShell.ts + inlined bootRecovery.js)
β”‚   β”œβ”€β”€ components/persistence/ ← IDB session store + createPersistor factory
β”‚   β”œβ”€β”€ workers/
β”‚   β”‚   β”œβ”€β”€ conversion.worker.ts   ← Runs handlers off the main thread
β”‚   β”‚   └── route-search.worker.ts ← Runs pathfinding off the main thread
β”‚   β”œβ”€β”€ components/         ← UI only: FormatModal, FilesModal, PdfWorkspace,
β”‚   β”‚                         CompressWorkspace,
β”‚   β”‚                         Toast, TopBar, UploadZone, Frogsworth, store, utils, …
β”‚   β”œβ”€β”€ conversion/         ← Conversion-flow orchestration (actions, cancellation, downloads)
β”‚   β”œβ”€β”€ constants/          ← UI constants (breakpoints, limits, defaults)
β”‚   β”œβ”€β”€ mcp/                ← MCP server for AI agents (Node.js, stdio)
β”‚   └── api/                ← REST API server (HTTP on localhost:3000)
β”œβ”€β”€ docs/
β”‚   β”œβ”€β”€ ARCHITECTURE.md     ← This file
β”‚   β”œβ”€β”€ CONVERTER.md        ← End-user converter guide
β”‚   β”œβ”€β”€ PDF_EDITOR.md       ← End-user PDF editor guide
β”‚   β”œβ”€β”€ HANDLERS.md         ← Authoring a new format handler
β”‚   β”œβ”€β”€ INTEGRATIONS.md     ← MCP/REST API reference
β”‚   β”œβ”€β”€ DEPLOYMENT.md       ← Self-host, Docker, desktop, CLI
β”‚   └── CONTRIBUTING.md     ← PR workflow, testing, style
β”œβ”€β”€ test/
β”‚   β”œβ”€β”€ e2e/                ← End-to-end browser tests (Puppeteer)
β”‚   β”œβ”€β”€ resources/          ← Fixture files
β”‚   β”œβ”€β”€ setup.ts            ← Vitest preload + MockWorker
β”‚   └── MockedHandler.ts    ← Stub FormatHandler for graph tests
β”œβ”€β”€ AGENTS.md               ← Rules for AI pair-programming agents
β”œβ”€β”€ SECURITY.md             ← Privacy posture and limits
β”œβ”€β”€ CHANGELOG.md            ← Release history
└── README.md               ← Landing page

Unit tests are colocated under src/**/*.test.ts (next to the code they cover); test/ holds only e2e, fixtures, and shared mocks.


State Management (Without React)

frogConvert doesn't use React or Vue. It's plain TypeScript + DOM manipulation.

Shared state lives in store.ts as simple objects:

// Example from store.ts
export const currentFiles: { value: File[] } = { value: [] };

Components read and write .value directly. It's simple on purpose - fast to load, easy to trace.


The Conversion Flow in Code

When you hit Convert:

sequenceDiagram
    actor User
    participant UI as conversion/actions.ts
    participant Worker as route-search.worker.ts
    participant CW as conversion.worker.ts
    participant Handler as e.g. FFmpeg

    User->>UI: clicks Convert
    UI->>Worker: "find path from JPG to PDF"
    Worker-->>UI: [ImageMagick β†’ Pandoc]
    loop for each step in path
        UI->>CW: "run ImageMagick on these bytes"
        CW->>Handler: handler.doConvert(files, from, to)
        Handler-->>CW: converted bytes
        CW-->>UI: done, here are the bytes
    end
    UI->>User: download file

The MCP Server & REST API

frogConvert exposes both the conversion engine and the PDF editor as a local server so scripts, automation tools, and AI assistants can drive them without opening a browser.

flowchart LR
    A[AI Agent\nor Script] -->|MCP stdio| M[MCP Server\nsrc/mcp/]
    A -->|HTTP| R[REST API\nsrc/api/]
    M --> E[Conversion Engine + PDF Workspace]
    R --> E
    E --> O[Output Files]

Both run 100% locally. The MCP server exposes 8 tools (list_formats, find_conversion_path, convert_file, compress_file, pdf_merge, pdf_organize, pdf_extract, pdf_watermark). The REST API mirrors the same surface. See INTEGRATIONS.md for request/response shapes.

Browser bridge. Conversions that need browser-only APIs (Canvas, WebGL, AudioContext, document) cannot run in pure Node.js. When a request lacks a native path, the server transparently launches headless Chromium via Puppeteer and executes the conversion there. Cold start is on the order of 30 seconds to 8 minutes depending on the handler's WASM size; warm calls are seconds. Full performance table and fallback strategy in INTEGRATIONS.md Β§ Browser-Assisted Conversions.

Cancellation. Mid-batch cancellation lives in src/conversion/cancellation.ts (isCancelled flag plus a state machine). On cancel, completed files in the batch are still offered to the user via showPartialDownloadPopup(). The cancellation path is the same whether the conversion ran in a worker or on the main thread.

Progress. One shared status line, owned by src/conversion/progressStatus.ts, serves Convert, Compress and the PDF editor. Handlers emit ProgressEvent { ratio?, detail? }; the worker forwards each event to the main thread (conversion.worker.ts β†’ workerClient.ts), and the surface hands it to StatusHandle.update(). startConversionStatus() owns the phase line, the standing reassurance and the elapsed clock suffixed to it, so a surface never formats progress itself.

Two properties are load-bearing rather than cosmetic:

This is also the seam where progress is most easily lost: a surface that forgets to pass onProgress down silences every engine at once without any type error, because the parameter is optional at every level. compressBatch's onEngineProgress exists for exactly this reason and is covered by tests that fail if the callback stops being forwarded.


Surface vs engine seam

Engine modules (src/tools/pdfWatermark.ts, src/handlers/) implement the full capability set. The three public surfaces, UI (src/components/), MCP (src/mcp/tools/), and REST (src/api/routes/), are curated views over that engine. The three surfaces stay aligned with each other for behavior-shaping fields; the engine may exceed them.

Re-introducing a previously-removed surface feature is a wire change at the surface layer, not an engine rewrite. Example: pdfWatermark.ts retains image source and 5-placement support (top-left, top-right, bottom-left, bottom-right, center) even though the UI, MCP, and REST surfaces all expose only text + center. If image watermarks return to the UI, the engine work is already done, only the surface layers need wiring.

Transport-affordance fields (filePath, base64Bytes, outputFilePath, outputDir) are an explicit carve-out: they are MCP/REST-only by necessity, since the browser UI has no filesystem to address. They do not violate alignment because they have no UI counterpart that could exist.

See AGENTS.md Β§ rule 12 for the enforcement rule.


Key Terms Glossary

Term What it means
Handler A wrapper around one conversion tool (FFmpeg, Pandoc, etc.)
FormatHandler The TypeScript interface every handler must follow
TraversionGraph The route-finding system that chains handlers together
Web Worker A background thread in the browser - keeps the UI responsive
requiresMainThread A flag that tells the engine "this handler needs browser APIs, don't offload it"
FileFormat An object describing a format: its name, MIME type, extension, category
FileData A wrapper for a file's bytes (Uint8Array) and its name
MCP Model Context Protocol - a standard way for AI agents to call tools
Dijkstra A graph algorithm that finds the cheapest path (here: fewest/cheapest conversion steps)
WASM WebAssembly - compiled native code (like FFmpeg) that runs inside a browser

How to Add a New Format

Quick version:

  1. Create src/handlers/myFormat.ts.
  2. Implement FormatHandler (or extend a base class); declare input/output formats.
  3. Register in src/handlers/index.ts.
  4. The route finder picks it up automatically; no other wiring needed.

Full guide with interface, base classes, builder API, quality presets, warnings, and registration patterns is in HANDLERS.md.

See also

All documentation