valhalla-wasm API
    Preparing search index...

    valhalla-wasm API

    valhalla-wasm

    Valhalla routing in WebAssembly for browsers, Node.js and experimental Cloudflare Workers. This repository provides two ESM packages with TypeScript declarations and a shared compiled WASM runtime:

    npm package Runtime Execution
    valhalla-browser Browser Dedicated Web Worker; supports bundlers and direct CDN imports
    valhalla-server Node.js and experimental Cloudflare Workers Dedicated Node worker thread, or a Cloudflare Worker isolate

    Both packages calculate routes with Valhalla and load standard graph tiles on demand through HTTP ranges from an indexed TAR, or as individual .gph objects. The Cloudflare adapter also supports private R2 bindings. Graph datasets are hosted separately from the routing engine.

    Demo · API documentation · Source · Release setup

    The examples use version 0.2.1. Both SDKs are published on npm; the local tarball instructions below are available for testing workspace changes.

    pnpm add valhalla-browser
    # npm install valhalla-browser also works for consumers.

    No asset-copy plugin, Docker or native compiler is needed in your application. The worker and WASM are located automatically. All https://routing.example.com/datasets/your-release-id/... URLs below are placeholders, not a hosted dataset service. Replace the complete manifest URL with your deployment's actual URL, including its immutable release directory. That directory must match the manifest's release value. Choose route coordinates inside your dataset's coverage. See the data preparation guide to build compatible graph data.

    Use this in a browser module or Vite application:

    import { createRouter } from 'valhalla-browser';

    const router = await createRouter({
    // Replace with your deployment's versioned dataset manifest URL.
    manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
    });
    try {
    const result = await router.route({
    origin: { lat: 47.0666667, lon: 9.5 },
    destination: { lat: 47.2397558, lon: 9.5262874 },
    });
    console.log(result.native);
    } finally {
    await router.dispose();
    }

    These coordinates are covered by the historical July 2015 Liechtenstein benchmark, not current navigation data. © OpenStreetMap contributors, ODbL 1.0. Graphs are supplied separately and are not included in the package.

    Results preserve Valhalla's full native JSON: distances in kilometers, times in seconds, and leg shapes encoded as polyline6. Driving (auto), two locations, English directions, a 30 m correlation radius and minimum reachability 0 are the validated defaults.

    Use the separate valhalla-server package for server execution. It includes compiled WASM and self-contained TypeScript declarations. Import valhalla-server/node for a dedicated Node worker thread, or valhalla-server/cloudflare for the experimental HTTP/private-R2 adapter. All adapters use the same WASM binary, graph format and four travel profiles.

    pnpm add valhalla-server
    
    import { createRouter } from 'valhalla-server/node';

    const router = await createRouter({
    // Replace with your deployment's versioned manifest URL.
    manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
    });
    try {
    const result = await router.route({
    origin: { lat: 47.0666667, lon: 9.5 },
    destination: { lat: 47.2397558, lon: 9.5262874 },
    });
    console.log(result.native.trip.summary);
    } finally {
    await router.dispose();
    }

    See Node.js and Cloudflare Workers for R2 bindings, per-request contexts, queue limits, cancellation and resource constraints. Cloudflare support has a local workerd proof; it remains experimental and is not a production capacity guarantee. The demo and docs stay on Pages.

    The SDK supports driving (auto, the default), cycling (bicycle), walking (pedestrian), and truck (truck). Each dataset must advertise the requested profile in its manifest. await router.initialize() returns supportedCostings; older auto-only datasets continue to support driving only. Transit and combined bike-and-train journeys are not supported.

    Use costing_options for the selected profile. Omit settings to use Valhalla's defaults. This example uses the same historical Liechtenstein graph:

    import { Router, type RouteRequest } from 'valhalla-browser';

    const router = new Router({
    // Replace with your deployment's multi-profile dataset manifest.
    manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
    });
    const endpoints = {
    origin: { lat: 47.1392862, lon: 9.5227962 },
    destination: { lat: 47.1450, lon: 9.5168 },
    };
    const requests: RouteRequest[] = [
    { ...endpoints, costing: 'bicycle',
    costing_options: { bicycle: { bicycle_type: 'road', cycling_speed: 24, use_roads: 0.2 } } },
    { ...endpoints, costing: 'pedestrian',
    costing_options: { pedestrian: { walking_speed: 4 } } },
    { ...endpoints, costing: 'truck',
    costing_options: { truck: { height: 2.5, width: 2, length: 6, weight: 5, axle_load: 2, hazmat: false } } },
    ];
    try {
    for (const request of requests) {
    const result = await router.route(request);
    console.log(result.native);
    }
    } finally {
    await router.dispose();
    }

    Speeds are km/h, vehicle dimensions are meters, and weights are metric tonnes. Profile changes reuse the same worker and tile cache. Unknown profiles or profiles absent from the dataset reject with UNSUPPORTED_COSTING; invalid settings reject with INVALID_REQUEST.

    See Travel profiles and options for supported settings, defaults, dataset upgrades, and current limits. This guide is also included in the generated API documentation.

    The package exports request, response, option, error and diagnostics types. Create one router per session and dispose it when the application no longer needs it. Only one native operation runs at a time; concurrent submissions queue.

    import { Router } from 'valhalla-browser';
    import type { RouteRequest, RouteResult, RouterOptions } from 'valhalla-browser';

    const options: RouterOptions = {
    // Replace with your deployment's versioned dataset manifest URL.
    manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
    memoryBudgetBytes: 32 * 1024 * 1024,
    // Optional: these are the defaults, in labels per search vector, not bytes.
    searchMemory: { astar: 16_384, bidirectionalAstar: 16_384, clearReservedMemory: false },
    };
    const request: RouteRequest = {
    locations: [{ lat: 47.0666667, lon: 9.5 }, { lat: 47.2397558, lon: 9.5262874 }],
    costing: 'auto',
    };
    const router = new Router(options); // Lazy: the engine loads on initialize/route.
    try {
    const result: RouteResult = await router.route(request);
    console.log(result.native.trip.summary);
    } finally {
    await router.dispose();
    }

    Importing valhalla-browser is safe without browser globals, but creating its Router requires a browser. For server execution, import valhalla-server/node or the experimental valhalla-server/cloudflare adapter instead.

    Search reservations grow when a route needs more labels; they are not search or memory limits. The defaults override the dataset's large A* reservations without changing its tiles, costing, or configuration file. Existing hosted datasets need no changes. initialize() reports the effective searchMemory settings and an effectiveConfigSha256, alongside the original dataset configuration hash. See Diagnostics and memory for tuning and measured reservation results for the native-equivalence checks and memory baseline.

    Save this as an HTML file and serve it over HTTP. No bundler is required. Keep the package version pinned in the import URL.

    <!doctype html>
    <html lang="en">
    <meta charset="utf-8">
    <title>Browser-local route</title>
    <button id="route">Calculate route</button>
    <pre id="result"></pre>
    <script type="module">
    import { createRouter } from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/index.js';
    const output = document.querySelector('#result');
    const button = document.querySelector('#route');
    button.onclick = async () => {
    button.disabled = true;
    let router;
    try {
    router = await createRouter({
    // Replace with your deployment's versioned dataset manifest URL.
    manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
    });
    const result = await router.route({
    origin: { lat: 47.0666667, lon: 9.5 },
    destination: { lat: 47.2397558, lon: 9.5262874 },
    });
    output.textContent = JSON.stringify(result.native, null, 2);
    } catch (error) {
    output.textContent = `${error.code ?? 'ERROR'}: ${error.message}`;
    } finally {
    await router?.dispose();
    button.disabled = false;
    }
    };
    </script>
    </html>

    UNPKG and ordinary static hosting can serve the same distribution. A cross-origin import uses a small blob module-worker bootstrap; all routing still runs inside the worker. Serve JavaScript as text/javascript and WASM as application/wasm, with CORS permitting your application origin.

    For CSP, allow the SDK origin in script-src and connect-src, the graph origin in connect-src, blob: in worker-src, and 'wasm-unsafe-eval' in script-src. If blob workers are disallowed, self-host matching assets and use the overrides shown below. See the API site's CDN and CSP guide for a complete policy.

    An AbortSignal or router.cancel() terminates the worker, rejects all its outstanding operations and discards its decoded cache. The next route creates a new worker. Termination also stops uninterrupted native CPU work.

    import { createRouter, RoutingError } from 'valhalla-browser';

    const router = await createRouter({
    // Replace with your deployment's versioned dataset manifest URL.
    manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
    });
    const request = {
    origin: { lat: 47.0666667, lon: 9.5 },
    destination: { lat: 47.2397558, lon: 9.5262874 },
    };
    try {
    const controller = new AbortController();
    const pending = router.route(request, { signal: controller.signal });
    controller.abort(); // In a UI, call this from the Cancel button handler.
    try {
    await pending;
    } catch (error) {
    if (!(error instanceof RoutingError) || error.code !== 'CANCELLED') throw error;
    }
    const recovered = await router.route(request);
    console.log(recovered.native.trip.summary);
    } finally {
    await router.dispose();
    }

    Keep the router alive between requests to reuse decoded graph tiles. Browser HTTP cache and CDN cache are separate; repeated range reuse is browser-dependent.

    import { createRouter } from 'valhalla-browser';

    const router = await createRouter({
    // Replace with your deployment's versioned dataset manifest URL.
    manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
    });
    const request = {
    origin: { lat: 47.0666667, lon: 9.5 },
    destination: { lat: 47.2397558, lon: 9.5262874 },
    };
    try {
    await router.route(request);
    const repeated = await router.route(request);
    console.log(repeated.diagnostics.loader.requests); // Zero for this warm route.
    console.log(repeated.diagnostics.decodedCacheHits);
    console.log(await router.diagnostics());
    } finally {
    await router.dispose();
    }

    The cache budget bounds retained decoded tiles, not total worker memory. Heap high-water diagnostics measure allocated WASM memory capacity, not live objects or browser RSS. Diagnostics expose tile URLs and IDs; the SDK sends no telemetry.

    Browser, Node and Cloudflare share one WASM binary with a 1,024 MiB upper ceiling. wasmMemory configures each instance's initial allocation and hard growth cap: 128 / 512 MiB by default in browsers, 256 / 512 MiB in Node, or 64 / 96 MiB in Cloudflare. Use whole MiB with 64 <= initialMiB <= maximumMiB <= 1024; resolved settings are available as router.startup.wasmMemory. This limits native heap, stack and static data, while JavaScript and other host overhead consume additional memory. memoryBudgetBytes separately bounds decoded tiles. A larger maximum does not allocate that amount at startup or increase a platform's own memory allowance.

    indexed-tar is the default. individual-tiles uses full GETs for the same standard tiles. A session pins a compatible, immutable manifest release.

    import { Router, RoutingError } from 'valhalla-browser';

    const router = new Router({
    // Replace with your deployment's versioned dataset manifest URL.
    manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
    transport: 'individual-tiles',
    timeoutMs: 10_000,
    retries: 2,
    // Hard WASM linear-memory cap; separate from the decoded-tile cache budget.
    wasmMemory: { initialMiB: 64, maximumMiB: 256 },
    onProgress: event => console.log(event.phase),
    // Optional matching assets hosted on your application's origin:
    // workerUrl: '/sdk/0.2.1/worker.js',
    // wasmUrl: '/sdk/0.2.1/valhalla-browser.wasm',
    });
    try {
    await router.route({
    origin: { lat: 47.0666667, lon: 9.5 },
    destination: { lat: 47.2397558, lon: 9.5262874 },
    });
    } catch (error) {
    if (error instanceof RoutingError) {
    console.error(error.code, error.message, { retryable: error.retryable });
    } else {
    throw error;
    }
    } finally {
    await router.dispose();
    }

    Errors distinguish invalid input, coverage, no-route, transport, integrity, compatibility and cancellation failures. Transient download errors are never converted into missing tiles or false no-route results. Initialization can be retried after failure. timeoutMs bounds fetches and worker loading; WASM startup receives at least ten seconds per attempt. An opaque worker-load failure or WASM initialization timeout gets at most one startup retry in a fresh worker; retries: 0 or a custom workerFactory disables this recovery. Native routes are never replayed. Relative asset overrides resolve against the page.

    Provide your own compatible OSM-derived graph dataset. The repository includes pnpm run data:osm to build standard tiles, Valhalla's native indexed graph.tar, and the SDK manifest/configuration from an .osm.pbf extract. It uses the pinned native Valhalla tools and preserves access for driving, cycling, walking, and truck routing.

    See Build graph data from OpenStreetMap for a reproducible small example, custom extracts, native/container invocation, coverage bounds, timezone/admin inputs, static hosting and deployment validators. The same guide is included in the API documentation site's navigation.

    Use the object-storage hosting guide to deploy datasets on S3, R2, MinIO or another compatible provider. It covers dashboard setup without AWS CLI, bucket CORS, cache rules for entire prefixes, HTTP metadata, byte ranges, deployment ETags and browser verification. It also explains stale CORS responses and CDN cache statuses. The guide is included in the generated API documentation.

    Use Node 22.22.2 and the pinned pnpm 12.4.2. The five packages are the browser SDK (valhalla-browser), server SDK (valhalla-server), private shared core (@tobilg/valhalla-core), demo (@tobilg/valhalla-browser-demo) and documentation (@tobilg/valhalla-browser-documentation). SDKs/demo use Vite 8.3.0.

    git clone https://github.com/tobilg/valhalla-wasm.git
    cd valhalla-wasm
    pnpm install --frozen-lockfile
    pnpm build:docs # No WASM, native toolchain or graph needed.
    pnpm preview:docs # http://localhost:8081
    pnpm build # Requires existing verified native artifacts.
    pnpm dev # http://localhost:8080
    pnpm pack:sdk # build/package/valhalla-browser-0.2.1.tgz
    pnpm pack:server # build/package/valhalla-server-0.2.1.tgz
    # Test local changes in another application:
    pnpm add /absolute/path/to/valhalla-browser-0.2.1.tgz

    A clean checkout needs the explicit native/data/WASM build steps in the development guide. The native pins remain Valhalla 3.8.3 at a60c7cbfc83e073f50887cd27e0109d02e6b64e5, Emscripten 6.0.0, Protobuf 21.12 and zlib 1.3.1. Package consumers do not need this toolchain.

    pnpm test
    pnpm test:package
    pnpm test:cdn-import
    pnpm test:docs
    pnpm test:examples
    pnpm test:browser
    pnpm test:server:node
    pnpm test:server:cloudflare
    pnpm test:server:package
    pnpm test:demo

    The root README is also the TypeDoc homepage and is copied into the valhalla-browser npm package. valhalla-server has its own package README. Set all five package versions with pnpm run version:set 0.1.0 (or npm run version:set -- 0.1.0), substituting your next stable version. The command also keeps the SDK version references in this README and the development guide in sync. See the release guide for the remaining release steps. Guides and API comments are maintained with the source. Stable version tags publish both verified SDKs through npm trusted publishing, then deploy documentation to valhalla-browser-api and the demo to valhalla-browser on Cloudflare Pages. See release setup for the initial npm publication, trust configuration, Pages setup and dry runs.

    For the hosted demo, set the GitHub repository variable VITE_DEMO_MANIFEST_URL to your public R2 manifest URL for the Liechtenstein 2015 graph. The build bundles the same journey presets and downloads graph tiles from that URL. Locally, the same environment variable can be passed to pnpm run dev or pnpm run build:demo.

    The demo displays routes over an interactive OpenStreetMap basemap with pan/zoom, endpoint markers, a basemap toggle and a Fit route button. Background map images come from OSM; routing still runs locally in the WASM worker. The current basemap may differ from the historical 2015 graph, and synthetic fixture roads are fictional. See the development guide for basemap configuration and hosting requirements.

    Desktop Chromium, Firefox and WebKit are the browser verification targets. Physical mobile devices, other bundlers and worldwide coverage are not established. There is no OPFS, IndexedDB, service-worker cache, offline guarantee or external routing fallback. Bike-and-train routing remains separate discovery work.

    See verification, MinIO testing, and R2 CORS policy.

    Both SDKs are MIT licensed. Compiled dependencies' license texts are included under dist/licenses/; preserve these notices when redistributing the runtime.