Skip to content

Commit f57fb49

Browse files
committed
working on rate limit / premium impl
1 parent ca5d78d commit f57fb49

19 files changed

Lines changed: 470 additions & 36 deletions

File tree

.roo/rules/rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ It has a TypeScript/React frontend and a Go backend. They talk together over `ws
3939
- NEVER use cursor-help (it looks terrible)
4040
- useAtom() and useAtomValue() are react HOOKS, so they must be called at the component level not inline in JSX
4141
- If you use React.memo(), make sure to add a displayName for the component
42+
- In general, when writing functions, we prefer _early returns_ rather than putting the majority of a function inside of an if block.
4243

4344
### Styling
4445

aiprompts/wps-events.md

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
# WPS Events Guide
2+
3+
## Overview
4+
5+
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.
6+
7+
## Key Files
8+
9+
- [`pkg/wps/wpstypes.go`](../pkg/wps/wpstypes.go) - Event type constants and data structures
10+
- [`pkg/wps/wps.go`](../pkg/wps/wps.go) - Broker implementation and core logic
11+
- [`pkg/wcore/wcore.go`](../pkg/wcore/wcore.go) - Example usage patterns
12+
13+
## Event Structure
14+
15+
Events in WPS have the following structure:
16+
17+
```go
18+
type WaveEvent struct {
19+
Event string `json:"event"` // Event type constant
20+
Scopes []string `json:"scopes,omitempty"` // Optional scopes for targeted delivery
21+
Sender string `json:"sender,omitempty"` // Optional sender identifier
22+
Persist int `json:"persist,omitempty"` // Number of events to persist in history
23+
Data any `json:"data,omitempty"` // Event payload
24+
}
25+
```
26+
27+
## Adding a New Event Type
28+
29+
### Step 1: Define the Event Constant
30+
31+
Add your event type constant to [`pkg/wps/wpstypes.go`](../pkg/wps/wpstypes.go:8-19):
32+
33+
```go
34+
const (
35+
Event_BlockClose = "blockclose"
36+
Event_ConnChange = "connchange"
37+
// ... other events ...
38+
Event_YourNewEvent = "your:newevent" // Use colon notation for namespacing
39+
)
40+
```
41+
42+
**Naming Convention:**
43+
- Use descriptive PascalCase for the constant name with `Event_` prefix
44+
- Use lowercase with colons for the string value (e.g., "namespace:eventname")
45+
- Group related events with the same namespace prefix
46+
47+
### Step 2: Define Event Data Structure (Optional)
48+
49+
If your event carries structured data, define a type for it:
50+
51+
```go
52+
type YourEventData struct {
53+
Field1 string `json:"field1"`
54+
Field2 int `json:"field2"`
55+
}
56+
```
57+
58+
### Step 3: Expose Type to Frontend (If Needed)
59+
60+
If your event data type isn't already exposed via an RPC call, you need to add it to [`pkg/tsgen/tsgen.go`](../pkg/tsgen/tsgen.go:29-56) so TypeScript types are generated:
61+
62+
```go
63+
// add extra types to generate here
64+
var ExtraTypes = []any{
65+
waveobj.ORef{},
66+
// ... other types ...
67+
uctypes.RateLimitInfo{}, // Example: already added
68+
YourEventData{}, // Add your new type here
69+
}
70+
```
71+
72+
Then run code generation:
73+
74+
```bash
75+
task generate
76+
```
77+
78+
This will update [`frontend/types/gotypes.d.ts`](../frontend/types/gotypes.d.ts) with TypeScript definitions for your type, ensuring type safety in the frontend when handling these events.
79+
80+
## Publishing Events
81+
82+
### Basic Publishing
83+
84+
To publish an event, use the global broker:
85+
86+
```go
87+
import "github.com/wavetermdev/waveterm/pkg/wps"
88+
89+
wps.Broker.Publish(wps.WaveEvent{
90+
Event: wps.Event_YourNewEvent,
91+
Data: yourData,
92+
})
93+
```
94+
95+
### Publishing with Scopes
96+
97+
Scopes allow targeted event delivery. Subscribers can filter events by scope:
98+
99+
```go
100+
wps.Broker.Publish(wps.WaveEvent{
101+
Event: wps.Event_WaveObjUpdate,
102+
Scopes: []string{oref.String()}, // Target specific object
103+
Data: updateData,
104+
})
105+
```
106+
107+
### Publishing in a Goroutine
108+
109+
To avoid blocking the caller, publish events asynchronously:
110+
111+
```go
112+
go func() {
113+
wps.Broker.Publish(wps.WaveEvent{
114+
Event: wps.Event_YourNewEvent,
115+
Data: data,
116+
})
117+
}()
118+
```
119+
120+
**When to use goroutines:**
121+
- When publishing from performance-critical code paths
122+
- When the event is informational and doesn't need immediate delivery
123+
- When publishing from code that holds locks (to prevent deadlocks)
124+
125+
### Event Persistence
126+
127+
Events can be persisted in memory for late subscribers:
128+
129+
```go
130+
wps.Broker.Publish(wps.WaveEvent{
131+
Event: wps.Event_YourNewEvent,
132+
Persist: 100, // Keep last 100 events
133+
Data: data,
134+
})
135+
```
136+
137+
## Complete Example: Rate Limit Updates
138+
139+
This example shows how rate limit information is published when AI chat responses include rate limit headers.
140+
141+
### 1. Define the Event Type
142+
143+
In [`pkg/wps/wpstypes.go`](../pkg/wps/wpstypes.go:19):
144+
145+
```go
146+
const (
147+
// ... other events ...
148+
Event_WaveAIRateLimit = "waveai:ratelimit"
149+
)
150+
```
151+
152+
### 2. Publish the Event
153+
154+
In [`pkg/aiusechat/usechat.go`](../pkg/aiusechat/usechat.go:94-108):
155+
156+
```go
157+
import "github.com/wavetermdev/waveterm/pkg/wps"
158+
159+
func updateRateLimit(info *uctypes.RateLimitInfo) {
160+
if info == nil {
161+
return
162+
}
163+
rateLimitLock.Lock()
164+
defer rateLimitLock.Unlock()
165+
globalRateLimitInfo = info
166+
167+
// Publish event in goroutine to avoid blocking
168+
go func() {
169+
wps.Broker.Publish(wps.WaveEvent{
170+
Event: wps.Event_WaveAIRateLimit,
171+
Data: info, // RateLimitInfo struct
172+
})
173+
}()
174+
}
175+
```
176+
177+
### 3. Subscribe to the Event (Frontend)
178+
179+
In the frontend, subscribe to events via WebSocket:
180+
181+
```typescript
182+
// Subscribe to rate limit updates
183+
const subscription = {
184+
event: "waveai:ratelimit",
185+
allscopes: true // Receive all rate limit events
186+
};
187+
```
188+
189+
## Subscribing to Events
190+
191+
### From Go Code
192+
193+
```go
194+
// Subscribe to all events of a type
195+
wps.Broker.Subscribe(routeId, wps.SubscriptionRequest{
196+
Event: wps.Event_YourNewEvent,
197+
AllScopes: true,
198+
})
199+
200+
// Subscribe to specific scopes
201+
wps.Broker.Subscribe(routeId, wps.SubscriptionRequest{
202+
Event: wps.Event_WaveObjUpdate,
203+
Scopes: []string{"workspace:123"},
204+
})
205+
206+
// Unsubscribe
207+
wps.Broker.Unsubscribe(routeId, wps.Event_YourNewEvent)
208+
```
209+
210+
### Scope Matching
211+
212+
Scopes support wildcard matching:
213+
- `*` matches a single scope segment
214+
- `**` matches multiple scope segments
215+
216+
```go
217+
// Subscribe to all workspace events
218+
wps.Broker.Subscribe(routeId, wps.SubscriptionRequest{
219+
Event: wps.Event_WaveObjUpdate,
220+
Scopes: []string{"workspace:*"},
221+
})
222+
```
223+
224+
## Best Practices
225+
226+
1. **Use Namespaces**: Prefix event names with a namespace (e.g., `waveai:`, `workspace:`, `block:`)
227+
228+
2. **Don't Block**: Use goroutines when publishing from performance-critical code or while holding locks
229+
230+
3. **Type-Safe Data**: Define struct types for event data rather than using maps
231+
232+
4. **Scope Wisely**: Use scopes to limit event delivery and reduce unnecessary processing
233+
234+
5. **Document Events**: Add comments explaining when events are fired and what data they carry
235+
236+
6. **Consider Persistence**: Use `Persist` for events that late subscribers might need (like status updates)
237+
238+
## Common Event Patterns
239+
240+
### Status Updates
241+
242+
```go
243+
wps.Broker.Publish(wps.WaveEvent{
244+
Event: wps.Event_ControllerStatus,
245+
Scopes: []string{blockId},
246+
Persist: 1, // Keep only latest status
247+
Data: statusData,
248+
})
249+
```
250+
251+
### Object Updates
252+
253+
```go
254+
wps.Broker.Publish(wps.WaveEvent{
255+
Event: wps.Event_WaveObjUpdate,
256+
Scopes: []string{oref.String()},
257+
Data: waveobj.WaveObjUpdate{
258+
UpdateType: waveobj.UpdateType_Update,
259+
OType: obj.GetOType(),
260+
OID: waveobj.GetOID(obj),
261+
Obj: obj,
262+
},
263+
})
264+
```
265+
266+
### Batch Updates
267+
268+
```go
269+
// Helper function for multiple updates
270+
func (b *BrokerType) SendUpdateEvents(updates waveobj.UpdatesRtnType) {
271+
for _, update := range updates {
272+
b.Publish(WaveEvent{
273+
Event: Event_WaveObjUpdate,
274+
Scopes: []string{waveobj.MakeORef(update.OType, update.OID).String()},
275+
Data: update,
276+
})
277+
}
278+
}
279+
```
280+
281+
## Debugging
282+
283+
To debug event flow:
284+
285+
1. Check broker subscription map: `wps.Broker.SubMap`
286+
2. View persisted events: `wps.Broker.ReadEventHistory(eventType, scope, maxItems)`
287+
3. Add logging in publish/subscribe methods
288+
4. Monitor WebSocket traffic in browser dev tools
289+
290+
## Related Documentation
291+
292+
- [Configuration System](config-system.md) - Uses WPS events for config updates
293+
- [Wave AI Architecture](waveai-architecture.md) - AI-related events

frontend/app/aipanel/aipanel.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => {
104104
loadMessages();
105105
}, [model, setMessages]);
106106

107+
useEffect(() => {
108+
model.ensureRateLimitSet();
109+
}, [model]);
110+
107111
const handleSubmit = async (e: React.FormEvent) => {
108112
e.preventDefault();
109113
if (!input.trim() || status !== "ready" || isLoadingChat) return;

frontend/app/aipanel/waveai-model.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,19 @@ export class WaveAIModel {
172172
return [];
173173
}
174174
}
175+
176+
async ensureRateLimitSet() {
177+
const currentInfo = globalStore.get(atoms.waveAIRateLimitInfoAtom);
178+
if (currentInfo != null) {
179+
return;
180+
}
181+
try {
182+
const rateLimitInfo = await RpcApi.GetWaveAIRateLimitCommand(TabRpcClient);
183+
if (rateLimitInfo != null) {
184+
globalStore.set(atoms.waveAIRateLimitInfoAtom, rateLimitInfo);
185+
}
186+
} catch (error) {
187+
console.error("Failed to fetch rate limit info:", error);
188+
}
189+
}
175190
}

frontend/app/store/global.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
149149
const notificationPopoverModeAtom = atom<boolean>(false);
150150
const reinitVersion = atom(0);
151151
const waveAIFocusedAtom = atom(false);
152+
const rateLimitInfoAtom = atom(null) as PrimitiveAtom<RateLimitInfo>;
152153
atoms = {
153154
// initialized in wave.ts (will not be null inside of application)
154155
clientId: clientIdAtom,
@@ -173,6 +174,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
173174
reinitVersion,
174175
isTermMultiInput: atom(false),
175176
waveAIFocusedAtom,
177+
waveAIRateLimitInfoAtom: rateLimitInfoAtom,
176178
};
177179
}
178180

@@ -213,6 +215,13 @@ function initGlobalWaveEventSubs(initOpts: WaveInitOpts) {
213215
fileSubject.next(fileData);
214216
}
215217
},
218+
},
219+
{
220+
eventType: "waveai:ratelimit",
221+
handler: (event) => {
222+
const rateLimitInfo: RateLimitInfo = event.data;
223+
globalStore.set(atoms.waveAIRateLimitInfoAtom, rateLimitInfo);
224+
},
216225
}
217226
);
218227
}

frontend/app/store/wshclientapi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,11 @@ class RpcApiType {
287287
return client.wshRpcCall("getwaveaichat", data, opts);
288288
}
289289

290+
// command "getwaveairatelimit" [call]
291+
GetWaveAIRateLimitCommand(client: WshClient, opts?: RpcOpts): Promise<RateLimitInfo> {
292+
return client.wshRpcCall("getwaveairatelimit", null, opts);
293+
}
294+
290295
// command "message" [call]
291296
MessageCommand(client: WshClient, data: CommandMessageData, opts?: RpcOpts): Promise<void> {
292297
return client.wshRpcCall("message", data, opts);

frontend/types/custom.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ declare global {
2929
reinitVersion: jotai.PrimitiveAtom<number>;
3030
isTermMultiInput: jotai.PrimitiveAtom<boolean>;
3131
waveAIFocusedAtom: jotai.PrimitiveAtom<boolean>;
32+
waveAIRateLimitInfoAtom: jotai.PrimitiveAtom<RateLimitInfo>;
3233
};
3334

3435
type WritableWaveObjectAtom<T extends WaveObj> = jotai.WritableAtom<T, [value: T], void>;

frontend/types/gotypes.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,14 @@ declare global {
673673
y: number;
674674
};
675675

676+
// uctypes.RateLimitInfo
677+
type RateLimitInfo = {
678+
remaining: number;
679+
premiumremaining: number;
680+
expirationepoch: number;
681+
unknown?: boolean;
682+
};
683+
676684
// wshrpc.RemoteInfo
677685
type RemoteInfo = {
678686
clientarch: string;

0 commit comments

Comments
 (0)