Understanding `context.async`

Edit: The event repeating is expected behavior, the same way it goes for loading data from Bubble’s server via .get().

I could definitely use some help here. Doing some tests, my setup is as follow:
Bubble workflows -

Both actions are client side and the code in each action is -

First action:

function(properties, context) {

console.log("first message in sync code under Async with context.async action (first workflow action)");
    
const asyncFunction = async () => {
    let response = await fetch("https:www.goe.com");
    console.log(`second message, now inside async code, the variable is ${response.status} (first workflow action)`);
}

context.async(async callback => {
    try {
        let result = await asyncFunction();
        callback(null, null);
    }
    catch (err) {
        callback(null);
    }
});
	
    
console.log("third message in sync code under Async with context.async action (first workflow action)");

}

Second action:

function(properties, context) {


console.log("first message in sync code under Async withOUT context.async action (second worflow action)");
    
const asyncFunction = async () => {
    let response = await fetch("https:www.goe.com");
    console.log(`second message withOUT, now inside async code, the variable is ${response.status} (second workflow action)`);
}

asyncFunction(); //simply calling it without async.context
    
console.log("third message in sync code under Async withOUT context.async action (second workflow action)");



}

Then the resulting order of events isn’t what I expected. The synchronous code before context.async repeats, it’s the “first message”. Why is this?

Take a look don’t mind the big blue Bubble log:

The second workflow action goes as expected, with synchronous events happening first then the asynchronous one happening after.

@sudsy pinging you because I don’t want to clutter that other thread. Any thoughts on this?
P.S: I used your template on this one.
P.P.S: It’s supposed to 404, not trying to actually fetch anything.

Link to the app here and to the editor here.

1 Like

Hmm, interesting. Not at my computer, but what’s the output if the asyncFunction() definition comes first (is at the top)?

Makes no difference; nor did I expect it to. It was just a shot in the dark. Anyway, I don’t really understand the double-output behavior. :thinking:

Anyone?

Aha! It was being double-awaited! :slightly_smiling_face:

EDIT: Ok, scratch that. The double output happens even if the fetch is directly inside the callback. I’m back to not understanding the behavior. :confused:

console.log("first message in sync code under Async with context.async action (first workflow action)");

context.async( async callback => {
    try {
        let response = await fetch("https:www.goe.com");
        console.log(`second message, now inside async code, the variable is ${response.status} (first workflow action)`);
        callback(null, null);
    }
    catch (err) {
        callback(null);
    }
});

console.log("third message in sync code under Async with context.async action (first workflow action)");

The above still repeats the first message.

Hey @vini_brito , i’m hoping you or someone else can help me out here

in the following code


  function toDataURL(src, callback, outputFormat) {
  let image = new Image();
  image.crossOrigin = 'Anonymous';
  image.onload = function () {
    let canvas = document.createElement('canvas');
    let ctx = canvas.getContext('2d');
    let dataURL;
    canvas.height = this.naturalHeight;
    canvas.width = this.naturalWidth;
    ctx.drawImage(this, 0, 0);
    dataURL = canvas.toDataURL(outputFormat);
    callback(dataURL);
      
    context.async(async callback => {
    try {
         dataURL = await canvas.toDataURL(outputFormat);
    	callback(dataURL);
        callback(null, null);
    }
    catch (err) {
        callback(null);
    }
	});  
      

      
  };
  image.src = src;
  if (image.complete || image.complete === undefined) {
    image.src = "data:image/gif;base64, R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
    image.src = src;
  }
}
    
toDataURL('https://www.gravatar.com/avatar/0c6523b4d3f60hj548962bfrg2350',
  function (dataUrl) {
    console.log('RESULT:', dataUrl)
    
   instance.data.pic = dataUrl
    
  }
)

i am trying to turn an image URL into a base64 image. I need this to occur before any other code in my plugin. currently, this runs asynchronously or at least it seems that way. Do i construct this in a different manner to hold up the code?

Any help would be greatly appreciated!

Ah, I faced that challenge right when I started making plugins, and I used that exact same script to work with that. Nostalgic :yum:
The solution I used back then was rather convoluted, it involved promises, capturing their “resolve” function into an instance.data.resolve function, resolving them at the last workflow action etc etc not pretty.

Try placing the entire function inside the context.async and awaiting all asynchronous operations. It’s not going to be fast or easy to do this change.

A much faster way but more cumbersome to the end user (possibly not even usable depending on the whole flow) would be to make the image loading a single workflow action, then it publishes the base64 in a state and triggers an event when it’s ready.

Another option would be to find another way to do the conversion with a code that you can better adapt to the “context.async” structure.
Keep in mind that context.async will cache the request done, so if you run it multiple times it will always bring the same image, faking that the subsequent requests were done.

That makes sense. I was thinking the same about adding a workflow step! I will likely take this approach first.

For now, I will just make everyone enter their own custom background with a base64 image code! :rofl:

if i see some $ i can put in better work! :rofl: :rofl:

Yeah, but think where would they get that base64 from… if it’s a server side action, they can just use file or image:converted to base64 Bubble operator.
If it’s a client side action it would maybe require an extra step, which is the server side action I just mentioned.

1 Like

Hi, you mentioned that “context.async will cache the request done”. Were you able to find any solution to get around the caching problem?

Yep! I have something ready to be shared about it, including, it’s an excerpt from my upcoming plugin making training material (you can join its waitlist if you want to be super powerful :smile:).

In short, you use traditional code. Here it goes:

…Alternatively, if you’re working on the browser, which means a client side action, you won’t use the context.async function anymore. It’s available there, but it will break your flow. Here’s how you do in the browser:

// declaring it

const requestToGetPlainText = async (fileUrl) => {

let response = await fetch(fileUrl);
let plainText = await response.text();

instance.publishState("attained_text", plainText)
instance.triggerEvent("Text is available")

}
// calling it
let myPlainTextValue = requestToGetPlainText("https://example.com/myFile.csv")

Noticed that I not only published the value to a state, but also triggered an event?
Because that’s how we can make sure that everything from this workflow is finished and will be available in the next workflow when we use async operations in the client side.

In other words: By triggering an event in the end of the async function.

Then, the next workflow that is ran under the event “Text available”, will have access to whatever text value you attained from this web request.


Let me know if this helped!

5 Likes

That worked! Thanks for the detailed explanation. Just signed up for the plugin making training material… looking forward to it!

1 Like

Here almost a year later, but I did notice that context.async seems to be working now.

Only bumping this incase someone did want to use it; I’d recommend declaring a function within the larger run_server function:

function(properties, context) {
//...
function myAsyncFunction() {
  return context.async(async callback => {
            try {
                let return_var = "never set in code";
                // ...
                // do your processing here
                // ...
                return_var = "something_here"; // update return var
                callback(null, return_var);
            }
            catch (err) {
                callback(err);
            }
        });
}
//...
}

The reason for this is so that you can control when to call the function. The traditional method @vini_brito mentions is probably still good to, but I went with context.async because I wanted to be sure it would work on Bubble’s backend workflows.

3 Likes