defmodule Jido.Chat.Adapters.Threading do
@moduledoc """
Behaviour for adapter-specific threading support.
Threading is fundamental to messaging but varies significantly across platforms.
This behaviour defines how adapters can implement platform-specific threading
logic while providing a normalized output for the messaging pipeline.
## Implementation
Adapters that support threading should implement this behaviour:
defmodule MyApp.Channels.Slack do
@behaviour Jido.Chat.Adapter
@behaviour Jido.Chat.Adapters.Threading
@impl Jido.Chat.Adapters.Threading
def supports_threads?, do: true
@impl Jido.Chat.Adapters.Threading
def compute_thread_root(raw) do
# Slack uses thread_ts as the thread identifier
raw["thread_ts"] || raw["ts"]
end
@impl Jido.Chat.Adapters.Threading
def extract_thread_context(raw) do
%{
thread_id: raw["thread_ts"],
is_thread_reply: raw["thread_ts"] != nil,
thread_root_ts: raw["thread_ts"] || raw["ts"]
}
end
end
## Default Implementations
All callbacks are optional. The defaults assume no threading support:
* `supports_threads?/0` - Returns `false`
* `compute_thread_root/1` - Returns `nil`
* `extract_thread_context/1` - Returns empty map
"""
@type thread_context :: %{
optional(:thread_id) => String.t() | nil,
optional(:is_thread_reply) => boolean(),
optional(:thread_root_ts) => String.t() | nil
}
@doc """
Returns whether this adapter supports threading.
Used for capability detection and feature gating.
"""
@callback supports_threads?() :: boolean()
@doc """
Computes the thread root identifier from a raw message payload.
The thread root is the first message in a thread. For messages that are
not part of a thread, this typically returns `nil` or the message's own ID.
## Parameters
* `raw` - The raw platform-specific message payload
## Returns
The thread root identifier as a string, or `nil` if not applicable.
"""
@callback compute_thread_root(raw :: map()) :: String.t() | nil
@doc """
Extracts threading context from a raw message payload.
Returns a map with thread-related information that can be used for
routing decisions and context building.
## Parameters
* `raw` - The raw platform-specific message payload
## Returns
A map that may contain:
* `:thread_id` - The thread identifier
* `:is_thread_reply` - Whether this message is a reply in a thread
* `:thread_root_ts` - The timestamp/ID of the thread root message
"""
@callback extract_thread_context(raw :: map()) :: thread_context()
@optional_callbacks supports_threads?: 0, compute_thread_root: 1, extract_thread_context: 1
@doc """
Checks if a module implements the Threading behaviour and supports threads.
Returns `true` only if the module implements `supports_threads?/0` and it returns `true`.
"""
@spec supports_threads?(module()) :: boolean()
def supports_threads?(module) do
function_exported?(module, :supports_threads?, 0) and module.supports_threads?()
end
@doc """
Safely computes the thread root for a module.
Returns `nil` if the module doesn't implement the callback.
"""
@spec compute_thread_root(module(), map()) :: String.t() | nil
def compute_thread_root(module, raw) do
if function_exported?(module, :compute_thread_root, 1) do
module.compute_thread_root(raw)
else
nil
end
end
@doc """
Safely extracts thread context for a module.
Returns an empty map if the module doesn't implement the callback.
"""
@spec extract_thread_context(module(), map()) :: thread_context()
def extract_thread_context(module, raw) do
if function_exported?(module, :extract_thread_context, 1) do
module.extract_thread_context(raw)
else
%{}
end
end
end