424 lines
12 KiB
Go
424 lines
12 KiB
Go
//go:build js && wasm
|
|
|
|
package wasmdb
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/aperturerobotics/go-indexeddb/idb"
|
|
"github.com/hack-pad/safejs"
|
|
"lol.mleku.dev/chk"
|
|
|
|
"git.mleku.dev/mleku/nostr/encoders/event"
|
|
"git.mleku.dev/mleku/nostr/encoders/hex"
|
|
"git.mleku.dev/mleku/nostr/encoders/kind"
|
|
"git.mleku.dev/mleku/nostr/encoders/tag"
|
|
"next.orly.dev/pkg/database"
|
|
"next.orly.dev/pkg/database/indexes"
|
|
"next.orly.dev/pkg/database/indexes/types"
|
|
)
|
|
|
|
var (
|
|
// ErrOlderThanExisting is returned when a candidate event is older than an existing replaceable/addressable event.
|
|
ErrOlderThanExisting = errors.New("older than existing event")
|
|
// ErrMissingDTag is returned when a parameterized replaceable event lacks the required 'd' tag.
|
|
ErrMissingDTag = errors.New("event is missing a d tag identifier")
|
|
)
|
|
|
|
// SaveEvent saves an event to the database, generating all necessary indexes.
|
|
func (w *W) SaveEvent(c context.Context, ev *event.E) (replaced bool, err error) {
|
|
if ev == nil {
|
|
err = errors.New("nil event")
|
|
return
|
|
}
|
|
|
|
// Reject ephemeral events (kinds 20000-29999) - they should never be stored
|
|
if ev.Kind >= 20000 && ev.Kind <= 29999 {
|
|
err = errors.New("blocked: ephemeral events should not be stored")
|
|
return
|
|
}
|
|
|
|
// Validate kind 3 (follow list) events have at least one p tag
|
|
if ev.Kind == 3 {
|
|
hasPTag := false
|
|
if ev.Tags != nil {
|
|
for _, t := range *ev.Tags {
|
|
if t != nil && t.Len() >= 2 {
|
|
key := t.Key()
|
|
if len(key) == 1 && key[0] == 'p' {
|
|
hasPTag = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !hasPTag {
|
|
w.Logger.Warnf("SaveEvent: rejecting kind 3 event without p tags from pubkey %x", ev.Pubkey)
|
|
err = errors.New("blocked: kind 3 follow list events must have at least one p tag")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check if the event already exists
|
|
var ser *types.Uint40
|
|
if ser, err = w.GetSerialById(ev.ID); err == nil && ser != nil {
|
|
err = errors.New("blocked: event already exists: " + hex.Enc(ev.ID[:]))
|
|
return
|
|
}
|
|
|
|
// If the error is "id not found", we can proceed
|
|
if err != nil && strings.Contains(err.Error(), "id not found") {
|
|
err = nil
|
|
} else if err != nil {
|
|
return
|
|
}
|
|
|
|
// Check for replacement - only validate, don't delete old events
|
|
if kind.IsReplaceable(ev.Kind) || kind.IsParameterizedReplaceable(ev.Kind) {
|
|
var werr error
|
|
if replaced, _, werr = w.WouldReplaceEvent(ev); werr != nil {
|
|
if errors.Is(werr, ErrOlderThanExisting) {
|
|
if kind.IsReplaceable(ev.Kind) {
|
|
err = errors.New("blocked: event is older than existing replaceable event")
|
|
} else {
|
|
err = errors.New("blocked: event is older than existing addressable event")
|
|
}
|
|
return
|
|
}
|
|
if errors.Is(werr, ErrMissingDTag) {
|
|
err = ErrMissingDTag
|
|
return
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// Get the next sequence number for the event
|
|
serial, err := w.nextEventSerial()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Generate all indexes for the event
|
|
idxs, err := database.GetIndexesForEvent(ev, serial)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Serialize event to binary
|
|
eventDataBuf := new(bytes.Buffer)
|
|
ev.MarshalBinary(eventDataBuf)
|
|
eventData := eventDataBuf.Bytes()
|
|
|
|
// Determine storage strategy
|
|
smallEventThreshold := 1024 // Could be made configurable
|
|
isSmallEvent := len(eventData) <= smallEventThreshold
|
|
isReplaceableEvent := kind.IsReplaceable(ev.Kind)
|
|
isAddressableEvent := kind.IsParameterizedReplaceable(ev.Kind)
|
|
|
|
// Create serial type
|
|
ser = new(types.Uint40)
|
|
if err = ser.Set(serial); chk.E(err) {
|
|
return
|
|
}
|
|
|
|
// Start a transaction to save the event and all its indexes
|
|
// We need to include all object stores we'll write to
|
|
storesToWrite := []string{
|
|
string(indexes.IdPrefix),
|
|
string(indexes.FullIdPubkeyPrefix),
|
|
string(indexes.CreatedAtPrefix),
|
|
string(indexes.PubkeyPrefix),
|
|
string(indexes.KindPrefix),
|
|
string(indexes.KindPubkeyPrefix),
|
|
string(indexes.TagPrefix),
|
|
string(indexes.TagKindPrefix),
|
|
string(indexes.TagPubkeyPrefix),
|
|
string(indexes.TagKindPubkeyPrefix),
|
|
string(indexes.WordPrefix),
|
|
}
|
|
|
|
// Add event storage store
|
|
if isSmallEvent {
|
|
storesToWrite = append(storesToWrite, string(indexes.SmallEventPrefix))
|
|
} else {
|
|
storesToWrite = append(storesToWrite, string(indexes.EventPrefix))
|
|
}
|
|
|
|
// Add specialized stores if needed
|
|
if isAddressableEvent && isSmallEvent {
|
|
storesToWrite = append(storesToWrite, string(indexes.AddressableEventPrefix))
|
|
} else if isReplaceableEvent && isSmallEvent {
|
|
storesToWrite = append(storesToWrite, string(indexes.ReplaceableEventPrefix))
|
|
}
|
|
|
|
// Start transaction
|
|
tx, err := w.db.Transaction(idb.TransactionReadWrite, storesToWrite[0], storesToWrite[1:]...)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to start transaction: %w", err)
|
|
}
|
|
|
|
// Save each index to its respective object store
|
|
for _, key := range idxs {
|
|
if len(key) < 3 {
|
|
continue
|
|
}
|
|
// Extract store name from 3-byte prefix
|
|
storeName := string(key[:3])
|
|
|
|
store, storeErr := tx.ObjectStore(storeName)
|
|
if storeErr != nil {
|
|
w.Logger.Warnf("SaveEvent: failed to get object store %s: %v", storeName, storeErr)
|
|
continue
|
|
}
|
|
|
|
// Use the full key as the IndexedDB key, empty value
|
|
keyJS := bytesToSafeValue(key)
|
|
_, putErr := store.PutKey(keyJS, safejs.Null())
|
|
if putErr != nil {
|
|
w.Logger.Warnf("SaveEvent: failed to put index %s: %v", storeName, putErr)
|
|
}
|
|
}
|
|
|
|
// Store the event data
|
|
if isSmallEvent {
|
|
// Small event: store inline with sev prefix
|
|
// Format: sev|serial|size_uint16|event_data
|
|
keyBuf := new(bytes.Buffer)
|
|
if err = indexes.SmallEventEnc(ser).MarshalWrite(keyBuf); chk.E(err) {
|
|
return
|
|
}
|
|
// Append size as uint16 big-endian
|
|
sizeBytes := []byte{byte(len(eventData) >> 8), byte(len(eventData))}
|
|
keyBuf.Write(sizeBytes)
|
|
keyBuf.Write(eventData)
|
|
|
|
store, storeErr := tx.ObjectStore(string(indexes.SmallEventPrefix))
|
|
if storeErr == nil {
|
|
keyJS := bytesToSafeValue(keyBuf.Bytes())
|
|
store.PutKey(keyJS, safejs.Null())
|
|
}
|
|
} else {
|
|
// Large event: store separately with evt prefix
|
|
keyBuf := new(bytes.Buffer)
|
|
if err = indexes.EventEnc(ser).MarshalWrite(keyBuf); chk.E(err) {
|
|
return
|
|
}
|
|
|
|
store, storeErr := tx.ObjectStore(string(indexes.EventPrefix))
|
|
if storeErr == nil {
|
|
keyJS := bytesToSafeValue(keyBuf.Bytes())
|
|
valueJS := bytesToSafeValue(eventData)
|
|
store.PutKey(keyJS, valueJS)
|
|
}
|
|
}
|
|
|
|
// Store specialized keys for replaceable/addressable events
|
|
if isAddressableEvent && isSmallEvent {
|
|
dTag := ev.Tags.GetFirst([]byte("d"))
|
|
if dTag != nil {
|
|
pubHash := new(types.PubHash)
|
|
pubHash.FromPubkey(ev.Pubkey)
|
|
kindVal := new(types.Uint16)
|
|
kindVal.Set(ev.Kind)
|
|
dTagHash := new(types.Ident)
|
|
dTagHash.FromIdent(dTag.Value())
|
|
|
|
keyBuf := new(bytes.Buffer)
|
|
if err = indexes.AddressableEventEnc(pubHash, kindVal, dTagHash).MarshalWrite(keyBuf); chk.E(err) {
|
|
return
|
|
}
|
|
sizeBytes := []byte{byte(len(eventData) >> 8), byte(len(eventData))}
|
|
keyBuf.Write(sizeBytes)
|
|
keyBuf.Write(eventData)
|
|
|
|
store, storeErr := tx.ObjectStore(string(indexes.AddressableEventPrefix))
|
|
if storeErr == nil {
|
|
keyJS := bytesToSafeValue(keyBuf.Bytes())
|
|
store.PutKey(keyJS, safejs.Null())
|
|
}
|
|
}
|
|
} else if isReplaceableEvent && isSmallEvent {
|
|
pubHash := new(types.PubHash)
|
|
pubHash.FromPubkey(ev.Pubkey)
|
|
kindVal := new(types.Uint16)
|
|
kindVal.Set(ev.Kind)
|
|
|
|
keyBuf := new(bytes.Buffer)
|
|
if err = indexes.ReplaceableEventEnc(pubHash, kindVal).MarshalWrite(keyBuf); chk.E(err) {
|
|
return
|
|
}
|
|
sizeBytes := []byte{byte(len(eventData) >> 8), byte(len(eventData))}
|
|
keyBuf.Write(sizeBytes)
|
|
keyBuf.Write(eventData)
|
|
|
|
store, storeErr := tx.ObjectStore(string(indexes.ReplaceableEventPrefix))
|
|
if storeErr == nil {
|
|
keyJS := bytesToSafeValue(keyBuf.Bytes())
|
|
store.PutKey(keyJS, safejs.Null())
|
|
}
|
|
}
|
|
|
|
// Commit transaction
|
|
if err = tx.Await(c); err != nil {
|
|
return false, fmt.Errorf("failed to commit transaction: %w", err)
|
|
}
|
|
|
|
w.Logger.Debugf("SaveEvent: saved event %x (kind %d, %d bytes, %d indexes)",
|
|
ev.ID[:8], ev.Kind, len(eventData), len(idxs))
|
|
|
|
return
|
|
}
|
|
|
|
// WouldReplaceEvent checks if the provided event would replace existing events
|
|
func (w *W) WouldReplaceEvent(ev *event.E) (bool, types.Uint40s, error) {
|
|
// Only relevant for replaceable or parameterized replaceable kinds
|
|
if !(kind.IsReplaceable(ev.Kind) || kind.IsParameterizedReplaceable(ev.Kind)) {
|
|
return false, nil, nil
|
|
}
|
|
|
|
// Build filter for existing events
|
|
var f interface{}
|
|
if kind.IsReplaceable(ev.Kind) {
|
|
// For now, simplified check - would need full filter implementation
|
|
return false, nil, nil
|
|
} else {
|
|
// Parameterized replaceable requires 'd' tag
|
|
dTag := ev.Tags.GetFirst([]byte("d"))
|
|
if dTag == nil {
|
|
return false, nil, ErrMissingDTag
|
|
}
|
|
// Simplified - full implementation would query existing events
|
|
_ = f
|
|
}
|
|
|
|
// Simplified implementation - assume no conflicts for now
|
|
// Full implementation would query the database and compare timestamps
|
|
return false, nil, nil
|
|
}
|
|
|
|
// GetSerialById looks up the serial number for an event ID
|
|
func (w *W) GetSerialById(id []byte) (ser *types.Uint40, err error) {
|
|
if len(id) != 32 {
|
|
return nil, errors.New("invalid event ID length")
|
|
}
|
|
|
|
// Create ID hash
|
|
idHash := new(types.IdHash)
|
|
if err = idHash.FromId(id); chk.E(err) {
|
|
return nil, err
|
|
}
|
|
|
|
// Build the prefix to search for
|
|
keyBuf := new(bytes.Buffer)
|
|
indexes.IdEnc(idHash, nil).MarshalWrite(keyBuf)
|
|
prefix := keyBuf.Bytes()[:11] // 3 prefix + 8 id hash
|
|
|
|
// Search in the eid object store
|
|
tx, err := w.db.Transaction(idb.TransactionReadOnly, string(indexes.IdPrefix))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
store, err := tx.ObjectStore(string(indexes.IdPrefix))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use cursor to find matching key
|
|
cursorReq, err := store.OpenCursor(idb.CursorNext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = cursorReq.Iter(w.ctx, func(cursor *idb.CursorWithValue) error {
|
|
keyVal, keyErr := cursor.Key()
|
|
if keyErr != nil {
|
|
return keyErr
|
|
}
|
|
|
|
keyBytes := safeValueToBytes(keyVal)
|
|
if len(keyBytes) >= len(prefix) && bytes.HasPrefix(keyBytes, prefix) {
|
|
// Found matching key, extract serial from last 5 bytes
|
|
if len(keyBytes) >= 16 { // 3 + 8 + 5
|
|
ser = new(types.Uint40)
|
|
ser.UnmarshalRead(bytes.NewReader(keyBytes[11:16]))
|
|
return errors.New("found") // Stop iteration
|
|
}
|
|
}
|
|
|
|
return cursor.Continue()
|
|
})
|
|
|
|
if ser != nil {
|
|
return ser, nil
|
|
}
|
|
if err != nil && err.Error() != "found" {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, errors.New("id not found in database")
|
|
}
|
|
|
|
// GetSerialsByIds looks up serial numbers for multiple event IDs
|
|
func (w *W) GetSerialsByIds(ids *tag.T) (serials map[string]*types.Uint40, err error) {
|
|
serials = make(map[string]*types.Uint40)
|
|
|
|
if ids == nil {
|
|
return
|
|
}
|
|
|
|
for i := 1; i < ids.Len(); i++ {
|
|
idBytes := ids.T[i]
|
|
if len(idBytes) == 64 {
|
|
// Hex encoded ID
|
|
var decoded []byte
|
|
decoded, err = hex.Dec(string(idBytes))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
idBytes = decoded
|
|
}
|
|
|
|
if len(idBytes) == 32 {
|
|
var ser *types.Uint40
|
|
ser, err = w.GetSerialById(idBytes)
|
|
if err == nil && ser != nil {
|
|
serials[hex.Enc(idBytes)] = ser
|
|
}
|
|
}
|
|
}
|
|
|
|
err = nil
|
|
return
|
|
}
|
|
|
|
// GetSerialsByIdsWithFilter looks up serial numbers with a filter function
|
|
func (w *W) GetSerialsByIdsWithFilter(ids *tag.T, fn func(ev *event.E, ser *types.Uint40) bool) (serials map[string]*types.Uint40, err error) {
|
|
allSerials, err := w.GetSerialsByIds(ids)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if fn == nil {
|
|
return allSerials, nil
|
|
}
|
|
|
|
serials = make(map[string]*types.Uint40)
|
|
for idHex, ser := range allSerials {
|
|
ev, fetchErr := w.FetchEventBySerial(ser)
|
|
if fetchErr != nil {
|
|
continue
|
|
}
|
|
if fn(ev, ser) {
|
|
serials[idHex] = ser
|
|
}
|
|
}
|
|
|
|
return serials, nil
|
|
}
|