transaction.go 9.2 KB

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