package main import ( "context" "errors" "strings" "time" ) type phase uint8 const ( idle phase = iota waiting loading ready failed stopped ) type ticket struct { ctx context.Context generation uint64 query string } type searchState struct { root context.Context cancel context.CancelFunc generation uint64 query string phase phase rows []string err error } // change is called synchronously in Update at the moment intent changes. func (s *searchState) change(query string) ticket { s.generation++ // Invalidate A before B's debounce, not when B's I/O starts. if s.cancel != nil { s.cancel() } ctx, cancel := context.WithTimeout(s.root, 3*time.Second) s.cancel = cancel s.query = query s.phase = waiting s.rows = nil s.err = nil return ticket{ctx: ctx, generation: s.generation, query: query} } func (s *searchState) begin(generation uint64) bool { if generation != s.generation || s.phase != waiting { return false } s.phase = loading return true } func (s *searchState) complete(generation uint64, rows []string, err error) bool { if generation != s.generation || s.phase != loading { return false } if s.cancel != nil { s.cancel() s.cancel = nil } s.err = err if err != nil { s.phase = failed s.rows = nil } else { s.phase = ready s.rows = append([]string(nil), rows...) } return true } func (s *searchState) stop() { s.generation++ s.phase = stopped if s.cancel != nil { s.cancel() s.cancel = nil } } // await is used by the actual command adapter. Cancellation stops the timer // instead of leaving a growing pile of superseded debounce timers alive. func await(ctx context.Context, d time.Duration) error { timer := time.NewTimer(d) defer timer.Stop() select { case <-ctx.Done(): return ctx.Err() case <-timer.C: return nil } } type searchFunc func(context.Context, string) ([]string, error) // demoSearch is an injected local service, NOT an external API. A backend must // honor context; generation guards alone do not bound backend resource use. func demoSearch(ctx context.Context, query string) ([]string, error) { delay := 60 * time.Millisecond if query == "slow" { delay = 500 * time.Millisecond } if err := await(ctx, delay); err != nil { return nil, err } if query == "error" { return nil, errors.New("demo service unavailable") } names := []string{"alpha", "beta", "queue worker", "東京 production", "slow worker"} rows := make([]string, 0, len(names)) for _, name := range names { if strings.Contains(strings.ToLower(name), strings.ToLower(query)) { rows = append(rows, name) } } return rows, nil }