wasmdb.go raw

   1  //go:build js && wasm
   2  
   3  // Package wasmdb provides a WebAssembly-compatible database implementation
   4  // using IndexedDB as the storage backend. It replicates the Badger database's
   5  // index schema for full query compatibility.
   6  //
   7  // This implementation uses aperturerobotics/go-indexeddb (a fork of hack-pad/go-indexeddb)
   8  // which provides full IndexedDB bindings with cursor/range support and transaction retry
   9  // mechanisms to handle IndexedDB's transaction expiration issues in Go WASM.
  10  //
  11  // Architecture:
  12  //   - Each index type (evt, eid, kc-, pc-, etc.) maps to an IndexedDB object store
  13  //   - Keys are binary-encoded using the same format as the Badger implementation
  14  //   - Range queries use IndexedDB cursors with KeyRange bounds
  15  //   - Serial numbers are managed using a dedicated "meta" object store
  16  package wasmdb
  17  
  18  import (
  19  	"context"
  20  	"encoding/binary"
  21  	"errors"
  22  	"fmt"
  23  	"sync"
  24  
  25  	"github.com/aperturerobotics/go-indexeddb/idb"
  26  	"github.com/hack-pad/safejs"
  27  	"next.orly.dev/pkg/lol"
  28  	"next.orly.dev/pkg/lol/chk"
  29  
  30  	"next.orly.dev/pkg/nostr/encoders/event"
  31  	"next.orly.dev/pkg/nostr/encoders/filter"
  32  	"next.orly.dev/pkg/database"
  33  	"next.orly.dev/pkg/database/indexes"
  34  )
  35  
  36  const (
  37  	// DatabaseName is the IndexedDB database name
  38  	DatabaseName = "orly-nostr-relay"
  39  
  40  	// DatabaseVersion is incremented when schema changes require migration
  41  	DatabaseVersion = 1
  42  
  43  	// MetaStoreName holds metadata like serial counters
  44  	MetaStoreName = "meta"
  45  
  46  	// EventSerialKey is the key for the event serial counter in meta store
  47  	EventSerialKey = "event_serial"
  48  
  49  	// PubkeySerialKey is the key for the pubkey serial counter in meta store
  50  	PubkeySerialKey = "pubkey_serial"
  51  
  52  	// RelayIdentityKey is the key for the relay identity secret
  53  	RelayIdentityKey = "relay_identity"
  54  )
  55  
  56  // Object store names matching Badger index prefixes
  57  var objectStoreNames = []string{
  58  	MetaStoreName,
  59  	string(indexes.EventPrefix),            // "evt" - full events
  60  	string(indexes.SmallEventPrefix),       // "sev" - small events inline
  61  	string(indexes.ReplaceableEventPrefix), // "rev" - replaceable events
  62  	string(indexes.AddressableEventPrefix), // "aev" - addressable events
  63  	string(indexes.IdPrefix),               // "eid" - event ID index
  64  	string(indexes.FullIdPubkeyPrefix),     // "fpc" - full ID + pubkey + timestamp
  65  	string(indexes.CreatedAtPrefix),        // "c--" - created_at index
  66  	string(indexes.KindPrefix),             // "kc-" - kind index
  67  	string(indexes.PubkeyPrefix),           // "pc-" - pubkey index
  68  	string(indexes.KindPubkeyPrefix),       // "kpc" - kind + pubkey index
  69  	string(indexes.TagPrefix),              // "tc-" - tag index
  70  	string(indexes.TagKindPrefix),          // "tkc" - tag + kind index
  71  	string(indexes.TagPubkeyPrefix),        // "tpc" - tag + pubkey index
  72  	string(indexes.TagKindPubkeyPrefix),    // "tkp" - tag + kind + pubkey index
  73  	string(indexes.WordPrefix),             // "wrd" - word search index
  74  	string(indexes.ExpirationPrefix),       // "exp" - expiration index
  75  	string(indexes.VersionPrefix),          // "ver" - schema version
  76  	string(indexes.PubkeySerialPrefix),     // "pks" - pubkey serial index
  77  	string(indexes.SerialPubkeyPrefix),     // "spk" - serial to pubkey
  78  	string(indexes.EventPubkeyGraphPrefix), // "epg" - event-pubkey graph
  79  	string(indexes.PubkeyEventGraphPrefix), // "peg" - pubkey-event graph
  80  	"markers",                              // metadata key-value storage
  81  	"subscriptions",                        // payment subscriptions
  82  	"nip43",                                // NIP-43 membership
  83  	"invites",                              // invite codes
  84  }
  85  
  86  // W implements the database.Database interface using IndexedDB
  87  type W struct {
  88  	ctx    context.Context
  89  	cancel context.CancelFunc
  90  
  91  	dataDir string // Not really used in WASM, but kept for interface compatibility
  92  	Logger  *logger
  93  
  94  	db    *idb.Database
  95  	dbMu  sync.RWMutex
  96  	ready chan struct{}
  97  
  98  	// Serial counters (cached in memory, persisted to IndexedDB)
  99  	eventSerial  uint64
 100  	pubkeySerial uint64
 101  	serialMu     sync.Mutex
 102  }
 103  
 104  // Ensure W implements database.Database interface at compile time
 105  var _ database.Database = (*W)(nil)
 106  
 107  // init registers the wasmdb database factory
 108  func init() {
 109  	database.RegisterWasmDBFactory(func(
 110  		ctx context.Context,
 111  		cancel context.CancelFunc,
 112  		cfg *database.DatabaseConfig,
 113  	) (database.Database, error) {
 114  		return NewWithConfig(ctx, cancel, cfg)
 115  	})
 116  }
 117  
 118  // NewWithConfig creates a new IndexedDB-based database instance
 119  func NewWithConfig(
 120  	ctx context.Context, cancel context.CancelFunc, cfg *database.DatabaseConfig,
 121  ) (*W, error) {
 122  	w := &W{
 123  		ctx:     ctx,
 124  		cancel:  cancel,
 125  		dataDir: cfg.DataDir,
 126  		Logger:  NewLogger(lol.GetLogLevel(cfg.LogLevel)),
 127  		ready:   make(chan struct{}),
 128  	}
 129  
 130  	// Open or create the IndexedDB database
 131  	if err := w.openDatabase(); err != nil {
 132  		return nil, fmt.Errorf("failed to open IndexedDB: %w", err)
 133  	}
 134  
 135  	// Load serial counters from storage
 136  	if err := w.loadSerialCounters(); err != nil {
 137  		return nil, fmt.Errorf("failed to load serial counters: %w", err)
 138  	}
 139  
 140  	// Start warmup goroutine
 141  	go w.warmup()
 142  
 143  	// Setup shutdown handler
 144  	go func() {
 145  		<-w.ctx.Done()
 146  		w.cancel()
 147  		w.Close()
 148  	}()
 149  
 150  	return w, nil
 151  }
 152  
 153  // New creates a new IndexedDB-based database instance with default configuration
 154  func New(
 155  	ctx context.Context, cancel context.CancelFunc, dataDir, logLevel string,
 156  ) (*W, error) {
 157  	cfg := &database.DatabaseConfig{
 158  		DataDir:  dataDir,
 159  		LogLevel: logLevel,
 160  	}
 161  	return NewWithConfig(ctx, cancel, cfg)
 162  }
 163  
 164  // openDatabase opens or creates the IndexedDB database with all required object stores
 165  func (w *W) openDatabase() error {
 166  	w.dbMu.Lock()
 167  	defer w.dbMu.Unlock()
 168  
 169  	// Get the IndexedDB factory (panics if not available)
 170  	factory := idb.Global()
 171  
 172  	// Open the database with upgrade handler
 173  	openReq, err := factory.Open(w.ctx, DatabaseName, DatabaseVersion, func(db *idb.Database, oldVersion, newVersion uint) error {
 174  		// This is called when the database needs to be created or upgraded
 175  		w.Logger.Infof("IndexedDB upgrade: version %d -> %d", oldVersion, newVersion)
 176  
 177  		// Create all object stores
 178  		for _, storeName := range objectStoreNames {
 179  			// Check if store already exists
 180  			if !w.hasObjectStore(db, storeName) {
 181  				// Create object store without auto-increment (we manage keys manually)
 182  				opts := idb.ObjectStoreOptions{}
 183  				if _, err := db.CreateObjectStore(storeName, opts); err != nil {
 184  					return fmt.Errorf("failed to create object store %s: %w", storeName, err)
 185  				}
 186  				w.Logger.Debugf("created object store: %s", storeName)
 187  			}
 188  		}
 189  		return nil
 190  	})
 191  	if err != nil {
 192  		return fmt.Errorf("failed to open IndexedDB: %w", err)
 193  	}
 194  
 195  	db, err := openReq.Await(w.ctx)
 196  	if err != nil {
 197  		return fmt.Errorf("failed to await IndexedDB open: %w", err)
 198  	}
 199  
 200  	w.db = db
 201  	return nil
 202  }
 203  
 204  // hasObjectStore checks if an object store exists in the database
 205  func (w *W) hasObjectStore(db *idb.Database, name string) bool {
 206  	names, err := db.ObjectStoreNames()
 207  	if err != nil {
 208  		return false
 209  	}
 210  	for _, n := range names {
 211  		if n == name {
 212  			return true
 213  		}
 214  	}
 215  	return false
 216  }
 217  
 218  // loadSerialCounters loads the event and pubkey serial counters from IndexedDB
 219  func (w *W) loadSerialCounters() error {
 220  	w.serialMu.Lock()
 221  	defer w.serialMu.Unlock()
 222  
 223  	// Load event serial
 224  	eventSerialBytes, err := w.getMeta(EventSerialKey)
 225  	if err != nil {
 226  		return err
 227  	}
 228  	if eventSerialBytes != nil && len(eventSerialBytes) == 8 {
 229  		w.eventSerial = binary.BigEndian.Uint64(eventSerialBytes)
 230  	}
 231  
 232  	// Load pubkey serial
 233  	pubkeySerialBytes, err := w.getMeta(PubkeySerialKey)
 234  	if err != nil {
 235  		return err
 236  	}
 237  	if pubkeySerialBytes != nil && len(pubkeySerialBytes) == 8 {
 238  		w.pubkeySerial = binary.BigEndian.Uint64(pubkeySerialBytes)
 239  	}
 240  
 241  	w.Logger.Infof("loaded serials: event=%d, pubkey=%d", w.eventSerial, w.pubkeySerial)
 242  	return nil
 243  }
 244  
 245  // getMeta retrieves a value from the meta object store
 246  func (w *W) getMeta(key string) ([]byte, error) {
 247  	tx, err := w.db.Transaction(idb.TransactionReadOnly, MetaStoreName)
 248  	if err != nil {
 249  		return nil, err
 250  	}
 251  
 252  	store, err := tx.ObjectStore(MetaStoreName)
 253  	if err != nil {
 254  		return nil, err
 255  	}
 256  
 257  	keyVal, err := safejs.ValueOf(key)
 258  	if err != nil {
 259  		return nil, err
 260  	}
 261  
 262  	req, err := store.Get(keyVal)
 263  	if err != nil {
 264  		return nil, err
 265  	}
 266  
 267  	val, err := req.Await(w.ctx)
 268  	if err != nil {
 269  		// Key not found is not an error
 270  		return nil, nil
 271  	}
 272  
 273  	if val.IsUndefined() || val.IsNull() {
 274  		return nil, nil
 275  	}
 276  
 277  	// Convert safejs.Value to []byte
 278  	return safeValueToBytes(val), nil
 279  }
 280  
 281  // setMeta stores a value in the meta object store
 282  func (w *W) setMeta(key string, value []byte) error {
 283  	tx, err := w.db.Transaction(idb.TransactionReadWrite, MetaStoreName)
 284  	if err != nil {
 285  		return err
 286  	}
 287  
 288  	store, err := tx.ObjectStore(MetaStoreName)
 289  	if err != nil {
 290  		return err
 291  	}
 292  
 293  	// Convert value to Uint8Array for IndexedDB storage
 294  	valueJS := bytesToSafeValue(value)
 295  
 296  	// Put with key - using PutKey since we're managing keys
 297  	keyVal, err := safejs.ValueOf(key)
 298  	if err != nil {
 299  		return err
 300  	}
 301  
 302  	_, err = store.PutKey(keyVal, valueJS)
 303  	if err != nil {
 304  		return err
 305  	}
 306  
 307  	return tx.Await(w.ctx)
 308  }
 309  
 310  // nextEventSerial returns the next event serial number and persists it
 311  func (w *W) nextEventSerial() (uint64, error) {
 312  	w.serialMu.Lock()
 313  	defer w.serialMu.Unlock()
 314  
 315  	w.eventSerial++
 316  	serial := w.eventSerial
 317  
 318  	// Persist to IndexedDB
 319  	buf := make([]byte, 8)
 320  	binary.BigEndian.PutUint64(buf, serial)
 321  	if err := w.setMeta(EventSerialKey, buf); err != nil {
 322  		return 0, err
 323  	}
 324  
 325  	return serial, nil
 326  }
 327  
 328  // nextPubkeySerial returns the next pubkey serial number and persists it
 329  func (w *W) nextPubkeySerial() (uint64, error) {
 330  	w.serialMu.Lock()
 331  	defer w.serialMu.Unlock()
 332  
 333  	w.pubkeySerial++
 334  	serial := w.pubkeySerial
 335  
 336  	// Persist to IndexedDB
 337  	buf := make([]byte, 8)
 338  	binary.BigEndian.PutUint64(buf, serial)
 339  	if err := w.setMeta(PubkeySerialKey, buf); err != nil {
 340  		return 0, err
 341  	}
 342  
 343  	return serial, nil
 344  }
 345  
 346  // warmup performs database warmup and closes the ready channel when complete
 347  func (w *W) warmup() {
 348  	defer close(w.ready)
 349  	// IndexedDB is ready immediately after opening
 350  	w.Logger.Infof("IndexedDB database warmup complete, ready to serve requests")
 351  }
 352  
 353  // Path returns the database path (not used in WASM)
 354  func (w *W) Path() string { return w.dataDir }
 355  
 356  // Init initializes the database (no-op, done in New)
 357  func (w *W) Init(path string) error { return nil }
 358  
 359  // Sync flushes pending writes (IndexedDB handles persistence automatically)
 360  func (w *W) Sync() error { return nil }
 361  
 362  // Close closes the database
 363  func (w *W) Close() error {
 364  	w.dbMu.Lock()
 365  	defer w.dbMu.Unlock()
 366  
 367  	if w.db != nil {
 368  		w.db.Close()
 369  		w.db = nil
 370  	}
 371  	return nil
 372  }
 373  
 374  // Wipe removes all data and recreates object stores
 375  func (w *W) Wipe() error {
 376  	w.dbMu.Lock()
 377  	defer w.dbMu.Unlock()
 378  
 379  	// Close the current database
 380  	if w.db != nil {
 381  		w.db.Close()
 382  		w.db = nil
 383  	}
 384  
 385  	// Delete the database
 386  	factory := idb.Global()
 387  	delReq, err := factory.DeleteDatabase(DatabaseName)
 388  	if err != nil {
 389  		return fmt.Errorf("failed to delete IndexedDB: %w", err)
 390  	}
 391  	if err := delReq.Await(w.ctx); err != nil {
 392  		return fmt.Errorf("failed to await IndexedDB delete: %w", err)
 393  	}
 394  
 395  	// Reset serial counters
 396  	w.serialMu.Lock()
 397  	w.eventSerial = 0
 398  	w.pubkeySerial = 0
 399  	w.serialMu.Unlock()
 400  
 401  	// Reopen the database (this will recreate all object stores)
 402  	w.dbMu.Unlock()
 403  	err = w.openDatabase()
 404  	w.dbMu.Lock()
 405  
 406  	return err
 407  }
 408  
 409  // SetLogLevel sets the logging level
 410  func (w *W) SetLogLevel(level string) {
 411  	w.Logger.SetLogLevel(lol.GetLogLevel(level))
 412  }
 413  
 414  // Ready returns a channel that closes when the database is ready
 415  func (w *W) Ready() <-chan struct{} { return w.ready }
 416  
 417  // RunMigrations runs database migrations (handled by IndexedDB upgrade)
 418  func (w *W) RunMigrations() {}
 419  
 420  // EventIdsBySerial retrieves event IDs by serial range
 421  func (w *W) EventIdsBySerial(start uint64, count int) ([]uint64, error) {
 422  	return nil, errors.New("not implemented")
 423  }
 424  
 425  // Query cache methods (simplified for WASM - no caching)
 426  func (w *W) GetCachedJSON(f *filter.F) ([][]byte, bool)             { return nil, false }
 427  func (w *W) CacheMarshaledJSON(f *filter.F, marshaledJSON [][]byte) {}
 428  func (w *W) GetCachedEvents(f *filter.F) (event.S, bool)            { return nil, false }
 429  func (w *W) CacheEvents(f *filter.F, events event.S)                {}
 430  func (w *W) InvalidateQueryCache()                                  {}
 431  
 432  // Placeholder implementations for remaining interface methods
 433  // Query methods are implemented in query-events.go
 434  // Delete methods are implemented in delete-event.go
 435  
 436  // Import, Export, and ImportEvents methods are implemented in import-export.go
 437  
 438  func (w *W) GetRelayIdentitySecret() (skb []byte, err error) {
 439  	return w.getMeta(RelayIdentityKey)
 440  }
 441  
 442  func (w *W) SetRelayIdentitySecret(skb []byte) error {
 443  	return w.setMeta(RelayIdentityKey, skb)
 444  }
 445  
 446  func (w *W) GetOrCreateRelayIdentitySecret() (skb []byte, err error) {
 447  	skb, err = w.GetRelayIdentitySecret()
 448  	if err != nil {
 449  		return nil, err
 450  	}
 451  	if skb != nil {
 452  		return skb, nil
 453  	}
 454  	// Generate new secret key (32 random bytes)
 455  	// In WASM, we use crypto.getRandomValues
 456  	skb = make([]byte, 32)
 457  	if err := cryptoRandom(skb); err != nil {
 458  		return nil, err
 459  	}
 460  	if err := w.SetRelayIdentitySecret(skb); err != nil {
 461  		return nil, err
 462  	}
 463  	return skb, nil
 464  }
 465  
 466  func (w *W) SetMarker(key string, value []byte) error {
 467  	return w.setStoreValue("markers", key, value)
 468  }
 469  
 470  func (w *W) GetMarker(key string) (value []byte, err error) {
 471  	return w.getStoreValue("markers", key)
 472  }
 473  
 474  func (w *W) HasMarker(key string) bool {
 475  	val, err := w.GetMarker(key)
 476  	return err == nil && val != nil
 477  }
 478  
 479  func (w *W) DeleteMarker(key string) error {
 480  	return w.deleteStoreValue("markers", key)
 481  }
 482  
 483  // Subscription methods are implemented in subscriptions.go
 484  // NIP-43 methods are implemented in nip43.go
 485  
 486  // Helper methods for object store operations
 487  
 488  func (w *W) setStoreValue(storeName, key string, value []byte) error {
 489  	tx, err := w.db.Transaction(idb.TransactionReadWrite, storeName)
 490  	if err != nil {
 491  		return err
 492  	}
 493  
 494  	store, err := tx.ObjectStore(storeName)
 495  	if err != nil {
 496  		return err
 497  	}
 498  
 499  	keyVal, err := safejs.ValueOf(key)
 500  	if err != nil {
 501  		return err
 502  	}
 503  
 504  	valueJS := bytesToSafeValue(value)
 505  
 506  	_, err = store.PutKey(keyVal, valueJS)
 507  	if err != nil {
 508  		return err
 509  	}
 510  
 511  	return tx.Await(w.ctx)
 512  }
 513  
 514  func (w *W) getStoreValue(storeName, key string) ([]byte, error) {
 515  	tx, err := w.db.Transaction(idb.TransactionReadOnly, storeName)
 516  	if err != nil {
 517  		return nil, err
 518  	}
 519  
 520  	store, err := tx.ObjectStore(storeName)
 521  	if err != nil {
 522  		return nil, err
 523  	}
 524  
 525  	keyVal, err := safejs.ValueOf(key)
 526  	if err != nil {
 527  		return nil, err
 528  	}
 529  
 530  	req, err := store.Get(keyVal)
 531  	if err != nil {
 532  		return nil, err
 533  	}
 534  
 535  	val, err := req.Await(w.ctx)
 536  	if err != nil {
 537  		return nil, nil
 538  	}
 539  
 540  	if val.IsUndefined() || val.IsNull() {
 541  		return nil, nil
 542  	}
 543  
 544  	return safeValueToBytes(val), nil
 545  }
 546  
 547  func (w *W) deleteStoreValue(storeName, key string) error {
 548  	tx, err := w.db.Transaction(idb.TransactionReadWrite, storeName)
 549  	if err != nil {
 550  		return err
 551  	}
 552  
 553  	store, err := tx.ObjectStore(storeName)
 554  	if err != nil {
 555  		return err
 556  	}
 557  
 558  	keyVal, err := safejs.ValueOf(key)
 559  	if err != nil {
 560  		return err
 561  	}
 562  
 563  	_, err = store.Delete(keyVal)
 564  	if err != nil {
 565  		return err
 566  	}
 567  
 568  	return tx.Await(w.ctx)
 569  }
 570  
 571  // Placeholder for unused variable
 572  var _ = chk.E
 573