Jido Signal Basics - Interactive Guide
Mix.install([
{:jido_signal, "~> 1.0"}
])
Introduction
Welcome to the Jido Signal interactive guide! This Livebook will walk you through the core concepts and basic usage of Jido Signal, a powerful toolkit for building event-driven and agent-based systems in Elixir.
Jido Signal provides:
- Standardized messaging using CloudEvents v1.0.2 specification
- High-performance Signal Bus for pub/sub communication
- Advanced routing with pattern matching and wildcards
- Flexible dispatch to multiple destinations
- Causality tracking for complete system observability
Let’s dive in and explore these concepts with hands-on examples!
Section 1: Creating Your First Signal
A Signal is a CloudEvents-compliant message envelope that carries your application’s events. Let’s create our first signal:
# Basic signal creation
{:ok, basic_signal} = Jido.Signal.new(%{
type: "user.created",
source: "/auth/registration",
data: %{user_id: "123", email: "user@example.com", name: "John Doe"}
})
IO.inspect(basic_signal, label: "Basic Signal")
Every signal has essential fields:
-
id
: Unique identifier (auto-generated) -
type
: Event type (e.g., “user.created”) -
source
: Where the event originated -
time
: When the event occurred (auto-generated) -
data
: The actual event payload
# Explore signal structure
IO.puts("Signal ID: #{basic_signal.id}")
IO.puts("Signal Type: #{basic_signal.type}")
IO.puts("Signal Source: #{basic_signal.source}")
IO.puts("Signal Time: #{basic_signal.time}")
IO.puts("Signal Data: #{inspect(basic_signal.data)}")
Section 2: Custom Signal Types
For better structure and validation, you can define custom signal types:
defmodule UserCreated do
use Jido.Signal,
type: "user.created.v1",
default_source: "/auth/users",
schema: [
user_id: [type: :string, required: true],
email: [type: :string, required: true, format: ~r/@/],
name: [type: :string, required: true]
]
end
# Create a typed signal
{:ok, typed_signal} = UserCreated.new(%{
user_id: "u_456",
email: "jane@example.com",
name: "Jane Smith"
})
IO.inspect(typed_signal, label: "Typed Signal")
Let’s see what happens with invalid data:
# This will fail validation
case UserCreated.new(%{user_id: "u_789", email: "invalid-email"}) do
{:ok, signal} -> IO.puts("Signal created successfully")
{:error, reason} -> IO.puts("Validation error: #{reason}")
end
Section 3: Starting a Signal Bus
The Signal Bus is the heart of Jido Signal - it manages subscriptions, routing, and message delivery. Let’s start one:
# Start a Signal Bus
{:ok, bus_pid} = Jido.Signal.Bus.start_link(name: :demo_bus)
IO.puts("Signal Bus started successfully!")
IO.inspect(bus_pid, label: "Bus PID")
Section 4: Subscribing to Signals
Now let’s create a simple subscriber that will receive signals:
defmodule SimpleSubscriber do
use GenServer
def start_link(name) do
GenServer.start_link(__MODULE__, %{name: name}, name: name)
end
def init(state) do
IO.puts("#{state.name} subscriber started")
{:ok, state}
end
def handle_info({:signal, signal}, state) do
IO.puts("#{state.name} received signal: #{signal.type}")
IO.puts(" Data: #{inspect(signal.data)}")
{:noreply, state}
end
end
# Start our subscriber
{:ok, _sub_pid} = SimpleSubscriber.start_link(:user_subscriber)
Now let’s subscribe to user-related events:
# Subscribe to all user events using wildcard pattern
{:ok, subscription_id} = Jido.Signal.Bus.subscribe(
:demo_bus,
"user.*",
dispatch: {:pid, target: :user_subscriber}
)
IO.puts("Subscribed with ID: #{subscription_id}")
Section 5: Publishing Signals
Let’s publish some signals and see our subscriber in action:
# Publish a basic signal
{:ok, signal1} = Jido.Signal.new(%{
type: "user.created",
source: "/registration",
data: %{user_id: "u_001", email: "alice@example.com"}
})
Jido.Signal.Bus.publish(:demo_bus, [signal1])
# Wait a moment to see the output
Process.sleep(100)
# Publish using our custom signal type
{:ok, signal2} = UserCreated.new(%{
user_id: "u_002",
email: "bob@example.com",
name: "Bob Johnson"
})
Jido.Signal.Bus.publish(:demo_bus, [signal2])
Process.sleep(100)
# Publish a signal that won't match our subscription
{:ok, signal3} = Jido.Signal.new(%{
type: "order.created",
source: "/ecommerce",
data: %{order_id: "ord_001", amount: 99.99}
})
Jido.Signal.Bus.publish(:demo_bus, [signal3])
Process.sleep(100)
IO.puts("Notice: order.created signal was published but user_subscriber didn't receive it!")
Section 6: Multiple Subscribers with Different Patterns
Let’s create more subscribers with different subscription patterns:
# Start additional subscribers
{:ok, _} = SimpleSubscriber.start_link(:order_subscriber)
{:ok, _} = SimpleSubscriber.start_link(:all_events_subscriber)
# Subscribe to order events
{:ok, _} = Jido.Signal.Bus.subscribe(
:demo_bus,
"order.*",
dispatch: {:pid, target: :order_subscriber}
)
# Subscribe to all events using multi-level wildcard
{:ok, _} = Jido.Signal.Bus.subscribe(
:demo_bus,
"**",
dispatch: {:pid, target: :all_events_subscriber}
)
IO.puts("Additional subscribers created!")
Now let’s publish various signals to see the routing in action:
signals = [
{:ok, user_signal} = Jido.Signal.new(%{
type: "user.updated",
source: "/profile",
data: %{user_id: "u_001", field: "email"}
}),
{:ok, order_signal} = Jido.Signal.new(%{
type: "order.shipped",
source: "/fulfillment",
data: %{order_id: "ord_001", tracking: "TRK123"}
}),
{:ok, payment_signal} = Jido.Signal.new(%{
type: "payment.processed",
source: "/billing",
data: %{payment_id: "pay_001", amount: 99.99}
})
]
# Publish all signals
Enum.each([user_signal, order_signal, payment_signal], fn signal ->
Jido.Signal.Bus.publish(:demo_bus, [signal])
end)
Process.sleep(500)
IO.puts("All signals published!")
Section 7: Signal History and Replay
The Signal Bus maintains a history of all published signals. Let’s explore this:
# Get all signals from the bus
{:ok, all_signals} = Jido.Signal.Bus.replay(:demo_bus, "**")
IO.puts("Total signals in bus: #{length(all_signals)}")
Enum.each(all_signals, fn signal ->
IO.puts("- #{signal.type} (#{signal.id})")
end)
You can also replay signals matching specific patterns:
# Get only user-related signals
{:ok, user_signals} = Jido.Signal.Bus.replay(:demo_bus, "user.*")
IO.puts("\nUser signals:")
Enum.each(user_signals, fn signal ->
IO.puts("- #{signal.type}: #{inspect(signal.data)}")
end)
Section 8: Dispatch to Multiple Destinations
Signals can be dispatched to multiple destinations simultaneously. Let’s see this in action:
# Create a signal with multiple dispatch configurations
{:ok, multi_dispatch_signal} = Jido.Signal.new(%{
type: "user.premium.upgraded",
source: "/billing",
data: %{user_id: "u_003", plan: "premium"},
jido_dispatch: [
{:pid, target: :user_subscriber},
{:console, format: :json},
{:logger, level: :info}
]
})
# Publish the signal
Jido.Signal.Bus.publish(:demo_bus, [multi_dispatch_signal])
Process.sleep(200)
IO.puts("Multi-dispatch signal sent!")
Section 9: Working with Signal Relationships
Signals can be related to each other, creating cause-and-effect chains:
# Create a parent signal
{:ok, parent_signal} = Jido.Signal.new(%{
type: "user.registration.started",
source: "/auth",
data: %{user_id: "u_004", email: "charlie@example.com"}
})
# Create a child signal that was caused by the parent
{:ok, child_signal} = Jido.Signal.new(%{
type: "user.verification.email.sent",
source: "/notifications",
data: %{user_id: "u_004", email: "charlie@example.com"},
jido_cause: parent_signal.id # Link to parent signal
})
# Publish both signals
Jido.Signal.Bus.publish(:demo_bus, [parent_signal, child_signal])
Process.sleep(100)
IO.puts("Parent signal ID: #{parent_signal.id}")
IO.puts("Child signal ID: #{child_signal.id}")
IO.puts("Child signal cause: #{child_signal.jido_cause}")
Section 10: Advanced Routing with Custom Functions
The router supports custom matching functions for complex routing logic:
# Let's create a more sophisticated subscriber
defmodule AdvancedSubscriber do
use GenServer
def start_link(name) do
GenServer.start_link(__MODULE__, %{name: name}, name: name)
end
def init(state) do
{:ok, state}
end
def handle_info({:signal, signal}, state) do
IO.puts("#{state.name} - Advanced routing matched: #{signal.type}")
{:noreply, state}
end
end
{:ok, _} = AdvancedSubscriber.start_link(:advanced_subscriber)
# Subscribe with a custom matching function
{:ok, _} = Jido.Signal.Bus.subscribe(
:demo_bus,
fn signal ->
# Match signals that contain "premium" in their data
signal.data
|> Enum.any?(fn {_k, v} ->
is_binary(v) and String.contains?(v, "premium")
end)
end,
dispatch: {:pid, target: :advanced_subscriber}
)
# Test the custom routing
{:ok, premium_signal} = Jido.Signal.new(%{
type: "subscription.created",
source: "/billing",
data: %{plan: "premium", user_id: "u_005"}
})
{:ok, basic_signal} = Jido.Signal.new(%{
type: "subscription.created",
source: "/billing",
data: %{plan: "basic", user_id: "u_006"}
})
Jido.Signal.Bus.publish(:demo_bus, [premium_signal, basic_signal])
Process.sleep(200)
Section 11: Bus Statistics and Monitoring
Let’s check some statistics about our Signal Bus:
# Get bus information
info = Jido.Signal.Bus.info(:demo_bus)
IO.puts("Bus Statistics:")
IO.puts("- Total signals published: #{info.total_signals}")
IO.puts("- Active subscriptions: #{length(info.subscriptions)}")
IO.puts("- Signals in history: #{length(info.signal_history)}")
IO.puts("\nActive Subscriptions:")
Enum.each(info.subscriptions, fn {sub_id, subscription} ->
IO.puts("- #{sub_id}: #{subscription.pattern}")
end)
Conclusion
Congratulations! You’ve learned the basics of Jido Signal:
- Creating Signals - Both basic and custom typed signals
- Starting a Signal Bus - The central hub for message routing
- Subscribing to Events - Using patterns and wildcards
- Publishing Signals - Sending messages through the bus
- Pattern Matching - Routing signals to the right subscribers
- Multiple Dispatch - Sending to multiple destinations
- Signal History - Replaying past events
- Causality Tracking - Linking related signals
- Advanced Routing - Custom matching functions
- Monitoring - Observing bus statistics
Next Steps
Now that you understand the basics, you can explore:
- Persistent Subscriptions - For reliable delivery with acknowledgments
- Middleware - Adding cross-cutting concerns like logging and metrics
- Snapshots - Creating point-in-time views of your signal log
- Journal System - Advanced causality tracking and analysis
- HTTP Webhooks - Dispatching signals to external services
- Integration Patterns - Using Jido Signal in real applications
Check out the full documentation for more advanced features and patterns!
Cleanup
# Stop the bus and clean up
Jido.Signal.Bus.stop(:demo_bus)
IO.puts("Demo complete - Signal Bus stopped")