Events System¶
The Cascade Chat events system is a centralized, event-driven layer for communication between components. All IRC activities, system events, and UI interactions flow through the EventBus.
Overview¶
The events system uses a publisher-subscriber pattern: - Publishers emit events to the EventBus. - Subscribers register interest in specific event types. - The EventBus routes events to all relevant subscribers asynchronously.
Architecture¶
flowchart LR
irc[IRC Client]
sys[System]
ui[UI]
irc --> bus
sys --> bus
ui --> bus
bus([EventBus])
bus --> pm[Plugin Manager]
bus --> storage["Storage<br/><small>future</small>"]
bus --> frontend["Frontend<br/><small>future</small>"]
subgraph Publishers
irc
sys
ui
end
subgraph Subscribers
pm
storage
frontend
end
Event Structure¶
All events follow this structure:
type Event struct {
Type string // Event type identifier
Data map[string]interface{} // Event-specific data
Timestamp time.Time // When the event occurred
Source EventSource // Source of the event
}
Event Sources¶
irc: Events from IRC protocol interactionsui: Events from user interface (future)system: Events from system components
Event Types¶
IRC Events¶
All IRC events are defined in internal/irc/events.go:
Connection Events¶
connection.established: Emitted when successfully connected to an IRC servernetworkId(int64): Network IDaddress(string): Server address-
port(int): Server port -
connection.lost: Emitted when connection to server is lost networkId(int64): Network IDerror(string): Error message if available
Message Events¶
message.received: Emitted when a message is receivednetworkId(int64): Network IDchannel(string): Channel name (empty for PMs)user(string): Sender nicknamemessage(string): Message content-
timestamp(int64): Unix timestamp -
message.sent: Emitted when a message is sent networkId(int64): Network IDchannel(string): Channel name (empty for PMs)message(string): Message content
User Events¶
user.joined: Emitted when a user joins a channelnetworkId(int64): Network IDchannel(string): Channel name-
userornickname(string): User nickname -
user.parted: Emitted when a user leaves a channel networkId(int64): Network IDchannel(string): Channel nameuserornickname(string): User nickname-
reason(string): Part reason (optional) -
user.quit: Emitted when a user quits the server networkId(int64): Network IDuserornickname(string): User nickname-
reason(string): Quit reason -
user.kicked: Emitted when a user is kicked from a channel networkId(int64): Network IDchannel(string): Channel nameuserornickname(string): Kicked userkicker(string): User who performed the kick-
reason(string): Kick reason -
user.nick: Emitted when a user changes nickname networkId(int64): Network IDold_nickname(string): Previous nicknamenickname(string): New nickname
Channel Events¶
channel.topic: Emitted when channel topic changesnetworkId(int64): Network IDchannel(string): Channel nametopic(string): New topic-
setter(string): User who set the topic (optional) -
channel.mode: Emitted when channel modes change networkId(int64): Network IDchannel(string): Channel namemodes(string): Mode string (e.g., "+o user")-
setter(string): User who set the mode (optional) -
channels.changed: Emitted when channel list changes networkId(int64): Network IDchannels([]string): List of channel names
SASL Authentication Events¶
sasl.started: Emitted when SASL authentication beginsnetworkId(int64): Network ID-
mechanism(string): SASL mechanism (e.g., "PLAIN", "SCRAM-SHA-256") -
sasl.success: Emitted when SASL authentication succeeds -
networkId(int64): Network ID -
sasl.failed: Emitted when SASL authentication fails networkId(int64): Network ID-
error(string): Error message -
sasl.aborted: Emitted when SASL authentication is aborted networkId(int64): Network ID
Other Events¶
whois.received: Emitted when WHOIS information is receivednetworkId(int64): Network ID-
whois(WhoisInfo): Parsed WHOIS information (seeinternal/irc/events.go) -
error: Emitted when an error occurs networkId(int64): Network ID (optional)error(string): Error messagetype(string): Error type (optional)
System Events¶
metadata.updated: Emitted when plugin metadata is updatedtype(string): Metadata type (e.g., "nickname_color")key(string): Metadata keyvalue(interface{}): Metadata valuenetwork_id(int64): Network ID (optional)channel(string): Channel name (optional)
UI Events (Future)¶
ui.pane.focused: Emitted when a pane gains focusui.pane.blurred: Emitted when a pane loses focus
Using the EventBus¶
Subscribing to Events¶
// Subscribe to a specific event type
eventBus.Subscribe("message.received", mySubscriber)
// Subscribe to all events (wildcard)
eventBus.Subscribe("*", mySubscriber)
Implementing a Subscriber¶
Any type implementing the Subscriber interface can receive events:
type MySubscriber struct{}
func (s *MySubscriber) OnEvent(event events.Event) {
// Handle the event
switch event.Type {
case "message.received":
// Process message
}
}
Emitting Events¶
eventBus.Emit(events.Event{
Type: "message.received",
Data: map[string]interface{}{
"networkId": 1,
"channel": "#general",
"user": "Alice",
"message": "Hello!",
},
Timestamp: time.Now(),
Source: events.EventSourceIRC,
})
Synchronous Events¶
For testing or when order matters, use EmitSync:
Note: Synchronous emission blocks until all subscribers process the event. Use sparingly.
Event Flow Example¶
- IRC Client receives a PRIVMSG from the server
- IRC Client emits
message.receivedevent to EventBus - EventBus routes event to all subscribers:
- Plugin Manager forwards to plugins subscribed to
message.received - Storage (future) saves message to database
- Frontend (future) updates UI
Best Practices¶
- Event Naming: Use dot-separated hierarchical names (e.g.,
user.joined,channel.mode) - Event Data: Include all relevant context (networkId, channel, etc.)
- Async Processing: Events are delivered asynchronously, so don't block in
OnEvent - Error Handling: Handle errors gracefully in subscribers
- Wildcard Subscriptions: Use sparingly; prefer specific event types for performance
Thread Safety¶
The EventBus is fully thread-safe: - Subscriptions/unsubscriptions are protected by mutex - Event emission copies subscriber lists to avoid blocking - Each subscriber receives events in a separate goroutine
Future Enhancements¶
- Event filtering by data fields
- Event replay for debugging
- Event metrics and monitoring
- Priority-based event delivery
- Event batching for high-frequency events
Frontend events¶
The Go EventBus described above is internal to the backend process. A separate
set of events travels the other direction: from the Go backend to the React
webview via Wails' Events.Emit API. The frontend listens with
Events.On(name, handler).
These are distinct from the Go bus events. They carry serialized JSON payloads
rather than events.Event structs, and they are consumed by the UI layer, not
by Go subscribers.
Deep-link events¶
Emitted when Cascade handles an irc:// or ircs:// URI (see
Opening irc:// links).
deeplink:join¶
The link matched exactly one saved network. The frontend should connect (if not already connected) and join or open each target.
{
"networkId": 42,
"targets": [
{ "name": "#cascade", "isNick": false, "key": "" },
{ "name": "alice", "isNick": true, "key": "" }
]
}
| Field | Type | Description |
|---|---|---|
networkId |
number |
ID of the saved network to use. |
targets |
Target[] |
One entry per channel or nick from the URI. |
targets[].name |
string |
Channel name (with #) or nickname. |
targets[].isNick |
boolean |
true when the target is a nick (opens a PM). |
targets[].key |
string |
Channel key (?key=… query param), or empty. |
deeplink:add-network¶
The server in the link has no saved network. The frontend should open the Add Network form prefilled with these values.
| Field | Type | Description |
|---|---|---|
host |
string |
Server hostname from the URI. |
port |
number |
Port, or the scheme default (6667 / 6697). |
tls |
boolean |
true when the scheme is ircs://. |
channel |
string |
First channel from the URI, for display only. Not auto-joined. |
deeplink:disambiguate¶
The server matches more than one saved network. The frontend should present a
picker; once the user chooses, proceed as with deeplink:join.
{
"candidates": [
{ "networkId": 1, "name": "Libera (work)" },
{ "networkId": 7, "name": "Libera (personal)" }
],
"targets": [
{ "name": "#cascade", "isNick": false, "key": "" }
]
}
| Field | Type | Description |
|---|---|---|
candidates |
Candidate[] |
Networks that match the server in the URI. |
candidates[].networkId |
number |
Saved network ID. |
candidates[].name |
string |
Display name of the network. |
targets |
Target[] |
Same shape as deeplink:join; pass through after the user picks. |