|
| 1 | +# distribute |
| 2 | + |
| 3 | +<p align="center"> |
| 4 | + <img src="assets/img/distribute.png" alt="distribute logo" width="200" /> |
| 5 | +</p> |
| 6 | + |
| 7 | +A Gleam library for sending typed messages between nodes on the BEAM. |
| 8 | + |
| 9 | +[](https://hex.pm/packages/distribute) |
| 10 | +[](https://hexdocs.pm/distribute/) |
| 11 | + |
| 12 | +## Why |
| 13 | + |
| 14 | +Erlang gives you distribution for free, but when you use it from Gleam you |
| 15 | +lose type information at the node boundary. The BEAM's distribution protocol |
| 16 | +sends raw terms. `distribute` wraps the moving parts (`:global`, `net_kernel`, |
| 17 | +`Subject`) behind binary codecs so the compiler can catch the obvious mistakes |
| 18 | +before your messages go over the wire. ("Typed" here means checked at |
| 19 | +encode/decode boundaries not a shared type system across nodes.) |
| 20 | + |
| 21 | +It won't protect you from everything (see _Honest caveats_ below), but it |
| 22 | +makes the common path less error-prone. |
| 23 | + |
| 24 | +## How it works |
| 25 | + |
| 26 | +You define a `TypedName(msg)` that bundles a name string with a codec |
| 27 | +(encoder + decoder). Use the same `TypedName` to register an actor and to |
| 28 | +look it up the compiler makes sure both sides agree on the `msg` type. |
| 29 | + |
| 30 | +Under the hood every actor is a regular `gen_statem` started through |
| 31 | +`gleam/otp/actor`, so `observer`, `sys:get_status` and supervision trees work |
| 32 | +as usual. |
| 33 | + |
| 34 | +## Quick start |
| 35 | + |
| 36 | +```sh |
| 37 | +gleam add distribute |
| 38 | +``` |
| 39 | + |
| 40 | +```gleam |
| 41 | +import distribute |
| 42 | +import distribute/codec |
| 43 | +import distribute/registry |
| 44 | +import distribute/receiver |
| 45 | +
|
| 46 | +pub fn main() { |
| 47 | + // start a node |
| 48 | + let assert Ok(Nil) = distribute.start("myapp@127.0.0.1", "secret") |
| 49 | +
|
| 50 | + // define the protocol — name + codec, shared across the codebase |
| 51 | + let greeter = registry.named("greeter", codec.string()) |
| 52 | +
|
| 53 | + // start a named actor and register it globally |
| 54 | + let assert Ok(gs) = distribute.start_actor(greeter, Nil, fn(msg, _state) { |
| 55 | + io.println("Got: " <> msg) |
| 56 | + receiver.Continue(Nil) |
| 57 | + }) |
| 58 | + let assert Ok(Nil) = distribute.register(greeter, gs) |
| 59 | +
|
| 60 | + // from any node: look up by the same TypedName and send |
| 61 | + let assert Ok(remote) = distribute.lookup(greeter) |
| 62 | + let assert Ok(Nil) = distribute.send(remote, "Hello, cluster!") |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +## Modules |
| 67 | + |
| 68 | +| Module | What it does | |
| 69 | +|--------|-------------| |
| 70 | +| `distribute` | Facade — start, connect, send, lookup | |
| 71 | +| `distribute/actor` | Named actor lifecycle, supervision, pools | |
| 72 | +| `distribute/cluster` | Node start/connect/ping | |
| 73 | +| `distribute/codec` | Binary encoder/decoder for primitives | |
| 74 | +| `distribute/codec/composite` | Option, Result, Tuple codecs | |
| 75 | +| `distribute/codec/tagged` | Tagged messages with version checks | |
| 76 | +| `distribute/global` | `GlobalSubject(msg)` — typed wrapper around `Subject(BitArray)` | |
| 77 | +| `distribute/registry` | `TypedName(msg)`, global registration and lookup | |
| 78 | +| `distribute/receiver` | Message reception, OTP actor loops | |
| 79 | + |
| 80 | +## Honest caveats |
| 81 | + |
| 82 | +**What the types catch:** |
| 83 | +- Within one codebase, `TypedName` and `GlobalSubject` prevent you from mixing |
| 84 | + up message types at compile time. |
| 85 | + |
| 86 | +**What they don't catch:** |
| 87 | +- Two separate projects using different codecs for the same name. The codec |
| 88 | + will reject the message at runtime, not at compile time. |
| 89 | +- Erlang code sending raw terms to a `distribute` actor. Same story — runtime |
| 90 | + rejection, no compile-time help. |
| 91 | +- The BEAM doesn't carry Gleam types across nodes. `distribute` validates via |
| 92 | + binary codecs, which is the best you can do, but it's not the same as a |
| 93 | + shared type system. |
| 94 | + |
| 95 | +**Subject construction:** Gleam's `Subject` is opaque. To build one from a |
| 96 | +remote PID and a deterministic tag (which is how registry lookup works), I |
| 97 | +construct the `{subject, Pid, Tag}` tuple in our own Erlang FFI |
| 98 | +([one function](src/distribute_ffi_utils.erl)). If `gleam_erlang` changes the |
| 99 | +representation, that's the one place to update. |
| 100 | + |
| 101 | +## Development |
| 102 | + |
| 103 | +```sh |
| 104 | +gleam test |
| 105 | +gleam docs build |
| 106 | +``` |
0 commit comments