Skip to content

Axelwickm/or-tools-wasm

 
 

Repository files navigation

or-tools-wasm - solve complex constraint problems in browser

Solve complex models directly in the browser with Google OR-Tools CP-SAT running as multithreaded WebAssembly.

Used in PragmaPlanner.com.

GitHub npm

Package Vite 7 dev Chromium Vite 7 dev Firefox Vite 7 static Chromium Vite 7 static Firefox Webpack 5 dev Chromium Webpack 5 dev Firefox Webpack 5 static Chromium Webpack 5 static Firefox Rollup 4 static Chromium Rollup 4 static Firefox Deno

Install

npm install or-tools-wasm
import { CpSat } from 'or-tools-wasm';

Currently supported solvers: CP-SAT.

Verified with:

  • Vite 7 in browser contexts
  • Webpack 5 in browser contexts
  • Rollup 4 static browser builds
  • Deno runtime solves

What is included

  • A CP-SAT WebAssembly runtime built with Emscripten pthread support.
  • A TypeScript API for solving, validating models, interrupting solves, and reading embedded proto schemas.
  • A worker bridge for keeping browser UI threads responsive while solving.
  • Generated TypeScript definitions for SAT parameters.
  • Demo pages for Magic Square, Sports Scheduling, Steel Mill Slab, and schema inspection.

This flow is verified with Vite, Webpack, and Rollup. The worker script and WebAssembly files are emitted automatically from the package import, with no manual copying into public/ or static/ required.

Usage

const model = {
  name: 'choose_one',
  variables: [
    { name: 'x', domain: [0, 1] },
    { name: 'y', domain: [0, 1] },
  ],
  constraints: [
    {
      name: 'exactly_one',
      linear: {
        vars: [0, 1],
        coeffs: [1, 1],
        domain: [1, 1],
      },
    },
  ],
  objective: {
    vars: [0, 1],
    coeffs: [1, 2],
  },
};

const modelBytes = await CpSat.createModel(model);
const validation = await CpSat.validate(modelBytes);

if (!validation.ok) {
  throw new Error(validation.message);
}

const result = await CpSat.solve(modelBytes, {
  numSearchWorkers: 1,
});

console.log(result.response);

Browser hosting requirements

This package uses a threaded WebAssembly runtime. Browser pages that load it must be served with cross-origin isolation enabled:

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

Without these headers, browsers may block the SharedArrayBuffer APIs required by Emscripten pthreads, and solving can fail during WebAssembly runtime or worker startup.

For Vite dev and preview servers, set the headers in vite.config.ts:

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
  preview: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
});

Bundler configuration

Vite

For Vite apps, keep or-tools-wasm out of dependency optimization so Vite handles the worker and WebAssembly URLs through its normal asset pipeline. protobufjs is CommonJS, so include it in dependency optimization. The worker runtime also needs ES module worker output:

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  optimizeDeps: {
    include: ['protobufjs'],
    exclude: ['or-tools-wasm'],
  },
  worker: {
    format: 'es',
  },
});

Webpack

For Webpack 5, enable async WebAssembly, emit .wasm files as resources, and use publicPath: 'auto' so worker and WebAssembly URLs resolve from the emitted bundle location:

// webpack.config.cjs
const headers = {
  'Cross-Origin-Opener-Policy': 'same-origin',
  'Cross-Origin-Embedder-Policy': 'require-corp',
};

module.exports = {
  output: {
    publicPath: 'auto',
  },
  experiments: {
    asyncWebAssembly: true,
  },
  module: {
    rules: [
      {
        test: /\.wasm$/,
        type: 'asset/resource',
      },
    ],
  },
  devServer: {
    headers,
  },
};

Rollup

Rollup core does not bundle module workers or emit new URL(..., import.meta.url) assets by itself. The verified fixture uses Rollup's standard plugin surface for those features:

// rollup.config.mjs
import { nodeResolve } from '@rollup/plugin-node-resolve';
import OMT from '@surma/rollup-plugin-off-main-thread';
import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets';

function moduleRelativeFileUrls() {
  return {
    name: 'module-relative-file-urls',
    resolveFileUrl({ fileName }) {
      return `new URL(${JSON.stringify(fileName)}, import.meta.url).href`;
    },
  };
}

export default {
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'es',
  },
  plugins: [
    nodeResolve({ browser: true }),
    moduleRelativeFileUrls(),
    OMT(),
    importMetaAssets(),
  ],
};

Other modern bundlers may also work if they support module workers and WebAssembly asset emission, but they are not yet officially verified.

Threading model

The WebAssembly runtime is built with Emscripten pthread support. When the runtime starts, Emscripten creates a pthread worker pool sized from navigator.hardwareConcurrency. This pool is separate from CP-SAT's own search worker setting.

SatParameters.numSearchWorkers controls how many CP-SAT search workers the solver should use for a solve. It does not change how many Emscripten pthread workers are created when the WebAssembly runtime is initialized.

By default, CpSat.solve runs through the package's worker bridge. The bridge loads the CP-SAT runtime in a dedicated JavaScript worker, so the browser's main thread remains available for rendering, input, progress UI, and cancellation. If the worker bridge is disabled, solving runs directly on the main thread. The solver still works, but the GUI can freeze until CP-SAT returns because the browser cannot repaint or process UI events during the synchronous WebAssembly call.

Local development

npm install
npm run dev

npm run dev / npm run start builds the library and launches the Vite dev server for the demo site.

Build

npm run build
npm run preview

npm run build runs the full pipeline: Emscripten/CMake builds the low-level CP-SAT WebAssembly runtime, Vite builds the package bundle, and Vite builds the static demo site.

The Emscripten SDK is tracked as a pinned emsdk git submodule. The build script initializes that submodule automatically if needed, so a normal clone can run npm run build directly after npm install. If you prefer to fetch submodules up front, clone with --recurse-submodules or run git submodule update --init --recursive.

npm scripts

  • npm run build:wasm rebuilds the cp_sat_runtime wasm/js bundle via emsdk + CMake.
  • npm run build:lib regenerates SAT parameter types, type-checks with tsc, and builds the library bundle with vite.lib.config.ts.
  • npm run build:site builds the demo site with vite.site.config.ts. The site imports or-tools-wasm directly, so Vite emits the worker/runtime/wasm assets from the package bundle automatically.
  • npm run build runs build:wasm, build:lib, and build:site.
  • npm run preview serves the already-built Vite site from build/javascript/site.
  • npm run clean removes the entire build/ tree.
  • npm run pack:lib rebuilds the library bundle and writes an npm tarball into build/javascript/lib.

Project layout

  • javascript/lib contains the TypeScript package API and worker bridge.
  • javascript/site contains the demo pages.
  • javascript/cp_sat_api.cc contains the C++ binding layer compiled into WebAssembly.
  • scripts/embed_proto.cmake embeds CP-SAT proto schemas into the runtime.
  • scripts/generate_sat_parameters_types.mjs generates TypeScript SAT parameter definitions.
  • vite.lib.config.ts builds the distributable JS package.
  • vite.site.config.ts builds the demo site.

Demos

  • The Magic Square and Sports Scheduling pages let users pick a CP-SAT search worker count; that value becomes SatParameters.num_search_workers, clamped to min(navigator.hardwareConcurrency, 8).
  • Each demo exposes a "Use worker bridge" checkbox. Keep it enabled for interactive use; disabling it runs the solve on the main browser thread and can freeze the GUI until CP-SAT returns.
  • Schema Viewer imports the bundled CpSat API automatically; no extra script ordering is required.

JSPI and Asyncify

The package ships two CP-SAT runtime builds:

  • cp_sat_runtime uses Emscripten JSPI support (-sJSPI=2) for browsers that expose WebAssembly.promising.
  • cp_sat_runtime_asyncify uses classic Asyncify stack rewriting for browsers without JSPI support.

javascript/lib/cp_sat_module_loader.ts chooses the runtime at startup. If WebAssembly.promising is available, it imports the JSPI runtime, which is the more modern and faster path. Otherwise it falls back to the Asyncify runtime. Both paths expose the same TypeScript API, so application code does not need to choose one manually. Current browser support for JSPI is tracked at caniuse.com/wf-wasm-jspi.

Upstream OR-Tools

This repository vendors Google OR-Tools and adds a JavaScript/WebAssembly packaging layer on top. OR-Tools is Google's open-source suite for solving combinatorial optimization problems, including CP-SAT, linear programming, routing, bin packing, and graph algorithms.

Upstream project:

Maintainer

Maintained by Axel Wickman.

License

This project is licensed under the Apache License 2.0. See LICENSE.

About

Running Google OR-Tools multithreaded in browser through wasm. Used in pragmaplanner.com

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • C++ 80.2%
  • Python 6.2%
  • Julia 3.2%
  • C# 2.1%
  • Starlark 2.1%
  • Java 2.1%
  • Other 4.1%