3

In the following code,

Promise.allSettled( [ entry_save(), save_state(), get_HTML() ] ).then( ... );

promises entry_save and save_state are both readwrite database transactions and get_HTML is readonly. The two readwrite transactions could be combined together but that complicates the undo/redo chain that is maintained and it ties the success and rollback of the two together which is undesired.

The entry_save transaction needs to write before the save_state transaction. Before moving entry_save into the Promise.allSettled that is how it worked because the entry_save transaction was created prior to those of the others. This MDN article explains how the order in which requests are performed is based upon when the transactions are created independently of the order in which the requests are made.

My question is does the synchronous code of each promise process in the order in which it is placed in the array, such that placing entry_save first will always result in its transaction being created first and guaranteeing its database requests will be performed first?

Although it works and is quick enough, I'd prefer to not do this:

entry_save().then( () => { Promise.allSettled( [ save_state(), get_HTML() ] ) } ).then( ... );

If it matters, that's not the exactly the way it is written, it's more consistent with:

entry_save().then( intermediate ); where intermediate invokes the Promise.allSettled.

Thank you.

To clarify a bit, below is the example given in the above cited MDN document.

var trans1 = db.transaction("foo", "readwrite");
var trans2 = db.transaction("foo", "readwrite");
var objectStore2 = trans2.objectStore("foo")
var objectStore1 = trans1.objectStore("foo")
objectStore2.put("2", "key");
objectStore1.put("1", "key");

After the code is executed the object store should contain the value "2", since trans2 should run after trans1.

If entry_save creates trans1 and save_state create trans2, and all in the synchronous code of the functions, meaning not within an onsuccess or onerror handler of a database request or something similar, will the MDN example not hold?

Thus, where @jfriend00 writes,

The functions are called in the order they are placed in the array, but that only determines the order in which the asynchronous are started.

will this order the timing of the write requests by that of the creation of the transactions, since the transactions are created in the synchronous code before the asynchronous can commence?

I'd like to test it but I'm not sure how. If two nearly identical promises are used in a Promise.allSettled, how can the write request of the first created transaction be delayed such that it takes place after the write request of the second created transaction, to test if it will be written first? A setTimeout should terminate the transaction. Perhaps a long-running synchronous loop placed before the request.


The code at the very end of this question may better illustrate more precisely what I have attempted to ask. It takes the MDN example in the article cited above and spreads it across two promises placed in a Promise.allSettled, both of which attempt to write to the same object store from within the onsuccess event of a get request.

The question was will the same principle in the article of the first transaction created writing before the second transaction created, regardless of the order the requests are made, still hold in this set up. Since the synchronous portions of the promises will process in the order the promises are placed in the array, the transaction in promise p_1 will be created before that of p_2. However, the put request in the onsuccess event of the get request in p_1 is delayed by the loop building a large string. The question is will p_1 still write before p_2?

In experimenting with this, I cannot get p_2 to write before p_1. Thus, it appears that the MDN example applies even in this type of set up. However, I cannot be sure of why because I don't understand how the JS code is really interpreted/processed.

For example, why can the req.onsuccess function be defined after the request is made? I asked that question sometime ago but still don't know enough to be sure that it doesn't affect the way I attempted to add in a delay here. I know that it won't work the other way around; but my point is I'm not sure how the browser handles that synchronous loop before the put request is made in p_1 to really know for sure that this example demonstrates that the MDN article ALWAYS holds in this set up. However, I can observe that it takes longer for the requests to complete as the number of loop iterations is increased; and, in all cases I have observed, p_1 always writes before p_2. The only way p_2 writes before p_1 is if p_1 doesn't write at all because of the string taking up to much memory causing the transaction in p_1 to be aborted.

That being said, and returning to the fuller set up of my question concerning three promises in the array of the Promise.allSettled compared to requiring entry_save to complete before commencing a Promise.allSettled on the two remaining promises, in the full code of my project, for reasons I am not sure of, the latter is quicker than the former, that is, waiting for entry_save to complete is quicker than including it in the Promise.allSettled.

I was expecting it to be the other way around. The only reason I can think of at this point is that, since entry_save and save_state are both writing to the same object store, perhaps whatever the browser does equivalent to locking the object store until the first transaction, which is that in entry_save, completes and removing the lock takes longer than requiring that entry_save complete before the Promise.allSettled commences and not involving a lock. I thought that everything would be ready "in advance" just waiting for the two put requests to take place in transaction order. They took place in order but more slowly or at least not as quick as using:

entry_save().then( () => { Promise.allSettled( [ save_state(), get_HTML() ] ) } ).then( ... );

instead of:

 Promise.allSettled( [ entry_save(), save_state(), get_HTML() ] ).then( ... );

function p_all() { Promise.allSettled( [ p_1(), p_2() ] ); }

function p_1()
  {
    return new Promise( ( resolve, reject ) =>
      {
        let T = DB.transaction( [ 'os_1', 'os_2' ], 'readwrite' ),
            q = T.objectStore( 'os_1' ),
            u = T.objectStore( 'os_2' ),
            req, i, t ='', x = '';    

     req = q.get( 1 );

     req.onsuccess = () =>
       {
         let i, t, r = req.result;
         for ( i = 1; i < 10000000; i++ ) t = t + 'This is a string';
         r.n = 'p1';
         u.put( r );
         console.log( r );
       };
    }); }

function p_2()
  {
    return new Promise( ( resolve, reject ) =>
      {
       let T = DB.transaction( [ 'os_1', 'os_2' ], 'readwrite' ),
           q = T.objectStore( 'os_1' ),
           u = T.objectStore( 'os_2' ),
           req;    

        req = q.get( 1 );

        req.onsuccess = () =>
          {
            let r = req.result;
            r.n = 'p2';
            u.put( r );
            console.log( r );
          };
    }); }
Gary
  • 2,393
  • 12
  • 31
  • A little code for those two functions would go 100 times further than all your words in showing us what they do and what the issue is. A good axiom here on stackoverflow is that: ***"questions about code will nearly always be better questions and more likely to get you a good, swift and correct answer if you include all relevant code in your question"***. It is generally a mistake to try to make the question generic without the code representing your specific situation. – jfriend00 Feb 18 '20 at 23:55
  • Thanks again. As I was trying to pull out the main sections of the code to provide, I see that `entry_save` isn't as simple as I was remembering and it does have a database operation from within the onsuccess event of another request. Unless the answer is that ALL requests of a created transaction will ALWAYS process before ALL the requests of the any subsequently created transactions, then the only safe approach will be to ensure `entry_save` completes before the smaller Promise.allSettled commences, which is what you wrote at the start. Thanks. – Gary Feb 19 '20 at 00:12
  • So `p_1()` and `p_2()` never resolve or reject their promise. So, I don't know what you're trying to accomplish with `Promise.allSettled( [ p_1(), p_2() ] );`. It doesn't do anything except call `p_1()` and then call `p_2()`, both of which NEVER settle. – jfriend00 Feb 20 '20 at 05:40
  • All, you can guarantee in this code is that the call to `Db.transaction()` in `p_1()` will occur before the call to `Db.transaction()` in `p_2()`. EVERYTHING else is racy (meaning indeterminate and could go in any order). Also, reading code full of single letter variables is very hard as they don't mean anything at all to the reader. – jfriend00 Feb 20 '20 at 05:44
  • @jfriend00 It is just an example to illustrate the specific question regarding indexedDB transactions. Of course, the promises in my project code resolve or reject but that isn't the point of the question. The first part of your last comment is key because, since it is guaranteed that the transaction in `p_1` is created before that of `p_2`, and because they involve the same object stores, all the requests of the `p_1` transaction will be performed before any of the requests of the `p_2` transaction. That part is not racy. That is what my question is about. – Gary Feb 20 '20 at 07:08
  • Well, we could have settled that a day ago if you just included the relevant code. I don't know why people posting to stackoverflow think they should write a theoretical question rather than posting the actual code that the question is about. We can always provide faster and more accurate answers if questions about code include the actual code. Always. – jfriend00 Feb 20 '20 at 07:16
  • It's not a theoretical question. It's just a question about how the code is processed by the browser or JS engine (how indexedDB works) rather than a debugging question. It's a very practical issue for me. If I had posted the code of my entire project, it would not have helped. I appreciate you taking the time to respond to my question and upvoted your response. – Gary Feb 20 '20 at 07:32
  • Well, there were two aspects to what you were asking. One was how the code you first disclosed worked, which my answer completely addressed. The second aspect of the question is about your specific database and how transactions work in your database. Since you awarded the best answer to the one that talked only about the transactions, that is apparently what you cared about, even though you disclosed none of that code originally. That's my point. Anyway, glad you got your answer. – jfriend00 Feb 20 '20 at 21:32

2 Answers2

1

When you do this:

Promise.allSettled( [ entry_save(), save_state(), get_HTML() ] ).then(...)

It's equivalent to this:

const p1 = entry_save();
const p2 = save_state();
const p3 = get_HTML();

Promise.allSettled([p1, p2, p3]).then(...);

So, the individual function calls you issue such as save_state() are STARTED in the order specified. But, each of those calls are asynchronous so the internal order of what happens before something else really depends upon what they do inside as they can all be in flight at the same time and parts of their execution can be interleaved in an indeterminate order.

Imagine that entry_save() actually consists of multiple asynchronous pieces such as first reading some data from disk, then modifying the data, then writing it to the database. It would call the first asynchronous operation to read some data from disk and then immediately return a promise. Then, save_state() would get to start executing. If save_state() just immediately issued a write to the database, then it very well may write to the database before entry_save() writes to the database. In fact, the sequencing of the two database writes is indeterminate and racy.

If you need entry_save() to complete before save_state(), then the above is NOT the way to code it at all. Your code is not guaranteeing that all of entry_save() is done before any of save_state() runs.

Instead, you SHOULD do what you seem to already know:

entry_save().then( () => { Promise.allSettled( [ save_state(), get_HTML() ] ) } ).then( ... );

Only that guarantees that entry_save() will complete before save_state() gets to run. And, this assumes that you're perfectly OK with save_state() and get_HTML() running concurrently and in an unpredictable order.

My question is does the synchronous code of each promise process in the order in which it is placed in the array, such that placing entry_save first will always result in its transaction being created first and guaranteeing its database requests will be performed first?

The functions are called in the order they are placed in the array, but that only determines the order in which the asynchronous are started. After that, they are all in-flight at the same time and the internal timing between them depends upon how long their individual asynchronous operations take and what those asynchronous operations do. If order matters, you can't just put them all in an indeterminate race. That's call a "race condition". Instead, you would need to structure your code to guarantee that the desired operation goes first before the ones that need to execute after it.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Thank you for the explanation. I added a bit more at the end of my question in attempt to make it a more precise. I believe I understand what you wrote and it makes sense to me; but I don't think that is exactly what I am asking. I don't need `entry_save` to complete before `save_state` commences but only its transaction to be created before that of `save_state`. That is, if I am properly understanding the MDN document and it applies in this scenario involving transactions being created within different promises. – Gary Feb 18 '20 at 23:50
  • @Gary - I don't think we can help you more completely without seeing the ACTUAL code for the functions in question here and then understanding exactly what has to be completed first. It sounds to me like you're micromanaging here a bit. The safe thing is to do `entry_save().then(...)`. Delving in any deeper requires examining the actual code of the two functions to try to understand exactly what would be naturally sequenced and what would be racy. There's no generic answer there. It depends upon the actual code and actual DB operations and what exactly you must have go first. – jfriend00 Feb 18 '20 at 23:54
1

indexedDB will maintain the order of the transactions in order created, except when those transactions do not overlap (e.g. do not involve the same store out of the set of stores each one involves). this is pretty much regardless of what you do at the higher promise layer.

at the same time, maybe it is unwise to rely on that behavior, because it is implicit and a bit confusing. so maybe it is ok to linearize with promises. the only reach catch is when you need maximum performance, which I doubt applies.

moreover, promises begin execution at the time they are created. they just do not necessarily end at that time, they end eventually instead of immediately. that means that the calls happen in the order you 'create' the promises that are wrapping the indexedDB calls. which means that it relies on the order in which you create the transactions.

regardless of which promise wins the race. regardless of using promise.all.

also, promise.all will retain order even if promises complete out of order, just fyi, but do not let that throw you off.

Josh
  • 17,834
  • 7
  • 50
  • 68
  • Thank you.This is the information I needed. It seemed as though this is the way transactions would process based on the documentation and the way they were processing in my little tests but it is difficult to test. The Promise.allSettled just provides notice of when all the promises have completed and doesn't change the creation order of the transactions. Is it correct that in indexedDB there is never a transaction race even if requests are made within onsuccess events of other requests? There may be a race to create a transaction but not among requests within different transactions. Thanks. – Gary Feb 20 '20 at 06:51
  • if there is a race amongst transactions, the first created wins, the second is "blocked from starting", which means that it "starts eventually" but not that it "starts immediately", where the second starts but implicitly waits, enqueued, for the first to complete, IF there is overlapping scope (shared object store) – Josh Feb 20 '20 at 06:53
  • Thank you. That makes sense to me. Perhaps the browser's handling of that enqueuing process takes a little extra time such that my first promise's transaction completes more quickly without it. – Gary Feb 20 '20 at 07:16