polar

Bridges

A bridge lets code on one host use an effect that lives on another. Browser code can call a Node effect as if it were local, and Polar handles the HTTP in between.

The problem

In Hosts, we saw a function that used Dom and Visits together, and couldn't run anywhere. The browser has the DOM, and Node has the data. In most stacks, you'd now write an API endpoint, a fetch call, and JSON parsing on both ends, and keep them in sync by hand.

Declaring a bridge

In Polar, it's one line in binds:

uses
  Std.Dom
  Std.Json

hosts
  Browser
  Node

effects
  Visits in Node {
    count() -> Int
  }

binds
  Visits in Node {
    count() {
      42
    }
  }

  Visits in Browser from Node

functions
  start() -> {} / {Dom, Visits} {
    Dom.append_text("p", "#{Visits.count()} visits so far")
  }

Visits in Browser from Node means: in the browser, Visits is available, and calls go to the Node implementation. Now start runs in the browser, and the error from before is gone.

What crosses the bridge

  • Arguments and results are sent as JSON. That's why the module needs uses Std.Json, and why the types an operation takes and returns need Json. Polar tells you if one doesn't.
  • Errors cross too. If the Node side throws a declared error, like Throws<Missing>, browser code can catch it with an ordinary try/catch.
  • Types are shared, because both sides are compiled from the same code. Rename a field and both ends change together.

Note: A bridge is not an emulation. It runs the real implementation on the real host, so native effects can be bridged without force.

Running it

The build writes two bundles, dist/Browser/ and dist/Node/. The Node bundle includes _polar/bridges.js, which lists every bridged effect. The runtime serves it with Node's built-in http module:

import { createServer } from "node:http";
import { bridges } from "./dist/Node/_polar/bridges.js";
import { bridge } from "./dist/Node/_polar/runtime.js";

createServer(bridge.nodeListener(bridges)).listen(3000);

By default, browser code sends bridge calls to /_polar/<Effect>/<operation> on the same origin. If the server lives somewhere else, point the browser runtime at it before calling start:

import { bridge } from "./dist/Browser/_polar/runtime.js";

bridge.configure({ base: "http://localhost:3000/_polar" });

A complete example

The repository's projects/bridge_posts project bridges a Posts effect, including an error case. Its e2e.mjs script starts a bridge server, points the browser bundle at it and runs start:

$ polar build --out /tmp/bp/dist
$ node e2e.mjs /tmp/bp/dist
2 posts on the server
1: Hello from the server
2: Bridges forward effects
no post 42
the same post twice is equal: true

no post 42 is a Missing error thrown in Node and caught in browser code.

Tip: For a full web app, the example simple_framework package does all of this wiring for you. Its launcher serves the browser bundle, the bridges and your routes from one server. See projects/web_posts, and Launchers.