JavaScript tips for using the SAFE API

Actually I was going to ask people here why we aren’t using ES6/ES7/ESNext whatever they’re calling it these days.

Since we’re using the beaker browser anyway you wouldn’t even need to transpile it, just use it directly on the site.

For example your code would change to:

async function sendMessage() {
  try {
    const mutationHandle = await window.safeMutableData.newMutation(auth)

    const date = new Date()
    const time = date.getTime()

    await window.safeMutableDataMutation.insert(mutationHandle, time.toString(), textarea.value)

    await window.safeMutableData.applyEntriesMutation(mdHandle, mutationHandle)

    Materialize.toast('Message has been sent to the network', 3000, 'rounded')
    window.safeMutableDataMutation.free(mutationHandle)
    getMutableDataHandle('getMessages')

    textarea.value = ''
  } catch (err) {
    console.log(err)
  }
}
10 Likes

Thank you, that looks a lot cleaner and is probably much better practice, I am still learning JavaScript and this will be a great help for me as a reference.

Also @DavidMtl this style of coding looks to me like chained as well as being very readable and clean.

1 Like

Yep, I wrap my calls within another function.

Now that is some clean code. Is that working with the current API?

I agree, it’s fine this way and if we can use the example of @markwylde, that would be golden.

2 Likes

Yeah it’s a native javascript feature now. You can use it on most browsers:
https://caniuse.com/#feat=async-functions

Normally in web development you would use something like Babel or TypeScript that would convert your code back to ES5 for older browsers. But in the case of MaidSafe we can only use the beaker browser (please correct me if I’m wrong). The beaker browser uses chromium (Google Chrome) which has supported this for the last few months.

I’ve been playing around with MaidSafe the last few days and async functions have been working fine natively for me.

7 Likes

Wow, I’m changing all my code to use async functions. Check this out.

Before

self.add_entry = function(container_name, key, value) {
        let mutation_handle;
        let md_handle;
        return window.safeApp.getContainer(self.app_handle, container_name)
        .then((md_handle) => {
            return window.safeMutableData.newMutation(md_handle)
            .then((mutation_handle) => {
                window.safeMutableDataMutation.insert(mutation_handle, key, value)
                .then(_ => window.safeMutableData.applyEntriesMutation(md_handle, mutation_handle))
                .then(_ => {
                    console.log("New entry added:" + key + ":" + value);
                    window.safeMutableData.free(mutation_handle);
                    window.safeMutableData.free(md_handle);
                });
            });
        });
    }

After

self.add_entry = async function(container_name, key, value) {

    let md_handle = await window.safeApp.getContainer(self.app_handle, container_name);
    let mutation_handle = await window.safeMutableData.newMutation(md_handle);

    await window.safeMutableDataMutation.insert(mutation_handle, key, value);
    await window.safeMutableData.applyEntriesMutation(md_handle, mutation_handle));

    console.log("New entry added:" + key + ":" + value);
    window.safeMutableData.free(md_handle);
    window.safeMutableData.free(mutation_handle);
}

It is so much better with async. Now @bochaco, you guys need to rewrite all doc and all apps using async functions, before alpha 2 :smiley: Kidding, but it is so much more readable. Thanks for the tip @markwylde!

7 Likes

Hey @markwylde, quick question:

How would you convert an API call like window.SafeMutableDataEntries.forEach? Because it’s supposed to iterate over all the entries (key/value tuples), I’m not sure how to use await with it.

Example from the docs:

window.safeMutableData.getEntries(mdHandle)
   .then((entriesHandle) => window.safeMutableDataEntries.forEach(entriesHandle, (k, v) => {
         console.log('Key: ', k.toString());
         console.log('Value: ', v.buf.toString());
         console.log('Version: ', v.version);
      }).then(_ => console.log('Iteration finished'))
   );
1 Like

@am2on

There’s a couple of ways to do this. Remember you’re still working with promises so the functions you’re running will not (well, may not) resolve immediately.

First if you need your promises to run one after the other (ie, wait for the last one to finish before continuing) you can just use for/of.

function timesBy1000 (num) {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      resolve(num * 1000)
    }, 100)
  })
}

async function main () {
  let nums = [1,2,3,4,5]

  // Testing parallel using for/of
  console.log('start parallel for/of')
  for (let num of nums) {
    let result = await timesBy1000 (num)
    console.log(result)
  }
  console.log('finish parallel')
}
main()

But normally you’ll want to run them all at the same time then work on the results once they’ve all finished. The function in a map (and forEach too actually) returns a promise. You can collect these promises into an array, then wait for them all to finish using Promise.all.

function timesBy1000 (num) {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      resolve(num * 1000)
    }, 100)
  })
}
async function main () {
  let nums = [1,2,3,4,5]

  // Testing parallel using map
  console.log('start parallel map')
  const promises = nums.map((num) => {
      return timesBy1000(num)
  })
  const result = await Promise.all(promises)
  console.log(result)
  console.log('finish parallel forEach')
  
}
main()

There’s no reason you can’t use forEach too, but I’m not really sure what use case you would want to use that. Basically just remember the returned value on the forEach function is not the result but the promise.

Play with my examples

Oh and sorry to actually answer your question, the following code should work for you:

async function main () {
  try {
    const entriesHandle = await window.safeMutableData.getEntries(mdHandle)

    const promises = window.safeMutableDataEntries.map((k, v) => {
       console.log('Key: ', k.toString());
       console.log('Value: ', v.buf.toString());
       console.log('Version: ', v.version);
       return v
    })

    const exampleResults = await Promise.all(promises)
    console.log(exampleResults) // returns a list of DataEntry values

    console.log('Iteration finished')
  } catch (err) {
    console.log('Make sure to handle errors')
    console.log(err)
    throw err
  }
}
main()

Some notes about what I did to your code:

  1. While this will work in the future, top level code can not use await, so I wrapped your code into an async function called main. I believe this shows the code’s actually been written, it’ll just take a while to be merged upstream into chrome/node then beaker.

  2. Note I am returning from inside the map function. This allows you to collect any value and get them in a nice array after running Promise.all

  3. Always handle errors. Node (for some reason I have no idea why) won’t handle errors inside promises. They’ll just disappear into the void so always wrap your functions in a try/catch for you can get better information on errors.

Thanks @happybeing for the spelling check

6 Likes

@markwylde as somebody who went from using JS years ago when there were no frameworks, just a cut down version of Java in IE, and returned years later like Rip van Winkle to find there was a mountain range to climb, with strange but beautiful and difficult peaks like jQuery to climb, and new space age tools like Promises, and cryptic signs to follow such as ‘_’ ‘=>’ …

… I really appreciate you taking the time to guide us through this new and exciting landscape. Thank you :slight_smile:

I shall be having a go at this later. Maybe we should split this into a new topic: “JavaScript tips for using the SAFE API”, what do you think?

[PS possible typo at ‘be’ in 1 above?]

4 Likes

@happybeing yeah the language has really matured a lot since when we first started using it for simple animation and show/hide in the browser.

I really like the idea of sharing some JavaScript tips. Since beaker already has most of the modern ES6 features built in we can really benefit from it. I’m actually in the process of making the Safe API playground use ES6, but with time restraints it might take a while. I’ll try and tidy up what I’ve done and push it to github.

2 Likes

@moderators Assuming the posters agree (shout if not!) I suggest splitting into a new topic “JavaScript tips for using the SAFE API” from the following post:

https://forum.safedev.org/t/dumb-dom-api-questions/813/75?u=happybeing

2 Likes

This style of handling Promises should be used in the examples in the documentation. Using async/await will be the way to go. Especially because the target environment supports it natively (it did not until about a month or two ago), which is awesome.

1 Like

I have started this for the API playground:
https://github.com/markwylde/safe_examples/tree/start-using-es6

7 Likes

This article has some examples on avoiding the promise nesting. I’ll copy the relevant parts from the article below:

Often times, one promise will depend on another, but we’ll want the output of both promises. For instance:

getUserByName('nolan').then(function (user) {
  return getUserAccountById(user.id);
}).then(function (userAccount) {
  // dangit, I need the "user" object too!
});

Wanting to be good JavaScript developers and avoid the pyramid of doom, we might just store the user object in a higher-scoped variable:

var user;
getUserByName('nolan').then(function (result) {
  user = result;
  return getUserAccountById(user.id);
}).then(function (userAccount) {
  // okay, I have both the "user" and the "userAccount"
});

This works, but I personally find it a bit kludgey. My recommended strategy: just let go of your preconceptions and embrace the pyramid:

getUserByName('nolan').then(function (user) {
  return getUserAccountById(user.id).then(function (userAccount) {
    // okay, I have both the "user" and the "userAccount"
  });
});

…at least, temporarily. If the indentation ever becomes an issue, then you can do what JavaScript developers have been doing since time immemorial, and extract the function into a named function:

function onGetUserAndUserAccount(user, userAccount) {
  return doSomething(user, userAccount);
}

function onGetUser(user) {
  return getUserAccountById(user.id).then(function (userAccount) {
    return onGetUserAndUserAccount(user, userAccount);
  });
}

getUserByName('nolan')
  .then(onGetUser)
  .then(function () {
  // at this point, doSomething() is done, and we are back to indentation 0
});

As your promise code starts to get more complex, you may find yourself extracting more and more functions into named functions. I find this leads to very aesthetically-pleasing code, which might look like this:

putYourRightFootIn()
  .then(putYourRightFootOut)
  .then(putYourRightFootIn)  
  .then(shakeItAllAbout);

That’s what promises are all about.

8 Likes

Wow! Great thread. @markwylde Thank you for the education.

Async / await is really pleasant on the eyes.

UPDATE:
Writing tests for beaker-plugin-safe-app and using async / await is making life so much easier.
Should be really nice for people reading tests.

3 Likes

Thank you so much for putting in the time to write up that great explanation! I’ll definitely have to play around more with this. Promises in general were new to me when I first started learning the SAFE APIs, and they felt very unintuitive. The async/await format makes a bit more sense.

Is this line correct? You just obtained entriesHandle which is never used, and thtere’s no such function as window.safeMutableDataEntries.map() so I’m confused here!

EDIT… and I’m trying to use await and it is causing a syntax error right away. It may be due to something I’m not aware of in the setup of the codebase I’m working on, but I think I’m going to have to live without it. For example, having obtained the mutable data handle (mdRoot) for the public container , the following is a syntax error:

        const rootVersion = await window.safeMutableData.getVersion(mdRoot);

… and below Mark explains this is because you can only use await inside an async function! Thank you :slight_smile:

2 Likes

Woops @happybeing you are absolutely correct. There is no map function. I will attempt to create a map function and put a PR in tonight. I will admit I just presumed the function was an iterable.

So looking here I can see the function accepts a third argument which is a promise that resolves when the iteration is finished.

In this case the code below should work. Sorry I don’t have my MaidSafe environment to test on at the moment.

window.safeMutableDataEntries
  .forEach(entriesHandle, (k, v) => {
    console.log('Key: ', k.toString());
    console.log('Value: ', v.buf.toString());
    console.log('Version: ', v.version);
    return promises.push(v)
  }, function () {
    console.log('Iteration finished')
  })

I think I can possible create a map function that works a bit better. I’ll have a go tonight and see how far I can get.

@happybeing you can (unfortunately) only use an await function inside an async function.

This will not work as it’s outside of an async function

const result = await someFunction()

This will work as it’s outside of an async function

async function main() {
  const result = await someFunction()
}

Note the async keyword before function.

3 Likes

So I should learn to read :stuck_out_tongue: window.safeMutableDataEntries.forEach actually returns the promise. So we should be able to await on the forEach function anyway.


await window.safeMutableDataEntries
  .forEach(entriesHandle, (k, v) => {
    console.log('Key: ', k.toString());
    console.log('Value: ', v.buf.toString());
    console.log('Version: ', v.version);
    return promises.push(v)
  })

4 Likes

p-progress - a handy promise library: “Useful for reporting progress to the user during long-running async operations.”

@markwylde thank you very much, just finished updating listy and chaty’s code to be more readable:
Before

function sendMessage() {
  window.safeMutableData.newMutation(auth)
    .then((mutationHandle) => {
      var date = new Date();
      var time = date.getTime();
      window.safeMutableDataMutation.insert(mutationHandle, time.toString(), textarea.value)
        .then(_ =>
          window.safeMutableData.applyEntriesMutation(mdHandle, mutationHandle))
        .then(_ => {
          Materialize.toast('Message has been sent to the network', 3000, 'rounded');
          window.safeMutableDataMutation.free(mutationHandle);
          getMutableDataHandle("getMessages");
        });
      textarea.value = "";

    });
}

and after

async function sendMessage() {
  try {
    let time = new Date().getTime().toString();

    let chatyHash = await window.safeCrypto.sha3Hash(auth, "chaty");
    let chatyHandle = await window.safeMutableData.newPublic(auth, chatyHash, 54321);
    let mutationHandle = await window.safeMutableData.newMutation(auth);
    await window.safeMutableDataMutation.insert(mutationHandle, time, textarea.value);
    await window.safeMutableData.applyEntriesMutation(chatyHandle, mutationHandle);

    Materialize.toast('Message has been sent to the network', 3000, 'rounded');
    window.safeMutableDataMutation.free(mutationHandle);
    window.safeMutableData.free(chatyHandle);

    getMessages();
    textarea.value = '';
  } catch (err) {
    console.log(err);
  }
}
4 Likes