I have a twilio Javascript function that gets executed in my studio flow immediately after somebody calls the associated studio flow phone #. This function is supposed to check if there is a currently active conference call going on and return either "True" or "False" so that I can then use that string in an if/else widget to either connect the caller OR initiate a new conference.
// This is your new function. To start, set the name and path on the left.
exports.handler = function(context, event, callback) {
var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open("GET", "https://api.twilio.com/2010-04-01/Accounts/myAccountSid/Conferences.json?FriendlyName=mySidDocumentName");
xhr.setRequestHeader("Authorization", "Basic myAuthString");
xhr.send();
xhr.addEventListener("readystatechange", function() {
if(this.readyState == 4) {
console.log(JSON.stringify(xhr.responseText));
var jsonResponse = JSON.parse(xhr.responseText);
var arrayLength = Object.keys(jsonResponse.Conferences[jsonResponse]).length;
if (arrayLength > 0) {
var isConferenceOngoing = "True"
} else {
var isConferenceOngoing = "False"
}
}
return callback(null, isConferenceOngoing);
});
};
The "conferences" key that I am interested in, in the response, is an array and that causes a problem because Twilio can not parse an array in the studio flow, so it has to be done in the function call: https://www.twilio.com/docs/studio/widget-library/http-request "Note that, although an array is valid JSON, if your request returns an array of objects, it will not be parsed."
So all I simply need to check for is whether or not the Conferences array is empty and if so return "False" to my studio flow OR if there is an active conference (i.e. the array length is > 0) then return "True". Returning either "True" or "False" will allow me to do an if/else widget in my studio flow to either connect the caller to the existing conference or start a new conference call.
Here is what the response looks like in Postman when there is not an active conference call (notice the conferences array is empty):
My knowledge of Javascript is next to zero, but I think I am close.
Update: Added async/await functions based on comments.
exports.handler = function(context, event, callback) {
const twilioClient = context.getTwilioClient();
let responseObject = twilioClient.conferences
.list({status: 'in-progress'})
var isConferenceOngoing = null
async function setConferenceVariable() {
if (responseObject.Length > 0)
{
isConferenceOngoing = "True"
} else {
isConferenceOngoing = "False"
}
}
async function getConferenceDetails() {
await setConferenceVariable();
return callback(null, isConferenceOngoing);
}
getConferenceDetails()
};
Latest Update: Simpler method below.
exports.handler = function(context, event, callback) {
const twilioClient = context.getTwilioClient();
twilioClient.conferences
.list({status: 'in-progress'})
.then(conferences => { callback(null, !conferences.length)})
};
Latest update looks much better #dylan. I am not seeing a return value when false, so did this:
exports.handler = function(context, event, callback) {
const twilioClient = context.getTwilioClient();
twilioClient.conferences
.list({status: 'in-progress'})
.then(conferences => callback(null, {result: !conferences.length}))
.catch(err => callback(err));
};
Related
This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
async/await implicitly returns promise?
(5 answers)
Closed 1 year ago.
In the below example from https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/onreadystatechange, I am trying to store the responseText in a variable that can be accessed outside of the onreadystatechange function.
const xhr = new XMLHttpRequest(),
method = "GET",
url = "https://developer.mozilla.org/";
xhr.open(method, url, true);
xhr.onreadystatechange = function () {
// In local files, status is 0 upon success in Mozilla Firefox
if(xhr.readyState === XMLHttpRequest.DONE) {
var status = xhr.status;
if (status === 0 || (status >= 200 && status < 400)) {
// The request has been completed successfully
console.log(xhr.responseText);
} else {
// Oh no! There has been an error with the request!
}
}
};
xhr.send();
At the moment, the only way I know to handle the return value from a function is to do the following
const variable = function()
But I can't figure out how to make that won't work due to the way the onreadystatechange event handler is initiated/called. I tried declaring a variable outside of the function scope, but it keeps returning 'undefined'.
Thanks for your help.
*Edit for async/await with fetch
I've been trying to implement the async/await fetch option but can't seem to get it to work right. I've been checking the output with alerts and the alert located outside the function always fires before the one inside the function. As a result the one outside is giving me "object Promise" and the one inside is the actual data. I'm returning the data but it's just not working.
const getData = async function (filePath) {
let myData
await fetch(filePath)
.then(response => response.text())
.then(data => myData = data)
alert(myData)
return (myData)
}
let theData = getData('./2021WeatherData.csv')
alert(theData)
So "alert(theData)" fires before "alert (myData)" and I can't understand why. Thanks so much.
This is a good question.
The XMLHttpRequest is code that runs asynchronous. And it can't run synchronously as the yellow warning on the Mozilla developer documentation explains.
So what does that mean?
(Pseudocode)
var result
console.log('start')
xhr.onreadystatechange = function() {
result = '5'
console.log('request done')
}
console.log('result', result)
This will output as you already noticed
start
result undefined
request done
and you are expecting:
start
request done
result 5
But this does not work, because the console.log('result', result) was
already executed before the variable was assigned.
Solutions
Using a callback
Whatever you want to do with the result, you need to do within the callback function. Like:
Console.log the value
Updating the browser dom (I added this as an example)
Store to the database on a node server
The callback function is the function you are assigning to the onreadystatechange.
(Pseudocode)
xhr.onreadystatechange = function() {
// This code is the callback function.
// You can do everything with the result, like updating the DOM,
// but you can't assign it to a variable outside.
}
Actual code
function logResultToTextBox(result) {
document.getElementById('#server-result-text-box').value = result
}
const xhr = new XMLHttpRequest(),
method = "GET",
url = "https://developer.mozilla.org/";
xhr.open(method, url, true);
xhr.onreadystatechange = function () {
// In local files, status is 0 upon success in Mozilla Firefox
if(xhr.readyState === XMLHttpRequest.DONE) {
var status = xhr.status;
if (status === 0 || (status >= 200 && status < 400)) {
// The request has been completed successfully
console.log(xhr.responseText);
// DO WHATEVER YOU NEED TO DO HERE WITH THE RESULT, BUT YOU CANT ASSIGN THE VALUE TO VARIABLE WHICH IS DECLARED OUTSIDE
logResultToTextBox(xhr.responseText);
} else {
// Oh no! There has been an error with the request!
}
}
};
xhr.send();
Wrap it using a promise
The promise is another way on how you can solve this. You wrap it into a promise function. Not that the promise has no return statement. It uses a resolver.
The actual work will then be done after you define the functions. It will be called in the then block.
Here you also can't assign a value to a variable that is declared outside.
Pseudocode:
Promise makeRequest(url) {
// doRequest to url and resolve result
}
makeRequest('http://example.com).then((result) => {
// do whatever you want with the result
})
Actual code:
function logResultToTextBox(result) {
document.getElementById('#server-result-text-box').value = result
}
function makeRequest (method, url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function () {
if (this.status >= 200 && this.status < 300) {
resolve(xhr.response);
} else {
reject({
status: this.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function () {
reject({
status: this.status,
statusText: xhr.statusText
});
};
xhr.send();
});
}
// Example:
makeRequest('GET', 'https://developer.mozilla.org/')
.then(function (result) {
console.log(result);
logResultToTextBox(result)
})
.catch(function (err) {
console.error('Augh, there was an error!', err.statusText);
});
Use async / await
For a few years, there is also a new syntax called async/await. This gives you the possibility to write code like it would a normal code.
Pseudocode
let myServerResult;
async function doAsyncCall(url) {
const serverResult = await callServer(url);
return serverResult
}
myServerResult = await doAsyncCall('http://example.com')
console.log(myServerResult)
In that case, I would use fetch instead of XMLHttpRequest because it is much simpler.
Actual code:
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const jsonResult = await response.json();
console.log(jsonResult)
(note that I am using jsonplaceholder.typicode.com as an API as the HTML output of the Mozilla documentation does not work with it. If you really need XMLHttpRequest then have a look into the answers on this question)
P. S. Don't use old synchronous code syntax
There is also an old-school way by using "Synchronous request". But this should not be used anymore, as it leads to a bad user experience because it makes the loading of the website slow.
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests#synchronous_request
Answer after the question edit
You need to await the following line
instead of
let theData = getData('./2021WeatherData.csv')
you need to write
let theData = await getData('./2021WeatherData.csv')
I used this post: IndexedDB: upgrade with promises?
And implemented the part here: https://stackoverflow.com/a/25565755/15778635
This works for what I need. the part I am having trouble with is this:
var newMigrationPromise = function (dbName, version, migration) {
return newPromise(function (deferred) {
var request = indexedDB.open(dbName, version);
// NB: caller must ensure upgrade callback always called
request.onupgradeneeded = function (event) {
var db = request.result;
newTransactionPromise(
function () {
var syncUPStore = transaction.objectStore("syncUP");
var syncCountRequest = syncUPStore.count();
syncCountRequest.oncomplete = function (event) {
if (syncCountRequest.result > 0)
deferred.reject(syncCountRequest.result + " SyncUp Records exist, database upgrade aborted, keeping at current version.");
else {
//Good, continue with update
migration(db, request.transaction);
return request.transaction;
}
}
})
.then(function () { db.close(); })
.then(deferred.resolve, deferred.reject);
};
request.onerror = function (ev) { deferred.reject(request.error); };
});
};
I have a syncUP object store that has data that needs to be sent to the server when the user goes online. In this particular case the service worker is installing (because they came online and a change was put on the server) and needs to know if syncUP records exist prior to allowing the service worker to update. If they do exist then it needs to abort the install until it is empty.
The service worker abort works fine, and the database aborting upgrade works fine if I were to throw an error where var syncCountRequest = syncUPStore.count(); is.
My question:
How can I check if there are records in the "syncUP" object store and still use the implementation I mentioned above? I had considered moving the logic to another method, but I found I was having the same issue of not knowing how to handle the reject/resolve. My Promises knowledge is ok, but not good enough yet to figure it out on my own.
a rushed example:
var request = indexedDb.open(...);
request.onupgradeneeded = function(event) {
if(conditionShouldDoMigrationFromVersionXToNowIsTrue) {
migrate(event.transaction);
}
};
function migrate(versionChangeTransaction) {
var store = versionChangeTransaction.objectStore('x');
var request = store.getAll();
request.onsuccess = function(event) {
var objects = event.target.result;
for (var object of objects) {
// do some mutation to the object
object.x++;
// write it back
store.put(object);
}
};
}
I am trying to store item in an array, newItemList, along with present hour and an object, which I get from a server as JSON response, which I parse into a js object. This simply means that a typical element of newItemList will be [item, 13(present_hour), obj]
Here's the code,
var item;
//some code to obtain item
chrome.storage.local.get({'itemList':[]}, function(item){
var newItemList = item.itemList;
var d = new Date();
if (isItemInArray(newItemList,item) == -1) {
//api call function as callback
apiCall(function(result){
newItemList.push([item,d.getHours(),result]);
});
} else {
var indexOfitem = findItem(newItemList,item);//finding item in the array
if(//some condition){
apiCall(function(result){
newItemList[indexOfTab][1] = d.getHours();
newItemList[indexOfTab][2] = result;
});
} else {
var indexOfitem = findItem(newItemList,item);
storedApiCall(newItemList[indexOfTab][2]);//sending the stored JSON response
}
}
chrome.storage.local.set({itemList: newItemList});
})
function apiCall(callback){
//API call, to obtain the JSON response from the web server
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var myObj = JSON.parse(this.responseText);//parsing the JSON response to js object
callback(myObj);
storedApiCall(myObj);//this function just works fine
}
};
xhttp.open("GET", "example.com", true);
xhttp.send();
}
newItemList isn't getting stored in local storage. It contains of only one element, [item, present hour, obj(from present apiCall)]. That's why, only the if part of the code runs each time leading to api calls each time, rendering the else part obsolete.
I read about callback from many famous questions asked around about asynchronous calls, but none connected local storage with callbacks. Before implementing callback, newItemList got stored in local storage, but I couldn't obtain the obj from JSON response for the first time, which is the typical behaviour of asynchronous calls.
Suggest edits, if any.
This line is being executed before the functions given to your calls to apiCall are invoked (i.e. with its initial value item.itemList):
chrome.storage.local.set({itemList: newItemList});
This is because callbacks are typically invoked after some asynchronous action completes. In your case, they will be invoked after whatever operation apiCall performs is complete, e.g. an HTTP response to an API is received.
So if you move that line into those callback functions, at their end after you have mutated newItemList, the set call on chrome.storage.local will be performed with the changes applied (e.g. newItemList.push([item,d.getHours(),result])):
chrome.storage.local.get({'itemList':[]}, function(item){
var newItemList = item.itemList;
var d = new Date();
if (isItemInArray(newItemList,item) == -1) {
//api call function as callback
apiCall(function(result){
newItemList.push([item,d.getHours(),result]);
chrome.storage.local.set({itemList: newItemList});
});
} else {
var indexOfitem = findItem(newItemList,item);//finding item in the array
if(//some condition){
apiCall(function(result){
newItemList[indexOfTab][1] = d.getHours();
newItemList[indexOfTab][2] = result;
chrome.storage.local.set({itemList: newItemList});
});
} else {
var indexOfitem = findItem(newItemList,item);
storedApiCall(newItemList[indexOfTab][2]);//sending the stored JSON response
}
}
})
I think that when the execution arrives on the chrome.storage.local.set({itemList: newItemList}); line, the variable is not set yet because the XMLHttpRequest is async you have to add an chrome.storage.local.set in your callback function or use a promise then set your local storage value
My Chrome extension uses message passing to retrieve various values from the extension's built in localstorage area on the background page.
The thing I love about chrome message passing, is it lets you include a callback function inside the sendMessage call, like so:
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
console.log(response.farewell);
});
and the corresponding message receiving code would look like the following (example code from the Chrome extensions documentation):
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
if (request.greeting == "hello")
sendResponse({farewell: "goodbye"});
});
I'm trying to convert my extension to the Safari extension format, but I can't figure out how to map the Chrome sendMessage / onMessage functions to Safari's
safari.self.tab.dispatchMessage(name, data) message handling functions
Is it even possible to include callback functions in Safari's dispatchMessage function call?
If not, how should I work around this limitation?
In environments that only provide a one-way message passing system, you can always implement "message-with-callback" yourself, by associating an unique ID with every message, and storing the callbacks in a dictionary that maps these IDs to the callbacks.
At the end of this answer, I've copy-pasted the Safari-specific storage module from the source code of my cross-browser Lyrics Here extension, which provides the following API:
config.getItem(key, callback)
config.setItem(key, value, callback)
config.removeItem(key, callback)
config.clear(callback)
The callback of getItem contains the value associated with the key (if found). The other callbacks receive a boolean that tells whether the operation succeeded or not.
The source code is annotated and contains snippets to handle a few edge-cases. Feel free to shoot a few questions at me if there's anything unclear.
config-safari.js (AMD)
// Adapter for maintaining preferences (Safari 5+)
// All methods are ASYNCHRONOUS
define('config-safari', function() {
var config = {};
var callbacks = {};
['getItem', 'setItem', 'removeItem', 'clear'].forEach(function(methodName) {
config[methodName] = function() {
var args = [].slice.call(arguments);
var callback = args.pop();
var messageID = Math.random();
callbacks[messageID] = callback;
var message = {
type: methodName,
messageID: messageID,
args: args
};
safari.self.tab.dispatchMessage('config-request', message);
};
});
config.init = function() {
if (typeof safari === 'undefined') {
// Safari bug: safari is undefined when current context is an iframe
// and src="javascript:''"
// This error is only expected on YouTube.
// config.getItem is triggered in main, so we just redefine
// it. Don't overwrite setItem, etc., so that errors are thrown
// when these methods are used.
config.getItem = function(key, callback){
callback();
};
return;
}
safari.self.addEventListener('message', function(event) {
if (event.name === 'config-reply') {
var messageID = event.message.messageID;
var callback = callbacks[messageID];
// Check if callback exists. It may not exist when the script is
// activated in multiple frames, because every frame receives the message
if (callback) {
delete callbacks[messageID];
callback(event.message.result);
}
}
}, true);
};
return config;
});
Fragment of global.html:
<script>
(function(exports) {
var config = {};
config.getItem = function(key, callback) {
var result = safari.extension.settings.getItem(key);
if (typeof result === 'string') {
try {
result = JSON.parse(result);
} catch (e) {
// Extremely unlikely to happen, but don't neglect the possibility
console.log('config.getItem error: ' + e);
result = undefined;
}
}
callback(result);
};
// callback's argument: true on success, false otherwise
config.setItem = function(key, value, callback) {
var success = false;
try {
value = JSON.stringify(value);
// Safari (5.1.5) does not enforce the database quota,
// let's enforce it manually (ok, set the quota per key, since
// the performance issue only occur when a specific key has an outrageous high value)
// 1 MB should be sufficient.
if (value.length > 1e6) {
throw new Error('QUOTA_EXCEEDED_ERR: length=' + value.length);
}
safari.extension.settings.setItem(key, value);
success = true;
} catch (e) {
console.log('config.setItem error: ' + e);
}
callback(success);
};
// callback's argument: true on success, false otherwise
config.removeItem = function(key, callback) {
safari.extension.settings.removeItem(key);
callback(true);
};
// callback's argument: true on success, false otherwise
config.clear = function(callback) {
safari.extension.settings.clear();
callback(true);
};
// config's message handler
function handleConfigRequest(event) {
var args = event.message.args;
// Last argument: Always a callback
args.push(function(result) {
// Note: All of the config methods send a callback only once
// Behavior for calling the callback twice is undefined.
// Send a reply to trigger the callback at the sender's end
event.target.page.dispatchMessage('config-reply', {
messageID: event.message.messageID,
result: result
});
});
config[event.message.type].apply(config, args);
}
// Export
exports.handleConfigRequest = handleConfigRequest;
})(window);
</script>
<script>
safari.application.addEventListener('message', function(event) {
switch (event.name) {
case 'config-request':
handleConfigRequest(event);
break;
/* ... other things removed ... */
}
}, true);
</script>
I have the following function:
getTasks: function()
{
var taskRequest = Titanium.Network.createHTTPClient();
var api_url = 'http://myawesomeapi.heroku.com/users/' + Ti.App.Properties.getString("userID") + '/tasks';
var tasks = [];
taskRequest.onload = function() {
var response = JSON.parse(this.responseText),
len = response.length,
i = 0,
t;
for(; i < len; i++)
{
task = response[i];
var newTask = {};
newTask.rowID = i;
newTask.title = task.title;
newTask.description = task.description;
newTask.id = task.id;
newTask.hasChild = true;
tasks.push(newTask);
}
alert(tasks);
}
taskRequest.open('GET', api_url, false);
taskRequest.setRequestHeader('Content-Type', 'application/json');
taskRequest.send();
alert(tasks);
// return tasks;
}
This function is in my controller; I call it in my view when I need to load the data in. However, I wish to return this data so I can assign it to a variable in the view.
Now what happens is that it returns emptiness. The last alert (bottom one) seems to be running too fast and it returns an empty array, while the one that only gets alerted after the onload function is done, contains what I need.
Now my obvious question, how can I get my function to return the array with the data, instead of without?
Putting a timer on it seems hardly the right decision.. Thanks!
"However, I wish to return this data so I can assign it to a variable in the view."
Aside from making the AJAX request synchronous (which you probably don't want), there isn't any way to return the data.
Whatever code relies on the response needs to be called from within the response handler.
Since functions can be passed around, you could have your getTasks method receive a callback function that is invoked and will receive the tasks Array.
getTasks: function( callback ) // receive a callback function
{
var taskRequest = Titanium.Network.createHTTPClient();
var api_url = 'http://myawesomeapi.heroku.com/users/' + Ti.App.Properties.getString("userID") + '/tasks';
taskRequest.onload = function() {
var tasks = [];
// code populating the tasks array
alert(tasks);
callback( tasks ); // invoke the callback
}
taskRequest.open('GET', api_url, false);
taskRequest.setRequestHeader('Content-Type', 'application/json');
taskRequest.send();
}
So you'd use it like this...
myObj.getTasks(function(tasks) {
alert('in the callback');
alert(tasks);
// Any and all code that relies on the response must be
// placed (or invoked from) inside here
some_other_function();
});
function some_other_function() {
// Some more logic that can't run until the tasks have been received.
// You could pass the tasks to this function if needed.
}
You are getting empty alert because when the bottom alert is executed the server response is not available and tasks array is empty.
When the server response comes the tasks array is populated by the code which you have in the onload handler so you see the tasks in the second alert.