|
| 1 | +# distribute |
| 2 | + |
| 3 | +<p align="center"> |
| 4 | + <img src="assets/img/distribute.png" alt="distribute logo" width="200" /> |
| 5 | +</p> |
| 6 | + |
| 7 | +Typed distributed messaging for Gleam on the BEAM. |
| 8 | + |
| 9 | +[](https://hex.pm/packages/distribute) |
| 10 | +[](https://hexdocs.pm/distribute/) |
| 11 | + |
| 12 | +## What this is |
| 13 | + |
| 14 | +Erlang already gives you distribution, but from Gleam you lose type info at |
| 15 | +the node boundary — everything crosses the wire as raw terms. `distribute` |
| 16 | +puts binary codecs in front of `:global` and `Subject` so the compiler can |
| 17 | +catch mismatches before messages leave the process. |
| 18 | + |
| 19 | +"Typed" here means checked at encode/decode boundaries. There is no shared |
| 20 | +type system across nodes — the BEAM doesn't work that way. |
| 21 | + |
| 22 | +## Install |
| 23 | + |
| 24 | +```sh |
| 25 | +gleam add distribute |
| 26 | +``` |
| 27 | + |
| 28 | +## Usage |
| 29 | + |
| 30 | +### Fire-and-forget |
| 31 | + |
| 32 | +Define a `TypedName(msg)` that pairs a name with a codec, then use it on |
| 33 | +both sides. The compiler won't let you register a `String` actor and look |
| 34 | +it up as `Int`. |
| 35 | + |
| 36 | +```gleam |
| 37 | +import distribute |
| 38 | +import distribute/codec |
| 39 | +import distribute/registry |
| 40 | +import distribute/receiver |
| 41 | +
|
| 42 | +// one TypedName, shared across the codebase |
| 43 | +let greeter = registry.named("greeter", codec.string()) |
| 44 | +
|
| 45 | +// start + register |
| 46 | +let assert Ok(gs) = distribute.start_actor(greeter, Nil, fn(msg, _state) { |
| 47 | + io.println("Got: " <> msg) |
| 48 | + receiver.Continue(Nil) |
| 49 | +}) |
| 50 | +let assert Ok(Nil) = distribute.register(greeter, gs) |
| 51 | +
|
| 52 | +// from any node |
| 53 | +let assert Ok(remote) = distribute.lookup(greeter) |
| 54 | +let assert Ok(Nil) = distribute.send(remote, "hello") |
| 55 | +``` |
| 56 | + |
| 57 | +### Request / response |
| 58 | + |
| 59 | +Include a reply `Subject(BitArray)` in your message type and use |
| 60 | +`codec.subject()` to serialize it. The `Subject` carries node info, so |
| 61 | +replies route back across nodes automatically. |
| 62 | + |
| 63 | +```gleam |
| 64 | +import distribute/codec |
| 65 | +import distribute/global |
| 66 | +import distribute/receiver |
| 67 | +import gleam/erlang/process |
| 68 | +
|
| 69 | +type CounterMsg { |
| 70 | + Inc(Int) |
| 71 | + Get(reply: process.Subject(BitArray)) |
| 72 | +} |
| 73 | +
|
| 74 | +// handler side |
| 75 | +fn handle(msg, state) { |
| 76 | + case msg { |
| 77 | + Inc(n) -> receiver.Continue(state + n) |
| 78 | + Get(reply) -> { |
| 79 | + let _ = global.reply(reply, state, codec.int_encoder()) |
| 80 | + receiver.Continue(state) |
| 81 | + } |
| 82 | + } |
| 83 | +} |
| 84 | +
|
| 85 | +// caller side |
| 86 | +let assert Ok(count) = global.call(counter, Get, codec.int_decoder(), 5000) |
| 87 | +``` |
| 88 | + |
| 89 | +`global.call` creates a temporary subject, sends the request, waits for |
| 90 | +the response, decodes it. Same idea as `gen_server:call`. |
| 91 | + |
| 92 | +### Codecs |
| 93 | + |
| 94 | +Primitives: `codec.int()`, `codec.string()`, `codec.float()`, `codec.bool()`, |
| 95 | +`codec.bitarray()`, `codec.nil()`. |
| 96 | + |
| 97 | +Composites: `codec.list(c)`, `codec.subject()`, `codec.map(c, wrap, unwrap)`, |
| 98 | +`composite.option(c)`, `composite.result(ok, err)`, |
| 99 | +`composite.tuple2(a, b)`, `composite.tuple3(a, b, c)`. |
| 100 | + |
| 101 | +For your own types, use `codec.map`: |
| 102 | + |
| 103 | +```gleam |
| 104 | +type UserId { UserId(Int) } |
| 105 | +
|
| 106 | +let user_id_codec = codec.map(codec.int(), UserId, fn(uid) { |
| 107 | + let UserId(n) = uid |
| 108 | + n |
| 109 | +}) |
| 110 | +``` |
| 111 | + |
| 112 | +Gleam has no derive macros or reflection, so codecs for complex types |
| 113 | +are manual. The combinators handle the serialization — you just wire |
| 114 | +the fields together. |
| 115 | + |
| 116 | +## Modules |
| 117 | + |
| 118 | +| Module | Does | |
| 119 | +|--------|------| |
| 120 | +| `distribute` | Facade — start node, connect, send, lookup | |
| 121 | +| `distribute/actor` | Named actors, supervision, pools | |
| 122 | +| `distribute/cluster` | `net_kernel` start/connect/ping | |
| 123 | +| `distribute/codec` | Binary codecs for primitives + `subject()` | |
| 124 | +| `distribute/codec/composite` | Option, Result, Tuple codecs | |
| 125 | +| `distribute/codec/tagged` | Tagged messages with version field | |
| 126 | +| `distribute/global` | `GlobalSubject(msg)`, `call`, `reply` | |
| 127 | +| `distribute/registry` | `TypedName(msg)`, `:global` registration | |
| 128 | +| `distribute/receiver` | Typed receive, OTP actor wrappers | |
| 129 | + |
| 130 | +## Caveats |
| 131 | + |
| 132 | +**What the types catch** — within one codebase, `TypedName` and |
| 133 | +`GlobalSubject` prevent mixing up message types at compile time. |
| 134 | + |
| 135 | +**What they don't** — two separate codebases using different codecs for |
| 136 | +the same name. The codec will reject the binary at runtime, not at compile |
| 137 | +time. Same for Erlang code sending raw terms to a `distribute` actor. |
| 138 | + |
| 139 | +**Subject construction** — Gleam's `Subject` is opaque. To build one from |
| 140 | +a remote PID and a deterministic tag (how registry lookup works), we |
| 141 | +construct the `{subject, Pid, Tag}` tuple in |
| 142 | +[one Erlang function](src/distribute_ffi_utils.erl). If `gleam_erlang` |
| 143 | +changes the internal representation, that single function needs updating. |
| 144 | + |
| 145 | +**No auto-derive** — Gleam doesn't have macros. Complex message codecs |
| 146 | +are manual. The combinators (`map`, `list`, `option`, `tuple2`, etc.) |
| 147 | +keep it manageable, but it's not zero-boilerplate. |
| 148 | + |
| 149 | +## Development |
| 150 | + |
| 151 | +```sh |
| 152 | +gleam test |
| 153 | +gleam docs build |
| 154 | +``` |
0 commit comments