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 needJson. 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 ordinarytry/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.