Skip to content

lupodevelop/distribute

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

distribute

distribute logo

Typed distributed messaging for Gleam on the BEAM.

Package Version Hex Docs

What this is

Erlang already gives you distribution, but from Gleam you lose type info at the node boundary, everything crosses the wire as raw terms. distribute puts binary codecs in front of :global and Subject so the compiler can catch mismatches before messages leave the process.

"Typed" here means checked at encode/decode boundaries. There is no shared type system across nodes. The BEAM doesn't work that way.

Install

gleam add distribute

Usage

Fire-and-forget

Define a TypedName(msg) that pairs a name with a codec, then use it on both sides. The compiler won't let you register a String actor and look it up as Int.

import distribute
import distribute/codec
import distribute/registry
import distribute/receiver

// one TypedName, shared across the codebase
let greeter = registry.named("greeter", codec.string())

// start + register
let assert Ok(gs) = distribute.start_actor(greeter, Nil, fn(msg, _state) {
  io.println("Got: " <> msg)
  receiver.Continue(Nil)
})
let assert Ok(Nil) = distribute.register(greeter, gs)

// from any node
let assert Ok(remote) = distribute.lookup(greeter)
let assert Ok(Nil) = distribute.send(remote, "hello")

Request / response

Include a reply Subject(BitArray) in your message type and use codec.subject() to serialize it. The Subject carries node info, so replies route back across nodes automatically.

import distribute/codec
import distribute/global
import distribute/receiver
import gleam/erlang/process

type CounterMsg {
  Inc(Int)
  Get(reply: process.Subject(BitArray))
}

// handler side
fn handle(msg, state) {
  case msg {
    Inc(n) -> receiver.Continue(state + n)
    Get(reply) -> {
      let _ = global.reply(reply, state, codec.int_encoder())
      receiver.Continue(state)
    }
  }
}

// caller side
let assert Ok(count) = global.call(counter, Get, codec.int_decoder(), 5000)

global.call creates a temporary subject, sends the request, waits for the response, decodes it. Same idea as gen_server:call.

Codecs

Primitives: codec.int(), codec.string(), codec.float(), codec.bool(), codec.bitarray(), codec.nil().

Composites: codec.list(c), codec.subject(), codec.map(c, wrap, unwrap), composite.option(c), composite.result(ok, err), composite.tuple2(a, b), composite.tuple3(a, b, c).

For your own types, use codec.map:

type UserId { UserId(Int) }

let user_id_codec = codec.map(codec.int(), UserId, fn(uid) {
  let UserId(n) = uid
  n
})

Gleam has no derive macros or reflection, so codecs for complex types are manual. The combinators handle the serialization so you just wire the fields together!

Modules

Module Does
distribute Facade: start node, connect, send, lookup
distribute/actor Named actors, supervision, pools
distribute/cluster net_kernel start/connect/ping
distribute/codec Binary codecs for primitives + subject()
distribute/codec/composite Option, Result, Tuple codecs
distribute/codec/variant Build codecs for Custom Types (ADTs)
distribute/codec/tagged Tagged messages with version field
distribute/global GlobalSubject(msg), call, reply
distribute/cluster/monitor NodeUp, NodeDown typed events
distribute/registry TypedName(msg), :global registration
distribute/receiver Typed receive, OTP actor wrappers

Custom Type Codecs

Seamlessly encode and decode your Algebraic Data Types (enums) with a fluent builder.

pub type MyMessage {
  Text(String)
  Ping
}

import distribute/codec
import distribute/codec/variant

let my_codec = 
  variant.new()
  |> variant.add(0, "Text", codec.string(), Text, fn(m) {
    case m { Text(s) -> Ok(s); _ -> Error(Nil) }
  })
  |> variant.unit(1, "Ping", Ping, fn(m) { m == Ping })
  |> variant.build()

Cluster Monitoring

Subscribe to cluster events (NodeUp, NodeDown) to react to node topology changes.

import distribute
import distribute/cluster/monitor

let subj = process.new_subject()
let assert Ok(m) = distribute.subscribe(subj)

// In your actor/process
case process.receive(subj, 5000) {
  Ok(monitor.NodeUp(node)) -> io.println("Node joined: " <> node)
  Ok(monitor.NodeDown(node)) -> io.println("Node left: " <> node)
  _ -> Nil
}

// Later
distribute.unsubscribe(m)

Caveats

What the types catch — within one codebase, TypedName and GlobalSubject prevent mixing up message types at compile time.

What they don't — two separate codebases using different codecs for the same name. The codec will reject the binary at runtime, not at compile time. Same for Erlang code sending raw terms to a distribute actor.

Subject construction — Gleam's Subject is opaque. To build one from a remote PID and a deterministic tag (how registry lookup works), we construct the {subject, Pid, Tag} tuple in one Erlang function. If gleam_erlang changes the internal representation, that single function needs updating.

No auto-derive — Gleam doesn't have macros. Complex message codecs are manual. The combinators (map, list, option, tuple2, etc.) keep it manageable, but it's not zero-boilerplate.

Development

gleam test
gleam docs build

About

Typed distributed messaging for Gleam on the BEAM.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors