I added PWA and service worker to my existing web app based on ReactJs and nextJs. Everything looks fine on the first release. However, when I try to release updates, something strange is happening.
After each release, I got an alert asking me to update to a new version.
However when a new version comes up eventually my app is updated, but it falls back again to the old version after I close and reopens it.
It should be noted that this happens only on some phones.
Please help me to solve it.
My SW:
navigator.serviceWorker
.getRegistrations()
.then(function (registrations) {
return Promise.all(registrations.map(function(r) {return r.unregister()}));
})
.then(function() {
return navigator.serviceWorker.register(serviceFilePath, {
scope: _this.options.scope
}).then(function (registration) {
console.info("Service worker has been registered for scope: " + registration.scope);
if (needsUpdate) {
return registration.update().then(function () {
_this.reload();
window.parent.postMessage("new-version", "*");
console.info("Service worker has been updated.");
window.location.reload();
return true;
});
}
return true;
});
});
ServiceWorker.prototype.reload = function () {
var _this = this;
return this.getOptions(true).then(function (options) {
return Promise.all([
_this.preload(),
// _this.checkPersistence(),
_this.checkPushServiceStatus(false)
]).then(function () {
_this.debug("Clear old caches... (< v" + options.version + ")");
var promises = [];
for (var i = 0; i < options.version; i++) {
promises.push(new AdvancedServiceWorker.BrowserCache(options.storageName, i).remove());
}
return Promise.all(promises).then(function () {
return true;
});
});
}).catch(function () {
return false;
});
};
BrowserCache.prototype.remove = function (condition) {
if (!condition) {
return caches.delete(this.storageName);
} else if (typeof condition === "string" ||
condition instanceof String ||
condition instanceof RegExp) {
if (!(condition instanceof RegExp)) {
condition = AdvancedServiceWorker.Condition.wildCardToRegEx((condition));
}
return this.open().then(function (cache) {
return cache.keys().then(function (keys) {
var promises = [];
keys.forEach(function (request) {
if (request.url && condition.test(request.url)) {
promises.push(cache.delete(request));
}
});
if (!promises.length) {
return Promise.resolve(false);
}
return Promise.all(promises).then(function (results) {
for (var i = 0; i < results.length; i++) {
if (results[i]) {
return true;
}
}
return false;
}, function () {
return false;
});
});
});
} else if (condition instanceof Array && condition.length) {
return this.open().then(function (cache) {
var promises = [];
for (var i = 0; i < condition.length; i++) {
promises.push(cache.delete((condition[i])));
}
return Promise.all(promises).then(function (results) {
for (var j = 0; j < results.length; j++) {
if (results[j]) {
return true;
}
}
return false;
}, function () {
return false;
});
});
} else {
return Promise.resolve(false);
}
};
Ok, my service worker below.
Some things to consider, as this is a SW that I've made for my own needs, as such you might need to tweak for yours.
It's designed to work for a simple SPA, that loads all it's resource's on page load. Generally speaking this is how most SPA's will work. But if you have resources that you say load dynamically via fetch etc, you will want to add those files manually using cache.addAll, some JS frameworks do load routes dynamically, so just be aware.
Because I'm loading the resources automatically and not using addAll in theory you could have an offLine app that's not got all it's resources fully loaded, eg. if you just happen to loose internet during update. In practice this is not likely, and if happens a simple refresh would fix, but if you do want to make sure everything is loaded you will have to manually add them using cache.addAll, see comments in file for were to do that.
Enough explaining, here is the code. ->
ps, it's also using Typescript here, but you can remove that easily by deleting the declare on the first line. (I just like my types).
declare var self: ServiceWorkerGlobalScope;
//remember to update version number if updating.
const cacheName = "my-spa-v1.0.0";
self.addEventListener("install", (e) => {
e.waitUntil(
(async () => {
const cache = await caches.open(cacheName);
//if you want to manually add resources
//await cache.addAll([contentToCache,...]);
return self.skipWaiting();
})()
);
});
//lets keep our cache's clean
self.addEventListener("activate", (e) => {
e.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(
keyList.map((key) => {
if (key === cacheName) {
return;
}
return caches.delete(key);
})
);
})
);
});
//lets auto cache GET requests
self.addEventListener("fetch", (e) => {
e.respondWith(
(async () => {
const r = await caches.match(e.request);
if (r) {
return r;
}
const response = await fetch(e.request);
const cache = await caches.open(cacheName);
if (e.request.method === 'GET')
cache.put(e.request, response.clone());
return response;
})()
);
});
Related
NB: this javascript uses the openui5 libraries
attachOnceAsync: function(binding, callback) {
var args = [...arguments]
args.splice(0, 2)
return new Promise((resolve, reject) => {
var dataReceived = function(oEvent) {
if (typeof callback == 'function') {
if (callback.constructor.name === "AsyncFunction") {
callback(oEvent, ...args).then(() =>
resolve(oEvent)
).catch((err) => {
reject(err)
})
} else {
try {
callback(oEvent, ...args);
resolve(oEvent)
} catch (err) {
reject(err)
}
}
} else {
resolve(oEvent, ...args);
}
}
binding.attachEventOnce("dataReceived", dataReceived);
})
}
There aren't any Promise versions of events like "dataReceived" so this is my attempt to wrap one up.
In this case, callback is undefined (it shouldn't be, but that's a different issue). The line resolve(oEvent, ...args); is hit instead, only it never returns out of the await!
I changed that line to just resolve(oEvent) but still no joy.
Any ideas what I'm doing wrong?
Thanks
PS: I'll keep an eye on this question so I can offer any extra info required
here's how I call it:
handleAsync: async function(controller) {
/* Lots of setup */
var response = await fetch(/*redacted*/ )
if (response.status === 400) {
console.log(await response.json());
throw new BadRequestException();
}
if (response.status === 204){
/* more setup */
context = oPanel.getBindingContext("odata");
var updatePurchaseOrderDocTotalFunc = this.updatePurchaseOrderDocTotal
var callback = async function() {
var docTotalUpdate = await updatePurchaseOrderDocTotalFunc(oView, context, oModel, agreedPrice);
if (docTotalUpdate && docTotalUpdate.status === 204) {
await context.refresh();
}
}
//-- HERE --
await AsyncBinding.attachOnceAsync(binding,callback)
await context.refresh();
}
return true;
}
Ok - The answer was staring at me.
Sorry I wasn't able to provide a working snippet but the ui5 scripts are quite extensive.
I have several other functions that return a Promise so I can use the await/async approach, but with this one I forgot to include the action that causes the event to fire.
I renamed the function to show what it now does, and included the action that causes the event to fire:
RefreshAsync : function(context,binding, callback){
var args = [...arguments]
args.splice(0,2)
return new Promise((resolve,reject)=>{
var dataReceived = function(oEvent){
if(typeof callback == 'function' ){
if(callback.constructor.name==="AsyncFunction"){
callback(oEvent, ...args).then(()=>{
resolve(oEvent)
}).catch((err)=>{
reject(err)
})
}else{
try{
callback(oEvent, ...args);
resolve(oEvent)
}catch(err){
reject(err)
}
}
}else{
resolve([oEvent, ..args])
}
}
binding.attachEventOnce("dataReceived", dataReceived);
//This is the action I missed
context.Refresh()
})
},
I have my function whose job is to go over a number of files (that use the values from the array as building blocks for file names) and download them using a reduce. It's more of a hack as of now but the Promise logic should work. Except it doesn.t
Here's my code:
function import_demo_files(data) {
/**
* Make a local copy of the passed data.
*/
let request_data = $.extend({}, data);
const get_number_of_files_1 = Promise.resolve({
'data' : {
'number_of_files' : 2
}
});
return new Promise((resolve, reject) => {
let import_files = get_number_of_files_1.then(function(response) {
new Array(response.data.number_of_files).fill(request_data.step_name).reduce((previous_promise, next_step_identifier) => {
let file_counter = 1;
return previous_promise.then((response) => {
if( response !== undefined ) {
if('finished_import' in response.data && response.data.finished_import === true || response.success === false) {
return import_files;
}
}
const recursively_install_step_file = () => import_demo_file({
demo_handle: request_data.demo_handle,
'step_name': request_data.step_name,
'file_counter': file_counter
}).call().then(function(response) {
file_counter++;
if('file_counter' in response.data && 'needs_resume' in response.data) {
if(response.data.needs_resume === true) {
file_counter = response.data.file_counter;
}
}
return response.data.keep_importing_more_files === true ? recursively_install_step_file() : response
});
return recursively_install_step_file();
}).catch(function(error) {
reject(error);
});
}, Promise.resolve())
}).catch(function(error) {
reject(error);
});
resolve(import_files);
});
}
Now, when I do:
const import_call = import_demo_files({ 'demo_handle' : 'demo-2', 'step_name' : 'post' });
console.log(import_call);
The console.log gives me back that import_call is, in fact a promise and it's resolved. I very much like the way return allows me to bail out of a promise-chain, but I have no idea how to properly resolve my promise chain in there, so clearly, it's marked as resolved when it isn't.
I would like to do import_call.then(... but that doesn't work as of now, it executes this code in here before it's actually done because of the improper handling in import_demo_files.
An asynchronous recursion inside a reduction isn't the simplest of things to cut your teeth on, and it's not immediately obvious why you would want to given that each iteration of the recursion is identical to every other iteration.
The reduce/recurse pattern is simpler to understand with the following pulled out, as outer members :
1. the `recursively_install_step_file()` function
1. the `new Array(...).fill(...)`, as `starterArray`
1. the object passed repeatedly to `import_demo_file()`, as `importOptions`)
This approach obviates the need for the variable file_counter, since importOptions.file_counter can be updated directly.
function import_demo_files(data) {
// outer members
let request_data = $.extend({}, data);
const importOptions = {
'demo_handle': request_data.demo_handle,
'step_name': request_data.step_name,
'file_counter': 1
};
const starterArray = new Array(2).fill(request_data.step_name);
function recursively_install_step_file() {
return import_demo_file(importOptions).then((res) => {
if('file_counter' in res.data && 'needs_resume' in res.data && res.data.needs_resume) {
importOptions.file_counter = res.data.file_counter; // should = be += ?
} else {
importOptions.file_counter++;
}
return res.data.keep_importing_more_files ? recursively_install_step_file() : res;
});
}
// the reduce/recurse pattern
return starterArray.reduce((previous_promise, next_step_identifier) => { // next_step_identifier is not used?
let importOptions.file_counter = 1; // reset to 1 at each stage of the reduction?
return previous_promise.then(response => {
if(response && ('finished_import' in response.data && response.data.finished_import || !response.success)) {
return response;
} else {
return recursively_install_step_file(); // execution will drop through to here on first iteration of the reduction
}
});
}, Promise.resolve());
}
May not be 100% correct but the overall pattern should be about right. Be prepared to work on it some.
I have a problem with accessing code that I am able to use through browser console.
In my case it is a Tawk_Api function Tawk_API.hideWidget();
I tried to use browser execute and call but the output saying that Tawk.Api is not defined
Code example
var expect = require('chai').expect;
function HideTawk (){
Tawk_API.hideWidget();
}
describe('', function() {
it('should be able to filter for commands', function () {
browser.url('https://arutech.ee/en/windows-price-request');
$('#uheosaline').click();
browser.execute(HideTawk());
var results = $$('.commands.property a').filter(function (link) {
return link.isVisible();
});
expect(results.length).to.be.equal(3);
results[1].click();
expect($('#getText').getText()).to.be.equal('GETTEXT');
});
});
Working fixed function:
function HideTawk (){
return new Promise(function(resolve, reject) {
Tawk_API.hideWidget();
})
}
And browser.execute(HideTawk()) is a mistake it should be browser.call(HideTawk());
docs: http://webdriver.io/api/utility/call.html
I have a below code in my application base object it can help you to understand call api:
_callClientAPI(func, args) {
let trial = 1;
return new Promise(async(res, rej) => {
while (true) {
if (trial > this._pollTrials) {
rej(`Could not retrieve the element in this method * this._pollTimeout} seconds.`);
break;
}
let result;
try {
result = await func.call(this.client, args, false);
} catch (e) { }
if (result && result !== '') {
res(result);
break;
}
await this.wait();
trial++;
}
});
}
I have started using axios, a promise based HTTP client and I haven't figured out how to return a result based on a promise value.
Here is a simplified version of my code:
function isUnique(cif) {
countClients(cif).then(function(count) {
return (count == 0);
});
}
function countClients(cif) {
return axios.get('/api/clients?cif=' + cif)
.then(function(response) {
let clients = response.data;
return clients.length;
})
.catch(function(error) {
console.log(error);
return false;
});
}
I expect the isUnique function to return a boolean value based on countClients output.
You cannot return a synchronous value based on asynchronous calculations. Javascript provides no way for that intentionally. What you can do is to return a Promise<boolean>:
function isUnique(cif) {
return countClients(cif).then(function(count) {
return (count == 0);
});
}
UPDATE
So you need to supply this function to a third party library, and it only works with functions of type (x:T) => boolean, but not (x:T) => Promise<boolean>. Unfortunately you still cannot "wait for" a promise, this is not how the JavaScript event loop works.
The right solution
Use a validation library that supports async validation functions.
The workaround
I don't recommend this, but you could cache all the values you might use prior to action.
So for example, lets say this is how you would call the third party:
function isUnique(cif) {
return true; // Dummy mock
}
var result = ThirdParty.doValidation(isUnique);
console.log(result);
Instead, you can write something like this:
function isUnique(cif) {
return countClients(cif).then(function(count) {
return (count == 0);
});
}
function getRelevantCifs() {
return axios.get("/api/all-client-cifs");
}
var isUniqueCache = new Map();
function isUniqueCached(cif) {
return isUniqueCache.get(cif);
}
function buildCache() {
return getRelevantCifs().then(cifs => {
return Promise.all(cifs.map(cif => {
return isUnique(cif).then(isUniqueResult => {
isUniqueCache.set(cif, isUniqueResult);
});
}));
});
}
buildCache().then(() => {
var result = ThirdParty.doValidation(isUniqueCached);
console.log(result);
});
This is a classic fallback solution. If the first el does not get rendered, then it's retrying it with other renderers. How to best refactor this?
What's wrong with this code is that:
Renderers need to be in an array, but here, they're in then blocks.
They need to fall down to another renderer when the prior renderer didn't work
The first renderer is kind of the same algo like the 2th and other renderers. It checks whether the result is empty. There is a duplication there. But, the first one needs to run first, and the 2nd and others can run together, they can stop whenever one of them returns a result.
let first = $('#id1');
return this.render('priorityRenderer', first).then(isEmpty => {
let els = [
$('#id2'), $('#id3')
];
if (isEmpty) {
// put back the first el to the pool to render it again below
els.unshift(first);
}
return Promise.all(els.map(el => {
return this.render('secondaryRenderer', el)
.then(result => {
if (result) {
return result;
} else {
return this.render('3thRenderer', el)
}
})
.then(result => {
if (result) {
return result;
} else {
return this.render('4thRenderer', el)
}
})
.then(result => {
if (result) {
return result;
} else {
return this.render('5thRenderer', el)
}
});
})
});
You can use one of these approaches to call a chain of renderers until the first one returns a result:
renderers(rs, el, i=0) {
if (i < rs.length)
return this.render(rs[i], el).then(result => result || this.renderers(rs, el, i+1));
else
return Promise.reject(new Error("no renderer worked, tried: "+rs));
}
Then inside your method, you can do
let first = $('#id1');
let els = [$('#id2'), $('#id3')];
let rs = ['secondaryRenderer', '3rdRenderer', '4thRenderer', '5thRenderer'];
return Promise.all([
this.renderers(['priorityRenderer'].concat(rs), first)
].concat(els.map(el =>
this.renderers(rs, el)
)));