1 — What runs where
Written: 2026-08-15 · Established by: reading src/, rust/ and src-tauri/ at commit
6052e10, plus package.json, vite.config.ts and bin/generate-service-worker.
Ranger is one static bundle and nothing else. There is no server, no API and no account. A grep for
fetch(, XMLHttpRequest, axios and WebSocket across src/ outside the tests returns nothing,
so a program dropped into the tab never leaves the machine it was dropped on. That is the single
most load-bearing fact about the architecture, and every other diagram here inherits it.
Three execution contexts do the work: the main thread, a pool of tri-dexel sweep workers, and one geometry-import worker. Two of the three reach WebAssembly.
What survives a reload, drawn separately so no edge has to cross the diagram above:
Nothing else persists. A loaded program is not saved anywhere — closing the tab loses it, which is the deliberate consequence of having no server to save it to.
The bundle itself is also cached, separately from any document state. bin/generate-service-worker
writes dist/sw.js as the last step of npm run build, precaching every file the build actually
emitted and versioning the cache on a hash of their content. src/pwa/registerServiceWorker.ts
registers it, gated on import.meta.env.PROD and 'serviceWorker' in navigator — a no-op under
vite dev and in a webview that never implements the API. This is what makes a cold start work with
no network at all, and it is orthogonal to the state diagram above: the service worker caches the
app, not what the user loaded into it.
| Box | Where it lives | Worth knowing |
|---|---|---|
| React UI | src/App.tsx, src/sidebar, src/transforms, src/export |
|
| Zustand store | src/store |
Six slices — diagram 3 |
| CodeMirror | src/codemirror |
Its own undo history is off |
| R3F canvas | src/viewer |
THREE via @react-three/fiber |
| localStorage | keys prefixed ranger: |
Viewer settings, export options, density limits, nav bindings, panel layout |
| IndexedDB | database ranger-geometry |
src/utils/geometry-db.ts; imported bodies only |
| service worker | src/pwa, generated dist/sw.js |
Cache-first, versioned by build hash — bin/generate-service-worker |
| step-import worker | src/workers/step-import.worker.ts |
One, spawned lazily by src/utils/geometry-worker.ts |
| tri-dexel workers | src/workers/tri-dexel.worker.ts |
N = min(16, cores - 1), spawned by src/sim/pool.ts |
| occt-import-js | npm package | OpenCascade STEP reader, 7.6 MB of wasm |
| tri-dexel kernel | rust/tri-dexel |
13.4 kB, base64-inlined into src/sim/kernel-wasm.ts |
| Tauri platform seam | src/platform |
The one place isTauri() is checked — see below |
What is worth knowing beyond the boxes
The sweep kernel is checked in, not built. src/sim/kernel-wasm.ts carries the compiled
WebAssembly as a base64 string with a GENERATED — do not edit banner. rust/tri-dexel/build.sh
rebuilds it from rust/tri-dexel/src/lib.rs, and the result is committed. So a clone needs no Rust
toolchain to run or test the simulator.
Both workers are spawned from the store's side, not from a component. src/sim/pool.ts creates
the sweep pool and src/utils/geometry-worker.ts creates the import worker; src/store/geometry-slice.ts
is the only importer of the latter.
src/sim/inline-runner.ts is a third execution path that the app never takes. It implements the
same SimulationRunner interface on the calling thread, so the whole engine can be driven from
Vitest's node project. Every real run goes through the pool.
The desktop shell defines two commands, both file access, and one seam is where src/ is allowed
to know it exists. src-tauri/src/commands.rs defines read_native_file (reads a path chosen
through the dialog plugin, dropped from Finder/Explorer, or handed in at launch by .nc/.ngc
file associations) and take_pending_open_file (the launch case, read once at startup). None of
this is exposed as window.__TAURI__ checks scattered through components — src/platform/nativeFileAccess.ts
is the only file that calls isTauri() (@tauri-apps/api/core), returning null outside Tauri so
every caller already has a browser fallback: App.tsx's hidden <input type=file> for the picker,
DropZone's own HTML5 drag-and-drop, and nothing to launch with since a URL is not a file.
src/platform/useNativeFileEvents.ts is the hook that wires the drop and launch cases into the same
loadFile call the browser path already used. Tauri's own dragDropEnabled (on by default)
intercepts the DOM's drag events entirely under this shell, which is why the native path is a
replacement for DropZone's onDrop there rather than an addition to it.