Skip to content

Integrate with a frontend

In a browser, the package runs SWMM in a dedicated module worker, keeping solver work off the UI thread. Serial WASM uses unshared memory and works without cross-origin isolation or SharedArrayBuffer.

Configure the server

Serve the application over HTTPS or localhost. For browser runs with multiple solver threads, enable cross-origin isolation with these headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin

The application page must report globalThis.crossOriginIsolated === true, and SharedArrayBuffer must be available. Third-party resources must satisfy the page's embedding policy. The npm start server in js/ provides these headers for local development. Use npm start -- --no-isolation to try serial execution without them. Serve WASM files with Content-Type: application/wasm.

Simulation.open() and runSwmm() default threads to navigator.hardwareConcurrency || 1 on an isolated page, or 1 otherwise. Explicit threads: 1 always selects serial WASM. An explicit count greater than one rejects on a non-isolated page rather than silently reducing the count.

JavaScript options.threads is separate from the INP THREADS setting. Serial WASM clamps an INP request above one solver thread, so an input containing THREADS 4 can run without isolation. Threaded WASM rejects an INP request above the initialized worker capacity. await simulation.info() reports the effective thread count.

Node.js 22 and later uses node:worker_threads instead of browser workers. It defaults to threads: 1 and supports explicit thread counts without COOP/COEP. Node still receives model and supporting-file contents as bytes or text, not host paths supplied to the runner.

Deploy worker assets

A static-asset deployment avoids relying on a bundler to discover nested WASM workers. Copy the built package under one asset directory, preserving its relative layout:

/swmmrs/
  index.js
  worker.js
  lib/swmmrs/          # compiled modules, including worker helpers
  dist/
    swmmrs.js
    swmmrs_bg.wasm
    snippets/           # generated thread-pool helpers
    serial/
      swmmrs.js
      swmmrs_bg.wasm

Include both WASM builds. The loader downloads only the build selected by threads; both expose the same public API. Import the API from the matching package build and set the public worker URL when needed:

import { Simulation, type FileContents } from "@swmmrs/swmmrs";

export function openBrowserModel(input: FileContents) {
    return Simulation.open(input, {}, {
        threads: 1,
        workerUrl: new URL("/swmmrs/worker.js", location.href),
    });
}

The JavaScript entry module and static assets must come from the same build. Without workerUrl, the API resolves the worker relative to the compiled module. Automatic asset rewriting by individual bundlers is not qualified. Inspect the browser network panel to confirm that worker, WASM, and snippet requests return files rather than an application's HTML fallback.

Report progress and stop cooperatively

Use steps() for periodic UI updates. Call terminate() when a stop request arrives while the iterator is active:

import {
    Simulation,
    type FileContents,
    type SimulationStatus,
} from "@swmmrs/swmmrs";

export async function runWithProgress(
    input: FileContents,
    onProgress: (status: SimulationStatus) => void,
    shouldStop: () => boolean,
) {
    const simulation = await Simulation.open(input, {}, {
        threads: 1,
        workerUrl: new URL("/swmmrs/worker.js", location.href),
    });
    try {
        for await (const _time of simulation.steps({ seconds: 60, strict: true })) {
            onProgress(await simulation.status());
            if (shouldStop()) simulation.terminate();
        }
        return await simulation.finish();
    } finally {
        await simulation.close();
    }
}

terminate() is cooperative. The iterator ends at its next observation boundary; it does not interrupt a native operation already in progress. Choose an observation cadence that balances UI updates and worker-message overhead. For network charts, collect a snapshot per observation instead of making one request per object.

run() and runSwmm() occupy the worker for the whole native batch request. They provide no mid-run progress callback, AbortSignal, or hard cancellation. close() queues cleanup but does not interrupt that request.

Manage application ownership

Keep the simulation in the controller or component that owns the run. Coordinate component teardown with the running task, and let that task's finally block close the owner. Do not let a background iterator and a UI timer advance the same simulation.

Importing the package is safe during server-side rendering, but opening a model requires worker support. Open models only in client-side code. Use explicit thread budgets for multiple scenarios. Each owner has its own worker, and threaded owners also allocate a worker pool. Independent scenarios can run concurrently with threads: 1 in separate workers without shared memory.