Internal event bus for cross-component notifications.
Wraps an Elixir Registry in :duplicate mode so multiple processes
can subscribe to the same topic and receive broadcasts without knowing
about each other. The Editor (or any event source) calls broadcast/2;
subscribers receive the payload in Registry.dispatch/3 callbacks.
Usage
# In a subscriber (GenServer init, or any long-lived process):
Minga.Events.subscribe(:buffer_saved)
# In an event source:
Minga.Events.broadcast(:buffer_saved, %Events.BufferEvent{buffer: buf, path: path})Subscribers receive the event synchronously in the dispatch callback, which runs in the broadcaster's process. For heavy work, subscribers should send themselves a message and handle it asynchronously.
Payload types
Each topic has a typed struct payload with @enforce_keys. This means
the compiler catches missing fields at construction time, and the type
checker flags wrong field types (e.g. atom instead of pid). Subscribers
pattern-match on the struct, so malformed payloads can't sneak through.
| Topic | Payload struct | Required fields |
|---|---|---|
:buffer_saved | BufferEvent | buffer: pid(), path: String.t() |
:buffer_opened | BufferEvent | buffer: pid(), path: String.t() |
| :buffer_closed | BufferClosedEvent | buffer: pid(), path: String.t() | :scratch |
| :buffer_changed | BufferChangedEvent | buffer: pid(), source: EditSource.t() |
| :mode_changed | ModeEvent | old: atom(), new: atom() |
| :git_status_changed | GitStatusEvent | git_root, entries, branch, ahead, behind |
| :diagnostics_updated | DiagnosticsUpdatedEvent | uri: String.t(), source: atom() |
| :lsp_status_changed | LspStatusEvent | name: atom(), status: atom(), uri: String.t() | nil |
| :project_rebuilt | ProjectRebuiltEvent | root: String.t() |
| :command_done | CommandDoneEvent | name: String.t(), exit_code: non_neg_integer() |
| :log_message | LogMessageEvent | text: String.t(), level: :info | :warning | :error |
| :face_overrides_changed | FaceOverridesChangedEvent | buffer: pid(), overrides: map() |
Why Registry?
Registry ships with OTP (no dependencies), supports pattern-based
dispatch, and has zero overhead for topics with no subscribers. It is
the same primitive that Phoenix.PubSub builds on, without the Phoenix
dependency. The wrapper module makes swapping to PubSub or :pg
a one-file change if distributed events are ever needed.
Summary
Functions
Broadcasts a typed payload to all subscribers of a topic.
Returns the child spec for the event bus Registry.
Broadcasts :buffer_changed to all subscribers.
Subscribes the calling process to a topic.
Subscribes the calling process to a topic with metadata.
Returns the list of pids subscribed to a topic.
Unsubscribes the calling process from a topic.
Types
@type payload() :: Minga.Events.BufferEvent.t() | Minga.Events.BufferClosedEvent.t() | Minga.Events.BufferChangedEvent.t() | Minga.Events.ModeEvent.t() | Minga.Events.ToolMissingEvent.t() | Minga.Events.ProjectRebuiltEvent.t() | Minga.Events.CommandDoneEvent.t() | Minga.Events.GitStatusEvent.t() | Minga.Events.DiagnosticsUpdatedEvent.t() | Minga.Events.LspStatusEvent.t() | Minga.Events.SupervisorRestartedEvent.t() | Minga.Events.LogMessageEvent.t() | Minga.Events.FaceOverridesChangedEvent.t() | MingaAgent.SessionManager.SessionStoppedEvent.t()
Typed event payloads. Each topic has a specific struct.
@type topic() ::
:buffer_saved
| :buffer_opened
| :buffer_closed
| :buffer_changed
| :content_replaced
| :mode_changed
| :git_status_changed
| :diagnostics_updated
| :lsp_status_changed
| :tool_install_started
| :tool_install_progress
| :tool_install_complete
| :tool_install_failed
| :tool_uninstall_complete
| :tool_missing
| :project_rebuilt
| :command_done
| :supervisor_restarted
| :log_message
| :face_overrides_changed
| :agent_session_stopped
| :changeset_merged
| :changeset_budget_exhausted
| :load_user_themes
| :buffer_fork_conflict
| :extension_updates_available
Known event topics.
Functions
@spec broadcast( :buffer_saved | :buffer_opened | :content_replaced, Minga.Events.BufferEvent.t() ) :: :ok
@spec broadcast(:buffer_closed, Minga.Events.BufferClosedEvent.t()) :: :ok
@spec broadcast(:buffer_changed, Minga.Events.BufferChangedEvent.t()) :: :ok
@spec broadcast(:mode_changed, Minga.Events.ModeEvent.t()) :: :ok
@spec broadcast(:tool_missing, Minga.Events.ToolMissingEvent.t()) :: :ok
@spec broadcast(:project_rebuilt, Minga.Events.ProjectRebuiltEvent.t()) :: :ok
@spec broadcast(:command_done, Minga.Events.CommandDoneEvent.t()) :: :ok
@spec broadcast(:git_status_changed, Minga.Events.GitStatusEvent.t()) :: :ok
@spec broadcast(:diagnostics_updated, Minga.Events.DiagnosticsUpdatedEvent.t()) :: :ok
@spec broadcast(:lsp_status_changed, Minga.Events.LspStatusEvent.t()) :: :ok
@spec broadcast(:supervisor_restarted, Minga.Events.SupervisorRestartedEvent.t()) :: :ok
@spec broadcast(:log_message, Minga.Events.LogMessageEvent.t()) :: :ok
@spec broadcast(:face_overrides_changed, Minga.Events.FaceOverridesChangedEvent.t()) :: :ok
@spec broadcast( :agent_session_stopped, MingaAgent.SessionManager.SessionStoppedEvent.t() ) :: :ok
@spec broadcast(:changeset_merged, MingaAgent.Changeset.MergedEvent.t()) :: :ok
@spec broadcast( :changeset_budget_exhausted, MingaAgent.Changeset.BudgetExhaustedEvent.t() ) :: :ok
@spec broadcast(:load_user_themes, Minga.Events.LoadUserThemesEvent.t()) :: :ok
@spec broadcast(:buffer_fork_conflict, map()) :: :ok
@spec broadcast( :extension_updates_available, Minga.Extension.UpdatesAvailableEvent.t() ) :: :ok
Broadcasts a typed payload to all subscribers of a topic.
Accepts BufferEvent for buffer topics and ModeEvent for mode changes.
Using structs with @enforce_keys means the compiler catches missing
fields and the type checker flags wrong field types at the call site,
before the event ever reaches subscribers.
Returns :ok. If no processes are subscribed to the topic, this is a
no-op with negligible cost.
@spec child_spec(keyword()) :: Supervisor.child_spec()
Returns the child spec for the event bus Registry.
Add this to your supervision tree before any process that subscribes.
@spec notify_buffer_changed(pid()) :: :ok
Broadcasts :buffer_changed to all subscribers.
Deprecated: use the 2-arity version that accepts a BufferChangedEvent
struct with delta and source fields. This 1-arity wrapper exists for
backward compatibility during migration.
@spec subscribe(topic()) :: :ok
Subscribes the calling process to a topic.
The process will be included in Registry.dispatch/3 callbacks when
broadcast/2 is called for this topic. A process can subscribe to
multiple topics. Subscribing to the same topic multiple times from
the same process is a no-op.
Subscribes the calling process to a topic with metadata.
The metadata value is passed to the dispatch callback alongside the pid, which lets subscribers filter or tag their registrations. Subscribing with the same topic and value from the same process is a no-op.
Returns the list of pids subscribed to a topic.
Useful for debugging and testing. Not intended for production dispatch
(use broadcast/2 instead).
@spec unsubscribe(topic()) :: :ok
Unsubscribes the calling process from a topic.
Removes all registrations for this process under the given topic key.