query-addressable.go raw

   1  //go:build !(js && wasm)
   2  
   3  package database
   4  
   5  import (
   6  	"bytes"
   7  
   8  	"github.com/dgraph-io/badger/v4"
   9  	"next.orly.dev/pkg/lol/chk"
  10  	"next.orly.dev/pkg/lol/log"
  11  	"next.orly.dev/pkg/database/bufpool"
  12  	"next.orly.dev/pkg/database/indexes"
  13  	"next.orly.dev/pkg/database/indexes/types"
  14  	"next.orly.dev/pkg/nostr/encoders/filter"
  15  	"next.orly.dev/pkg/nostr/encoders/kind"
  16  )
  17  
  18  // IsAddressableEventQuery checks if a filter matches the NIP-33 addressable event
  19  // query pattern: exactly one kind (30000-39999), one author, and one d-tag.
  20  // This pattern uniquely identifies a single parameterized replaceable event.
  21  func IsAddressableEventQuery(f *filter.F) bool {
  22  	// Must have exactly one kind
  23  	if f.Kinds == nil || f.Kinds.Len() != 1 {
  24  		return false
  25  	}
  26  	// Kind must be parameterized replaceable (30000-39999)
  27  	kindVal := f.Kinds.K[0].K
  28  	if !kind.IsParameterizedReplaceable(kindVal) {
  29  		return false
  30  	}
  31  	// Must have exactly one author
  32  	if f.Authors == nil || f.Authors.Len() != 1 {
  33  		return false
  34  	}
  35  	// Must have a d-tag filter
  36  	if f.Tags == nil {
  37  		return false
  38  	}
  39  	dTagFilter := f.Tags.GetFirst([]byte("#d"))
  40  	if dTagFilter == nil || dTagFilter.Len() != 2 {
  41  		return false
  42  	}
  43  	// Must not have IDs filter (would bypass this optimization)
  44  	if f.Ids != nil && f.Ids.Len() > 0 {
  45  		return false
  46  	}
  47  	return true
  48  }
  49  
  50  // QueryForAddressableEvent performs a direct O(1) lookup for a NIP-33 parameterized
  51  // replaceable event using the AddressableEvent index.
  52  // Returns the serial if found, nil if not found, or an error.
  53  func (d *D) QueryForAddressableEvent(f *filter.F) (serial *types.Uint40, err error) {
  54  	if !IsAddressableEventQuery(f) {
  55  		return nil, nil
  56  	}
  57  
  58  	// Extract components from filter
  59  	kindVal := f.Kinds.K[0].K
  60  	author := f.Authors.T[0]
  61  	dTagFilter := f.Tags.GetFirst([]byte("#d"))
  62  	dTagValue := dTagFilter.T[1]
  63  
  64  	// Build pubkey hash
  65  	pubHash := new(types.PubHash)
  66  	if err = pubHash.FromPubkey(author); chk.E(err) {
  67  		return nil, err
  68  	}
  69  
  70  	// Build kind type
  71  	kindType := new(types.Uint16)
  72  	kindType.Set(kindVal)
  73  
  74  	// Build d-tag hash
  75  	dTagHash := new(types.Ident)
  76  	dTagHash.FromIdent(dTagValue)
  77  
  78  	// Build the AddressableEvent index key
  79  	aevKey := indexes.AddressableEventEnc(pubHash, kindType, dTagHash)
  80  	keyBuf := bufpool.GetSmall()
  81  	defer bufpool.PutSmall(keyBuf)
  82  	if err = aevKey.MarshalWrite(keyBuf); chk.E(err) {
  83  		return nil, err
  84  	}
  85  	keyBytes := bufpool.CopyBytes(keyBuf)
  86  
  87  	// Direct key lookup - O(1)
  88  	err = d.View(func(txn *badger.Txn) error {
  89  		item, err := txn.Get(keyBytes)
  90  		if err == badger.ErrKeyNotFound {
  91  			// Not found - this is not an error
  92  			return nil
  93  		}
  94  		if err != nil {
  95  			return err
  96  		}
  97  
  98  		// Read the serial from the value
  99  		return item.Value(func(val []byte) error {
 100  			if len(val) < 5 {
 101  				log.W.F("QueryForAddressableEvent: invalid value length %d", len(val))
 102  				return nil
 103  			}
 104  			serial = new(types.Uint40)
 105  			rdr := bytes.NewReader(val)
 106  			if err := serial.UnmarshalRead(rdr); err != nil {
 107  				log.W.F("QueryForAddressableEvent: failed to read serial: %v", err)
 108  				serial = nil
 109  				return nil
 110  			}
 111  			return nil
 112  		})
 113  	})
 114  
 115  	if err != nil {
 116  		return nil, err
 117  	}
 118  
 119  	if serial != nil {
 120  		log.T.F("QueryForAddressableEvent: found serial %d for kind=%d author=%x d=%s",
 121  			serial.Get(), kindVal, author[:8], string(dTagValue))
 122  	}
 123  
 124  	return serial, nil
 125  }
 126  
 127  // BuildAddressableEventKey builds the key for an AddressableEvent index entry.
 128  // This is used by both save-event.go (for writing) and deletion (for cleanup).
 129  func BuildAddressableEventKey(pubkey []byte, eventKind uint16, dTagValue []byte) ([]byte, error) {
 130  	pubHash := new(types.PubHash)
 131  	if err := pubHash.FromPubkey(pubkey); err != nil {
 132  		return nil, err
 133  	}
 134  
 135  	kindType := new(types.Uint16)
 136  	kindType.Set(eventKind)
 137  
 138  	dTagHash := new(types.Ident)
 139  	dTagHash.FromIdent(dTagValue)
 140  
 141  	aevKey := indexes.AddressableEventEnc(pubHash, kindType, dTagHash)
 142  	keyBuf := bufpool.GetSmall()
 143  	defer bufpool.PutSmall(keyBuf)
 144  	if err := aevKey.MarshalWrite(keyBuf); err != nil {
 145  		return nil, err
 146  	}
 147  
 148  	return bufpool.CopyBytes(keyBuf), nil
 149  }
 150