dexie / dexie-website

Website dexie.org
https://dexie.org
Apache License 2.0
32 stars 295 forks source link

Drop jQuery dependency in documentation/examples #123

Open tacman opened 7 months ago

tacman commented 7 months ago

On https://dexie.org/docs/Dexie/Dexie.on.populate#ajax-populate-sample

 return new Promise(function (resolve, reject) {
                $.ajax(url, {
                    type: 'get',
                    dataType: 'json',
                    error: function (xhr, textStatus) {
                        // Rejecting promise to make db.open() fail.
                        reject(textStatus);
                    },
                    success: function (data) {
                        // Resolving Promise will launch then() below.
                        resolve(data);
                    }
                });

fetch() is supported on all browsers that support IndexedDB, using jquery in the documentation makes this library seem a bit "old-school", back when jquery was used to harmonize all the different ways ajax calls were made.

I think developers may simply cut and paste when getting started, so using something that works everywhere and was also a best practice for handling errors would be better than requiring jQuery.

Thanks.

dfahlander commented 7 months ago

Thanks! Planned it. Feel free to do a PR, otherwise I will do it some time after March 27

tacman commented 7 months ago

I'm so confused about Promises, I'm trying to follow the examples in my stimulus controller, and I get depressed and start questioning my career choices...

Since a jquery ajax request returns a jQuery promise, (see https://web.dev/articles/promises#compatibility_with_other_libraries), I think it's even more important to use a vanilla fetch call in the above example.

What I'd really like to see is a set of paginated api calls, so I could properly load an set of data, e.g.

do {
   fetch(/api/data?page=N
while (nextPage);

Anyway, this is an awesome library, I can't wait to understand Promises more so I can take advantage of it.

tacman commented 7 months ago

Here's an example that works with dummy data. It only gets the first 30 out of 100 though, it'd be great if you could tweak it to show fully populating the table via a sequence of API calls.

        import Dexie from 'dexie';

        var db = new Dexie('someDB');
        db.version(1).stores({
            productTable: "++id,price,brand,category"
        });

        // Populate from AJAX:
        db.on('ready', function (db) {
            // on('ready') event will fire when database is open but
            // before any other queued operations start executing.
            // By returning a Promise from this event,
            // the framework will wait until promise completes before
            // resuming any queued database operations.
            // Let's start by using the count() method to detect if
            // database has already been populated.
            return db.productTable.count(function (count) {
                if (count > 0) {
                    console.log("Already populated");
                } else {
                    console.log("Database is empty. Populating from ajax call...");
                    // We want framework to continue waiting, so we encapsulate
                    // the ajax call in a Promise that we return here.
                    return new Promise(  (resolve, reject) => {
                        const response = loadData().then( (response) => {
                            console.log("Calling bulkAdd() to insert objects...", response);
                            console.assert(db);

                            return db.productTable.bulkAdd(response.products);
                        });
                    }).then(function (data) {
                        console.log("Got ajax response. We'll now add the objects.");
                        // By returning the a promise, framework will keep
                        // waiting for this promise to complete before resuming other
                        // db-operations.
                    }).then(function () {
                        console.log ("Done populating.");
                    });
                }
            });
        });

        // Following operation will be queued until we're finished populating data:
        db.productTable.each(function (obj) {
            // When we come here, data is fully populated and we can log all objects.
            console.log("Found object: " + JSON.stringify(obj));
        }).then(function () {
            console.log("Finished.");
        }).catch(function (error) {
            // In our each() callback above fails, OR db.open() fails due to any reason,
            // including our ajax call failed, this operation will fail and we will get
            // the error here!
            console.error(error.stack || error);
            // Note that we could also have caught it on db.open() but in this sample,
            // we show it here.
        });

        async function loadData() {
            let url = 'https://dummyjson.com/products';
            const response = await fetch(url);
            return await response.json();
        }
dfahlander commented 7 months ago

Some of the old examles would really need a rewrite and this is one of them. In this case I don't think we need to create a Promise there but simply:

db.on('ready', async vipDB => {
  const count = await vipDB.productTable.count();
  if (count > 0) {
    console.log("Already populated");
  } else {
    const data = await loadData();
    await vipDB.productTable.bulkAdd(data);
    console.log ("Done populating.");
});

The db instance passed to on('ready') is "VIP" meaning that it lets the request bypass the blocked queue that waits for the promise from the callback to complete before resuming other request.

If you want to fetch several chunks of data rather than a single one it could be done entirely within loadData().

I would also like to change db.productTable.each() call to db.productTable.toArray()or simplydb.productTable.count()` because even if each is supported, it's mostly not very useful and in almost all scenarios, toArray() is a better alternative.

If you would have the time to do a PR and update this example, please verify that the code runs and does what it is expected to do (I haven't actually run the snippet I posted). Else, I will update these docs some day - it's too much other things I need to prioritize right now. Thanks for taking bringing this up. The docs feel a bit old fashioned with these old samples.

tacman commented 7 months ago

I'll play around with this -- I'm working on a demo where I preload data for use with the awesome datatables.net, and now that pagination is working, it looks great. I'll submit a PR hopefully later today.

How can I optionally delete the database first, before populating? I'll add that to this example. This isn't working, I'm sure I need to wait for the delete to finish before doing the next step.

In fact, I seem to have gone into a Promises wormhole, and 'ready' seems to never fire now. Sigh. By chance, is there a way to turn on debug and see the events that are fired? My code is filled with console.logs, I'm sure I'm doing something wrong with promises.

    startup()
    {
        let db = new Dexie(this.dbNameValue);
        console.log(this.dbNameValue + ' database startup.')
        db.on('ready', async vipDB => {
            console.log(this.dbNameValue + ' ready')

        var db = new Dexie('test');
        if (forceReload) {
            db.delete().then( () => db = new Dexie('test'));
        }

// Populate from AJAX:
        db.on('ready',  (db) => {

            // db.delete().then( () => {});
        db.version(1).stores({
            productTable: "++id,price,rating,brand",
            friendTable: "++id,state,age,zip"
        });
tacman commented 7 months ago

I'm sure this is something easy, but I can't wrap my head around the promises.

Here's a jsFiddle with the example you provided. https://jsfiddle.net/tacman1123/1uhvg5nw/1/

I can see that is it loading the data, but the count promise is never filled, almost certainly because something is out of order.

Thanks

dfahlander commented 7 months ago

There are 2 problems with the loadData() in the fiddle.

  1. It returns an object containing query parameters limit, skip, total and the array of object in a "products" property but this object is what loadData() returns and this is sent to bulkAdd() but that method expects an array of objects
  2. The call to bulkAdd is not awaited so the callback could return too early.

I do not know about the dummyjson API but I suppose you can call it several times to download chunks of the data. This is outside the scope of Dexie but typically you could do a for...of loop and await each result. You could either build up an entire result to return from loadData(), or maybe better, let loadData do bulkAdd after each chunk has been downloaded.

tacman commented 7 months ago

Thanks! The updated and working fiddle is at https://jsfiddle.net/tacman1123/1uhvg5nw/2/

Should I make a PR using this code to replace the jQuery example?

dfahlander commented 7 months ago

Yes but just remove const addPromise = because an awaited promise is not a promise anymore. It's enough with just awaiting it and don't put the result anywhere.