Files
next.orly.dev/pkg/bbolt/graph.go
woikos 9fed1261ad Add BBolt database backend for HDD-optimized archival relays (v0.48.0)
- BBolt B+tree backend with sequential access patterns for spinning disks
- Write batching (5000 events / 128MB / 30s flush) to reduce disk thrashing
- Adjacency list storage for graph data (one key per vertex, not per edge)
- Bloom filter for fast negative edge existence checks (~12MB for 10M edges)
- No query cache (saves RAM, B+tree reads are fast enough on HDD)
- Migration tool: orly migrate --from badger --to bbolt
- Configuration: ORLY_BBOLT_* environment variables

Files modified:
- app/config/config.go: Added BBolt configuration options
- main.go: Added migrate subcommand and BBolt config wiring
- pkg/database/factory.go: Added BBolt factory registration
- pkg/bbolt/*: New BBolt database backend implementation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 06:50:58 +01:00

251 lines
6.8 KiB
Go

//go:build !(js && wasm)
package bbolt
import (
"bytes"
"encoding/binary"
"io"
)
// EventVertex stores the adjacency list for an event.
// Contains the author and all edges to other events/pubkeys.
type EventVertex struct {
AuthorSerial uint64 // Serial of the author pubkey
Kind uint16 // Event kind
PTagSerials []uint64 // Serials of pubkeys mentioned (p-tags)
ETagSerials []uint64 // Serials of events referenced (e-tags)
}
// Encode serializes the EventVertex to bytes.
// Format: author(5) | kind(2) | ptag_count(varint) | [ptag_serials(5)...] | etag_count(varint) | [etag_serials(5)...]
func (ev *EventVertex) Encode() []byte {
// Calculate size
size := 5 + 2 + 2 + len(ev.PTagSerials)*5 + 2 + len(ev.ETagSerials)*5
buf := make([]byte, 0, size)
// Author serial (5 bytes)
authorBuf := make([]byte, 5)
encodeUint40(ev.AuthorSerial, authorBuf)
buf = append(buf, authorBuf...)
// Kind (2 bytes)
kindBuf := make([]byte, 2)
binary.BigEndian.PutUint16(kindBuf, ev.Kind)
buf = append(buf, kindBuf...)
// P-tag count and serials
ptagCountBuf := make([]byte, 2)
binary.BigEndian.PutUint16(ptagCountBuf, uint16(len(ev.PTagSerials)))
buf = append(buf, ptagCountBuf...)
for _, serial := range ev.PTagSerials {
serialBuf := make([]byte, 5)
encodeUint40(serial, serialBuf)
buf = append(buf, serialBuf...)
}
// E-tag count and serials
etagCountBuf := make([]byte, 2)
binary.BigEndian.PutUint16(etagCountBuf, uint16(len(ev.ETagSerials)))
buf = append(buf, etagCountBuf...)
for _, serial := range ev.ETagSerials {
serialBuf := make([]byte, 5)
encodeUint40(serial, serialBuf)
buf = append(buf, serialBuf...)
}
return buf
}
// Decode deserializes bytes into an EventVertex.
func (ev *EventVertex) Decode(data []byte) error {
if len(data) < 9 { // minimum: author(5) + kind(2) + ptag_count(2)
return io.ErrUnexpectedEOF
}
reader := bytes.NewReader(data)
// Author serial
authorBuf := make([]byte, 5)
if _, err := reader.Read(authorBuf); err != nil {
return err
}
ev.AuthorSerial = decodeUint40(authorBuf)
// Kind
kindBuf := make([]byte, 2)
if _, err := reader.Read(kindBuf); err != nil {
return err
}
ev.Kind = binary.BigEndian.Uint16(kindBuf)
// P-tags
ptagCountBuf := make([]byte, 2)
if _, err := reader.Read(ptagCountBuf); err != nil {
return err
}
ptagCount := binary.BigEndian.Uint16(ptagCountBuf)
ev.PTagSerials = make([]uint64, ptagCount)
for i := uint16(0); i < ptagCount; i++ {
serialBuf := make([]byte, 5)
if _, err := reader.Read(serialBuf); err != nil {
return err
}
ev.PTagSerials[i] = decodeUint40(serialBuf)
}
// E-tags
etagCountBuf := make([]byte, 2)
if _, err := reader.Read(etagCountBuf); err != nil {
return err
}
etagCount := binary.BigEndian.Uint16(etagCountBuf)
ev.ETagSerials = make([]uint64, etagCount)
for i := uint16(0); i < etagCount; i++ {
serialBuf := make([]byte, 5)
if _, err := reader.Read(serialBuf); err != nil {
return err
}
ev.ETagSerials[i] = decodeUint40(serialBuf)
}
return nil
}
// PubkeyVertex stores the adjacency list for a pubkey.
// Contains all events authored by or mentioning this pubkey.
type PubkeyVertex struct {
AuthoredEvents []uint64 // Event serials this pubkey authored
MentionedIn []uint64 // Event serials that mention this pubkey (p-tags)
}
// Encode serializes the PubkeyVertex to bytes.
// Format: authored_count(varint) | [serials(5)...] | mentioned_count(varint) | [serials(5)...]
func (pv *PubkeyVertex) Encode() []byte {
size := 2 + len(pv.AuthoredEvents)*5 + 2 + len(pv.MentionedIn)*5
buf := make([]byte, 0, size)
// Authored events
authoredCountBuf := make([]byte, 2)
binary.BigEndian.PutUint16(authoredCountBuf, uint16(len(pv.AuthoredEvents)))
buf = append(buf, authoredCountBuf...)
for _, serial := range pv.AuthoredEvents {
serialBuf := make([]byte, 5)
encodeUint40(serial, serialBuf)
buf = append(buf, serialBuf...)
}
// Mentioned in events
mentionedCountBuf := make([]byte, 2)
binary.BigEndian.PutUint16(mentionedCountBuf, uint16(len(pv.MentionedIn)))
buf = append(buf, mentionedCountBuf...)
for _, serial := range pv.MentionedIn {
serialBuf := make([]byte, 5)
encodeUint40(serial, serialBuf)
buf = append(buf, serialBuf...)
}
return buf
}
// Decode deserializes bytes into a PubkeyVertex.
func (pv *PubkeyVertex) Decode(data []byte) error {
if len(data) < 4 { // minimum: authored_count(2) + mentioned_count(2)
return io.ErrUnexpectedEOF
}
reader := bytes.NewReader(data)
// Authored events
authoredCountBuf := make([]byte, 2)
if _, err := reader.Read(authoredCountBuf); err != nil {
return err
}
authoredCount := binary.BigEndian.Uint16(authoredCountBuf)
pv.AuthoredEvents = make([]uint64, authoredCount)
for i := uint16(0); i < authoredCount; i++ {
serialBuf := make([]byte, 5)
if _, err := reader.Read(serialBuf); err != nil {
return err
}
pv.AuthoredEvents[i] = decodeUint40(serialBuf)
}
// Mentioned in events
mentionedCountBuf := make([]byte, 2)
if _, err := reader.Read(mentionedCountBuf); err != nil {
return err
}
mentionedCount := binary.BigEndian.Uint16(mentionedCountBuf)
pv.MentionedIn = make([]uint64, mentionedCount)
for i := uint16(0); i < mentionedCount; i++ {
serialBuf := make([]byte, 5)
if _, err := reader.Read(serialBuf); err != nil {
return err
}
pv.MentionedIn[i] = decodeUint40(serialBuf)
}
return nil
}
// AddAuthored adds an event serial to the authored list if not already present.
func (pv *PubkeyVertex) AddAuthored(eventSerial uint64) {
for _, s := range pv.AuthoredEvents {
if s == eventSerial {
return
}
}
pv.AuthoredEvents = append(pv.AuthoredEvents, eventSerial)
}
// AddMention adds an event serial to the mentioned list if not already present.
func (pv *PubkeyVertex) AddMention(eventSerial uint64) {
for _, s := range pv.MentionedIn {
if s == eventSerial {
return
}
}
pv.MentionedIn = append(pv.MentionedIn, eventSerial)
}
// RemoveAuthored removes an event serial from the authored list.
func (pv *PubkeyVertex) RemoveAuthored(eventSerial uint64) {
for i, s := range pv.AuthoredEvents {
if s == eventSerial {
pv.AuthoredEvents = append(pv.AuthoredEvents[:i], pv.AuthoredEvents[i+1:]...)
return
}
}
}
// RemoveMention removes an event serial from the mentioned list.
func (pv *PubkeyVertex) RemoveMention(eventSerial uint64) {
for i, s := range pv.MentionedIn {
if s == eventSerial {
pv.MentionedIn = append(pv.MentionedIn[:i], pv.MentionedIn[i+1:]...)
return
}
}
}
// HasAuthored checks if the pubkey authored the given event.
func (pv *PubkeyVertex) HasAuthored(eventSerial uint64) bool {
for _, s := range pv.AuthoredEvents {
if s == eventSerial {
return true
}
}
return false
}
// IsMentionedIn checks if the pubkey is mentioned in the given event.
func (pv *PubkeyVertex) IsMentionedIn(eventSerial uint64) bool {
for _, s := range pv.MentionedIn {
if s == eventSerial {
return true
}
}
return false
}