]> git.giacomolli.uk - onechat/commitdiff
Name and order the view code
authorAnderson A. Giacomolli <agiacomolli@gmail.com>
Sat, 7 Nov 2026 16:45:37 +0000 (13:45 -0300)
committerAnderson A. Giacomolli <agiacomolli@gmail.com>
Sat, 7 Nov 2026 16:45:37 +0000 (13:45 -0300)
Every package-level name of the view starts with view: its size limits,
the pending mark, the saved state ViewState, and the interface
ViewNodeReader. The layout of the view messages in the turn requests is
in view_layout.go, as ViewLayout. The limits chatHigh and compactionHigh
read as limits of the chat and of the compactor, but they are limits of
the two views.

The declarations of the view files follow the order of the go-code
skill. The behavior does not change.

Issue: 947bddb

memory.go
memory_test.go
replay_test.go
tools.go
turn.go
turn_test.go
view.go
view_layout.go [moved from layout.go with 74% similarity]
view_layout_test.go [moved from layout_test.go with 88% similarity]
view_test.go

index a3665a1d64a57d91e9b4255178b09dba63e66c8c..f8171b2239b9b502b422447cf81ae4f73fe0ae33 100644 (file)
--- a/memory.go
+++ b/memory.go
@@ -66,8 +66,8 @@ type Memory struct {
 
 // viewFile is the content of view.json.
 type viewFile struct {
-       Chat       viewState `json:"chat"`
-       Compaction viewState `json:"compaction"`
+       Chat       ViewState `json:"chat"`
+       Compaction ViewState `json:"compaction"`
 }
 
 // openMemory opens the chat in root and starts the builds that are ready. The
@@ -126,8 +126,8 @@ func openMemory(ctx context.Context, root string, clock func() time.Time, summar
 func (m *Memory) load() error {
        data, err := os.ReadFile(m.viewPath())
        if errors.Is(err, os.ErrNotExist) && m.log.Len() == 0 {
-               m.chat = newView(chatHigh, chatLow)
-               m.cview = newView(compactionHigh, compactionLow)
+               m.chat = newView(viewChatHigh, viewChatLow)
+               m.cview = newView(viewCompactionHigh, viewCompactionLow)
 
                return nil
        }
@@ -146,12 +146,12 @@ func (m *Memory) load() error {
                return fmt.Errorf("decode view: %w", err)
        }
 
-       m.chat, err = restoreView(file.Chat, chatHigh, chatLow, m.tree)
+       m.chat, err = restoreView(file.Chat, viewChatHigh, viewChatLow, m.tree)
        if err != nil {
                return fmt.Errorf("chat view: %w", err)
        }
 
-       m.cview, err = restoreView(file.Compaction, compactionHigh, compactionLow, m.tree)
+       m.cview, err = restoreView(file.Compaction, viewCompactionHigh, viewCompactionLow, m.tree)
        if err != nil {
                return fmt.Errorf("compaction view: %w", err)
        }
index 7b3979503a012f666acc99d887ed792fd16bd485..b2076011913912453f32bc98b02096aa5a6f7c65 100644 (file)
@@ -261,7 +261,7 @@ func TestMemoryKeepsALongMessageOutOfTheView(t *testing.T) {
        _, err = memory.Append("echo", longText(0))
        require.NoError(t, err)
        require.NoError(t, memory.Wait(t.Context()))
-       require.Equal(t, "<chat>\n0+1|"+pending+"\n</chat>", memory.View())
+       require.Equal(t, "<chat>\n0+1|"+viewPending+"\n</chat>", memory.View())
 
        summarizer.SetFailing(false)
 
index 32ffebe0778acaca6732a5a05ac5d48821bdd0ff..a19ea9e8ba6646900bf72c230dc8528afb75c623 100644 (file)
@@ -34,7 +34,7 @@ func TestReplaySyntheticLog(t *testing.T) {
        }()
 
        batches := 0
-       low := chatHigh
+       low := viewChatHigh
        long := make([]bool, syntheticMessages)
        for id := range syntheticMessages {
                if id == syntheticMessages/2 {
@@ -118,12 +118,12 @@ func requireViews(t *testing.T, memory *Memory, lines int, long []bool, full boo
        defer memory.mutex.Unlock()
 
        size := appendSize(memory.chat)
-       require.LessOrEqual(t, size, chatHigh)
-       require.LessOrEqual(t, appendSize(memory.cview), compactionHigh)
+       require.LessOrEqual(t, size, viewChatHigh)
+       require.LessOrEqual(t, appendSize(memory.cview), viewCompactionHigh)
 
        merged := len(memory.chat.lines) < lines
        if merged {
-               require.LessOrEqual(t, size, chatLow)
+               require.LessOrEqual(t, size, viewChatLow)
        }
 
        requireOnlySummaries(t, memory, long, full)
@@ -151,7 +151,7 @@ func requireOnlySummaries(t *testing.T, memory *Memory, long []bool, full bool)
 
                if !built {
                        require.Equal(t, 0, line.level)
-                       require.Equal(t, head+pending, rendered[n+1])
+                       require.Equal(t, head+viewPending, rendered[n+1])
 
                        continue
                }
@@ -166,7 +166,7 @@ func requireOnlySummaries(t *testing.T, memory *Memory, long []bool, full bool)
 // appendSize returns the size of view with its last line as the pending mark.
 func appendSize(view *View) int {
        last := view.lines[len(view.lines)-1]
-       return view.Size() - view.lineSize(last) + len(pending)
+       return view.Size() - view.lineSize(last) + len(viewPending)
 }
 
 // viewLines returns the number of lines in the chat view of memory.
index 73c584ae19c5a41bbd66642879e05e78b31ab39b..7ca031c7d4566212486e1e42ee103a4aff5465a9 100644 (file)
--- a/tools.go
+++ b/tools.go
@@ -198,7 +198,7 @@ func (b *toolbox) zoom(id, n int) (string, error) {
                        return "", err
                }
 
-               text := pending
+               text := viewPending
                if built {
                        text = strings.ReplaceAll(node.Text, "\n", " ")
                }
diff --git a/turn.go b/turn.go
index 08a144e48ac7dafcda1588e4e4387946d36177ca..1593f434be72635911ef72958a43d960fc3fa7f1 100644 (file)
--- a/turn.go
+++ b/turn.go
@@ -17,7 +17,7 @@ var errTurnLogWrite = errors.New("log write failed")
 // newTurnLoop returns the loop of agent with its memory. Its calls go through
 // client, and notify gets each event. The clock gives the date of a turn.
 func newTurnLoop(agent *Agent, memory *Memory, client TurnClient, clock func() time.Time, notify func(Event)) (*TurnLoop, error) {
-       layout, err := loadLayout(filepath.Join(agent.dir, layoutFileName))
+       layout, err := loadViewLayout(filepath.Join(agent.dir, viewLayoutFileName))
        if err != nil {
                return nil, err
        }
@@ -64,8 +64,8 @@ type Event struct {
 // of turnInput. The loop runs the tool calls of each response and sends their
 // results back, until a response has no tool call: the final answer.
 //
-// The view goes out as the messages of a viewLayout, so the first request of
-// a turn reads the view of the turn before from the prompt cache.
+// The view goes out as the messages of a ViewLayout, so the first request of a
+// turn reads the view of the turn before from the prompt cache.
 //
 // Each later request of a turn begins with the whole input of the request
 // before it, then its output items, then the tool results. The output items
@@ -79,7 +79,7 @@ type TurnLoop struct {
        memory *Memory
        client TurnClient
        tools  *toolbox
-       layout *viewLayout
+       layout *ViewLayout
        clock  func() time.Time
        notify func(Event)
        wake   chan struct{}
index 69c9236081ba743fd49ab88d55a3c403767a7efe..b89a0ec8727e6cc238dc747d61c955adfbb10eaa 100644 (file)
@@ -170,7 +170,7 @@ func TestTurnWaitsForTheSummaries(t *testing.T) {
        close(summarizer.gate)
        test.events.WaitFor(t, "idle", 1)
        require.Equal(t, 1, server.Calls())
-       require.NotContains(t, turnInputTexts(t, server.Call(0))[0], pending)
+       require.NotContains(t, turnInputTexts(t, server.Call(0))[0], viewPending)
 }
 
 func TestInterruptEndsTheTurn(t *testing.T) {
diff --git a/view.go b/view.go
index a75b1d8418fc9857d6aafbf38a822c989a817d77..e39236401ca08dde025c4f91efefb7f39b54eaed 100644 (file)
--- a/view.go
+++ b/view.go
@@ -9,55 +9,20 @@ import (
 )
 
 const (
-       // chatHigh and chatLow are the limits of the chat view in bytes.
-       chatHigh = 128000
-       chatLow  = 64000
-
-       // compactionHigh and compactionLow are the limits of the compaction
-       // view in bytes.
-       compactionHigh = 32000
-       compactionLow  = 16000
-
-       // pending is the text of a line that is not built yet.
-       pending = "(not summarized yet: zoom it)"
+       // viewChatHigh and viewChatLow are the limits of the chat view in
+       // bytes.
+       viewChatHigh = 128000
+       viewChatLow  = 64000
+
+       // viewCompactionHigh and viewCompactionLow are the limits of the
+       // compaction view in bytes.
+       viewCompactionHigh = 32000
+       viewCompactionLow  = 16000
+
+       // viewPending is the text of a line that is not built yet.
+       viewPending = "(not summarized yet: zoom it)"
 )
 
-// nodeReader gives the built nodes of a tree.
-type nodeReader interface {
-       Built(level, index int) (bool, error)
-       Node(level, index int) (Node, bool, error)
-}
-
-// View is a list of tree nodes that covers messages 0 to end-1 without a gap,
-// oldest first. Each new message appends its leaf as a line. Once the size of
-// the view goes above high, one batch merges the most due sibling pairs until
-// the size is at most low. A batch merges only pairs whose parent is built. If
-// the batch cannot get to low, it continues at each new message until it does.
-//
-// The view keeps the nodes of its built lines in memory, so its size and its
-// text need no read of the tree. The size of a line is the size of its node
-// text, or the size of the pending text when the node is not built.
-//
-// For the sibling pair (l, i) and (l, i+1), due is (T - last) / 2^l, with T the
-// number of messages and last the ID of the last message of the pair. That is
-// how long ago the pair ended, measured in its own line size. With a budget of
-// the length of the rollback push list of the reference design, this order
-// makes the same merges as the push. The oldest pair wins a tie.
-type View struct {
-       high    int
-       low     int
-       lines   []nodeKey
-       nodes   map[nodeKey]Node
-       end     int
-       merging bool
-}
-
-// viewState is the saved form of a View. The limits are not part of it.
-type viewState struct {
-       Lines   [][2]int `json:"lines"`
-       Merging bool     `json:"merging"`
-}
-
 // newView returns an empty view with the limits high and low.
 func newView(high, low int) *View {
        return &View{
@@ -69,7 +34,7 @@ func newView(high, low int) *View {
 
 // restoreView returns the view of state with the limits high and low. Each line
 // must continue the one before it, and each line above level 0 must be built.
-func restoreView(state viewState, high, low int, tree nodeReader) (*View, error) {
+func restoreView(state ViewState, high, low int, tree ViewNodeReader) (*View, error) {
        v := newView(high, low)
        v.merging = state.Merging
        for _, pair := range state.Lines {
@@ -94,21 +59,33 @@ func restoreView(state viewState, high, low int, tree nodeReader) (*View, error)
        return v, nil
 }
 
-// load keeps the node of line when it is built, and tells if it is.
-func (v *View) load(tree nodeReader, line nodeKey) (bool, error) {
-       node, built, err := tree.Node(line.level, line.index)
-       if err != nil || !built {
-               return false, err
-       }
-
-       v.nodes[line] = node
-
-       return true, nil
+// View is a list of tree nodes that covers messages 0 to end-1 without a gap,
+// oldest first. Each new message appends its leaf as a line. Once the size of
+// the view goes above high, one batch merges the most due sibling pairs until
+// the size is at most low. A batch merges only pairs whose parent is built. If
+// the batch cannot get to low, it continues at each new message until it does.
+//
+// The view keeps the nodes of its built lines in memory, so its size and its
+// text need no read of the tree. The size of a line is the size of its node
+// text, or the size of the pending text when the node is not built.
+//
+// For the sibling pair (l, i) and (l, i+1), due is (T - last) / 2^l, with T the
+// number of messages and last the ID of the last message of the pair. That is
+// how long ago the pair ended, measured in its own line size. With a budget of
+// the length of the rollback push list of the reference design, this order
+// makes the same merges as the push. The oldest pair wins a tie.
+type View struct {
+       high    int
+       low     int
+       lines   []nodeKey
+       nodes   map[nodeKey]Node
+       end     int
+       merging bool
 }
 
 // State returns the saved form of the view.
-func (v *View) State() viewState {
-       state := viewState{
+func (v *View) State() ViewState {
+       state := ViewState{
                Lines:   make([][2]int, len(v.lines)),
                Merging: v.merging,
        }
@@ -122,7 +99,7 @@ func (v *View) State() viewState {
 
 // Append adds the leaf of the next message as a line and merges when the view
 // is too large. It tells if a merge changed the view.
-func (v *View) Append(tree nodeReader) (bool, error) {
+func (v *View) Append(tree ViewNodeReader) (bool, error) {
        line := nodeKey{0, v.end}
        _, err := v.load(tree, line)
        if err != nil {
@@ -135,47 +112,12 @@ func (v *View) Append(tree nodeReader) (bool, error) {
        return v.fit(tree)
 }
 
-// fit merges the most due pairs while a batch runs, and tells if a merge
-// changed the view.
-func (v *View) fit(tree nodeReader) (bool, error) {
-       size := v.Size()
-       if size > v.high {
-               v.merging = true
-       }
-
-       merged := false
-       for v.merging && size > v.low {
-               best, err := v.mostDue(tree)
-               if err != nil || best < 0 {
-                       return merged, err
-               }
-
-               left, right := v.lines[best], v.lines[best+1]
-               parent := nodeKey{left.level + 1, left.index / 2}
-               _, err = v.load(tree, parent)
-               if err != nil {
-                       return merged, err
-               }
-
-               size += v.lineSize(parent) - v.lineSize(left) - v.lineSize(right)
-               delete(v.nodes, left)
-               delete(v.nodes, right)
-               v.lines[best] = parent
-               v.lines = append(v.lines[:best+1], v.lines[best+2:]...)
-               merged = true
-       }
-
-       v.merging = false
-
-       return merged, nil
-}
-
 // Coarsen takes each merge of other, a view of the same messages, and then
 // merges in one batch until the size is at most low. Each line of the result is
 // the coarser of the two lines that start at its first message. Dyadic ranges
 // either nest or do not overlap, so the result covers the messages without a
 // gap.
-func (v *View) Coarsen(other *View, tree nodeReader) error {
+func (v *View) Coarsen(other *View, tree ViewNodeReader) error {
        lines := make([]nodeKey, 0, len(v.lines))
        nodes := make(map[nodeKey]Node, len(v.nodes))
        mine, theirs := 0, 0
@@ -210,37 +152,6 @@ func (v *View) Coarsen(other *View, tree nodeReader) error {
        return err
 }
 
-// mostDue returns the position of the left line of the most due sibling pair
-// whose parent is built, or -1 when there is no such pair.
-func (v *View) mostDue(tree nodeReader) (int, error) {
-       best := -1
-       bestDue := 0.0
-       for n := 0; n+1 < len(v.lines); n++ {
-               left, right := v.lines[n], v.lines[n+1]
-               if left.index%2 != 0 || right.level != left.level || right.index != left.index+1 {
-                       continue
-               }
-
-               last := nodeEnd(right.level, right.index) - 1
-               due := math.Ldexp(float64(v.end-last), -left.level)
-               if best >= 0 && due <= bestDue {
-                       continue
-               }
-
-               built, err := tree.Built(left.level+1, left.index/2)
-               if err != nil {
-                       return -1, err
-               }
-
-               if built {
-                       best = n
-                       bestDue = due
-               }
-       }
-
-       return best, nil
-}
-
 // Refresh keeps node when it is the node of a pending line.
 func (v *View) Refresh(node Node) {
        key := nodeKey{node.Level, node.Index}
@@ -257,26 +168,6 @@ func (v *View) Refresh(node Node) {
        }
 }
 
-// Size returns the sum of the line sizes.
-func (v *View) Size() int {
-       size := 0
-       for _, line := range v.lines {
-               size += v.lineSize(line)
-       }
-
-       return size
-}
-
-// lineSize returns the size of a view line.
-func (v *View) lineSize(line nodeKey) int {
-       node, built := v.nodes[line]
-       if !built {
-               return len(pending)
-       }
-
-       return node.Size
-}
-
 // Render returns the view as the model reads it: one line "id+n|text" for each
 // node inside <chat> tags, with the newlines of the text as spaces.
 func (v *View) Render() string {
@@ -301,9 +192,76 @@ func (v *View) LinesWithout(skip map[int]bool) []string {
        return lines
 }
 
+// Context returns the lines of the view before message end as the model reads
+// them. It stops at the first line that is not built, so a compaction never
+// sees a pending mark or a part of a message.
+func (v *View) Context(end int) string {
+       var text strings.Builder
+       text.WriteString("<chat>\n")
+       for _, line := range v.lines {
+               _, built := v.nodes[line]
+               if !built || nodeEnd(line.level, line.index) > end {
+                       break
+               }
+
+               v.renderLine(&text, line)
+       }
+
+       text.WriteString("</chat>")
+
+       return text.String()
+}
+
+// load keeps the node of line when it is built, and tells if it is.
+func (v *View) load(tree ViewNodeReader, line nodeKey) (bool, error) {
+       node, built, err := tree.Node(line.level, line.index)
+       if err != nil || !built {
+               return false, err
+       }
+
+       v.nodes[line] = node
+
+       return true, nil
+}
+
+// fit merges the most due pairs while a batch runs, and tells if a merge
+// changed the view.
+func (v *View) fit(tree ViewNodeReader) (bool, error) {
+       size := v.Size()
+       if size > v.high {
+               v.merging = true
+       }
+
+       merged := false
+       for v.merging && size > v.low {
+               best, err := v.mostDue(tree)
+               if err != nil || best < 0 {
+                       return merged, err
+               }
+
+               left, right := v.lines[best], v.lines[best+1]
+               parent := nodeKey{left.level + 1, left.index / 2}
+               _, err = v.load(tree, parent)
+               if err != nil {
+                       return merged, err
+               }
+
+               size += v.lineSize(parent) - v.lineSize(left) - v.lineSize(right)
+               delete(v.nodes, left)
+               delete(v.nodes, right)
+               v.lines[best] = parent
+               v.lines = append(v.lines[:best+1], v.lines[best+2:]...)
+               merged = true
+       }
+
+       v.merging = false
+
+       return merged, nil
+}
+
 // renderLine writes one view line, "id+n|text", and a newline.
 func (v *View) renderLine(text *strings.Builder, line nodeKey) {
-       body := pending
+       body := viewPending
        node, built := v.nodes[line]
        if built {
                body = strings.ReplaceAll(node.Text, "\n", " ")
@@ -317,22 +275,65 @@ func (v *View) renderLine(text *strings.Builder, line nodeKey) {
        text.WriteByte('\n')
 }
 
-// Context returns the lines of the view before message end as the model reads
-// them. It stops at the first line that is not built, so a compaction never
-// sees a pending mark or a part of a message.
-func (v *View) Context(end int) string {
-       var text strings.Builder
-       text.WriteString("<chat>\n")
+// Size returns the sum of the line sizes.
+func (v *View) Size() int {
+       size := 0
        for _, line := range v.lines {
-               _, built := v.nodes[line]
-               if !built || nodeEnd(line.level, line.index) > end {
-                       break
+               size += v.lineSize(line)
+       }
+
+       return size
+}
+
+// mostDue returns the position of the left line of the most due sibling pair
+// whose parent is built, or -1 when there is no such pair.
+func (v *View) mostDue(tree ViewNodeReader) (int, error) {
+       best := -1
+       bestDue := 0.0
+       for n := 0; n+1 < len(v.lines); n++ {
+               left, right := v.lines[n], v.lines[n+1]
+               if left.index%2 != 0 || right.level != left.level || right.index != left.index+1 {
+                       continue
                }
 
-               v.renderLine(&text, line)
+               last := nodeEnd(right.level, right.index) - 1
+               due := math.Ldexp(float64(v.end-last), -left.level)
+               if best >= 0 && due <= bestDue {
+                       continue
+               }
+
+               built, err := tree.Built(left.level+1, left.index/2)
+               if err != nil {
+                       return -1, err
+               }
+
+               if built {
+                       best = n
+                       bestDue = due
+               }
        }
 
-       text.WriteString("</chat>")
+       return best, nil
+}
 
-       return text.String()
+// lineSize returns the size of a view line.
+func (v *View) lineSize(line nodeKey) int {
+       node, built := v.nodes[line]
+       if !built {
+               return len(viewPending)
+       }
+
+       return node.Size
+}
+
+// ViewState is the saved form of a View. The limits are not part of it.
+type ViewState struct {
+       Lines   [][2]int `json:"lines"`
+       Merging bool     `json:"merging"`
+}
+
+// ViewNodeReader gives the built nodes of a tree.
+type ViewNodeReader interface {
+       Built(level, index int) (bool, error)
+       Node(level, index int) (Node, bool, error)
 }
similarity index 74%
rename from layout.go
rename to view_layout.go
index c5e9ac3a5a812cc74fae830970408bcae9f56faf..0d4e7560581a5d554932bf46280f344aa5b0d75b 100644 (file)
--- a/layout.go
@@ -10,36 +10,14 @@ import (
        "strings"
 )
 
-// layoutFileName is the file of the view layout in the directory of an agent.
-const layoutFileName = "layout.json"
-
-// viewLayout splits the view of a turn request into user messages that only
-// grow between batches. The view goes out as the same messages as in the
-// request before, while their lines give the same text, and the new lines
-// follow as one new message. So the end of the view of each earlier turn stays
-// a message end. On the ChatGPT backend, the first request of a turn with this
-// layout read the view of the turn before from the prompt cache in each of 10
-// turns, and with the view in one message it missed once in 5.
-//
-// The first message starts with "<chat>\n". The closing tag goes into the
-// state message after the view, so that no view message changes when lines
-// come. The layout keeps the line count and a hash of the text of each message
-// in layout.json, so it outlives a restart.
-type viewLayout struct {
-       path     string
-       Messages []layoutMessage `json:"messages"`
-}
-
-// layoutMessage is one view message: its number of lines and the hash of its
-// text.
-type layoutMessage struct {
-       Lines int    `json:"lines"`
-       Hash  string `json:"hash"`
-}
-
-// loadLayout reads the layout at path. A missing file gives an empty layout.
-func loadLayout(path string) (*viewLayout, error) {
-       layout := &viewLayout{
+// viewLayoutFileName is the file of the view layout in the directory of an
+// agent.
+const viewLayoutFileName = "layout.json"
+
+// loadViewLayout reads the layout at path. A missing file gives an empty
+// layout.
+func loadViewLayout(path string) (*ViewLayout, error) {
+       layout := &ViewLayout{
                path: path,
        }
 
@@ -60,12 +38,29 @@ func loadLayout(path string) (*viewLayout, error) {
        return layout, nil
 }
 
+// ViewLayout splits the view of a turn request into user messages that only
+// grow between batches. The view goes out as the same messages as in the
+// request before, while their lines give the same text, and the new lines
+// follow as one new message. So the end of the view of each earlier turn stays
+// a message end. On the ChatGPT backend, the first request of a turn with this
+// layout read the view of the turn before from the prompt cache in each of 10
+// turns, and with the view in one message it missed once in 5.
+//
+// The first message starts with "<chat>\n". The closing tag goes into the
+// state message after the view, so that no view message changes when lines
+// come. The layout keeps the line count and a hash of the text of each message
+// in layout.json, so it outlives a restart.
+type ViewLayout struct {
+       path     string
+       Messages []ViewLayoutMessage `json:"messages"`
+}
+
 // Split returns the view messages for lines and saves the layout. It keeps the
 // earlier messages up to the first one whose text differs, and puts the rest
 // of the lines into one new message.
-func (l *viewLayout) Split(lines []string) ([]string, error) {
+func (l *ViewLayout) Split(lines []string) ([]string, error) {
        var texts []string
-       var kept []layoutMessage
+       var kept []ViewLayoutMessage
        start := 0
        for _, message := range l.Messages {
                if start+message.Lines > len(lines) {
@@ -73,7 +68,7 @@ func (l *viewLayout) Split(lines []string) ([]string, error) {
                }
 
                text := viewMessage(len(texts) == 0, lines[start:start+message.Lines])
-               if textHash(text) != message.Hash {
+               if viewTextHash(text) != message.Hash {
                        break
                }
 
@@ -85,9 +80,9 @@ func (l *viewLayout) Split(lines []string) ([]string, error) {
        if start < len(lines) || len(texts) == 0 {
                text := viewMessage(len(texts) == 0, lines[start:])
                texts = append(texts, text)
-               kept = append(kept, layoutMessage{
+               kept = append(kept, ViewLayoutMessage{
                        Lines: len(lines) - start,
-                       Hash:  textHash(text),
+                       Hash:  viewTextHash(text),
                })
        }
 
@@ -105,6 +100,13 @@ func (l *viewLayout) Split(lines []string) ([]string, error) {
        return texts, nil
 }
 
+// ViewLayoutMessage is one view message: its number of lines and the hash of
+// its text.
+type ViewLayoutMessage struct {
+       Lines int    `json:"lines"`
+       Hash  string `json:"hash"`
+}
+
 // viewMessage returns the text of a view message with lines. The first one
 // opens the chat.
 func viewMessage(first bool, lines []string) string {
@@ -116,8 +118,8 @@ func viewMessage(first bool, lines []string) string {
        return text
 }
 
-// textHash returns a short hash of text.
-func textHash(text string) string {
+// viewTextHash returns a short hash of text.
+func viewTextHash(text string) string {
        sum := sha256.Sum256([]byte(text))
        return hex.EncodeToString(sum[:16])
 }
similarity index 88%
rename from layout_test.go
rename to view_layout_test.go
index 3fab19760d5c3c83227d72c371246a5a36d15504..58c0e1f8ac3bc511dcb9326d23718e71bc55575a 100644 (file)
@@ -8,8 +8,8 @@ import (
 )
 
 func TestViewLayoutKeepsTheMessagesItSent(t *testing.T) {
-       path := filepath.Join(t.TempDir(), layoutFileName)
-       layout, err := loadLayout(path)
+       path := filepath.Join(t.TempDir(), viewLayoutFileName)
+       layout, err := loadViewLayout(path)
        require.NoError(t, err)
 
        texts, err := layout.Split(nil)
@@ -21,7 +21,7 @@ func TestViewLayoutKeepsTheMessagesItSent(t *testing.T) {
        require.Equal(t, []string{"<chat>\n", "0+1|a\n1+1|b\n"}, texts)
 
        // The same lines give the same messages, also after a restart.
-       layout, err = loadLayout(path)
+       layout, err = loadViewLayout(path)
        require.NoError(t, err)
 
        texts, err = layout.Split([]string{"0+1|a\n", "1+1|b\n"})
@@ -34,7 +34,7 @@ func TestViewLayoutKeepsTheMessagesItSent(t *testing.T) {
 }
 
 func TestViewLayoutStartsAgainAtAChangedMessage(t *testing.T) {
-       layout, err := loadLayout(filepath.Join(t.TempDir(), layoutFileName))
+       layout, err := loadViewLayout(filepath.Join(t.TempDir(), viewLayoutFileName))
        require.NoError(t, err)
 
        _, err = layout.Split([]string{"0+1|a\n"})
index 201483e8c79d966d6782aa5a7c0ced7dca96d02a..7184f8c7b2c89cab9d6139567916dfc071fccfea 100644 (file)
@@ -9,11 +9,11 @@ import (
 )
 
 func TestViewMergesInTheOrderOfTheRollbackPush(t *testing.T) {
-       tree := builtTree{}
+       tree := ViewBuiltTree{}
        view := newView(0, 0)
-       var states *pushEntry
+       var states *ViewPushEntry
        for tick := range 20001 {
-               states = push(tick, states)
+               states = viewPush(tick, states)
                starts := states.starts()
                view.high = len(starts)
                view.low = len(starts)
@@ -30,7 +30,7 @@ func TestViewMergesInTheOrderOfTheRollbackPush(t *testing.T) {
 }
 
 func TestViewMergesInOneBatchFromHighToLow(t *testing.T) {
-       tree := builtTree{}
+       tree := ViewBuiltTree{}
        view := newView(10, 5)
        sizes := make([]int, 0, 20)
        for range 20 {
@@ -43,7 +43,7 @@ func TestViewMergesInOneBatchFromHighToLow(t *testing.T) {
 }
 
 func TestViewMergesOnlyBuiltParents(t *testing.T) {
-       tree := partTree{
+       tree := ViewPartTree{
                sizes: map[nodeKey]int{
                        {0, 0}: 1,
                        {0, 1}: 1,
@@ -79,7 +79,7 @@ func TestViewMergesOnlyBuiltParents(t *testing.T) {
 }
 
 func TestViewRendersPendingLinesWithoutTheirMessage(t *testing.T) {
-       tree := partTree{
+       tree := ViewPartTree{
                sizes: map[nodeKey]int{
                        {0, 0}: 3,
                        {0, 1}: 3,
@@ -87,36 +87,36 @@ func TestViewRendersPendingLinesWithoutTheirMessage(t *testing.T) {
                },
        }
 
-       view, err := restoreView(viewState{
+       view, err := restoreView(ViewState{
                Lines: [][2]int{{1, 0}, {0, 2}},
-       }, chatHigh, chatLow, tree)
+       }, viewChatHigh, viewChatLow, tree)
        require.NoError(t, err)
-       require.Equal(t, "<chat>\n0+2|aaa aaa\n2+1|"+pending+"\n</chat>", view.Render())
-       require.Equal(t, 7+len(pending), view.Size())
+       require.Equal(t, "<chat>\n0+2|aaa aaa\n2+1|"+viewPending+"\n</chat>", view.Render())
+       require.Equal(t, 7+len(viewPending), view.Size())
 }
 
 func TestViewCoarsenTakesTheCoarserLines(t *testing.T) {
-       view, err := restoreView(viewState{
+       view, err := restoreView(ViewState{
                Lines: [][2]int{{1, 0}, {0, 2}, {0, 3}, {0, 4}, {0, 5}},
-       }, 100, 100, builtTree{})
+       }, 100, 100, ViewBuiltTree{})
        require.NoError(t, err)
 
-       other, err := restoreView(viewState{
+       other, err := restoreView(ViewState{
                Lines: [][2]int{{0, 0}, {0, 1}, {1, 1}, {0, 4}, {0, 5}},
-       }, 100, 100, builtTree{})
+       }, 100, 100, ViewBuiltTree{})
        require.NoError(t, err)
 
-       require.NoError(t, view.Coarsen(other, builtTree{}))
+       require.NoError(t, view.Coarsen(other, ViewBuiltTree{}))
        require.Equal(t, []nodeKey{{1, 0}, {1, 1}, {0, 4}, {0, 5}}, view.lines)
 
        view.low = 2
-       require.NoError(t, view.Coarsen(other, builtTree{}))
+       require.NoError(t, view.Coarsen(other, ViewBuiltTree{}))
        require.Equal(t, []nodeKey{{2, 0}, {1, 2}}, view.lines)
        require.False(t, view.merging)
 }
 
 func TestViewContextStopsAtTheFirstPendingLine(t *testing.T) {
-       tree := partTree{
+       tree := ViewPartTree{
                sizes: map[nodeKey]int{
                        {0, 0}: 1,
                        {0, 1}: 1,
@@ -125,7 +125,7 @@ func TestViewContextStopsAtTheFirstPendingLine(t *testing.T) {
                },
        }
 
-       view, err := restoreView(viewState{
+       view, err := restoreView(ViewState{
                Lines: [][2]int{{1, 0}, {0, 2}, {0, 3}},
        }, 100, 100, tree)
        require.NoError(t, err)
@@ -135,44 +135,34 @@ func TestViewContextStopsAtTheFirstPendingLine(t *testing.T) {
 }
 
 func TestRestoreViewRejectsAGap(t *testing.T) {
-       _, err := restoreView(viewState{
+       _, err := restoreView(ViewState{
                Lines: [][2]int{{0, 0}, {0, 2}},
-       }, chatHigh, chatLow, builtTree{})
+       }, viewChatHigh, viewChatLow, ViewBuiltTree{})
        require.ErrorContains(t, err, "does not start at message 1")
 }
 
-// pushEntry is one entry of the rollback state list of the reference design,
-// newest first. Under push alone, life stays 0, so the entry leaves it out.
-type pushEntry struct {
-       keep  bool
-       state int
-       older *pushEntry
-}
+// ViewBuiltTree is a tree with each node built, with 1 byte of text.
+type ViewBuiltTree struct{}
 
-// push is the push of rollback_state_list.js.
-func push(state int, states *pushEntry) *pushEntry {
-       if states == nil {
-               return &pushEntry{
-                       state: state,
-               }
-       }
+func (ViewBuiltTree) Built(level, index int) (bool, error) {
+       return true, nil
+}
 
-       if !states.keep {
-               return &pushEntry{
-                       keep:  true,
-                       state: states.state,
-                       older: states.older,
-               }
-       }
+func (ViewBuiltTree) Node(level, index int) (Node, bool, error) {
+       return testNode(level, index, "a"), true, nil
+}
 
-       return &pushEntry{
-               state: state,
-               older: push(states.state, states.older),
-       }
+// ViewPushEntry is one entry of the rollback state list of the reference
+// design, newest first. Under push alone, life stays 0, so the entry leaves it
+// out.
+type ViewPushEntry struct {
+       keep  bool
+       state int
+       older *ViewPushEntry
 }
 
 // starts returns the states of the list, oldest first.
-func (e *pushEntry) starts() []int {
+func (e *ViewPushEntry) starts() []int {
        var starts []int
        for entry := e; entry != nil; entry = entry.older {
                starts = append(starts, entry.state)
@@ -183,28 +173,17 @@ func (e *pushEntry) starts() []int {
        return starts
 }
 
-// builtTree is a tree with each node built, with 1 byte of text.
-type builtTree struct{}
-
-func (builtTree) Built(level, index int) (bool, error) {
-       return true, nil
-}
-
-func (builtTree) Node(level, index int) (Node, bool, error) {
-       return testNode(level, index, "a"), true, nil
-}
-
-// partTree is a tree with the built nodes of its sizes.
-type partTree struct {
+// ViewPartTree is a tree with the built nodes of its sizes.
+type ViewPartTree struct {
        sizes map[nodeKey]int
 }
 
-func (p partTree) Built(level, index int) (bool, error) {
+func (p ViewPartTree) Built(level, index int) (bool, error) {
        _, built := p.sizes[nodeKey{level, index}]
        return built, nil
 }
 
-func (p partTree) Node(level, index int) (Node, bool, error) {
+func (p ViewPartTree) Node(level, index int) (Node, bool, error) {
        size, built := p.sizes[nodeKey{level, index}]
        if !built {
                return Node{}, false, nil
@@ -217,3 +196,25 @@ func (p partTree) Node(level, index int) (Node, bool, error) {
 
        return testNode(level, index, text), true, nil
 }
+
+// viewPush is the push of rollback_state_list.js.
+func viewPush(state int, states *ViewPushEntry) *ViewPushEntry {
+       if states == nil {
+               return &ViewPushEntry{
+                       state: state,
+               }
+       }
+
+       if !states.keep {
+               return &ViewPushEntry{
+                       keep:  true,
+                       state: states.state,
+                       older: states.older,
+               }
+       }
+
+       return &ViewPushEntry{
+               state: state,
+               older: viewPush(states.state, states.older),
+       }
+}