Not sure what I'm doing wrong with my service worker implementation.
Ideally, I'm facing problems with either "The Website has been refreshed in the background" with this one.
I didn't have this problem before with the code, but once the Push Notification subscribers count got to around 600 it started to cause issues with every single one of them.
var init = { method: 'GET',
headers: {
"Content-Type" : "application/javascript"
},
mode: 'cors',
cache: 'no-cache'
};
var worker = self;
var the_endpoint = '';
fetch(the_endpoint, init ).then(function(response) {
return response.json();
}).then(function(data){
worker.addEventListener('push', function(event) {
event.waitUntil(
worker.registration.showNotification(data.title, {
body : data.body,
icon : data.icon,
requireInteraction : true
})
);
});
if( data.link != '' ) {
worker.addEventListener('notificationclick', function(event) {
event.notification.close();
var url = data.link;
event.waitUntil(
clients.matchAll({
type: 'window'
})
.then(function(windowClients) {
for (var i = 0; i < windowClients.length; i++) {
var client = windowClients[i];
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
});
}
});
Attempt 2 that doesn't have the background refresh issue, but doesn't always work.
var init = { method: 'GET',
headers: {
"Content-Type" : "application/javascript"
},
mode: 'cors',
cache: 'no-cache'
};
var worker = self;
var the_endpoint = '';
self.addEventListener('push', function(event) {
event.waitUntil(
fetch(the_endpoint, init).then(function (response) {
return response.json().then(function (data) {
var response = self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
requireInteraction: true
});
if (data.link != '') {
worker.addEventListener('notificationclick', function (event) {
event.notification.close();
var url = data.link;
event.waitUntil(
clients.matchAll({
type: 'window'
}).then(function (windowClients) {
for (var i = 0; i < windowClients.length; i++) {
var client = windowClients[i];
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
});
}
return response;
});
})
);
});
The reason this often occurs is the promise returned to event.waitUntil() didn't resolve with a notification being shown.
An example that might show the default "The Website has been refreshed in the background" notification:
function handlePush() {
// BAD: The fetch's promise isn't returned
fetch('/some/api')
.then(function(response) {
return response.json();
})
.then(function(data) {
// BAD: the showNotification promise isn't returned
showNotification(data.title, {body: data.body});
});
}
self.addEventListener(function(event) {
event.waitUntil(handlePush());
});
Instead you could should write this as:
function handlePush() {
// GOOD
return fetch('/some/api')
.then(function(response) {
return response.json();
})
.then(function(data) {
// GOOD
return showNotification(data.title, {body: data.body});
});
}
self.addEventListener(function(event) {
const myNotificationPromise = handlePush();
event.waitUntil(myNotificationPromise);
});
The reason this is all important is that browsers wait for the promise passed into event.waitUntil to resolve / finish so they know the service worker needs to be kept alive and running.
When the promise resolves for a push event, chrome will check that a notification has been shown and it falls into a race condition / specific circumstance as to whether Chrome shows this notification or not. Best bet is to ensure you have a correct promise chain.
I put some extra notes on promises on this post (See: 'Side Quest: Promises' https://gauntface.com/blog/2016/05/01/push-debugging-analytics)
Related
I want to run the function configurePushSub down bellow whenever an event is fired on a page for my create-react-app... It is working everything fine on development mode, however when I hosted my app on Netlify the promise navigator.serviceWorker.ready is not getting resolved as I cannot see the message in the console navigator has loaded... I have already tried everything possible to get that working but everything unsuccessfull
function configurePushSub() {
if (!('serviceWorker' in navigator)) {
return;
}
let hasUnsubscribed;
var reg;
console.log(
'running',
navigator.serviceWorker,
navigator.serviceWorker.ready
);
navigator.serviceWorker.ready
.then(function (swreg) {
console.log(swreg, 'navigator has loaded');
reg = swreg;
return swreg.pushManager.getSubscription();
})
.then((sub) => {
if (!!sub) {
return sub.unsubscribe().then(function (s) {
hasUnsubscribed = true;
return navigator.serviceWorker.ready;
});
}
return navigator.serviceWorker.ready;
})
.then(function (sub) {
var vapidPublicKey =
'BOChVD1tKTc0Of3c-0JplT1y5FPOm6oijP_4stWBXwoQe6xI4GGt6cnpdu4JLwt_Znj23bj_hku8OSois1y9fLE';
var convertedVapidPublicKey = urlBase64ToUint8Array(vapidPublicKey);
return reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidPublicKey,
});
})
.then(async function (newSub) {
let key;
let authSecret;
let endPoint;
if (newSub) {
const rawKey = newSub.getKey ? newSub.getKey('p256dh') : '';
key = rawKey
? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey)))
: '';
var rawAuthSecret = newSub.getKey ? newSub.getKey('auth') : '';
authSecret = rawAuthSecret
? btoa(
String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))
)
: '';
endPoint = newSub.endpoint;
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
};
const gpuTier = await getGPUTier();
const info = getOperatingSystemName(this);
console.log(info);
if (hasUnsubscribed) {
await axios.put(
`${Config.SERVER_ADDRESS}/api/pushNotifications/id`,
{
endpoint: endPoint,
key,
auth: authSecret,
gpu: gpuTier.gpu,
operating_system: `${info.os} ${info.osVersion}`,
device: info.mobile ? 'mobile' : 'desktop',
browser: `${info.browser} ${info.browserMajorVersion}`,
operating_system_architecture: info.arch,
},
config
);
} else {
await axios.post(
`${Config.SERVER_ADDRESS}/api/pushNotifications/`,
{
endpoint: endPoint,
key,
auth: authSecret,
gpu: gpuTier.gpu,
operating_system: `${info.os} ${info.osVersion}`,
device: info.mobile ? 'mobile' : 'desktop',
browser: `${info.browser} ${info.browserMajorVersion}`,
operating_system_architecture: info.arch,
},
config
);
}
}
})
.then(function (res) {
displayConfirmNotification();
})
.catch(function (err) {
console.log(err);
});
}
I have just managed to get that working...
It is just that I haven't registered the service worker for the production mode...
When I was on development I have written the following code, but then I moved to the production and I have forgotten about it.
navigator.serviceWorker.register(`/sw.js`)
I am using web workers to fetch information of websites including their subsites(nth number) recursively. When all of the calls are done I want to fire off a function that would format the data it receives (allSites array). So I thought it would be a great idea to use a Promise.all with an object that has all my resolved promises.
The problem is that it doesn't wait for all the resolved promises because it's waiting to hear messages posted from the worker. I can't define a length because it could be any number of websites + subsites.
Bonus: I have an object with resolved promises. Can I call a certain resolve like this?
keyName[index]()
It says it's not a function but shouldn't I be able to call it like that? Any help is greatly appreciated.
function getTreeData(cb) {
let allSites = [];
let baseUrl = "https://www.somewebsite.com/"
let resolver = {};
let rejecter = {};
let workerUrl =
"https://www.somewebsite.com/siteassets/worker.js";
let myWorker = new Worker(workerUrl);
function firstIteration() {
return new Promise((resolve, reject) => {
resolver[baseUrl] = resolve;
rejecter[baseUrl] = reject;
myWorker.postMessage({
requestDigest: document.getElementById("__REQUESTDIGEST").value,
qs1: "/_api/web/webinfos?$select=ServerRelativeUrl,Title",
qs2:
"/_api/Web/RoleAssignments?$expand=Member/Users,RoleDefinitionBindings",
url: baseUrl,
});
});
}
firstIteration();
//spawn a worker
myWorker.onmessage = function (e) {
allSites = allSites.concat([
{ pathname: e.data.url, groups: e.data.permissions },
]);
e.data.sites.forEach(function (props) {
return new Promise((resolve, reject) => {
myWorker.postMessage({
requestDigest: document.getElementById("__REQUESTDIGEST").value,
qs1: "/_api/web/webinfos?$select=ServerRelativeUrl,Title",
qs2:
"/_api/Web/RoleAssignments?$expand=Member/Users,RoleDefinitionBindings",
url: "www.somewebsite.com" + props.url,
});
resolver[props.url] = resolve;
rejecter[props.url] = reject;
});
});
resolver[e.data.url](); //it says that it is not a function
};
myWorker.onerror = function (e) {
rejecter[e.data.url]();
};
//After my first inital promises resovles resolve the rest (checks object of resolves)
resolver[baseUrl]().then(() => {
Promise.all(Object.values(resolver)).then(() => {
reduceData(cb, allSites);
});
});
}
Though it is working properly here's the code for the web worker. (worker.js)
function formatSites(props) {
return {
url: "www.someSite.com",
};
}
function formatSitesInfo(props) {
//get all info of the site or subsite
var accessArr = props.RoleDefinitionBindings.results
.reduce(function (r, a) {
return r.concat([a.Name]);
}, [])
.sort()
.join(", ");
return {
access: accessArr,
isGroup: props.Member.hasOwnProperty("AllowRequestToJoinLeave")
? true
: false,
name: props.Member.Title,
members: (props.Member.Users?.results || []).map(function (member) {
return {
access: accessArr,
email: member.Email,
groupName: props.Member.Title,
id: member.Id,
isAdmin: member.IsSiteAdmin,
title: member.Title,
};
}),
};
}
function _getRequest(props) {
return new Promise(function (resolve, reject) {
fetch(props.url + props.qs, {
method: "GET",
headers: {
Accept: "application/json; odata=verbose",
"Content-type": "application/json; odata=verbose",
"X-RequestDigest": props.requestDigest,
},
})
.then(function (resp) {
return resp.json();
})
.then(resolve)
.catch(reject);
});
}
self.addEventListener("message", function (e) {
if (!e.data.data) {
var promise1 = _getRequest(Object.assign(e.data, { qs: e.data.qs1 }));
var promise2 = _getRequest(Object.assign(e.data, { qs: e.data.qs2 }));
Promise.all([promise1, promise2]).then(function ([data1, data2]) {
self.postMessage({
info: data2.d.results.map(formatSitesInfo),
sites: data1.d.results.map(formatSites),
url: e.data.url,
});
});
}
});
I ended up making a timer to would be reset if there's a another call to the worker(another subsite was found to have childern). If the timer stops that means that we have reached the end of childern for the sites. It then goes to the next function. There will be a dash of latency, but it's something I can live with.
I have a yale smart alarm and come across the the below javascript that allows you to access the alarm to get the status and set it. I'm wanting to use this in my home assistant set to which uses python.
const fetch = require('node-fetch');
const setCookie = require('set-cookie-parser');
const urls = {
login: 'https://www.yalehomesystem.co.uk/homeportal/api/login/check_login',
getStatus: 'https://www.yalehomesystem.co.uk/homeportal/api/panel/get_panel_mode',
setStatus: 'https://www.yalehomesystem.co.uk/homeportal/api/panel/set_panel_mode?area=1&mode=',
};
function getSessionCookie(username, password) {
let sessionCookie = null;
return fetch(urls.login, {
method: 'POST',
body: `id=${encodeURIComponent(username)}&password=${password}&rememberme=on¬ify_id=®_id=Name`,
headers: {
'Accept': 'application/json, application/xml, text/plain, text/html, *.*',
'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'
},
})
.then((res) => {
sessionCookie = res.headers._headers['set-cookie'];
return res.json();
}).then(json => {
if (json.result === '0') {
return Promise.reject('Incorrect account details');
}
else {
return sessionCookie[0];
}
})
}
function getStatus(sessionCookie) {
return fetch(urls.getStatus, {
method: 'POST',
headers: {
'Cookie': sessionCookie,
},
}).then(res => res.text()).then(textResponse => {
// When initially writing this code I found if cookie payload
// was invalid I got this text response so I added this code to
// handle this, shouldn't happen but good to have an error message
// for this use case
if (textResponse === 'Disallowed Key Characters.') {
return Promise.reject('Invalid request');
}
else {
try {
// Hopefully if we got to this point we can parse the json
const json = JSON.parse(textResponse);
if (json.result === '0') {
return Promise.reject('Unable to get status');
}
else {
return json;
}
} catch (error) {
// If you get this error message I likely have not handled
// a error state that I wasnt aware of
return Promise.reject('Unable to parse response');
}
}
});
}
function setStatus (sessionCookie, mode) {
return new Promise((resolve, reject) => {
if (!sessionCookie || sessionCookie.length === 0) {
reject('Please call getSessionCookie to get your session cookie first');
}
if (mode !== 'arm' && mode !== 'home' && mode !== 'disarm') {
reject('Invalid mode passed to setStatus');
}
resolve(fetch(`${urls.setStatus}${mode}`, {
method: 'POST',
headers: {
'Cookie': sessionCookie,
},
}));
});
}
module.exports = {
getSessionCookie,
getStatus,
setStatus,
}
i'm every new to coding but was able to piece the below together to return the current status of my alarm. the problem is I'm unable to get it to work. based on the above code could someone please tell me what I'm missing, or if I'm going down the wrong rabbit hole....
import requests
import webbrowser
url = “https://www.yalehomesystem.co.uk/homeportal/api/login/check_login”
payload = {‘username’: ‘email#domaim.com’, ‘password’: ‘mypass’}
with requests.session() as s:
# fetch the login page
s.get(url, data=payload)
url1='https://www.yalehomesystem.co.uk/homeportal/api/panel/get_panel_mode'
# post to the login form
r = s.post(url1, data=payload)
print(r.text)
To add more contexts I'm getting the following error
{"result":"0","message":"system.permission_denied","code":"999"}
I'm trying to parallelize uploading files to dropbox and am stuck at the maximum number of parallel requests a browser allows AND those requests needing to finish before the next requests commence. The second part slows things down considerably so I'm wondering if someone has an idea where I'm making things too complicated... Here is the code:
...
function DropboxStorage(spec) {
...
if (spec.batch_upload) {
this._batch_upload = spec.batch_upload;
this._batch_buffer = parseInt(spec.batch_buffer || 3000, 10);
this._batch_dict = {"entries": [], "done": [], "defer": {}};
this._batch_resolver_list = [];
}
this._access_token = spec.access_token;
}
// I'm calling putAttachment for both single and batch upload. My idea
// was to use a trigger, which fires after _buffer_batch milliseconds
// finishing pending items unless resolved "redundant" - rejecting would
// also reject the RSVP.all call. Here is the putAttachment code:
DropboxStorage.prototype.putAttachment = function (id, name, blob) {
var context = this,
id = restrictDocumentId(id),
path;
restrictAttachmentId(name);
if (!context._batch_upload) {
...
}
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
type: "POST",
url: BATCH_START_URL,
headers: {
"Authorization": "Bearer " + context._access_token,
"Content-Type": "application/octet-stream",
"Dropbox-API-Arg": JSON.stringify({"close": false}),
}
});
})
.push(function (evt) {
var session_id = JSON.parse(evt.target.response).session_id;
path = id + "/" + name;
context._batch_dict.entries.push({
"cursor": {"session_id": session_id, "offset": blob.size},
"commit": {
"path": path,
"mode": "overwrite",
"autorename": false,
"mute": false
}
});
return jIO.util.ajax({
type: "POST",
url: BATCH_UPLOAD_URL,
headers: {
"Authorization": "Bearer " + context._access_token,
"Content-Type": "application/octet-stream",
"Dropbox-API-Arg": JSON.stringify({
"cursor": {"session_id": session_id, "offset": 0},
"close": true
})
},
data: blob
});
})
.push(function () {
var len = context._batch_resolver_list.length,
call_result_defer = new RSVP.defer(),
trigger = new RSVP.defer(),
resolver = triggerCancelableBatchResolver(context, trigger.promise);
// resolve previous call without finishing the batch
if (0 < len && len < 1000 &&
context._batch_dict.entries.length !== 0) {
context._batch_resolver_list[len - 1].resolve(true);
}
context._batch_dict.defer[path] = call_result_defer;
context._batch_resolver_list.push(trigger);
// ISSUE: doing the below will parallelize to the max of concurrent
// requests a browser supports and wait for those to finish
// return RSVP.all([call_result_defer.promise, resolver]);
// this works, but without an answer for every request made
return;
})
.push(undefined, function (error) {
throw error;
});
};
The triggerCancelableBatchResolver looks like this:
function triggerCancelableBatchResolver(context, trigger) {
return new RSVP.Queue()
// wait for 3000ms OR the trigger being resolved
.push(function () {
return RSVP.any([RSVP.delay(context._batch_buffer), trigger]);
})
.push(function (is_redundant) {
// trigger => nothing happens
if (is_redundant) {
return;
}
// finish all pending items
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
type: "POST",
url: BATCH_FINISH_URL,
headers: {
"Authorization": "Bearer " + context._access_token,
"Content-Type": "application/json",
},
data: JSON.stringify({
"entries": context._batch_dict.entries
})
});
})
.push(function (evt) {
context._batch_dict.entries = [];
context._batch_resolver_list = [];
return pollForCommitFinish(
context, JSON.parse(evt.target.response).async_job_id
);
})
.push(function (entries) {
context._batch_dict.done = context._batch_dict.done.concat(entries);
// no more requests coming in, finish
if (context._batch_dict.entries.length === 0) {
return finishBatch(context);
}
});
});
}
// recursively loop until commits on Dropbox have completed
function pollForCommitFinish(context, job_id) {
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
type: "POST",
url: BATCH_STATUS_URL,
headers: {
"Authorization": "Bearer " + context._access_token,
"Content-Type": "application/json",
},
data: JSON.stringify({"async_job_id": job_id})
});
})
.push(function (evt) {
var status = JSON.parse(evt.target.response);
if (status['.tag'] === "in_progress") {
return new RSVP.Queue()
.push(function () {
return RSVP.delay(context._batch_buffer);
})
.push(function () {
return pollForCommitFinish(context, job_id);
});
}
return status.entries;
})
}
// finish the batch returning the correct response for every request
function finishBatch(context) {
return new RSVP.Queue()
.push(function () {
var defers = context._batch_dict.defer;
return RSVP.all(context._batch_dict.done.map(function (item) {
var path = item.path_display;
if (defers[path] === undefined) {
//throw;
}
// return a response to every batch request made
if (item[".tag"] === "failure") {
return context._batch_dict.defer[path].reject(item);
}
return context._batch_dict.defer[path].resolve(item);
}));
})
.push(function () {
context._batch_dict.done = [];
context._batch_dict.defer = {};
console.log("DONE")
});
}
The code works, but as mentioned above, if I want to return a response for every request made (or throw in case of issues), I need to return:
return RSVP.all([call_result_defer.promise, resolver]);
which will stall until call_result_defer.promise is resolved at the end of the batch with batch size being limited to number of parallel requests allowed by the browser.
If I instead just
return;
I can load up to 1000 files into the batch before resolving. However at the cost of being able to return a reply for individual calls to putAttachment.
I have tried returning a promise instead of a defer and calling it's resolve/reject methods once done, but with the same result.
Question:
Is there a promise way to return something which "flags" the request as done and returns a result once available? I thought promises where just that, but somehow I cannot get it to work here.
Thanks for help!
Controller.js
var vm = this;
vm.admin = {};
vm.add = function () {
API.addAdmin(token, vm.admin)
.then(function (resp) {
vm.hideForm = true;
vm.showButton = true;
Notify.green(resp);
}, function (resp) {
Notify.red(resp);
});
};
API.js
function addAdmin(token, dataObj) {
return Constant.getApiUrl()
.then(function (url) {
$http({
method: 'POST',
url: url + '/client/admin',
headers: {
'Token': token
},
data: dataObj
}).then(handleResp);
function handleResp(resp) {
var responseStatus = (resp.status >= 200 && resp.status < 300) ? 'good' : 'bad';
if (responseStatus === 'good') {
console.log("Success" + resp);
return resp;
} else {
console.log("Failed" + resp);
return resp;
}
}
})
}
If I get a success response in API then i need to connect it to success function in my controller and if i get error message in my API, then i need it to connect it to error function in my controller.How should I evaluate the response status from my API(is either success or error).
I don't want to pass successfn, errorfn from my controller to API(only if there's no alternative).
I need to get the response data from API to controller to show it in Notify message.
Thank You!
In service (assign response values in "originalData"):
angular.module('appname').service('myserviceName', function(yourExistingService){
this.myFunction= function(originalData) {
//for next line to work return promise from your addAdmin method.
var promise = yourExistingService.getResponseFromURL(originalData);
return promise;
}
});
And in your controller :
var promise = myserviceName.myFunction($scope.originalData);
promise.$promise.then(function() {
console.log($scope.originalData);
});
And then you can check you "originalData" and write code according to your need.For more detail you can have a look on this http://andyshora.com/promises-angularjs-explained-as-cartoon.html.