Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.1] - 2026-06-16

### Fixed

- Elixir 1.20 compatibility. The generated struct `channel/2` no longer trips the new type
checker's "redundant clause" warning in consuming applications, and
`Amplified.PubSub.Protocol` no longer uses a deprecated default argument in its `channel`
definition. No behaviour change.

## [0.2.0] - 2026-03-20

### Added
Expand Down
27 changes: 18 additions & 9 deletions lib/amplified/pubsub.ex
Original file line number Diff line number Diff line change
Expand Up @@ -462,19 +462,28 @@ defmodule Amplified.PubSub do
subject
end

# Default channel: derives from module name + struct id
# Default channel: derives from module name + struct id. A struct without an `:id`
# field falls through to `nil` and should override `channel/2` (see moduledoc).
#
# The `:id` check uses `Map.has_key?/2` (a plain call) inside a single clause rather than a
# `%module{id: id}` pattern or an `is_map_key/2` guard plus a `_` fallback. Each generated
# impl is `for:` a single struct, so the Elixir 1.20+ type checker narrows both the pattern
# and the guard to statically cover every value, flagging the `nil` fallback as redundant.
# A plain function call is opaque to the checker, keeping the `nil` branch reachable.
def channel(subject, ns \\ nil)

def channel(%module{id: id}, ns) do
module
|> Module.split()
|> List.last()
|> Recase.to_snake()
|> then(&PubSub.channel("#{&1}:#{id}", ns))
def channel(%module{} = subject, ns) do
if Map.has_key?(subject, :id) do
module
|> Module.split()
|> List.last()
|> Recase.to_snake()
|> then(&PubSub.channel("#{&1}:#{Map.fetch!(subject, :id)}", ns))
else
nil
end
end

def channel(_subject, _ns), do: nil

# Default subscribe/unsubscribe: delegates via channel
def subscribe(subject) do
subject |> channel() |> PubSub.subscribe()
Expand Down
8 changes: 7 additions & 1 deletion lib/amplified/pubsub/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,17 @@ defprotocol Amplified.PubSub.Protocol do
e.g. `"post:abc-123"`. The optional `namespace` is appended with a `:`
separator.
"""
@spec channel(subject :: struct()) :: binary()
def channel(subject)

@doc """
Returns the PubSub channel name for the given subject, scoped to `namespace`.
"""
@spec channel(
subject :: struct(),
namespace :: String.t() | atom() | nil
) :: binary()
def channel(subject, namespace \\ nil)
def channel(subject, namespace)

@doc """
Subscribes the current process to the PubSub channel for the given subject.
Expand Down
2 changes: 1 addition & 1 deletion lib/amplified/pubsub/tuple.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ defimpl Amplified.PubSub.Protocol, for: Tuple do
def broadcast(error, _message), do: error
def broadcast({:ok, subject}, event, attrs), do: {:ok, PubSub.broadcast(subject, event, attrs)}
def broadcast(error, _message, _attrs), do: error
def channel(tuple, _ns), do: raise("No channel for #{inspect(tuple)}")
def channel(tuple, _ns \\ nil), do: raise("No channel for #{inspect(tuple)}")
def subscribe(tuple), do: raise("Cannot subscribe to #{inspect(tuple)}")
def unsubscribe(tuple), do: raise("Cannot unsubscribe from #{inspect(tuple)}")

Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Amplified.PubSub.MixProject do
use Mix.Project

@version "0.2.0"
@version "0.2.1"
@source_url "https://github.com/amplifiedai/amplified_pubsub"

def project do
Expand Down
Loading