Skip to content

Commit 3d18fa7

Browse files
committed
add timer hooks
1 parent 77f8a00 commit 3d18fa7

2 files changed

Lines changed: 131 additions & 14 deletions

File tree

tsunami/app/hooks.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package app
55

66
import (
77
"context"
8+
"time"
89

910
"github.com/wavetermdev/waveterm/tsunami/engine"
1011
"github.com/wavetermdev/waveterm/tsunami/vdom"
@@ -174,3 +175,41 @@ func UseGoRoutine(fn func(ctx context.Context), deps []any) {
174175
}
175176
}, deps)
176177
}
178+
179+
// UseTicker manages a ticker lifecycle within a component.
180+
// It creates a ticker that calls the provided function at regular intervals.
181+
// The ticker is automatically stopped on dependency changes or component unmount.
182+
// This hook must be called within a component context.
183+
func UseTicker(interval time.Duration, tickFn func(), deps []any) {
184+
UseGoRoutine(func(ctx context.Context) {
185+
ticker := time.NewTicker(interval)
186+
defer ticker.Stop()
187+
188+
for {
189+
select {
190+
case <-ctx.Done():
191+
return
192+
case <-ticker.C:
193+
tickFn()
194+
}
195+
}
196+
}, deps)
197+
}
198+
199+
// UseAfter manages a timeout lifecycle within a component.
200+
// It creates a timer that calls the provided function after the specified duration.
201+
// The timer is automatically canceled on dependency changes or component unmount.
202+
// This hook must be called within a component context.
203+
func UseAfter(duration time.Duration, timeoutFn func(), deps []any) {
204+
UseGoRoutine(func(ctx context.Context) {
205+
timer := time.NewTimer(duration)
206+
defer timer.Stop()
207+
208+
select {
209+
case <-ctx.Done():
210+
return
211+
case <-timer.C:
212+
timeoutFn()
213+
}
214+
}, deps)
215+
}

tsunami/prompts/system.md

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ var MyComponent = app.DefineComponent("MyComponent", func(props MyProps) any {
267267

268268
- **State Management**: app.UseLocal creates local component atoms (covered in State Management with Atoms)
269269
- **Component Lifecycle**: app.UseEffect, app.UseRef, app.UseVDomRef (covered in Component Lifecycle Hooks)
270-
- **Async Operations**: app.UseGoRoutine manages goroutine lifecycle (covered in Async Operations and Goroutines)
270+
- **Async Operations**: app.UseGoRoutine, app.UseTicker, app.UseAfter manage goroutine and timer lifecycle (covered in Async Operations and Goroutines)
271271
- **Utility**: app.UseSetAppTitle, app.UseId, app.UseRenderTs, app.UseResync
272272

273273
## State Management with Atoms
@@ -925,31 +925,107 @@ var App = app.DefineComponent("App", func(_ struct{}) any {
925925

926926
When working with goroutines, timers, or other async operations in Tsunami, follow these patterns to safely update state and manage cleanup:
927927

928-
### Goroutine Management
928+
### Timer Hooks
929929

930-
For async operations like timers, background tasks, or data polling, use app.UseGoRoutine to safely manage goroutine lifecycle:
930+
For common timing operations, Tsunami provides simplified hooks that handle cleanup automatically:
931+
932+
#### UseTicker for Recurring Operations
933+
934+
Use `app.UseTicker` for operations that need to run at regular intervals:
931935

932936
```go
933-
var MyComponent = app.DefineComponent("MyComponent", func(_ struct{}) any {
934-
seconds := app.UseLocal(0)
937+
var ClockComponent = app.DefineComponent("ClockComponent", func(_ struct{}) any {
938+
currentTime := app.UseLocal(time.Now().Format("15:04:05"))
939+
940+
// Update every second - automatically cleaned up on unmount
941+
app.UseTicker(time.Second, func() {
942+
currentTime.Set(time.Now().Format("15:04:05"))
943+
app.SendAsyncInitiation()
944+
}, []any{})
945+
946+
return vdom.H("div", map[string]any{
947+
"className": "text-2xl font-mono",
948+
}, "Current time: ", currentTime.Get())
949+
})
950+
```
951+
952+
#### UseAfter for Delayed Operations
953+
954+
Use `app.UseAfter` for one-time delayed operations:
955+
956+
```go
957+
type ToastComponentProps struct {
958+
Message string
959+
Duration time.Duration
960+
}
961+
962+
var ToastComponent = app.DefineComponent("ToastComponent", func(props ToastComponentProps) any {
963+
visible := app.UseLocal(true)
964+
965+
// Auto-hide after specified duration - cancelled if component unmounts
966+
app.UseAfter(props.Duration, func() {
967+
visible.Set(false)
968+
app.SendAsyncInitiation()
969+
}, []any{props.Duration})
970+
971+
if !visible.Get() {
972+
return nil
973+
}
974+
975+
return vdom.H("div", map[string]any{
976+
"className": "bg-blue-500 text-white p-4 rounded",
977+
}, props.Message)
978+
})
979+
```
980+
981+
**Benefits of Timer Hooks:**
982+
983+
- **Automatic cleanup**: Timers are stopped when component unmounts or dependencies change
984+
- **No goroutine leaks**: Built on top of `UseGoRoutine` with proper context cancellation
985+
- **Simpler API**: No need to manually manage ticker channels or timer cleanup
986+
- **Dependency tracking**: Change dependencies to restart timers with new intervals
987+
988+
### Complex Async Operations with UseGoRoutine
935989

936-
timerFn := func(ctx context.Context) {
990+
For more complex async operations like data polling, background processing, or custom timing logic, use `app.UseGoRoutine` directly:
991+
992+
```go
993+
var DataPollerComponent = app.DefineComponent("DataPollerComponent", func(_ struct{}) any {
994+
data := app.UseLocal([]APIResult{})
995+
status := app.UseLocal("idle")
996+
997+
pollDataFn := func(ctx context.Context) {
937998
for {
938999
select {
9391000
case <-ctx.Done():
9401001
return
941-
case <-time.After(time.Second):
942-
// Update state from goroutine
943-
seconds.SetFn(func(s int) int { return s + 1 })
944-
app.SendAsyncInitiation() // Trigger UI update
1002+
case <-time.After(30 * time.Second):
1003+
status.Set("fetching")
1004+
app.SendAsyncInitiation()
1005+
1006+
// Complex async operation: fetch, process, validate
1007+
newData, err := fetchAndProcessData()
1008+
if err != nil {
1009+
status.Set("error")
1010+
} else {
1011+
data.SetFn(func(current []APIResult) []APIResult {
1012+
// Merge new data with existing, handle deduplication
1013+
return mergeResults(current, newData)
1014+
})
1015+
status.Set("success")
1016+
}
1017+
app.SendAsyncInitiation()
9451018
}
9461019
}
9471020
}
9481021

949-
// Start timer on mount, cleanup on unmount
950-
app.UseGoRoutine(timerFn, []any{})
1022+
// Start polling on mount, cleanup on unmount
1023+
app.UseGoRoutine(pollDataFn, []any{})
9511024

952-
return vdom.H("div", nil, "Seconds: ", seconds.Get())
1025+
return vdom.H("div", nil,
1026+
vdom.H("div", nil, "Status: ", status.Get()),
1027+
vdom.H("div", nil, "Data count: ", len(data.Get())),
1028+
)
9531029
})
9541030
```
9551031

@@ -1282,6 +1358,8 @@ Key points:
12821358
**Async Operation Guidelines**
12831359

12841360
- Use app.UseGoRoutine instead of raw go statements for component-related async work
1361+
- Use app.UseTicker instead of manual time.Ticker management for recurring operations
1362+
- Use app.UseAfter instead of time.AfterFunc for delayed operations
12851363
- Always respect ctx.Done() in app.UseGoRoutine functions to prevent goroutine leaks
1286-
- Use app.UseEffect with cleanup functions for subscriptions, timers, and other lifecycle management
1364+
- All timer and goroutine cleanup is handled automatically on component unmount or dependency changes
12871365
- Call app.SendAsyncInitiation after state updates to trigger re-rendering

0 commit comments

Comments
 (0)