بنقرة واحدة
gleam-otp-actors
gleam-otp-actors
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
gleam-otp-actors
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Diagnose Azedarach command latency and daemon/TUI performance with OpenTelemetry traces exported to a local Jaeger backend. Use when investigating slow `az` commands, TUI stalls, daemon RPC latency, missing spans, local Jaeger setup, or trace-driven performance regressions in this repository.
Review and integrate Azedarach issues that are in_review. Use when Codex is asked to run a reviewer/integrator session, sweep active issues that are ready for review, inspect worker evidence and diffs, return actionable findings to live or paused issue sessions, close accepted work, or get in_review issues integrated.
Review, consolidate, test, and harden Azedarach SQLite migrations before integration. Use for any migration, schema ensure/repair logic, trigger or index change, persistence-authority change, migration failure, or pre-merge review of a branch containing database changes. Also use when validating upgrades against real root-user or registered project databases.
Run an iterative code review and fix cycle until findings stabilize. Use when the user asks Codex to review code, review and fix, loop review/fix, address review findings, harden a change before handoff, or keep reviewing until repeated passes find no new actionable issues. Also use proactively before Codex declares coding work done, marks an issue in_review, says a change is ready, or hands off implementation that modified code, tests, build scripts, migrations, config, or developer tooling.
Go Testing Skill
Build and review production-grade Go logging and observability with `log/slog` and OpenTelemetry. Use when adding or refactoring logs, instrumenting request paths, defining event schemas, reducing log cost/cardinality, enforcing redaction/PII policy, correlating logs with traces, or reviewing Go services for observability gaps.
| name | gleam-otp-actors |
| description | gleam-otp-actors |
Essential patterns for building correct, message-passing actors in Gleam using gleam_otp.
In Gleam OTP, a Subject(Msg) is a typed mailbox. Messages sent to a subject are received by whoever is blocking on process.receive(subject, timeout).
Critical insight: process.new_subject() creates a brand new, independent mailbox that is NOT connected to any actor's message handler. If nobody calls receive on it, messages are silently discarded.
// ❌ WRONG: Orphan subject - nobody listens!
fn handle_message(state, msg) {
case msg {
DoAsyncWork -> {
let reply_to = process.new_subject() // Creates orphan!
spawn_worker(reply_to) // Worker sends result to orphan
actor.continue(state) // Result is LOST
}
}
}
The spawned worker sends its result to reply_to, but:
Symptoms:
Actors don't have built-in access to their own subject. Pass it explicitly:
// Define message with self parameter
pub type Msg {
Initialize(self: Subject(Msg))
PeriodicTick
WorkerResult(data: String)
}
// Caller passes subject when initializing
pub fn initialize(subject: Subject(Msg)) -> Nil {
process.send(subject, Initialize(subject))
}
// Handler stores and uses self reference
fn handle_message(state, msg) {
case msg {
Initialize(self) -> {
// Store self for later use
schedule_tick(self)
actor.continue(State(..state, self_subject: Some(self)))
}
PeriodicTick -> {
do_periodic_work()
// Use stored self to schedule next tick
case state.self_subject {
Some(self) -> schedule_tick(self)
None -> Nil
}
actor.continue(state)
}
}
}
// Timer sends back to actor's real subject
fn schedule_tick(self: Subject(Msg)) -> Nil {
let _ = process.spawn(fn() {
process.sleep(1000)
process.send(self, PeriodicTick) // Goes to actor's handler!
})
Nil
}
For spawning async work that returns results to the actor:
pub type Msg {
StartWork(id: String)
WorkComplete(id: String, result: Result(Data, Error))
}
fn handle_message(state, msg) {
case msg {
StartWork(id) -> {
// Use self_subject to receive result
case state.self_subject {
Some(self) -> spawn_async_worker(self, id)
None -> Nil // Not initialized, skip
}
actor.continue(state)
}
WorkComplete(id, result) -> {
// Process result and update state
let new_state = apply_result(state, id, result)
actor.continue(new_state)
}
}
}
fn spawn_async_worker(reply_to: Subject(Msg), id: String) -> Nil {
let _ = process.spawn(fn() {
let result = do_expensive_work(id)
process.send(reply_to, WorkComplete(id, result))
})
Nil
}
When forwarding messages between actors with different message types:
// Source actor sends: SourceMsg
// Target actor expects: TargetMsg
/// Create a bridge that converts SourceMsg -> TargetMsg
fn create_bridge(
target: Subject(TargetMsg),
) -> Subject(SourceMsg) {
let bridge_subject = process.new_subject()
// Spawn process that listens on bridge and forwards to target
let _ = process.spawn(fn() {
forward_loop(bridge_subject, target)
})
bridge_subject // Give this to the source actor
}
fn forward_loop(
from: Subject(SourceMsg),
to: Subject(TargetMsg),
) -> Nil {
case process.receive(from, 60_000) {
Ok(SourceEvent(id, data)) -> {
// Convert and forward
process.send(to, TargetEvent(id, data))
forward_loop(from, to) // Continue listening
}
Ok(SourceError(id, reason)) -> {
process.send(to, TargetError(id, reason))
forward_loop(from, to)
}
Error(_) -> {
// Timeout - continue listening
forward_loop(from, to)
}
}
}
Key insight: The bridge subject is NOT orphan because we spawn a process that immediately starts receiving on it.
For synchronous calls where caller blocks waiting for response:
// In the API module
pub fn get_monitors(supervisor: Subject(Msg)) -> List(String) {
let reply_subject = process.new_subject() // OK here!
process.send(supervisor, ListMonitors(reply_subject))
// Caller blocks until reply received
case process.receive(reply_subject, 5000) {
Ok(monitors) -> monitors
Error(_) -> [] // Timeout
}
}
// In the actor's message handler
fn handle_message(state, msg) {
case msg {
ListMonitors(reply_to) -> {
let monitors = dict.keys(state.monitors)
process.send(reply_to, monitors) // Reply to waiting caller
actor.continue(state)
}
}
}
Why this works: The caller immediately calls process.receive() on the subject, so someone IS listening. The subject isn't orphaned.
When actors depend on each other's subjects, start in dependency order:
// ❌ WRONG: Start order doesn't respect dependencies
fn start_children() {
let callback = process.new_subject() // Orphan!
start_worker(callback) // Worker will send to orphan
start_coordinator() // Coordinator should receive, but started too late
}
// ✅ CORRECT: Start receiver first, then sender
fn start_children() {
// Start coordinator first - it will receive updates
case coordinator.start() {
Ok(coord) -> {
// Create bridge that forwards to coordinator
let callback = create_bridge(coord)
// Now start worker with valid callback
start_worker(callback)
}
Error(_) -> // Handle error
}
}
Before using process.new_subject(), verify:
Is someone calling receive() on it?
For async results, am I using the actor's self?
process.new_subject() for results: WRONGstate.self_subject for results: CORRECTFor timers/schedulers, where do messages go?
process.send(new_subject, Tick): Lostprocess.send(self_subject, Tick): HandledFor bridges, is there a receiver process?
Symptom: "Actor discarding unexpected message"
Symptom: State never updates after async operation
self_subject is None when spawningSymptom: Periodic tasks stop after first run
// Get actor's self: Pass via Initialize message, store in state
// Schedule timer: spawn + sleep + send to self
// Async with reply: spawn + work + send result to self
// Sync request-reply: new_subject + send request + receive reply
// Bridge pattern: new_subject + spawn receiver loop + return subject
// NEVER: new_subject for fire-and-forget results