okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

dawnadd charm tui skillf7fcae1

main
2.6 KiB123 linesraw
1package main
2
3import (
4	"context"
5	"errors"
6	"strings"
7	"time"
8)
9
10type phase uint8
11
12const (
13	idle phase = iota
14	waiting
15	loading
16	ready
17	failed
18	stopped
19)
20
21type ticket struct {
22	ctx        context.Context
23	generation uint64
24	query      string
25}
26type searchState struct {
27	root       context.Context
28	cancel     context.CancelFunc
29	generation uint64
30	query      string
31	phase      phase
32	rows       []string
33	err        error
34}
35
36// change is called synchronously in Update at the moment intent changes.
37func (s *searchState) change(query string) ticket {
38	s.generation++ // Invalidate A before B's debounce, not when B's I/O starts.
39	if s.cancel != nil {
40		s.cancel()
41	}
42	ctx, cancel := context.WithTimeout(s.root, 3*time.Second)
43	s.cancel = cancel
44	s.query = query
45	s.phase = waiting
46	s.rows = nil
47	s.err = nil
48	return ticket{ctx: ctx, generation: s.generation, query: query}
49}
50
51func (s *searchState) begin(generation uint64) bool {
52	if generation != s.generation || s.phase != waiting {
53		return false
54	}
55	s.phase = loading
56	return true
57}
58
59func (s *searchState) complete(generation uint64, rows []string, err error) bool {
60	if generation != s.generation || s.phase != loading {
61		return false
62	}
63	if s.cancel != nil {
64		s.cancel()
65		s.cancel = nil
66	}
67	s.err = err
68	if err != nil {
69		s.phase = failed
70		s.rows = nil
71	} else {
72		s.phase = ready
73		s.rows = append([]string(nil), rows...)
74	}
75	return true
76}
77
78func (s *searchState) stop() {
79	s.generation++
80	s.phase = stopped
81	if s.cancel != nil {
82		s.cancel()
83		s.cancel = nil
84	}
85}
86
87// await is used by the actual command adapter. Cancellation stops the timer
88// instead of leaving a growing pile of superseded debounce timers alive.
89func await(ctx context.Context, d time.Duration) error {
90	timer := time.NewTimer(d)
91	defer timer.Stop()
92	select {
93	case <-ctx.Done():
94		return ctx.Err()
95	case <-timer.C:
96		return nil
97	}
98}
99
100type searchFunc func(context.Context, string) ([]string, error)
101
102// demoSearch is an injected local service, NOT an external API. A backend must
103// honor context; generation guards alone do not bound backend resource use.
104func demoSearch(ctx context.Context, query string) ([]string, error) {
105	delay := 60 * time.Millisecond
106	if query == "slow" {
107		delay = 500 * time.Millisecond
108	}
109	if err := await(ctx, delay); err != nil {
110		return nil, err
111	}
112	if query == "error" {
113		return nil, errors.New("demo service unavailable")
114	}
115	names := []string{"alpha", "beta", "queue worker", "東京 production", "slow worker"}
116	rows := make([]string, 0, len(names))
117	for _, name := range names {
118		if strings.Contains(strings.ToLower(name), strings.ToLower(query)) {
119			rows = append(rows, name)
120		}
121	}
122	return rows, nil
123}