okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
f7fcae1
main
1package main 2 3import ( 4"context" 5"errors" 6"strings" 7"time" 8) 9 10type phase uint8 11 12const ( 13idle phase = iota 14waiting 15loading 16ready 17failed 18stopped 19) 20 21type ticket struct { 22ctx context.Context 23generation uint64 24query string 25} 26type searchState struct { 27root context.Context 28cancel context.CancelFunc 29generation uint64 30query string 31phase phase 32rows []string 33err error 34} 35 36// change is called synchronously in Update at the moment intent changes. 37func (s * searchState )change (query string )ticket { 38s .generation ++ // Invalidate A before B's debounce, not when B's I/O starts. 39if s .cancel != nil { 40s .cancel () 41 } 42ctx ,cancel := context .WithTimeout (s .root ,3 * time .Second ) 43s .cancel = cancel 44s .query = query 45s .phase = waiting 46s .rows = nil 47s .err = nil 48return ticket {ctx :ctx ,generation :s .generation ,query :query } 49} 50 51func (s * searchState )begin (generation uint64 )bool { 52if generation != s .generation || s .phase != waiting { 53return false 54 } 55s .phase = loading 56return true 57} 58 59func (s * searchState )complete (generation uint64 ,rows []string ,err error )bool { 60if generation != s .generation || s .phase != loading { 61return false 62 } 63if s .cancel != nil { 64s .cancel () 65s .cancel = nil 66 } 67s .err = err 68if err != nil { 69s .phase = failed 70s .rows = nil 71 }else { 72s .phase = ready 73s .rows = append ([]string (nil ),rows ... ) 74 } 75return true 76} 77 78func (s * searchState )stop () { 79s .generation ++ 80s .phase = stopped 81if s .cancel != nil { 82s .cancel () 83s .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 { 90timer := time .NewTimer (d ) 91defer timer .Stop () 92select { 93case <- ctx .Done (): 94return ctx .Err () 95case <- timer .C : 96return 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 ) { 105delay := 60 * time .Millisecond 106if query == "slow" { 107delay = 500 * time .Millisecond 108 } 109if err := await (ctx ,delay );err != nil { 110return nil ,err 111 } 112if query == "error" { 113return nil ,errors .New ("demo service unavailable" ) 114 } 115names := []string {"alpha" ,"beta" ,"queue worker" ,"東京 production" ,"slow worker" } 116rows := make ([]string ,0 ,len (names )) 117for _ ,name := range names { 118if strings .Contains (strings .ToLower (name ),strings .ToLower (query )) { 119rows = append (rows ,name ) 120 } 121 } 122return rows ,nil 123}