I'm trying to learn exactly how to oversee the order in which my script runs using asynchronous functions and promises. At this point I've read/watched many information resources to try to get this right but something about my code just isn't working:
I have a generic vue.js object containing my methods, and running the series of functions asynchronously when the app is mounted:
var testAPP = new Vue({
el: '#test-app',
data: {
},
methods: {
fake_fetch: function () {
return new Promise((resolve) => {
setTimeout(() => {console.log("Returning fetch results."); resolve("good boy!")}, 10000)
})
},
first_step: function () {
console.log('Playing with my puppy!')
this.fake_fetch()
.then((data) => {
let praise = "Buddy is a " + data
console.log(praise)
return ("nap.")
})
.then((data) => {
return new Promise((resolve) => {
setTimeout(() => {console.log("Phew! We are taking a " + data); resolve(true)}, 20000)
})
})
},
second_step: function () {
console.log('Baking some cookies!')
this.fake_fetch()
.then((data) => {
data = data.replace(" ", ", ")
let praise = "Oh man these are " + data
console.log(praise)
return ("nap.")
})
.then((data) => {
return new Promise((resolve) => {
setTimeout(() => {console.log("I'm so full, I'm taking a " + data); resolve(true)}, 20000)
})
})
},
third_step: function () {
console.log('Putting out a hit on Polly')
this.fake_fetch()
.then((data) => {
let praise = "The job was a success? You did " + data
console.log(praise)
return ("nap.")
})
.then((data) => {
return new Promise((resolve) => {
setTimeout(() => {console.log("A moment of silence for Polly as he takes a dirt " + data); resolve(true)}, 20000)
})
})
},
get_started: function () {
v = this
async function my_day(v) {
const task_one = await v.first_step()
const task_two = await v.second_step()
const task_three = await v.third_step()
return ([task_one, task_two, task_three])
}
my_day(v).then((data) => {
if (false in data) {
return ("Something went wrong.")
} else {
return ("My day is done, time to rest... again.")
}
})
},
},
mounted: function () {
this.get_started()
}
})
The result I expect to get based on the order I 'thought' would be correct is this:
Playing with my puppy!
Returning fetch results.
Buddy is a good boy!
Phew! We are taking a nap.
Baking some cookies!
Returning fetch results.
Oh man these are good, boy!
I'm so full, I'm taking a nap.
Putting out a hit on Polly
Returning fetch results.
The job was a success? You did good boy!
A moment of silence for Polly as he takes a dirt nap.
My day is done, time to rest... again.
Ignoring the silliness of the output statements, thats the order they should appear. My current code gives me this:
Playing with my puppy!
Baking some cookies!
Putting out a hit on Polly
My day is done, time to rest... again.
Returning fetch results.
Buddy is a good boy!
Returning fetch results.
Oh man these are good, boy!
Returning fetch results.
The job was a success? You did good boy!
Phew! We are taking a nap.
I'm so full, I'm taking a nap.
A moment of silence for Polly as he takes a dirt nap.
The first four lines come up right away which doesn't feel right as there should be my built in delays stalling each 'step' function before the next one fires.
I've tried many things from making 'my_day()' its own function within methods and calling it via this.my_day() but that didn't make a difference. I've tried playing around with different return statements on the promises as well as making each following step function dependent on the variable of its predecessor but these resulted in the same functionality.
Why are my functions starting at the same time and not following the 'order' I have built into my async function?
Thank you for any help!
Also the html I used to run this in Edge is simple, I was just interested in the console:
<!DOCTYPE html>
<script src="https://unpkg.com/vue#2.6.10/dist/vue.min.js"></script>
<script type="text/javascript" src="C:\Users\adarwoo\Documents\Python Scripts\JS\async2.js"></script>
You are using await, but there is no async
first_step: async function() { // <-- async needed
console.log('Playing with my puppy!')
return this.fake_fetch() // <-- return needed
.then((data) => {
let praise = "Buddy is a " + data
console.log(praise)
return ("nap.")
})
.then((data) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Phew! We are taking a " + data);
resolve(true)
}, 20000)
})
})
}
Related
how can I edit the same message multiple times with a delay...?
Here is what i tried... :
message.channel.send(`a`)
.then(message => {
setTimeout( () => {
message.edit(`ab`)
},1000);
})
.then(message => {
setTimeout( () => {
message.edit(`abc`)
},1000);
})
.then(message => {
setTimeout( () => {
message.edit(`abcd`)
},1000);
})
.then(message => {
setTimeout( () => {
message.edit(`abcde`)
},1000);
});
smth like this should work, but if i try this one the error is: of course Cannot read property 'edit' of undefined (the error is at the "message.edit (abc)" part.)
The problem is that there is nothing is passed into the second .then
The value passed to the next .then is always the return value of the previous .then
For example:
Promise.resolve('foo').then(value => {
console.log(value)
return 'abc'
}).then(console.log)
will fist log foo and then abc
The easiest solution I can think of is to increase the setTimeout delay by 1000 each time
message.channel.send('a').then(message => {
setTimeout(() => message.edit('ab'), 1000)
setTimeout(() => message.edit('abc'), 2000)
setTimeout(() => message.edit('abcd'), 3000)
setTimeout(() => message.edit('abcde'), 4000)
})
If you wanted this to be more general you could do something like this:
function changeMessage(delay, messages, message) {
for(i in messages) {
setTimeout(() => message.edit(messages[i]), delay * i)
}
}
message.channel.send('a').then(message => changeMessage(1000, ['ab', 'abc', 'abcd', 'abcde'], message))
The function changeMessage takes in a delay time, an array of messages, and the actual message object you want to edit, and just edits the message to each new string in the array of messages every x seconds
This problem has a nice syntactic solution, but oftentimes it's easier to use async/await if you want to run a bunch of Promises sequentially
How do I write the login and get functions in javascript? I have a feeling that it's possible with some mixture of inline functions, bind and this magic. Or is it impossible?
Promise.all([
login("user1", () => {
console.log(get("/healthy")); // prints "user1/healthy"
}),
login("user2", () => {
console.log(get("/ready")); // prints "user2/ready"
})
]);
I know it would be possible to write it like this. But I got curious about writing it without the obj.
login("user1", (obj) => {
obj.get("/ready");
});
Isn't this similar to how Jest have coded the descript/it pattern?
describe("Login test", () => {
test("Login", async () => {
expect("ready").toEqual("ready");
});
});
So you can technically make it work, but I don't recommend for reasons I'll explain later.
Here is a working example with the get function as a local variable. We assign to this get variable immediately before the callback is called.
It holds the login context in its closure scope. Because JavaScript is single threaded we know that the variable cannot be re-assigned a second time before the callback is run.
Here you can see it working with a random timeout to simulate a http call. The users and urls will be pair up correctly even though they execute async and in a random order. (Try running this snippet multiple times to checkout the output is always consistent.)
const sleep = () =>
new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));
let get;
async function login(username, callback) {
console.log("logging in as", username);
await sleep();
get = async(url) => {
await sleep();
return `/${username}${url}`;
};
callback();
}
Promise.all([
login("Alice", async() => {
console.log(await get("/Active"));
}),
login("Bob", async() => {
console.log(await get("/Build"));
}),
login("Colin", async() => {
console.log(await get("/Compile"));
}),
]);
The reason I don't recommend, is because this is very fragile code. We have to be very careful to make sure that the get function is called only at the start of the callback.
If for example we call sleep then get, all bets are off. We won't know which context get is using.
const sleep = () =>
new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));
let get;
async function login(username, callback) {
console.log("logging in as", username);
await sleep();
get = async(url) => {
await sleep();
return `/${username}${url}`;
};
callback();
}
Promise.all([
login("Alice", async() => {
await sleep(); // <-- The only change from the code above. DANGER
console.log(await get("/Active"));
}),
login("Bob", async() => {
await sleep();
console.log(await get("/Build"));
}),
login("Colin", async() => {
await sleep();
console.log(await get("/Compile"));
}),
]);
So while this is all very interesting and fun to code, I believe the best option is just to be explicit about the obj context you are using (as you already described in your question) and save yourself a headache.
UPDATE
Combining both solutions below, I wrote:
const startMusic = async () => {
let currentSong
let songPath
const songArray = [
{ path: require("../assets/sounds/Katsu.mp3"), song: mainTheme },
{ path: require("../assets/sounds/MainTheme2.mp3"), song: mainTheme2 },
{ path: require("../assets/sounds/MainTheme3.mp3"), song: mainTheme3 },
]
for (var i = 0; i < songArray.length; i++) {
currentSong = songArray[i].song
songPath = songArray[i].path
try {
await currentSong.loadAsync(songPath)
await currentSong.playAsync()
// setSoundObject(currentSong)
console.log("Music will start")
return new Promise(resolve => {
currentSong.setOnPlaybackStatusUpdate(playbackStatus => {
if (playbackStatus.didJustFinish) {
console.log("Just finished playing")
resolve()
}
})
})
} catch (error) {
console.log(`Error: ${error}`)
return
}
}
}
This actually plays the song, and the console logs occur on time ("Just finished playing" happens exactly when the song ends)
I'm trying to figure out how to play the next song.. and how will it know when it has reached the final song?
return new Promise(resolve => {
currentSong.setOnPlaybackStatusUpdate(playbackStatus => {
if (playbackStatus.didJustFinish) {
console.log("Just finished playing")
resolve()
}
})
}).then(() => console.log("Next song?"))
Figured how where to put the .then to get it to console log right after "Just finished playing" I'm just trying to see how to actually put the next song there
(then of course, telling it when to go back to the first song in the array)
Original Post
Working on an assignment for a react native app using expo-av library for Sound files.
Right now, the app has a startMusic function set in a Context file that is responsible for playing the app's background music. It only has one song for now:
const startMusic = async () => {
try {
await mainTheme.loadAsync(require("../assets/sounds/Katsu.mp3"))
await mainTheme.playAsync()
setSoundObject(mainTheme)
console.log("The first song is playing! Enjoy!")
} catch (error) {
console.log(`Couldnt load main theme: ${error}`)
return
}
}
It is used in the homescreen component's file like so:
const { startMusic } = useContext(MusicContext)
useEffect(() => {
startMusic()
}, [])
For the second song, I wrote another const in the MusicContext file:
const secondSong = async () => {
try {
await mainTheme2.loadAsync(require("../assets/sounds/MainTheme2.mp3"))
await mainTheme2.playAsync()
setSoundObject(mainTheme2)
console.log("Now playing the second track. Enjoy!")
} catch (error) {
console.log(`Could not play the second song: ${error}`)
return
}
}
Annnnnd… here is where my trouble lies. I know this wasn't gonna work but I wrote this in the component file to try to get the second song playing after the first song
useEffect(() => {
startMusic()
.then(secondSong())
}, [])
I know there's more to it than that but I'm having trouble.
Problem with your code is not just running one function after another (that would be as simple as startMusic().then(() => secondSong()) but still won't solve the problem), but the fact that your functions actually don't wait for a song to finish playing before resolving
You expect this line await mainTheme.playAsync() to pause function execution until the song has finished, but what it in fact does according to docs https://docs.expo.io/versions/latest/sdk/av/ is exactly only starting the playback (without waiting for it to finish)
With that being said, you need to determine the moment your playback finishes, then create a Promise that will only resolve after the playback is finished so that your second song can only start after the first
In the simplest form without error handling and such, it can look like this
const startAndWaitForCompletion = async () => {
try {
await mainTheme.loadAsync(require('../assets/sounds/Katsu.mp3'))
await mainTheme.playAsync()
console.log('will start playing soon')
return new Promise((resolve) => {
mainTheme.setOnPlaybackStatusUpdate(playbackStatus => {
if (playbackStatus.didJustFinish) {
console.log('finished playing')
resolve()
}
}
})
} catch (error) {
console.log('error', error)
}
}
the trick is of course the .setOnPlaybackStatusUpdate listener that will be called every so often with playback status, and by analyzing the status you can tell the song has finished playing. If you scroll to the bottom of the page I linked you will find other examples with status update
updated
const startAndWaitForCompletion = async (playbackObject, file) => {
try {
await playbackObject.loadAsync(file)
await playbackObject.playAsync()
console.log('will start playing soon')
return new Promise((resolve) => {
playbackObject.setOnPlaybackStatusUpdate(playbackStatus => {
if (playbackStatus.didJustFinish) {
console.log('finished playing')
resolve()
}
}
})
} catch (error) {
console.log('error', error)
}
}
////
const songs = [
{ path: require('../assets/sounds/Katsu.mp3'), song: mainTheme },
{ path: require('../assets/sounds/MainTheme2.mp3'), song: mainTheme2 },
{ path: require('../assets/sounds/MainTheme3.mp3'), song: mainTheme3 },
]
useEffect(() => {
(async () => {
for (let i = 0; i < songs.length; i++) {
await startAndWaitForCompletion(songs[i].song, songs[i].path)
}
})()
}, [])
I think you need to rethink this problem/solution to be more abstract.
Instead of making a new const and promise for every single song you want to play (which, as you said, isn't workable, and isn't scalable, like say if you wanted 10 songs instead of 2), make "startMusic" a function that plays an array of songs (each array index being a filepath to an MP3, like in your example), and
then resolve/reject the promise as needed.
A quick "startMusic" rewrite:
const startMusic(songArray) = async () => {
for (var songIndex in songArray) {
try {
await mainTheme.loadAsync(require(songArray[songIndex]))
await mainTheme.playAsync()
setSoundObject(mainTheme)
console.log("Song #", songIndex, "of ", songArray.length " is playing. Enjoy!")
} catch (error) {
console.log(`Couldnt load song: ${error}`)
return
}
}
}
A "Promise.all" chain could be useful here, too, if the above for-try-catch structure doesn't work: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
Not familiar with the expo-av library, so there might be some specific quirks to look out for there, but I think re-writing "startMusic" to be an abstract function that plays an array of "N" songs is a much more optimal approach, and will minimize/eliminate your problems.
.then() accepts a function but you've provided the result of function execution by calling secondSong.
Do:
useEffect(() => {
startMusic()
.then(() => secondSong())
}, [])
Or just get rid of () after secondSong:
useEffect(() => {
startMusic()
.then(secondSong)
}, [])
I've been stuck working on this exercise FOR AGES, I'm finally throwing in the towel and asking for some help.
Make an AJAX call to the Star Wars API [https://swapi.co/] and get the opening crawl for each film in the series. Once you have finished that, loop through the array of planets for each movie and make more AJAX calls to collect the name of each planet, organized by film. Then, console log an array of objects in which each object contains the opening crawl for a specific movie, along with the names of every planet featured in that movie.
I've read a few articles and watched a few videos on Asynchronous js, and I 'think?', I sorta get it. This really doesn't want to work though.
var promiseList = [];
var fetchArray = function(arr) {
promiseArr = arr
.map(url => fetch(url)
.then(res => res.json())
.then(planet => planet.name)
);
Promise.all(promiseArr)
.then(data => console.log(data));
}
// fetchArray doesn't work at all. Curious if it's possible to run this code as it's technicall synchronous (idk).
for (let number = 1; number < 8; number ++) {
var t = fetch(`https://swapi.co/api/films/${number}/`)
.then(res => res.json())
promiseList.push(t)
}
console.log("NAHANH", promiseList)
// Prints out [[object Promise] { ... }, [object Promise] { ... }, [object Promise] { ... }, [object Promise] { ... },
[object Promise] { ... }, [object Promise] { ... }, [object Promise] { ... }]
Promise.all(promiseList)
.then(films => films.map(film => ({
"title": film.title,
"planets": film.planets,
"opening_crawl": film.opening_crawl,
})))
// Works up untill this point, this next then doesn't work, my aim is to alter the film.plants property in every
array object and actually fetch the planet rather than just the url!
// An example print out would be...
// {
// opening_crawl: "Luke Skywalker has vanis...",
// planets: ["https://swapi.co/api/planets/61/"],
// title: "The Force Awakens"
// }]
.then(films => films.map(film => {
film.planets = film.planets
.map(url => fetch(url)
.then(res => res.json())
.then(planet => planet.name)
.catch(error => console.log(error.message))
);
}
.then(data => console.log(data))
// Which would then finally resolve to (this is what I want but cannot get at the moment!)
// {
// opening_crawl: "Luke Skywalker has vanis...",
// planets: ["Yavin IV"],
// title: "The Force Awakens"
// }]
It almost works I can return an object. The fetch arrays doesn't work at all. and my second alteration attempting to retrieve the planet name doesn't work.
First, rename function fetchArray as fetchPlanetNames, as that's what it does, and add a couple of missing returns. Then you have a worker function that makes the main code block much simpler ( as was your intention :-) ).
In the main code block, fetchPlanetNames(...) returns Promise, not Array, therefore, inside the films.map() functor, you need fetchPlanetNames(film.planets).then(planets => {/* compose the required film object here */}). It's a kind of "inside-out" version of what you tried.
var fetchPlanetNames = function(arr) {
return Promise.all(arr.map(url => { // return the Promise returned by Promise.all(...).then(...)
// ^^^^^^
return fetch(url)
.then(res => res.json())
.then(planet => planet.name);
}))
.then(data => {
console.log(data);
return data; // Remember to return `data`, otherwise undefined will be dlivered.
// ^^^^^^^^^^^^
});
};
var promiseList = [];
for (let number = 1; number < 8; number ++) {
promiseList.push(fetch(`https://swapi.co/api/films/${number}/`).then(res => res.json()));
}
Promise.all(promiseList)
.then(films => {
return Promise.all(films.map(film => {
// Here, a nested Promise chain allows `film` to remain in scope at the point where 'planets' become available.
return fetchPlanetNames(film.planets)
.then(planets => {
return {
'title': film.title,
'planets': planets,
'opening_crawl': film.opening_crawl
};
});
}));
})
.then(data => console.log(data));
I'm trying to extend some existing code with additional promises, but they are a new topic for me at the moment and i'm obviously missing something. This is running as part of a build scrip for npm.
All i am currently trying to make happen is for the final then to be called after the pack operation has happened for each architecture. I have tried wrapping it in a
return new Promise
But at the moment i am not returning anything from that function so i'm not sure what i should include in the resolve call at the end. If i just call the resolve with a true nothing happens, and wrapping it in a promise seems to cause the function to not actually run, and no errors are caught anywhere?
I'm guessing i am going about this completely wrong, all i want to achieve is to run another function once the previous one has completed?
Here's the code as it stands with the additional .then that i can't get to be called.
function build(cfg) {
return new Promise((resolve, reject) => {
webpack(cfg, (err, stats) => {
if (err) return reject(err);
resolve(stats);
});
});
}
function startPack() {
console.log('start pack...');
build(electronCfg)
.then(() => build(cfg))
.then(() => del('release'))
.then(paths => {
if (shouldBuildAll) {
// build for all platforms
const archs = ['ia32', 'x64'];
const platforms = ['linux', 'win32', 'darwin'];
platforms.forEach(plat => {
archs.forEach(arch => {
pack(plat, arch, log(plat, arch));
});
});
} else {
// build for current platform only
pack(os.platform(), os.arch(), log(os.platform(), os.arch()));
}
})
.then(() => {
console.log('then!');
})
.catch(err => {
console.error(err);
});
}
function pack(plat, arch, cb) {
// there is no darwin ia32 electron
if (plat === 'darwin' && arch === 'ia32') return;
const iconObj = {
icon: DEFAULT_OPTS.icon + (() => {
let extension = '.png';
if (plat === 'darwin') {
extension = '.icns';
} else if (plat === 'win32') {
extension = '.ico';
}
return extension;
})()
};
const opts = Object.assign({}, DEFAULT_OPTS, iconObj, {
platform: plat,
arch,
prune: true,
'app-version': pkg.version || DEFAULT_OPTS.version,
out: `release/${plat}-${arch}`,
'osx-sign': true
});
packager(opts, cb);
}
You didn't say what log is, but if it's a plain logging function, then it looks like you're passing in undefined (the result from calling log(...)) as the cb argument to pack. Perhaps you meant:
pack(plat, arch, () => log(plat, arch));
In any case, this won't do anything to wait for packing to finish. I don't know why you're not seeing any console output, but if you're looking for this output to happen after all the packing has finished, then you need to wrap packager in a promise. Something like:
var pack = (plat, arch) => new Promise(resolve => {
// ...
packager(opts, resolve);
});
And then use Promise.all instead of forEach to do all the packaging (in parallel if that's OK):
.then(paths => {
if (!shouldBuildAll) {
return pack(os.platform(), os.arch());
}
return Promise.all(['linux', 'win32', 'darwin'].map(plat =>
Promise.all(['ia32', 'x64'].map(arch => pack(plat, arch))));
})
.then(() => console.log('then!'))
.catch(err => console.error(err));