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