Search code examples
javascriptgoogle-chrome-extensionpromiseindexeddb

Promise returns undefined after switching tabs only


I'm making a chrome extension that allows users to take notes on YouTube videos. The notes are stored using IndexedDB. I'm running into a problem where a promise returns undefined if I switch to another tab and then switch back. First, most of the code that I'm using to make the issue easier to understand.

// background.js

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if(request.message === 'get_notes') {
    let getNotes_request = get_records(request.payload)

        getNotes_request.then(res => { // This is where the error occurs, so the above function returns undefined
            chrome.runtime.sendMessage({
                message: 'getNotes_success',
                payload: res
            })
        })
    }
});

function create_database() {
    const request = self.indexedDB.open('MyTestDB');

    request.onerror = function(event) {
        console.log("Problem opening DB.");
    }

    request.onupgradeneeded = function(event) {
        db = event.target.result;

        let objectStore = db.createObjectStore('notes', {
            keypath: "id", autoIncrement: true
        });
        objectStore.createIndex("videoID, videoTime", ["videoID", "videoTime"], {unique: false});
    
        objectStore.transaction.oncomplete = function(event) {
            console.log("ObjectStore Created.");
        }
    }

    request.onsuccess = function(event) {
        db = event.target.result;
        console.log("DB Opened.")
    // Functions to carry out if successfully opened:
    
    // insert_records(notes); This is only done when for the first run, so I will have some notes to use for checking and debugging. The notes are in the form of an array.
    }

}

function get_records(vidID) {
    if(db) {
        const get_transaction = db.transaction("notes", "readonly");
        const objectStore = get_transaction.objectStore("notes");
        const myIndex = objectStore.index("videoID, videoTime");
        console.log("Pre-promise reached!");

        return new Promise((resolve, reject) => {
            get_transaction.oncomplete = function() {
                console.log("All Get Transactions Complete!");
            }

            get_transaction.onerror = function() {
                console.log("Problem Getting Notes!");
            }

            let request = myIndex.getAll(IDBKeyRange.bound([vidID], [vidID, []]));

            request.onsuccess = function(event) {
                console.log(event.target.result);
                resolve(event.target.result);
            }
        });

    }
}

create_database();

Now for the popup.js code:

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    if (request.message === 'getNotes_success') {
        notesTable.innerHTML = ""; // Clear the table body
        if (request.payload) {
            // Code to display the notes.
        } else {
            // Display a message to add notes or refresh the table.
        }
    }
}

chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
    site = tabs[0].url; // Variables declared earlier, not shown here

    // Valid YouTube video page
    if (isYTVid.test(site)) { // isYTVid is a previously declared regex string

        refNotesbtn.click(); // Auto-click the refresh notes button
    }

refNotesbtn.addEventListener('click', function() {

    videoID = get_videoID();

    chrome.runtime.sendMessage({
        message: 'get_notes',
        payload: videoID
    });
});

My issue right now is that the above code to display the notes works fine most of the time, but if I switch to another tab, then switch back to the YouTube tab and open the extension, the function to retrieve the notes returns undefined, and the message for no notes found is displayed. If I click on the button to refresh the notes, they are displayed correctly. This bug could cause a major issue for the user if it happens with the insert, edit, or delete functions (not displayed here), so I want to resolve it before proceeding.

I've noticed that when the error occurs, the "Pre-promise reached!" message is also not displayed, so is the get_notes function not being triggered at all, or is the issue after it is triggered? Apologies for the wall of code, and thanks for any help.


Solution

  • As db is discovered asynchronously, you need to cache it Promise-wrapped (as eg const dbPromise = ...) rather than raw db. Then you can reliably access db with dbPromise.then(db => {...}) (or the async/await equivalent).

    This is how I would write it. Plenty of explanatory comments in code.

    // background.js
    
    // cache for Promise-wrapped `db` object
    let dbPromise = null;
    
    // listener (top-level code)
    chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
        if(request.message === 'get_notes') {
            if(!dbPromise) {
                dbPromise = create_database()
                .catch(error => {
                    console.log(error);
                    throw error;
                });
            } else {
                // do nothing ... dbPromise is already cached.
            }
            dbPromise // read the cached promise and chain .then().then() .
            .then(db => get_records(db, request.payload)) // this is where you were struggling.
            .then(res => {
                chrome.runtime.sendMessage({
                    'message': 'getNotes_success',
                    'payload': res
                });
            })
            .catch(error => { // catch to avoid unhandled error exception
                console.log(error); // don't re-throw
            });
        } else {
            // do nothing ... presumably.
        }
    });
    
    function create_database() {
        return new Promise((resolve, reject) => {
            const request = self.indexedDB.open('MyTestDB'); // scope of self?
            request.onerror = function(event) {
                reject(new Error('Problem opening DB.'));
            }
            request.onupgradeneeded = function(event) {
                let db = event.target.result;
                let objectStore = db.createObjectStore('notes', {
                    'keypath': 'id', 
                    'autoIncrement': true
                });
                objectStore.createIndex('videoID, videoTime', ['videoID', 'videoTime'], {'unique': false});
                objectStore.transaction.oncomplete = function(event) {
                    console.log('ObjectStore Created.');
                    resolve(db);
                }
            }
            request.onsuccess = function(event) {
                let db = event.target.result;
                console.log('DB Opened.')
                // Functions to carry out if successfully opened:
                
                // insert_records(notes); This is only done when for the first run, so I will have some notes to use for checking and debugging. The notes are in the form of an array.
                resolve(db);
            }
        });
    }
    
    function get_records(db, vidID) {
        // first a couple of low-lwvel utility functions to help keep the high-level code clean.
        function getTransaction = function() {
            const transaction = db.transaction('notes', 'readonly');
            transaction.oncomplete = function() {
                console.log('All Get Transactions Complete!');
            }
            transaction.onerror = function() {
                console.log('Problem Getting Notes!');
            }
            return transaction;
        };
        function getAllAsync = function(transaction) {
            return new Promise((resolve, reject) {
                const objectStore = transaction.objectStore('notes');
                let request = objectStore.index('videoID, videoTime').getAll(IDBKeyRange.bound([vidID], [vidID, []]));
                request.onsuccess = function(event) {
                    // console.log(event.target.result);
                    resolve(event.target.result);
                }
                request.onerror = function(event) { // presumably
                    reject(new Error('getAll request failed'));
                }
            }); 
        };
        return getAllAsync(getTransaction(db));
    }
    

    The only part I'm not really sure of is the interplay between request.onupgradeneeded and request.onsuccess. I am assuming that one or other of these events will fire. If they fire sequentially(?) then maybe the code needs to be slightly different.