WPS (Wave PubSub) is Wave Terminal's publish-subscribe event system that enables different parts of the application to communicate asynchronously. The system uses a broker pattern to route events from publishers to subscribers based on event types and scopes.
pkg/wps/wpstypes.go- Event type constants and data structurespkg/wps/wps.go- Broker implementation and core logicpkg/wcore/wcore.go- Example usage patterns
Events in WPS have the following structure:
type WaveEvent struct {
Event string `json:"event"` // Event type constant
Scopes []string `json:"scopes,omitempty"` // Optional scopes for targeted delivery
Sender string `json:"sender,omitempty"` // Optional sender identifier
Persist int `json:"persist,omitempty"` // Number of events to persist in history
Data any `json:"data,omitempty"` // Event payload
}Add your event type constant to pkg/wps/wpstypes.go:
const (
Event_BlockClose = "blockclose"
Event_ConnChange = "connchange"
// ... other events ...
Event_YourNewEvent = "your:newevent" // Use colon notation for namespacing
)Naming Convention:
- Use descriptive PascalCase for the constant name with
Event_prefix - Use lowercase with colons for the string value (e.g., "namespace:eventname")
- Group related events with the same namespace prefix
If your event carries structured data, define a type for it:
type YourEventData struct {
Field1 string `json:"field1"`
Field2 int `json:"field2"`
}If your event data type isn't already exposed via an RPC call, you need to add it to pkg/tsgen/tsgen.go so TypeScript types are generated:
// add extra types to generate here
var ExtraTypes = []any{
waveobj.ORef{},
// ... other types ...
uctypes.RateLimitInfo{}, // Example: already added
YourEventData{}, // Add your new type here
}Then run code generation:
task generateThis will update frontend/types/gotypes.d.ts with TypeScript definitions for your type, ensuring type safety in the frontend when handling these events.
To publish an event, use the global broker:
import "github.com/wavetermdev/waveterm/pkg/wps"
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_YourNewEvent,
Data: yourData,
})Scopes allow targeted event delivery. Subscribers can filter events by scope:
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_WaveObjUpdate,
Scopes: []string{oref.String()}, // Target specific object
Data: updateData,
})To avoid blocking the caller, publish events asynchronously:
go func() {
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_YourNewEvent,
Data: data,
})
}()When to use goroutines:
- When publishing from performance-critical code paths
- When the event is informational and doesn't need immediate delivery
- When publishing from code that holds locks (to prevent deadlocks)
Events can be persisted in memory for late subscribers:
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_YourNewEvent,
Persist: 100, // Keep last 100 events
Data: data,
})This example shows how rate limit information is published when AI chat responses include rate limit headers.
const (
// ... other events ...
Event_WaveAIRateLimit = "waveai:ratelimit"
)import "github.com/wavetermdev/waveterm/pkg/wps"
func updateRateLimit(info *uctypes.RateLimitInfo) {
if info == nil {
return
}
rateLimitLock.Lock()
defer rateLimitLock.Unlock()
globalRateLimitInfo = info
// Publish event in goroutine to avoid blocking
go func() {
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_WaveAIRateLimit,
Data: info, // RateLimitInfo struct
})
}()
}In the frontend, subscribe to events via WebSocket:
// Subscribe to rate limit updates
const subscription = {
event: "waveai:ratelimit",
allscopes: true, // Receive all rate limit events
};// Subscribe to all events of a type
wps.Broker.Subscribe(routeId, wps.SubscriptionRequest{
Event: wps.Event_YourNewEvent,
AllScopes: true,
})
// Subscribe to specific scopes
wps.Broker.Subscribe(routeId, wps.SubscriptionRequest{
Event: wps.Event_WaveObjUpdate,
Scopes: []string{"workspace:123"},
})
// Unsubscribe
wps.Broker.Unsubscribe(routeId, wps.Event_YourNewEvent)Scopes support wildcard matching:
*matches a single scope segment**matches multiple scope segments
// Subscribe to all workspace events
wps.Broker.Subscribe(routeId, wps.SubscriptionRequest{
Event: wps.Event_WaveObjUpdate,
Scopes: []string{"workspace:*"},
})-
Use Namespaces: Prefix event names with a namespace (e.g.,
waveai:,workspace:,block:) -
Don't Block: Use goroutines when publishing from performance-critical code or while holding locks
-
Type-Safe Data: Define struct types for event data rather than using maps
-
Scope Wisely: Use scopes to limit event delivery and reduce unnecessary processing
-
Document Events: Add comments explaining when events are fired and what data they carry
-
Consider Persistence: Use
Persistfor events that late subscribers might need (like status updates). This is normally not used. We normally do a live RPC call to get the current value and then subscribe for updates.
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_ControllerStatus,
Scopes: []string{blockId},
Persist: 1, // Keep only latest status
Data: statusData,
})wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_WaveObjUpdate,
Scopes: []string{oref.String()},
Data: waveobj.WaveObjUpdate{
UpdateType: waveobj.UpdateType_Update,
OType: obj.GetOType(),
OID: waveobj.GetOID(obj),
Obj: obj,
},
})// Helper function for multiple updates
func (b *BrokerType) SendUpdateEvents(updates waveobj.UpdatesRtnType) {
for _, update := range updates {
b.Publish(WaveEvent{
Event: Event_WaveObjUpdate,
Scopes: []string{waveobj.MakeORef(update.OType, update.OID).String()},
Data: update,
})
}
}To debug event flow:
- Check broker subscription map:
wps.Broker.SubMap - View persisted events:
wps.Broker.ReadEventHistory(eventType, scope, maxItems) - Add logging in publish/subscribe methods
- Monitor WebSocket traffic in browser dev tools
- Configuration System - Uses WPS events for config updates
- Wave AI Architecture - AI-related events