X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/feda522eba3e2a27c91741c77d0d01f1840c0004..b961d7cdc59213673c3e4f86d3dae99daf0a1935:/tree.go diff --git a/tree.go b/tree.go index 9fb033f..4309597 100644 --- a/tree.go +++ b/tree.go @@ -19,23 +19,6 @@ import ( // few bytes over it, because the view measures the real sizes. const nodeBytes = 512 -// Node is one line of the summary tree. Node (l, i) covers the 2^l messages -// from i*2^l on. A node of level 0 holds one message. A node of a higher level -// merges its two children, (l-1, 2i) and (l-1, 2i+1). Size is the UTF-8 byte -// length of Text. -type Node struct { - Level int `json:"l"` - Index int `json:"i"` - Text string `json:"text"` - Size int `json:"size"` -} - -// nodeKey names the node (l, i). -type nodeKey struct { - level int - index int -} - // treeRecordBytes is the size of a record of index/tree.idx: the position of // the node line, the node index, and the node level. const treeRecordBytes = 32 @@ -54,13 +37,30 @@ const treeRecordBytes = 32 // built again from the day files. type Tree struct { clock func() time.Time - journal journal + journal Journal dir string - order *records - levels map[int]*records + order *Records + levels map[int]*Records failure error } +// Node is one line of the summary tree. Node (l, i) covers the 2^l messages +// from i*2^l on. A node of level 0 holds one message. A node of a higher level +// merges its two children, (l-1, 2i) and (l-1, 2i+1). Size is the UTF-8 byte +// length of Text. +type Node struct { + Level int `json:"l"` + Index int `json:"i"` + Text string `json:"text"` + Size int `json:"size"` +} + +// NodeKey names the node (l, i). +type NodeKey struct { + level int + index int +} + // openTree opens the tree of the chat in root and indexes its new lines. A chat // of total messages cannot have a node that covers a later message. func openTree(ctx context.Context, root string, total int, clock func() time.Time) (*Tree, error) { @@ -73,11 +73,11 @@ func openTree(ctx context.Context, root string, total int, clock func() time.Tim t := &Tree{ clock: clock, - journal: journal{ + journal: Journal{ dir: filepath.Join(root, "tree"), }, dir: filepath.Join(root, "index"), - levels: make(map[int]*records), + levels: make(map[int]*Records), } err := t.open(ctx, total) @@ -90,12 +90,13 @@ func openTree(ctx context.Context, root string, total int, clock func() time.Tim func (t *Tree) open(ctx context.Context, total int) error { var err error + t.order, err = openRecords(filepath.Join(t.dir, "tree.idx"), treeRecordBytes) if err != nil { return err } - start := position{} + start := Position{} if t.order.count == 0 { err = t.removeLevels() if err != nil { @@ -109,23 +110,24 @@ func (t *Tree) open(ctx context.Context, total int) error { // A crash can stop an add between the two index writes, so the // last node gets its level record again. - node, last := decodeTreeRecord(record) + node, last := treeDecodeRecord(record) err = t.writeLevel(node, last) if err != nil { return err } - start = position{ + start = Position{ day: last.day, offset: last.offset + uint64(last.length), } } - return t.journal.Scan(ctx, start, func(line []byte, place position) error { + return t.journal.Scan(ctx, start, func(line []byte, place Position) error { var node Node + err := json.Unmarshal(line, &node) if err != nil { - return err + return fmt.Errorf("decode tree node: %w", err) } err = t.check(node) @@ -137,10 +139,27 @@ func (t *Tree) open(ctx context.Context, total int) error { return fmt.Errorf("node (%d, %d) covers messages after the log", node.Level, node.Index) } - return t.addIndex(nodeKey{node.Level, node.Index}, place) + return t.addIndex(NodeKey{node.Level, node.Index}, place) }) } +// Close closes the files of the tree. +func (t *Tree) Close() error { + err := t.journal.Close() + if t.order != nil { + err = errors.Join(err, t.order.Close()) + t.order = nil + } + + for level, index := range t.levels { + err = errors.Join(err, index.Close()) + + delete(t.levels, level) + } + + return err +} + // removeLevels removes the level indexes, which are not valid without the order // index. func (t *Tree) removeLevels() error { @@ -160,7 +179,7 @@ func (t *Tree) removeLevels() error { } // writeLevel writes the position of node to its level index. -func (t *Tree) writeLevel(node nodeKey, place position) error { +func (t *Tree) writeLevel(node NodeKey, place Position) error { level, err := t.level(node.level) if err != nil { return err @@ -172,23 +191,6 @@ func (t *Tree) writeLevel(node nodeKey, place position) error { return level.Write(node.index, record) } -// level returns the index of level, and opens it at first use. -func (t *Tree) level(level int) (*records, error) { - index, open := t.levels[level] - if open { - return index, nil - } - - index, err := openRecords(filepath.Join(t.dir, fmt.Sprintf("tree-%d.idx", level)), positionBytes) - if err != nil { - return nil, err - } - - t.levels[level] = index - - return index, nil -} - // check makes sure that node is valid and can join the tree now. func (t *Tree) check(node Node) error { if node.Level < 0 || node.Level >= bits.UintSize-2 || node.Index < 0 { @@ -227,7 +229,7 @@ func (t *Tree) check(node Node) error { } // addIndex adds the node line at place to both indexes. -func (t *Tree) addIndex(node nodeKey, place position) error { +func (t *Tree) addIndex(node NodeKey, place Position) error { record := make([]byte, treeRecordBytes) encodePosition(record, place) binary.LittleEndian.PutUint64(record[positionBytes:], uint64(node.index)) @@ -241,20 +243,48 @@ func (t *Tree) addIndex(node nodeKey, place position) error { return t.writeLevel(node, place) } -// Close closes the files of the tree. -func (t *Tree) Close() error { - err := t.journal.Close() - if t.order != nil { - err = errors.Join(err, t.order.Close()) - t.order = nil +// level returns the index of level, and opens it at first use. +func (t *Tree) level(level int) (*Records, error) { + index, open := t.levels[level] + if open { + return index, nil } - for level, index := range t.levels { - err = errors.Join(err, index.Close()) - delete(t.levels, level) + index, err := openRecords(filepath.Join(t.dir, fmt.Sprintf("tree-%d.idx", level)), positionBytes) + if err != nil { + return nil, err } - return err + t.levels[level] = index + + return index, nil +} + +// Built tells if the node (l, i) is built. +func (t *Tree) Built(level, index int) (bool, error) { + _, built, err := t.place(level, index) + return built, err +} + +// place returns the position of the node (l, i) if it is built. +func (t *Tree) place(level, index int) (Position, bool, error) { + if level < 0 || level >= bits.UintSize-2 || index < 0 { + return Position{}, false, nil + } + + records, err := t.level(level) + if err != nil { + return Position{}, false, err + } + + record, found, err := records.Read(index) + if err != nil || !found { + return Position{}, false, err + } + + place := decodePosition(record) + + return place, place.length > 0, nil } // Add writes a new node to the tree. @@ -275,7 +305,7 @@ func (t *Tree) Add(node Node) error { place, err := t.journal.Write(t.clock().Format(time.DateOnly), append(line, '\n')) if err == nil { - err = t.addIndex(nodeKey{node.Level, node.Index}, place) + err = t.addIndex(NodeKey{node.Level, node.Index}, place) } if err != nil { @@ -286,33 +316,6 @@ func (t *Tree) Add(node Node) error { return nil } -// Built tells if the node (l, i) is built. -func (t *Tree) Built(level, index int) (bool, error) { - _, built, err := t.place(level, index) - return built, err -} - -// place returns the position of the node (l, i) if it is built. -func (t *Tree) place(level, index int) (position, bool, error) { - if level < 0 || level >= bits.UintSize-2 || index < 0 { - return position{}, false, nil - } - - records, err := t.level(level) - if err != nil { - return position{}, false, err - } - - record, found, err := records.Read(index) - if err != nil || !found { - return position{}, false, err - } - - place := decodePosition(record) - - return place, place.length > 0, nil -} - // Node reads the node (l, i) if it is built. func (t *Tree) Node(level, index int) (Node, bool, error) { place, built, err := t.place(level, index) @@ -326,6 +329,7 @@ func (t *Tree) Node(level, index int) (Node, bool, error) { } var node Node + err = json.Unmarshal(line, &node) if err != nil { return Node{}, false, fmt.Errorf("decode node (%d, %d): %w", level, index, err) @@ -338,41 +342,52 @@ func (t *Tree) Node(level, index int) (Node, bool, error) { return node, true, nil } -// Count returns the number of built nodes. -func (t *Tree) Count() int { - return t.order.count -} - // Keys returns the keys of the built nodes, ordered by level and then by index. -func (t *Tree) Keys() ([]nodeKey, error) { - keys := make([]nodeKey, 0, t.order.count) +func (t *Tree) Keys() ([]NodeKey, error) { + keys := make([]NodeKey, 0, t.order.count) for n := range t.order.count { record, _, err := t.order.Read(n) if err != nil { return nil, err } - key, _ := decodeTreeRecord(record) + key, _ := treeDecodeRecord(record) keys = append(keys, key) } - slices.SortFunc(keys, func(a, b nodeKey) int { + slices.SortFunc(keys, func(a, b NodeKey) int { return cmp.Or(cmp.Compare(a.level, b.level), cmp.Compare(a.index, b.index)) }) return keys, nil } -// leafSource returns the text that the leaf of message holds or summarizes. -func leafSource(message Message) string { - return message.Kind + ": " + message.Text +// Count returns the number of built nodes. +func (t *Tree) Count() int { + return t.order.count } -// freeLeaf returns the leaf of message when its source fits in a node. Such a -// leaf holds the source word for word and needs no model call. +// treeDecodeRecord reads the key and the position of a record of +// index/tree.idx. +func treeDecodeRecord(record []byte) (NodeKey, Position) { + key := NodeKey{ + level: int(binary.LittleEndian.Uint32(record[positionBytes+8:])), + index: int(binary.LittleEndian.Uint64(record[positionBytes:])), + } + + return key, decodePosition(record) +} + +// nodeEnd returns the ID after the last message of node (l, i). +func nodeEnd(level, index int) int { + return (index + 1) << level +} + +// freeLeaf returns a text-only leaf when its source fits in a node. Images need +// a model description even when their text fits. func freeLeaf(message Message) (Node, bool) { text := leafSource(message) - if len(text) > nodeBytes { + if len(text) > nodeBytes || len(imageFiles(message.Text)) > 0 { return Node{}, false } @@ -384,6 +399,11 @@ func freeLeaf(message Message) (Node, bool) { }, true } +// leafSource returns the text that the leaf of message holds or summarizes. +func leafSource(message Message) string { + return message.Kind + ": " + message.Text +} + // freeMerge returns the parent of two sibling nodes when both texts, joined by // a newline, fit in a node. Such a parent needs no model call. func freeMerge(left, right Node) (Node, bool) { @@ -400,19 +420,3 @@ func freeMerge(left, right Node) (Node, bool) { Size: len(text), }, true } - -// decodeTreeRecord reads the key and the position of a record of -// index/tree.idx. -func decodeTreeRecord(record []byte) (nodeKey, position) { - key := nodeKey{ - level: int(binary.LittleEndian.Uint32(record[positionBytes+8:])), - index: int(binary.LittleEndian.Uint64(record[positionBytes:])), - } - - return key, decodePosition(record) -} - -// nodeEnd returns the ID after the last message of node (l, i). -func nodeEnd(level, index int) int { - return (index + 1) << level -}