Getting updates from a backend process to the frontend with WixRealtime

Hi all!

I’ve started asking questions here but since nothing is free, I wanted to share a little feature I’ve developed for one of my projects in return!

The setup is as follow… Clicking on a button in my UI will trigger a rather lengthy process in the backend and I want my front-end to display a progress bar and an execution log as the execution occurs.

What you’ll need:

  • A page with Button, a ProgressBar and a Collapsible Text (MultiStateBox optional)
  • A backend code file in your Wix Block or Wix Site
  • Documentation for WixRealtime

Backend:
Starting with the Backend, I’ll first import WixRealtime like so:

import wixRealtimeBackend from 'wix-realtime-backend';

Then I’ll make a reusable function to call at intervals in my process :

function updateProgress(channel, progress, text) {
  let message = {
    "progress": Math.round(progress),
    "text": text,
    "timestamp": new Date().toISOString()
  }
  wixRealtimeBackend.publish(channel, message)
}

The channel param above is a Channel object that shall be subscribed to by your frontend page and passed along when calling your backend function.
The progress will be the percentage of completion (0 to 100) for your Progress Bar.
The text will be what’s logged on the page for where the progress is at.
NOTE: The timestamp here is somewhat of a hack because WixRealtime is not exactly real-time. It can take a variable amount of milliseconds to publish messages to whoever is listening so if you call two updates close to one another, they can actually compete and invert themselves. This timestamp will enable us to sort messages when displaying them in the frontend (later)

Let’s finish our backend implementation with a Sync function like so:

export async function sync(channel) {
  try {
    updateProgress(channel, 0, "Starting Process");

    //do some lengthy stuff

    updateProgress(channel, 50, "We're halfway there");

    // do some more lengthy stuff

    updateProgress(channel, 100, "Completed");
  } catch (error) {
    //Handle errors by sending an invalid progress value to my frontend
    updateProgress(channel, -1, error);
    throw error;
  }

}

Now in the frontend you’ll obviously import the Sync function from your backend but also the Wix Realtime module:

import { sync } from 'backend/myFile.js'
import wixRealtime from 'wix-realtime';

You will also need a handler function to wrap all the logic for subscribing to a WixRealtime channel, reacting to any updates posted by the backend, handling completion (or errors) and calling the backend function itself:

//Define a WixRealtime Channel object with a name
const syncProgressUpdatesChannel = { "name" : "syncUpdates" };

//Let's define an array to store all progress updates we'll receive and sort them by timestamp to eliminate any concurrency issues
let updates = [];

//The handler function itself
const handleSync = () => {

    //Ensure the updates array is reset upon successive Sync operations
    updates = [];

    //Subscribe to progress updates and handle completion
    wixRealtime.subscribe(syncProgressUpdatesChannel, (message, channel) => {

        //Lets deconstruct the payload for better readability/usage
        let { timestamp, progress, text } = message.payload;

        //Push the newly received message to the updates array and then sort by latest timestamp
        updates.push({ timestamp, progress, text });
        updates = updates.sort((a, b) => {
            return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
        });

        // Update the page with the latest progress and message
        const latestUpdate = updates[0];
        $w('#myProgressBar').value = latestUpdate.progress;

        //We're reconstructing the whole log in proper order from latest (at the top) to earliest (at the bottom) to ensure concurrency issues dont cause inconsistencies. With the CollapsibleText element, if the user doesn't click Read More, they'll only see the latest update (or two) at the top. If they click Read More, they'll have the whole log in proper order.
        $w('#myCollapsibleText').text = updates.map(u => `${u.text}`).join("\n"); 

        // Handle completion if progress is 100
        if (latestUpdate.progress === 100) {

            // Update your UI or do whatever. Here I like to leave 3 seconds to view the latest logged messages before switching to a different, completed, state
            setTimeout(() => {
                $w('#myMultiStateBox').changeState('SyncCompleted').then(() => {

                        // This state has a Lottie animation with a checkmark indicating it's done. After another 3 seconds it reverts back to my Initial state where myButton is visible to perform the action again.

                        setTimeout(() => {
                            $w('#myMultiStateBox').changeState('Initial').then(() => {

                                // I'll reenable the button that does this magic for another go
                                $w('#myButton').enable();

                                //Reset the progress and log so it doesn't look too crowded when starting another execution later
                                $w('#myProgressSynchronize').value = 0;
                                $w('#myCollapsibleText').text = "";

                                //Always unsubscribe when no longer needed!
                                wixRealtime.unsubscribe({ channel });
                            });
                        }, 3000);
                });
            }, 3000);
        } else if (latestUpdate.progress === -1) {
            //Here I handle the case where the progress failed and display the Progress Bar in red. Notice I don't update its progress value to leave it stuck to where it failed. I also do not offer any remedies like a retry or back button. Do as you please in your case.
            $w('#myProgressBar').style.foregroundColor = '#FF0000';
        }
    })

    // And finally the actual backend function call for that process. We'll disable the button to avoid multiple executions and transition to the SyncInProgress state where the Progress Bar and Collapsible Text elements are visible
    $w('#myButton').disable().then(() => {
        $w('#myMultiStateBox').changeState('SyncInProgress').then(() => {
            // Ensure the progress bar is back to its original blue color in case an error occurred previously
            $w('#myProgressBar').style.foregroundColor = '#0F62E6';

           //Call the backend function and pass the channel on which we expect to receive progress updates.
            sync(syncProgressUpdatesChannel);
        })
    });
}

Now is time to register the button’s Click event listener in your onReady function:

$w.onReady(async function () {
    $w('#myButton').onClick(handleSync);
});

And voilà! Whatever update or failure or completion your backend wants to report back will be displayed to your user for a better UX!

Side note: Try to space out your updateProgress calls. Doing one every millisecond puts strain on the whole thing and prevents reading updates anyway.

Other side note: In the example above, I specified progress manually at various stages of execution but you can use math to calculate where you’re at if you know how many actions need to be performed. When processing lots of items, I like to report back on every 10 or so items completed like so:

//Suppose I'm midway through the larger process and want to update on progress on handling a large array of data between 25% and 60% completion. The 25 and 60 values are purely arbitrary in my case.
const previousUpdateProgress = 25;
const differenceToNextUpdateProgress = 60 - previousUpdateProgress; //35% of progress will occur during this loop

for (let i = 0; i < myArray.length; i++) {
  //Do whatever you need done
  console.log(myArray[i]);

  //Update the progress every 10 items
  if (j % 10 === 0) {
    updateProgress(channel, previousUpdateProgress+ ((j / myArray.length) * differenceToNextUpdateProgress), `Synced ${j} / ${myArray.length} Items`);
  }
}

Cheers!

2 Likes