Failed to execute 'waitUntil' on 'ExtendableEvent' - javascript

I need the chrome registration id to send it as a parameter to the API call, so I can fetch message corresponding to the registration id. My code is as follows:
self.addEventListener('push', function(event) {
var apiPath = 'http://localhost/api/v1/notification/getNotification?regId=';
event.waitUntil(registration.pushManager.getSubscription().then(function (subscription){
apiPath = apiPath + subscription.endpoint.split("/").slice(-1);
event.waitUntil(fetch(apiPath).then(function(response){
if(response.status !== 200){
console.log("Problem Occurred:"+response.status);
throw new Error();
}
return response.json().then(function(data){
var title = data.title;
var message = data.body;
var icon = data.icon;
var tag = data.tag;
var url = data.url;
return self.registration.showNotification(title,{
body: message,
icon: icon,
tag: tag,
data: url
});
})
}).catch(function(err){
var title = 'Notification';
var message = 'You have new notifications';
return self.registration.showNotification(title,{
body: message,
icon: '/images/Logo.png',
tag: 'Demo',
data: 'http://www.google.com/'
});
})
)
}));
return;
});
The error I am getting with the above code is:
Uncaught (in promise) DOMException:
Failed to execute 'waitUntil' on 'ExtendableEvent': The event handler is already finished.(…)
along with the extra notification 'The site has been updated in the background'.
Now even if I remove event.waitUntil before the fetch(apiPath) part, I am still getting that extra notification.
Please help me find a solution to this.
P.S: The question at Chrome Push Notification: This site has been updated in the background does'nt seem to be of any use in my case.

You don't need to call event.waitUntil multiple times. You just need to call it once and pass it a promise, the event's lifetime will be extended until the promise is resolved.
self.addEventListener('push', function(event) {
var apiPath = 'http://localhost/api/v1/notification/getNotification?regId=';
event.waitUntil(
registration.pushManager.getSubscription()
.then(function(subscription) {
apiPath = apiPath + subscription.endpoint.split("/").slice(-1);
return fetch(apiPath)
.then(function(response) {
if (response.status !== 200){
console.log("Problem Occurred:"+response.status);
throw new Error();
}
return response.json();
})
.then(function(data) {
return self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
tag: data.tag,
data: data.url,
});
})
.catch(function(err) {
return self.registration.showNotification('Notification', {
body: 'You have new notifications',
icon: '/images/Logo.png',
tag: 'Demo',
data: 'http://www.google.com/'
});
});
})
);
});

Related

document.querySelectorAll.forEach strange behaviour

I have this small piece of code which manually get the value of checked checkboxes and emit through socket io. The thing is, if I comment out payload.permission.subscriptionPlans.push(item.value); it works fine, but if I uncomment it, socket.emit still run and I still get all the data I needed server side without any errors, but it doesnt run the callback function. Here's the code:
let payload = {
permission: {},
};
payload.permission.subscriptionPlans = [];
document.querySelectorAll(".form-add-sub-plan:checked").forEach((item) => {
console.log(item.value); // this still works
payload.permission.subscriptionPlans.push(item.value); // problem here
});
socket.emit("admin/manage-permission/create", payload, (res) => {
let addModal = bootstrap.Modal.getOrCreateInstance(document.getElementById("add-permission-modal"));
addModal.hide();
table.reload();
table.displayMessage(res.payload.type, res.payload.message);
});
// server code
socket.on("admin/manage-permission/create", async(req, done) => {
try {
if (await Permission.exists({
url: req.permission.url
})) {
return done(
new SocketPayload("error", 409, {
type: "danger",
message: "Url already exists",
})
);
}
let newPermission = new Permission(req.permission);
// console.log(newPermission);
await newPermission.save();
return done(
new SocketPayload("success", 200, {
type: "success",
message: "New permission was added",
})
);
} catch (e) {
console.log(e);
return done(
new SocketPayload("error", 500, {
type: "danger",
message: "An error occured",
})
);
}
});
Any ideas why this happens?
Edit: added server code if anyone is wondering
It turned out it's mongoose middleware stopping the socket to return a callback. It took me a while to figured out cuz the server did not throw any error.

Javascript: Await in 'for .. in' loop for object not waiting for async

I am currently working with the WP-API where I am creating pages from elements in my object. What I basically want to do is to iterate over the object, and for each element: create the page, get the pageID returned, save it to the element and proceed with the next element in the object.
However, what's currently happening is that it does not wait for the element to be finished creating the WP page.
I know this is a common question but I did not find an answer suitable to the 'for ... in ...' loop used.
That's my code rn:
async function checkContent(content) {
let currentObj;
for(const key in content) {
currentObj = content[key][0];
console.log(currentObj);
currentObj.postId = await createPage(currentObj.title, currentObj.content);
}
}
function createPage(title, content) {
var wp = new WPAPI({
endpoint: 'http://localhost:8888/wordpress/wp-json',
username: 'admin',
password: 'pass'
});
wp.pages().create({
title: title,
content: content,
status: 'publish'
})
.catch(error => {
console.error('Error: ' + error.message);
})
.then(function (response) {
console.log(response.id);
return response.id;
})
}
Can anyone tell me what I am doing wrong? Am I not using 'await' correctly?
You don't return any Promise from createPage so there is nothing await can wait for.
You have return the Promise chain create with wp.pages().create:
function createPage(title, content) {
var wp = new WPAPI({
endpoint: 'http://localhost:8888/wordpress/wp-json',
username: 'admin',
password: 'pass'
});
return wp.pages().create({
title: title,
content: content,
status: 'publish'
})
.catch(error => {
console.error('Error: ' + error.message);
})
.then(function(response) {
console.log(response.id);
return response.id;
})
}

Function not getting called in Mocha test

I am working on an Express.js app. The current feature is creating an appointment with a post request and getting and saving data from third party API, then sending updated API data in the subsequent request. The feature is fully working but in the test, the function to get API data is not getting called.
Route to create appointment:
app.post('/schedule', requestHandler.postSchedule);
The request handler for creating appointment:
requestHandler.postSchedule = function (req, res) {
let appointment = {
// posted data
};
new Appointment(appointment)
.save()
.then(newAppointment => {
if(req.body.cityName && req.body.cityName !== '') {
console.log('req.body.cityName', req.body.cityName);
weatherHelper.addNewCityWeatherData(req.body.cityName);
}
return newAppointment;
})
.then(newAppointment => {
// do some other stuff
res.send(newAppointment);
})
.catch(err => {
error(err);
});
};
Function to add weather data:
exports.addNewCityWeatherData = (city) => {
console.log('City in addNewCityWeatherData', city);
getCurrentTrackingCities(cities => {
if(cities.indexOf(city) < 0) {
console.log(city + ' data not in weather');
getWeatherData(city, data => {
console.log('Got weather data');
addWeatherDataToDB(city, data);
});
} else {
console.log('City exists');
}
});
};
Function to get weather data from API:
getWeatherData = (city, callback) => {
console.log('getWeatherData called', city);
let url = `http://api.apixu.com/v1/forecast.json?key=${weatherApiKey}&q=${city}&days=${10}`
request(url, (err, res, body) => {
console.log('Weather data received body');
callback(body);
});
};
When testing, this feature fails and all console logs are printed except the 'Weather data received body' and the logs in consequent functions.
Here is my test:
describe.only('Weather data', function() {
let requestWithSession = request.defaults({jar: true});
let hashedPass = bcrypt.hashSync('testpass', null);
beforeEach((done) => {
new User({
'name': 'Test User',
'email': 'testuser#test.com',
'password': hashedPass
})
.save()
.then(() => {
let options = {
'method': 'POST',
'uri': testHost + '/login',
'form': {
'email': 'testuser#test.com',
'password': 'testpass'
}
};
requestWithSession(options, (err, res, body) => {
done();
});
});
}); // beforeEach
afterEach((done) => {
// remove test stuff from db
}); // afterEach
it('Adds weather data when an appointment with new city is posted', (done) => {
let options = {
'method': 'POST',
'uri': testHost + '/schedule',
'form': {
'title': 'Test title',
'description': 'Test description',
'start_date_time': '2017-07-19 01:00',
'end_date_time': '2017-07-19 02:00',
'cityName': 'New York',
'isTrackingWeather': 'true'
}
};
// post request to add appointment data
requestWithSession(options, (err, res, body) => {
if(err) {
console.log('DatabaseError in Weather Data');
throw {
type: 'DatabaseError',
message: 'Failed to create test setup data'
};
}
let options = {
'method': 'GET',
'uri': testHost + '/allweather'
};
// subsequesnt request to get updated weather data
requestWithSession(options, (err, res, body) => {
let found = false;
weatherData = JSON.parse(body);
// console.log('weatherData in test', weatherData);
weatherData.forEach(weather => {
if(weather.location && weather.location.name === 'New York') {
found = true;
}
});
expect(found).to.be.true;
done();
});
});
});
}); // Weather Data
Here is the terminal output:
Can anyone please tell me what am I doing wrong?
When you run your test is that the test suite make a request to your test server, and the code that handles the request in your test server makes another request to another host.
You do not get to see 'Weather data received body' because the request handled by your test server is not waiting for the request that the test server itself makes. addNewCityWeatherData has no callback and does not return a promise, so the code that calls it goes on its merry way without waiting for it to complete. You should modify it to allow for the calling code to wait for a result.
Also, I'm not seeing how the data from the request initiated by your test server is folded back into the request that comes from your test suite. You may have to add some code for that too, unless addWeatherDataToDB(city, data); is taking care of it automatically somehow.

Play a sound from a Service Worker

Is there a way to play an audio file from a service worker?
I'm trying to use io.sound library but it is a JavaScript plugin that requires window, so it doesn't work.
EDIT
As suggested by Jeff I'm trying to open a new window and post a message to that window. this is my code:
function notifyClientToPlaySound() {
idbKeyval.get('pageToOpenOnNotification')
.then(url => {
console.log("notifyClientToPlaySound", url);
clients.matchAll({
type: "window"
//includeUncontrolled: true
})
.then((windowClients) => {
console.log("notifyClientToPlaySound - windowClients", windowClients);
for (var i = 0; i < windowClients.length; i++) {
var client = windowClients[i];
if (client.url === url && "focus" in client) {
notify({ event: "push" });
return client.focus();
}
}
//https://developer.mozilla.org/en-US/docs/Web/API/Clients/openWindow
if (clients.openWindow) {
return clients.openWindow("/")
.then(() => {
notify({ event: "push" });
});
}
})
});
}
This function is now called from event.waitUntil(..) inside self.addEventListener("push", (event) => { ... }
self.addEventListener("push", (event) => {
console.log("[serviceWorker] Push message received", event);
event.waitUntil(
idbKeyval.get('fetchNotificationDataUrl')
.then(url => {
console.log("[serviceWorker] Fetching notification data from -> " + url);
return fetch(url, {
credentials: "include"
});
})
.then(response => {
if (response.status !== 200) {
// Either show a message to the user explaining the error
// or enter a generic message and handle the
// onnotificationclick event to direct the user to a web page
console.log("[serviceWorker] Looks like there was a problem. Status Code: " + response.status);
throw new Error();
}
// Examine the text in the response
return response.json();
})
.then(data => {
if (!data) {
console.error("[serviceWorker] The API returned no data. Showing default notification", data);
//throw new Error();
showDefaultNotification({ url: "/" });
}
notifyClientToPlaySound(); <------ HERE
var title = data.Title;
var message = data.Message;
var icon = data.Icon;
var tag = data.Tag;
var url = data.Url;
return self.registration.showNotification(title, {
body: message,
icon: icon,
tag: tag,
data: {
url: url
},
requireInteraction: true
});
})
.catch(error => {
console.error("[serviceWorker] Unable to retrieve data", error);
var title = "An error occurred";
var message = "We were unable to get the information for this push message";
var icon = "/favicon.ico";
var tag = "notification-error";
return self.registration.showNotification(title, {
body: message,
icon: icon,
tag: tag,
data: {
url: "/"
},
requireInteraction: true
});
})
);
});
But when clients.openWindow is called, it returns the following exception:
Uncaught (in promise) DOMException: Not allowed to open a window.
How can I solve this?
The living specification for the Web Notifications API does reference a sound property that could be specified when showing a notification, and would theoretically allow you to play the sound of your choosing when showing a notification from a service worker.
However, while the specification references this property, as of the time of this writing, it's not supported in any browsers.
Update (Aug. '19): It looks like reference to sound has been removed from https://notifications.spec.whatwg.org/#alerting-the-user
Your best bet would be post a message along to an open window that's controlled by the current service worker, and have the window play the sound in response to the message event.
If there is no controlled client available (e.g. because your service worker has been awoken by a push event, and your site isn't currently open in a browser) then you'd have the option of opening a new window inside your notificationclick handler, which is triggered in response to a user clicking on the notification you display in your push event handler. You can then post a message to that new window.

Push WebAPI + IndexedDB + ServiceWorker

I've implemented the Push WebAPI in my web application using Service Worker as many articles explain on the web.
Now I need to store some data inside IndexedDB to make them available while the web app is closed (chrome tab closed, service worker in background execution).
In particular I would like to store a simple url from where retrieve the notification data (from server).
Here is my code:
self.addEventListener("push", (event) => {
console.log("[serviceWorker] Push message received", event);
notify({ event: "push" }); // This notifies the push service for handling the notification
var open = indexedDB.open("pushServiceWorkerDb", 1);
open.onsuccess = () => {
var db = open.result;
var tx = db.transaction("urls");
var store = tx.objectStore("urls");
var request = store.get("fetchNotificationDataUrl");
request.onsuccess = (ev) => {
var fetchNotificationDataUrl = request.result;
console.log("[serviceWorker] Fetching notification data from ->", fetchNotificationDataUrl);
if (!(!fetchNotificationDataUrl || fetchNotificationDataUrl.length === 0 || !fetchNotificationDataUrl.trim().length === 0)) {
event.waitUntil(
fetch(fetchNotificationDataUrl, {
credentials: "include"
}).then((response) => {
if (response.status !== 200) {
console.log("[serviceWorker] Looks like there was a problem. Status Code: " + response.status);
throw new Error();
}
return response.json().then((data) => {
if (!data) {
console.error("[serviceWorker] The API returned no data. Showing default notification", data);
//throw new Error();
showDefaultNotification({ url: "/" });
}
var title = data.Title;
var message = data.Message;
var icon = data.Icon;
var tag = data.Tag;
var url = data.Url;
return self.registration.showNotification(title, {
body: message,
icon: icon,
tag: tag,
data: {
url: url
},
requireInteraction: true
});
});
}).catch((err) => {
console.error("[serviceWorker] Unable to retrieve data", err);
var title = "An error occurred";
var message = "We were unable to get the information for this push message";
var icon = "/favicon.ico";
var tag = "notification-error";
return self.registration.showNotification(title, {
body: message,
icon: icon,
tag: tag,
data: {
url: "/"
},
requireInteraction: true
});
})
);
} else {
showDefaultNotification({ url: "/" });
}
}
};
});
Unfortunately when I receive a new push event it doesn't work, showing this exception:
Uncaught DOMException: Failed to execute 'waitUntil' on 'ExtendableEvent': The event handler is already finished.
at IDBRequest.request.onsuccess (https://192.168.0.102/pushServiceWorker.js:99:23)
How can I resolve this?
Thanks in advance
The initial call to event.waitUntil() needs to be done synchronously when the event handler is first invoked. You can then pass in a promise chain to event.waitUntil(), and inside that promise chain, carry out any number of asynchronous actions.
Your current code invokes an asynchronous IndexedDB callback before it calls event.waitUntil(), which is why you're seeing that error.
The easiest way to include IndexedDB operations inside a promise chain is to use a wrapper library, like idb-keyval, which takes the callback-based IndexedDB API and converts it into a promise-based API.
Your code could then look like:
self.addEventListener('push', event => {
// Call event.waitUntil() immediately:
event.waitUntil(
// You can chain together promises:
idbKeyval.get('fetchNotificationDataUrl')
.then(url => fetch(url))
.then(response => response.json())
.then(json => self.registration.showNotification(...)
);
});

Categories