# Run in the browser

Load the WASM engine in a Web Worker, run expectGeo on bytes the page already holds, and serve the module.

Canonical page: https://www.geospec.xyz/docs/guides/run-in-the-browser

The spec code that runs under Node runs in a browser tab unchanged; only the engine differs. In a browser `geospec-engine` resolves to the WASM engine, which the demos on this site run in a worker, and `geospec/model` and `expectGeo` run on top of it. The page below was built with Vite 8 and run in headless Chromium.

**Python**

Python has no browser form: the wheel runs the engine inside CPython, and the TypeScript worker on this page is the way to run a spec in a tab.

### 1. Load the engine in a worker [#1-load-the-engine-in-a-worker]

The module is 16.7 MB and compiles once; admission and evaluation run on the thread that calls them. In a worker, none of that blocks input handling. Initialize once per worker and keep the loader.

```ts title="engine.worker.ts"
import { expectGeo } from 'geospec';
import { GeoSpecAssertionError } from 'geospec/assertion-client';
import { createModelLoader } from 'geospec/model';
import { Engine, initialize } from 'geospec-engine';

const ready = initialize().then(() => createModelLoader({ engine: new Engine() }));

self.addEventListener('message', async ({ data }: MessageEvent<ArrayBuffer>) => {
  const loadModel = await ready;
  const plate = await loadModel({ source: new Uint8Array(data), format: 'step' });
  try {
    const { report } = expectGeo(plate).toHaveCircularHole({ diameter: 10 });
    self.postMessage(`${report?.claimId} ${report?.status}`);
  } catch (error) {
    if (!(error instanceof GeoSpecAssertionError)) throw error;
    self.postMessage(`${error.report.claimId} ${error.report.status}`);
  }
});
```

`loadModel` takes the bytes themselves, so it needs no source reader. A failed or refused claim throws, so the worker catches `GeoSpecAssertionError` and posts the report's status either way. Bytes cross `postMessage`; the engine never does.

### 2. Send the model from the page [#2-send-the-model-from-the-page]

Only the model's source differs: a fetch, a file input or a buffer the page already holds. Transfer the buffer rather than copy it.

```ts title="main.ts"
const worker = new Worker(new URL('./engine.worker.ts', import.meta.url), { type: 'module' });
const out = document.querySelector('#out')!;
worker.addEventListener('message', ({ data }) => {
  out.textContent = data;
});

const response = await fetch('/plate-with-through-bore.step');
const model = await response.arrayBuffer();
worker.postMessage(model, [model]);
```

The page prints `geospec-claim-1 passed` after one fetch of the module. The same claim runs here:

```ts
expectGeo(part)
  .toHaveCircularHole({ diameter: 10 });
```

### 3. Serve the module [#3-serve-the-module]

`initialize()` with no argument loads the `.wasm` file from beside the package's own script, through `new URL('geospec_engine_native.wasm', import.meta.url)`. Vite follows that reference and emits the file as a hashed asset, so the worker above needs no configuration; `initialize(url)` names the file yourself for a bundler or a server that places it elsewhere. Vite reports the `node:` imports that the engine and `geospec/model` keep for Node as externalized for browser compatibility; the page runs without them. [Install](https://www.geospec.xyz/docs/install.mdx) covers the engine entry points.

**MIME type.** The module compiles with `WebAssembly.instantiateStreaming`, which needs `Content-Type: application/wasm`. Served as anything else, Chromium logs `wasm streaming compile failed: TypeError: Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'.` and then `falling back to ArrayBuffer instantiation`, fetches the 16.7 MB file a second time, and prints the same verdict.

**Caching.** The asset's name carries a content hash, so serve it with `Cache-Control: public, max-age=31536000, immutable`. Compressed, the module is 5.8 MB as gzip and 4.1 MB as Brotli.

## Variations [#variations]

**Cross-origin isolation.** The page needs no COOP or COEP headers: the run above was served with neither.

**Node and browser from one codebase.** The worker's spec runs unchanged under Node, where `initialize()` takes about 90 ms; for the native addon, import `Engine` from `geospec-engine/node` and drop `initialize()`. [Install](https://www.geospec.xyz/docs/install.mdx) covers the two engines.

**Stopping a run.** A WebAssembly call cannot be interrupted: `terminate()` the worker and start a fresh one. Give each chain a `workUnitLimit`, so a runaway claim refuses with `MATCHER_TIMEOUT` instead of holding the worker; [Set tolerances and budgets](https://www.geospec.xyz/docs/guides/set-tolerances-and-budgets.mdx) covers budgets.