refactor(net): reuse a single buffered reader; manage broadcast loop lifecycle
Client: - Keep a persistent bufio.Reader on the connection (model.reader) - On connect, initialize reader and drain initial greetings with deadlines - Pass reader into fetchMenuCmd, submitOrderCmd, and listenForBroadcastsCmd - Avoid starting duplicate broadcast listeners with broadcastListening flag - Start broadcast listener after menu is loaded and form is initialized - Update commands to read via shared reader instead of creating new ones - Remove greeting drain from connectCmd (now handled on connectedMsg) - Minor: small flow tweaks for opening form and handling reconnection Benefits: - Prevents interleaved reads from multiple readers on the same net.Conn - Reduces race conditions and broken protocol reads - Ensures a single, continuous read loop for broadcasts and responses
This commit is contained in:
parent
616017267e
commit
aee33003bf
66
main.go
66
main.go
@ -70,6 +70,9 @@ type model struct {
|
|||||||
|
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
|
||||||
|
reader *bufio.Reader
|
||||||
|
broadcastListening bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialModel creates a base model.
|
// initialModel creates a base model.
|
||||||
@ -123,7 +126,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.err = nil
|
m.err = nil
|
||||||
m.loading = true
|
m.loading = true
|
||||||
m.status = "Submitting order..."
|
m.status = "Submitting order..."
|
||||||
return m, submitOrderCmd(m.conn, *ord)
|
return m, submitOrderCmd(m.conn, *ord, m.reader)
|
||||||
}
|
}
|
||||||
m.status = "Order canceled."
|
m.status = "Order canceled."
|
||||||
return m, cmd
|
return m, cmd
|
||||||
@ -141,8 +144,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case connectedMsg:
|
case connectedMsg:
|
||||||
m.conn = msg.conn
|
m.conn = msg.conn
|
||||||
|
m.reader = bufio.NewReader(m.conn)
|
||||||
m.status = fmt.Sprintf("Connected to %s", m.host)
|
m.status = fmt.Sprintf("Connected to %s", m.host)
|
||||||
return m, listenForBroadcastsCmd(m.conn)
|
|
||||||
|
_ = m.conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if _, err := m.reader.ReadString('\n'); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = m.conn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case menuLoadedMsg:
|
case menuLoadedMsg:
|
||||||
m.loading = false
|
m.loading = false
|
||||||
@ -154,9 +167,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.err = nil
|
m.err = nil
|
||||||
m.menu = msg.items
|
m.menu = msg.items
|
||||||
m.status = "Menu loaded."
|
m.status = "Menu loaded."
|
||||||
// Open the form now that we have a menu.
|
|
||||||
|
var cmds []tea.Cmd
|
||||||
m.form = m.buildForm()
|
m.form = m.buildForm()
|
||||||
return m, m.form.Init()
|
cmds = append(cmds, m.form.Init())
|
||||||
|
|
||||||
|
if !m.broadcastListening {
|
||||||
|
m.broadcastListening = true
|
||||||
|
cmds = append(cmds, listenForBroadcastsCmd(m.conn, m.reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
|
||||||
case orderSubmittedMsg:
|
case orderSubmittedMsg:
|
||||||
m.loading = false
|
m.loading = false
|
||||||
@ -181,7 +202,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.broadcasts = m.broadcasts[1:]
|
m.broadcasts = m.broadcasts[1:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, listenForBroadcastsCmd(m.conn)
|
return m, listenForBroadcastsCmd(m.conn, m.reader)
|
||||||
|
|
||||||
case statusMsg:
|
case statusMsg:
|
||||||
m.status = string(msg)
|
m.status = string(msg)
|
||||||
@ -203,7 +224,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.status = "Reconnecting..."
|
m.status = "Reconnecting..."
|
||||||
return m, connectCmd(m.host)
|
return m, connectCmd(m.host)
|
||||||
case "n":
|
case "n":
|
||||||
// Start a new order
|
|
||||||
if m.loading || m.form != nil {
|
if m.loading || m.form != nil {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@ -218,7 +238,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
m.loading = true
|
m.loading = true
|
||||||
m.status = "Loading menu..."
|
m.status = "Loading menu..."
|
||||||
return m, fetchMenuCmd(m.conn)
|
return m, fetchMenuCmd(m.conn, m.reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
@ -447,15 +467,6 @@ func connectCmd(addr string) tea.Cmd {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return statusMsg(fmt.Sprintf("Connect failed: %v", err))
|
return statusMsg(fmt.Sprintf("Connect failed: %v", err))
|
||||||
}
|
}
|
||||||
// Try to read up to two greeting lines with short deadline (optional).
|
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
|
||||||
br := bufio.NewReader(conn)
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
if _, err := br.ReadString('\n'); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
|
||||||
|
|
||||||
return connectedMsg{conn: conn}
|
return connectedMsg{conn: conn}
|
||||||
}
|
}
|
||||||
@ -465,27 +476,24 @@ func connectCmd(addr string) tea.Cmd {
|
|||||||
// Protocol (proposed):
|
// Protocol (proposed):
|
||||||
// - client: "MENU\n"
|
// - client: "MENU\n"
|
||||||
// - server: single line JSON array: [{"id":"x","name":"..."}]\n
|
// - server: single line JSON array: [{"id":"x","name":"..."}]\n
|
||||||
func fetchMenuCmd(conn net.Conn) tea.Cmd {
|
func fetchMenuCmd(conn net.Conn, reader *bufio.Reader) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return menuLoadedMsg{err: errors.New("not connected")}
|
return menuLoadedMsg{err: errors.New("not connected")}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send request
|
|
||||||
if _, err := fmt.Fprintln(conn, "MENU"); err != nil {
|
if _, err := fmt.Fprintln(conn, "MENU"); err != nil {
|
||||||
return menuLoadedMsg{err: fmt.Errorf("send MENU: %w", err)}
|
return menuLoadedMsg{err: fmt.Errorf("send MENU: %w", err)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read single JSON line
|
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
_ = conn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||||
defer func() { _ = conn.SetReadDeadline(time.Time{}) }()
|
defer func() { _ = conn.SetReadDeadline(time.Time{}) }()
|
||||||
r := bufio.NewReader(conn)
|
|
||||||
line, err := r.ReadString('\n')
|
line, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return menuLoadedMsg{err: fmt.Errorf("read MENU: %w", err)}
|
return menuLoadedMsg{err: fmt.Errorf("read MENU: %w", err)}
|
||||||
}
|
}
|
||||||
line = strings.TrimRight(line, "\r\n")
|
line = strings.TrimRight(line, "\r\n")
|
||||||
// If the server sent an error-ish line, surface it.
|
|
||||||
if strings.HasPrefix(line, "[error]") {
|
if strings.HasPrefix(line, "[error]") {
|
||||||
return menuLoadedMsg{err: fmt.Errorf("server: %s", line)}
|
return menuLoadedMsg{err: fmt.Errorf("server: %s", line)}
|
||||||
}
|
}
|
||||||
@ -502,7 +510,7 @@ func fetchMenuCmd(conn net.Conn) tea.Cmd {
|
|||||||
// Protocol (proposed):
|
// Protocol (proposed):
|
||||||
// - client: "ORDER <json>\n"
|
// - client: "ORDER <json>\n"
|
||||||
// - server: a single line acknowledgement (freeform), e.g. "OK\n"
|
// - server: a single line acknowledgement (freeform), e.g. "OK\n"
|
||||||
func submitOrderCmd(conn net.Conn, ord order) tea.Cmd {
|
func submitOrderCmd(conn net.Conn, ord order, reader *bufio.Reader) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return orderSubmittedMsg{err: errors.New("not connected")}
|
return orderSubmittedMsg{err: errors.New("not connected")}
|
||||||
@ -516,11 +524,10 @@ func submitOrderCmd(conn net.Conn, ord order) tea.Cmd {
|
|||||||
return orderSubmittedMsg{err: fmt.Errorf("send ORDER: %w", err)}
|
return orderSubmittedMsg{err: fmt.Errorf("send ORDER: %w", err)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read single-line ack
|
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
_ = conn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||||
defer func() { _ = conn.SetReadDeadline(time.Time{}) }()
|
defer func() { _ = conn.SetReadDeadline(time.Time{}) }()
|
||||||
r := bufio.NewReader(conn)
|
|
||||||
line, err := r.ReadString('\n')
|
line, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return orderSubmittedMsg{err: fmt.Errorf("read ORDER ack: %w", err)}
|
return orderSubmittedMsg{err: fmt.Errorf("read ORDER ack: %w", err)}
|
||||||
}
|
}
|
||||||
@ -537,13 +544,12 @@ func submitOrderCmd(conn net.Conn, ord order) tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listenForBroadcastsCmd(conn net.Conn) tea.Cmd {
|
func listenForBroadcastsCmd(conn net.Conn, reader *bufio.Reader) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
if conn == nil {
|
if conn == nil || reader == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
r := bufio.NewReader(conn)
|
line, err := reader.ReadString('\n')
|
||||||
line, err := r.ReadString('\n')
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return statusMsg(fmt.Sprintf("Connection closed: %v", err))
|
return statusMsg(fmt.Sprintf("Connection closed: %v", err))
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user