From: Anderson A. Giacomolli Date: Sat, 7 Nov 2026 16:45:37 +0000 (-0300) Subject: Name and order the view code X-Git-Url: http://git.giacomolli.uk/onechat/commitdiff_plain/7b6d0a81e0f3c8e33b1b76d5ecbc5bdb25a87249?ds=inline Name and order the view code 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 --- diff --git a/memory.go b/memory.go index a3665a1..f8171b2 100644 --- 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) } diff --git a/memory_test.go b/memory_test.go index 7b39795..b207601 100644 --- a/memory_test.go +++ b/memory_test.go @@ -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, "\n0+1|"+pending+"\n", memory.View()) + require.Equal(t, "\n0+1|"+viewPending+"\n", memory.View()) summarizer.SetFailing(false) diff --git a/replay_test.go b/replay_test.go index 32ffebe..a19ea9e 100644 --- a/replay_test.go +++ b/replay_test.go @@ -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. diff --git a/tools.go b/tools.go index 73c584a..7ca031c 100644 --- 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 08a144e..1593f43 100644 --- 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{} diff --git a/turn_test.go b/turn_test.go index 69c9236..b89a0ec 100644 --- a/turn_test.go +++ b/turn_test.go @@ -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 a75b1d8..e392364 100644 --- 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 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("\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("") + + 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("\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("") + 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) } diff --git a/layout.go b/view_layout.go similarity index 74% rename from layout.go rename to view_layout.go index c5e9ac3..0d4e756 100644 --- a/layout.go +++ b/view_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 "\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 "\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]) } diff --git a/layout_test.go b/view_layout_test.go similarity index 88% rename from layout_test.go rename to view_layout_test.go index 3fab197..58c0e1f 100644 --- a/layout_test.go +++ b/view_layout_test.go @@ -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{"\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"}) diff --git a/view_test.go b/view_test.go index 201483e..7184f8c 100644 --- a/view_test.go +++ b/view_test.go @@ -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, "\n0+2|aaa aaa\n2+1|"+pending+"\n", view.Render()) - require.Equal(t, 7+len(pending), view.Size()) + require.Equal(t, "\n0+2|aaa aaa\n2+1|"+viewPending+"\n", 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), + } +}