transaction.go raw

   1  //go:build js && wasm
   2  // +build js,wasm
   3  
   4  package idb
   5  
   6  import (
   7  	"context"
   8  	"errors"
   9  
  10  	"github.com/aperturerobotics/go-indexeddb/idb/internal/jscache"
  11  	"github.com/hack-pad/safejs"
  12  )
  13  
  14  var (
  15  	supportsTransactionCommit = checkSupportsTransactionCommit()
  16  
  17  	errNotInTransaction = errors.New("Not part of a transaction")
  18  )
  19  
  20  func checkSupportsTransactionCommit() bool {
  21  	idbTransaction, err := safejs.Global().Get("IDBTransaction")
  22  	if err != nil {
  23  		return false
  24  	}
  25  	prototype, err := idbTransaction.Get("prototype")
  26  	if err != nil {
  27  		return false
  28  	}
  29  	commit, err := prototype.Get("commit")
  30  	if err != nil {
  31  		return false
  32  	}
  33  	supported, err := commit.Truthy()
  34  	return supported && err == nil
  35  }
  36  
  37  var (
  38  	modeCache       jscache.Strings
  39  	durabilityCache jscache.Strings
  40  )
  41  
  42  // TransactionMode defines the mode for isolating access to data in the transaction's current object stores.
  43  type TransactionMode int
  44  
  45  const (
  46  	// TransactionReadOnly allows data to be read but not changed.
  47  	TransactionReadOnly TransactionMode = iota
  48  	// TransactionReadWrite allows reading and writing of data in existing data stores to be changed.
  49  	TransactionReadWrite
  50  )
  51  
  52  func parseMode(s string) TransactionMode {
  53  	switch s {
  54  	case "readwrite":
  55  		return TransactionReadWrite
  56  	default:
  57  		return TransactionReadOnly
  58  	}
  59  }
  60  
  61  func (m TransactionMode) String() string {
  62  	switch m {
  63  	case TransactionReadWrite:
  64  		return "readwrite"
  65  	default:
  66  		return "readonly"
  67  	}
  68  }
  69  
  70  func (m TransactionMode) jsValue() safejs.Value {
  71  	return modeCache.Value(m.String())
  72  }
  73  
  74  // TransactionDurability is a hint to the user agent of whether to prioritize performance or durability when committing a transaction.
  75  type TransactionDurability int
  76  
  77  const (
  78  	// DurabilityDefault indicates the user agent should use its default durability behavior for the storage bucket. This is the default for transactions if not otherwise specified.
  79  	DurabilityDefault TransactionDurability = iota
  80  	// DurabilityRelaxed indicates the user agent may consider that the transaction has successfully committed as soon as all outstanding changes have been written to the operating system, without subsequent verification.
  81  	DurabilityRelaxed
  82  	// DurabilityStrict indicates the user agent may consider that the transaction has successfully committed only after verifying all outstanding changes have been successfully written to a persistent storage medium.
  83  	DurabilityStrict
  84  )
  85  
  86  func parseDurability(s string) TransactionDurability {
  87  	switch s {
  88  	case "relaxed":
  89  		return DurabilityRelaxed
  90  	case "strict":
  91  		return DurabilityStrict
  92  	default:
  93  		return DurabilityDefault
  94  	}
  95  }
  96  
  97  func (d TransactionDurability) String() string {
  98  	switch d {
  99  	case DurabilityRelaxed:
 100  		return "relaxed"
 101  	case DurabilityStrict:
 102  		return "strict"
 103  	default:
 104  		return "default"
 105  	}
 106  }
 107  
 108  func (d TransactionDurability) jsValue() safejs.Value {
 109  	return durabilityCache.Value(d.String())
 110  }
 111  
 112  // Transaction provides a static, asynchronous transaction on a database.
 113  // All reading and writing of data is done within transactions. You use Database to start transactions,
 114  // Transaction to set the mode of the transaction (e.g. is it TransactionReadOnly or TransactionReadWrite),
 115  // and you access an ObjectStore to make a request. You can also use a Transaction object to abort transactions.
 116  type Transaction struct {
 117  	db            *Database
 118  	jsTransaction safejs.Value
 119  	objectStores  map[string]*ObjectStore
 120  }
 121  
 122  func wrapTransaction(db *Database, jsTransaction safejs.Value) *Transaction {
 123  	return &Transaction{
 124  		db:            db,
 125  		jsTransaction: jsTransaction,
 126  		objectStores:  make(map[string]*ObjectStore, 1),
 127  	}
 128  }
 129  
 130  // Database returns the database connection with which this transaction is associated.
 131  func (t *Transaction) Database() (*Database, error) {
 132  	return t.db, nil
 133  }
 134  
 135  // Durability returns the durability hint the transaction was created with.
 136  func (t *Transaction) Durability() (TransactionDurability, error) {
 137  	durability, err := t.jsTransaction.Get("durability")
 138  	if err != nil {
 139  		return 0, err
 140  	}
 141  	durabilityString, err := durability.String()
 142  	if err != nil {
 143  		return 0, err
 144  	}
 145  	return parseDurability(durabilityString), nil
 146  }
 147  
 148  // Err returns an error indicating the type of error that occurred when there is an unsuccessful transaction. Returns nil if the transaction is not finished, is finished and successfully committed, or was aborted with Transaction.Abort().
 149  func (t *Transaction) Err() error {
 150  	jsErr, err := t.jsTransaction.Get("error")
 151  	if err != nil {
 152  		return err
 153  	}
 154  	return domExceptionAsError(jsErr)
 155  }
 156  
 157  // Abort rolls back all the changes to objects in the database associated with this transaction.
 158  func (t *Transaction) Abort() error {
 159  	_, err := t.jsTransaction.Call("abort")
 160  	return tryAsDOMException(err)
 161  }
 162  
 163  // Mode returns the mode for isolating access to data in the object stores that are in the scope of the transaction. The default value is TransactionReadOnly.
 164  func (t *Transaction) Mode() (TransactionMode, error) {
 165  	mode, err := t.jsTransaction.Get("mode")
 166  	if err != nil {
 167  		return 0, err
 168  	}
 169  	modeStr, err := mode.String()
 170  	return parseMode(modeStr), err
 171  }
 172  
 173  // ObjectStoreNames returns a list of the names of ObjectStores associated with the transaction.
 174  func (t *Transaction) ObjectStoreNames() ([]string, error) {
 175  	objectStoreNames, err := t.jsTransaction.Get("objectStoreNames")
 176  	if err != nil {
 177  		return nil, err
 178  	}
 179  	return stringsFromArray(objectStoreNames)
 180  }
 181  
 182  // ObjectStore returns an ObjectStore representing an object store that is part of the scope of this transaction.
 183  func (t *Transaction) ObjectStore(name string) (*ObjectStore, error) {
 184  	if store, ok := t.objectStores[name]; ok {
 185  		return store, nil
 186  	}
 187  	jsObjectStore, err := t.jsTransaction.Call("objectStore", name)
 188  	if err != nil {
 189  		return nil, tryAsDOMException(err)
 190  	}
 191  	store := wrapObjectStore(t, jsObjectStore)
 192  	t.objectStores[name] = store
 193  	return store, nil
 194  }
 195  
 196  // Commit for an active transaction, commits the transaction. Note that this doesn't normally have to be called — a transaction will automatically commit when all outstanding requests have been satisfied and no new requests have been made. Commit() can be used to start the commit process without waiting for events from outstanding requests to be dispatched.
 197  func (t *Transaction) Commit() error {
 198  	if !supportsTransactionCommit {
 199  		return nil
 200  	}
 201  
 202  	_, err := t.jsTransaction.Call("commit")
 203  	return tryAsDOMException(err)
 204  }
 205  
 206  // Await waits for success or failure, then returns the results.
 207  func (t *Transaction) Await(ctx context.Context) error {
 208  	resultErr := t.listenFinished()
 209  	select {
 210  	case err := <-resultErr:
 211  		return tryAsDOMException(err)
 212  	case <-ctx.Done():
 213  		return ctx.Err()
 214  	}
 215  }
 216  
 217  // listenFinished listens to this transaction's completion events which eventually resolves with nil or an error.
 218  // Resolves with the first IDBRequest's error
 219  func (t *Transaction) listenFinished() <-chan error {
 220  	result := make(chan error, 1)
 221  	pushResult := func(err error) {
 222  		select {
 223  		case result <- err:
 224  		default:
 225  		}
 226  	}
 227  
 228  	if err := t.addEventListener("abort", result, func(safejs.Value) error {
 229  		return t.Err() // catch abort errors not already caught by the error event handler, like QuotaExceededError
 230  	}); err != nil {
 231  		pushResult(err)
 232  		return result
 233  	}
 234  
 235  	if err := t.addEventListener("complete", result, func(safejs.Value) error {
 236  		return nil // transaction was successful
 237  	}); err != nil {
 238  		pushResult(err)
 239  		return result
 240  	}
 241  
 242  	if err := t.addEventListener("error", result, func(event safejs.Value) error {
 243  		// Error event target is always an IDBRequest, which is guaranteed to be a DOMException with a 'name' property.
 244  		properties, err := jsGetNested(event, "target", "error")
 245  		if err != nil {
 246  			return err
 247  		}
 248  		return domExceptionAsError(properties[1])
 249  	}); err != nil {
 250  		pushResult(err)
 251  		return result
 252  	}
 253  
 254  	return result
 255  }
 256  
 257  func jsGetNested(value safejs.Value, keys ...string) ([]safejs.Value, error) {
 258  	if len(keys) == 0 {
 259  		return []safejs.Value{value}, nil
 260  	}
 261  	nextValue, err := value.Get(keys[0])
 262  	if err != nil {
 263  		return nil, err
 264  	}
 265  	values, err := jsGetNested(nextValue, keys[1:]...)
 266  	if err != nil {
 267  		return nil, err
 268  	}
 269  	return append([]safejs.Value{nextValue}, values...), nil
 270  }
 271  
 272  // addCancelingEventListener adds an event listener for fn()
 273  //
 274  // Sends fn's error return value to result.
 275  //
 276  // Result must be a buffered channel.
 277  func (t *Transaction) addEventListener(
 278  	eventName string,
 279  	result chan<- error,
 280  	fn func(event safejs.Value) error,
 281  ) error {
 282  	jsFunc, err := safejs.FuncOf(func(_ safejs.Value, args []safejs.Value) interface{} {
 283  		var event safejs.Value
 284  		if len(args) > 0 {
 285  			event = args[0]
 286  		}
 287  		select {
 288  		case result <- fn(event):
 289  		default:
 290  		}
 291  		return nil
 292  	})
 293  	if err != nil {
 294  		return err
 295  	}
 296  	_, err = t.jsTransaction.Call(addEventListener, t.db.callStrings.Value(eventName), jsFunc)
 297  	if err != nil {
 298  		return tryAsDOMException(err)
 299  	}
 300  	return nil
 301  }
 302