There are numerous examples out there for initializing a service worker with a single cache similar to the following:
let cacheName = 'myCacheName-v1';
let urlsToCache = ['url1', 'url2', url3'];
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(cacheName).then(function (cache) {
return cache.addAll(urlsToCache);
}).then(function () {
return this.skipWaiting();
})
);
});
I wish to initialize multiple caches on my service worker. The motivation is to group assets by their tendency for change (e.g., static app data vs. css, javascript, etc.). With multiple caches, I can update individual caches (via versioned cache names) as files within that cache change. Ideally, I wish to setup a structure similar to the following:
let appCaches = [{
name: 'core-00001',
urls: [
'./',
'./index.html', etc...
]
},
{
name: 'data-00001',
urls: [
'./data1.json',
'./data2.json', etc...
]
},
etc...
];
My best attempt so far is something similar to:
self.addEventListener('install', function (event) {
appCaches.forEach(function (appCache) {
event.waitUntil(
caches.open(appCache.name).then(function (cache) {
return cache.addAll(appCache.urls);
}));
});
self.skipWaiting();
});
This approach seems to work. However, I'm still a newbie to service workers and promises. Something tells me that this approach has a pitfall that I'm too inexperienced to recognize. Is there a better way to implement this?
It's better to call event.waitUntil only once in a handler, but the good thing is it's relatively easy to get a single Promise to wait for.
Something like that should work:
event.waitUntil(Promise.all(
myCaches.map(function (myCache) {
return caches.open(myCache.name).then(function (cache) {
return cache.addAll(myCache.urls);
})
)
));
Promise.all takes an Array of Promises and resolves only after all the promises in the Array resolve, which means that install handler will wait till all the caches are initialized.
Thanks pirxpilot! Your answer, combined with JavaScript Promises: an Introduction, and a lot of trial and error figuring out how to chain all these Promises together resulted in a nice little implementation.
I added a requirement to only cache changes. Per best practices, the old cache is deleted during the 'activate' event. The end product is:
let cacheNames = appCaches.map((cache) => cache.name);
self.addEventListener('install', function (event) {
event.waitUntil(caches.keys().then(function (keys) {
return Promise.all(appCaches.map(function (appCache) {
if (keys.indexOf(appCache.name) === -1) {
return caches.open(appCache.name).then(function (cache) {
console.log(`caching ${appCache.name}`);
return cache.addAll(appCache.urls);
})
} else {
console.log(`found ${appCache.name}`);
return Promise.resolve(true);
}
})).then(function () {
return this.skipWaiting();
});
}));
});
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (keys) {
return Promise.all(keys.map(function (key) {
if (cacheNames.indexOf(key) === -1) {
console.log(`deleting ${key}`);
return caches.delete(key);
}
}));
})
);
});
Related
I am new to IndexedDB and serviceworkers and am having a very difficult time understanding how to turn these into a funcitonal application. I've done extensive reading on both, but even the "complete" examples don't incorporate the two.
I am tasked with creating an application that will allow users to work offline. The first time they connect to the site, I want to pull specific information from the database and store it in IndexedDB. When they go offline, I need to use that data to display information on the page. Certain interactions will cause the data to update, then to be synced later once an internet connection is reestablished. From a high-level, I udnerstand how this works.
It is my understanding that we cannot call functions from the serviceworker.js file due to the asynchronous nature of serviceworkers. Additionally, serviceworkers.js cannot directly update the DOM. However, the examples I have seen are creating and managing the IndexedDB data within the serviceworkers.js file.
So let's say I have a file:
<!-- index.html -->
<html>
<body>
Hello <span id="name"></span>
</body>
</html>
And a serviceworker.js:
var CACHE_NAME = 'my-cache-v1';
var urlsToCache = [
'/'
// More to be added later
];
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('activate', function(event) {
event.waitUntil(
createDB() //Use this function to create or open the database
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request).then(
function(response) {
// Check if we received a valid response
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
function createDB() {
idb.open('mydata', 1, function(upgradeDB) {
var store = upgradeDB.createObjectStore('user', {
keyPath: 'id'
});
store.put({id: 1, name: 'John Doe'}); //This can be updated with an AJAX call to the database later
});
}
How do I now update the element "name" with the value for key = 1 from the "user" objectstore in the "mydata" database?
Depending on your use case, you've got several options :
You dont need the service worker. Just pull your data from iDB directly from the page. The DOM has access to iDB.
Set a template for your index.html. At the activate step in service worker, pre-render the page with the value from iDB and cache it.
My PWA has a large data payload. I want to display a 'Please wait...' load page and wait until all caching is complete before launching the full app. Therefore, I need to detect when all caching has completed. The snippet of my service worker is:
let appCaches = [{
name: 'pageload-core-2018-02-14.002',
urls: [
'./',
'./index.html',
'./manifest.json',
'./sw.js',
'./sw-register.js'
]
},
{
name: 'pageload-icon-2018-02-14.002',
urls: [
'./icon-32.png',
'./icon-192.png',
'./icon-512.png'
]
},
{
name: 'pageload-data-2019-02-14.002',
urls: [
'./kjv.js'
]
}
];
let cacheNames = appCaches.map((cache) => cache.name);
self.addEventListener('install', function (event) {
console.log('install');
event.waitUntil(caches.keys().then(function (keys) {
return Promise.all(appCaches.map(function (appCache) {
if (keys.indexOf(appCache.name) === -1) {
caches.open(appCache.name).then(function (cache) {
return cache.addAll(appCache.urls).then(function () {
console.log(`Cached: ${appCache.name} # ${Math.floor(Date.now() / 1000)}`);
});
});
} else {
console.log(`Found: ${appCache.name}`);
return Promise.resolve(true);
}
})).then(function () {
// Happens first; expected last.
console.log(`Cache Complete # ${Math.floor(Date.now() / 1000)}`);
});
}));
self.skipWaiting();
});
When I test this with a simulated 3G network, the trace is:
I do not understand why the 'Cache Complete' message is logged before any of the individual 'Cached' messages are logged; I would expect it to be last. Is there something different about the way Promise.all behaves compared to other promises?
Oy! What a silly oversight. After breaking the promise chains into individual promises and stepping through the code, the problem became obvious.
self.addEventListener('install', function (event) {
console.log('install');
event.waitUntil(caches.keys().then(function (keys) {
return Promise.all(appCaches.map(function (appCache) {
if (keys.indexOf(appCache.name) === -1) {
// Never returned the promise chain to map!!!
return caches.open(appCache.name).then(function (cache) {
return cache.addAll(appCache.urls).then(function () {
console.log(`Cached: ${appCache.name} # ${Math.floor(Date.now() / 1000)}`);
});
});
} else {
console.log(`Found: ${appCache.name}`);
return Promise.resolve(true);
}
})).then(function () {
console.log(`Cache Complete # ${Math.floor(Date.now() / 1000)}`);
});
}));
self.skipWaiting();
});
I never returned the promise chain to the map function (no explicit return always returns undefined). So the array passed to Promise.all contained only undefined values. Therefore, it resolved immediately and hence logged its message before the others.
Live and learn...
#claytoncarney Do you know how to pass a callback.. or any way to listen to this event from my app?
I try to send a toast message to my user telling them that the data has been cached...
In this case, I send an alert (it's do not work)...
Currently i'm working with this module for angular: Angular web bluetooth
I can find an example how to read characteristics and got it working. But there are no samples to write (or notify). I am pretty sure that this module is capable of this tasks, see here, but my knowledge of Angular (or even JS?) isnt enough.
Did someone have any idea where to find examples or/and can provide them?
Snippet to read battery level:
getBatteryLevel() {
console.log('Getting Battery Service...');
try {
return this.ble
.discover$({
acceptAllDevices: true,
optionalServices: [BatteryLevelService.GATT_PRIMARY_SERVICE]
})
.mergeMap((gatt: BluetoothRemoteGATTServer) => {
return this.ble.getPrimaryService$(
gatt,
BatteryLevelService.GATT_PRIMARY_SERVICE
);
})
.mergeMap((primaryService: BluetoothRemoteGATTService) => {
return this.ble.getCharacteristic$(
primaryService,
BatteryLevelService.GATT_CHARACTERISTIC_BATTERY_LEVEL
);
})
.mergeMap((characteristic: BluetoothRemoteGATTCharacteristic) => {
return this.ble.readValue$(characteristic);
})
.map((value: DataView) => value.getUint8(0));
} catch (e) {
console.error('Oops! can not read value from %s');
}
}
Thank you.
Edit: thanks to the assistance of Aluan Haddad i was able to perform a write request, with the right Service and Characteristic UUID
.mergeMap((characteristic: BluetoothRemoteGATTCharacteristic) => {
let value = new Uint8Array(1);
value[0] = 2;
return this.ble.writeValue$(characteristic, value);
}).subscribe();
I need to call an API recursively using request promise after getting result from API need to write in an excel file , API sample response given below
{
"totalRecords": 9524,
"size": 20,
"currentPage": 1,
"totalPages": 477,
"result": [{
"name": "john doe",
"dob": "1999-11-11"
},
{
"name": "john1 doe1",
"dob": "1989-12-12"
}
]
}
Now I want to call this API n times, here n is equal to totalPages, after calling each API I want to write response result to the excel files.
First write page 1 response result to excel then append page 2 response result to excel file and so on..
I have written some sample code given below
function callAPI(pageNo) {
var options = {
url: "http://example.com/getData?pageNo="+pageNo,
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
json: true
}
return request(options)
}
callAPI(1).then(function (res) {
// Write res.result to excel file
}).catch(function (err) {
// Handle error here
})
But facing problem calling recursively API and maintaining sequentially like write page 1 result first to excel file then page 2 result append to excel and so on..
Any code sample how to achieve in nodejs
You want to do something like this:
function getAllPages() {
function getNextPage(pageNo) {
return callAPI(pageNo).then(response => {
let needNextPage = true;
if (pageNo === 1) {
// write to file
} else {
// append to file
}
if (needNextPage) {
return getNextPage(pageNo+1);
} else {
return undefined;
}
});
}
return getNextPage(1);
}
Obviously change that 'needNextPage' to false to stop the recursion when you're done
So you want to do 477 requests in sequence? How long do you wanna wait for this to finish? Even in paralell, this would be still too long for me.
Best: write an API that can return you a batch of pages at once. Reducing the number of requests to the backend. Maybe something like http://example.com/getData?pages=1-100 and let it return an Array; maybe like
[
{
"totalRecords": 9524,
"currentPage": 1,
"totalPages": 477,
"result": [...]
},
{
"totalRecords": 9524,
"currentPage": 2,
"totalPages": 477,
"result": [...]
},
...
]
or more compact
{
"totalRecords": 9524,
"totalPages": 477,
"pages": [
{
"currentPage": 1,
"result": [...]
},
{
"currentPage": 2,
"result": [...]
},
...
]
}
Sidenote: writing the size of the results array into the json is unnecessary. This value can easily be determined from data.result.length
But back to your question
Imo. all you want to run in sequence is adding the pages to the sheet. The requests can be done in paralell. That already saves you a lot of overall runtime for the whole task.
callApi(1).then(firstPage => {
let {currentPage, totalPages} = firstPage;
//`previous` ensures that the Promises resolve in sequence,
//even if some later request finish sooner that earlier ones.
let previous = Promise.resolve(firstPage).then(writePageToExcel);
while(++currentPage <= totalPages){
//make the next request in paralell
let p = callApi(currentPage);
//execute `writePageToExcel` in sequence
//as soon as all previous ones have finished
previous = previous.then(() => p.then(writePageToExcel));
}
return previous;
})
.then(() => console.log("work done"));
or you wait for all pages to be loaded, before you write them to excel
callApi(1).then(firstPage => {
let {currentPage, totalPages} = firstPage;
let promises = [firstPage];
while(++currentPage < totalPages)
promises.push(callApi(currentPage));
//wait for all requests to finish
return Promise.all(promises);
})
//write all pages to excel
.then(writePagesToExcel)
.then(() => console.log("work done"));
or you could batch the requests
callApi(1).then(firstPage => {
const batchSize = 16;
let {currentPage, totalPages} = firstPage;
return Promise.resolve([ firstPage ])
.then(writePagesToExcel)
.then(function nextBatch(){
if(currentPage > totalPages) return;
//load a batch of pages in paralell
let batch = [];
for(let i=0; i<batchSize && ++currentPage <= totalPages; ++i){
batch[i] = callApi(currentPage);
}
//when the batch is done ...
return Promise.all(batch)
//... write it to the excel sheet ...
.then(writePagesToExcel)
//... and process the next batch
.then(nextBatch);
});
})
.then(() => console.log("work done"));
But don't forget to add the error handling. Since I'm not sure how you'd want to handle errors with the approaches I've posted, I didn't include the error-handling here.
Edit:
can u pls modify batch requests, getting some error, where you are assigning toalPages it's not right why the totalPages should equal to firstPage
let {currentPage, totalPages} = firstPage;
//is just a shorthand for
let currentPage = firstPage.currentPage, totalPages = firstPage.totalPages;
//what JS version are you targeting?
This first request, callApi(1).then(firstPage => ...) is primarily to determine currentIndex and totalLength, as you provide these properties in the returned JSON. Now that I know these two, I can initiate as many requests in paralell, as I'd want to. And I don't have to wait for any one of them to finish to determine at what index I am, and wether there are more pages to load.
and why you are writing return Promise.resolve([ firstPage ])
To save me some trouble and checking, as I don't know anything about how you'd implement writePagesToExcel.
I return Promise.resolve(...) so I can do .then(writePagesToExcel). This solves me two problems:
I don't have to care wether writePagesToExcel returns sync or a promise and I can always follow up with another .then(...)
I don't need to care wether writePagesToExcel may throw. In case of any Error, it all ends up in the Promise chain, and can be taken care of there.
So ultimately I safe myself a few checks, by simply wrapping firstPage back up in a Promise and continue with .then(...). Considering the amounts of data you're processing here, imo. this ain't too much of an overhead to get rid of some potential pitfalls.
why you are passing array like in resolve
To stay consistent in each example. In this example, I named the function that processes the data writePagesToExcel (plural) wich should indicate that it deals with multiple pages (an array of them); I thought that this would be clear in that context.
Since I still need this seperate call at the beginning to get firstPage, and I didn't want to complicate the logic in nextBatch just to concat this first page with the first batch, I treat [firstPage] as a seperate "batch", write it to excel and continue with nextBatch
function callAPI(pageNo) {
var options = {
url: "http://example.com/getData?pageNo="+pageNo,
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
json: true
}
return request(options)
}
function writeToExcel(res){console.log(res)} //returns promise.
callAPI(1).then(function (res) {
if(res){
writeToExcel(res).then(() => {
var emptyPromise = new Promise(res => setTimeout(res, 0));
while(res && res.currentPage < res.totalPages){
emptyPromise = emptyPromise.then(() => {
return callAPI(res.currentPage).then(function (res){
if(res){
writeToExcel(res)
}
});
}
}
return emptyPromise;
});
}
}).catch(function (err) {
// Handle error here
})
I have a use case where all models relations in Ember Data are loaded async. I have a route which renders Grandparents in the example below based on whether the parent.child matches a particular model.
So far I've been able to manage to resolve the grandparent and parent model async loading but then my code becomes a massive jumble.
Are there any useful strategies for filtering out the grandparents without having to deal with promises at every level?
Example model definitions
App.Grandparent = DS.Model.extend({
...
parents: DS.hasMany('Parent', { async: true })
});
App.Parent = DS.Model.extend({
...
grandParent: DS.belongsTo('Grandparent', { async: true }),
child: DS.belongsTo('Child', { async: true })
});
App.Child = DS.Model.extend({
...
});
Code Sample
var client = this.modelFor('workspace.client');
var promise = new Ember.RSVP.Promise(function(resolve)
{
client.get('sessions').then(function(sessions)
{
Ember.RSVP.all(sessions.getEach('exercises')).then(function(exercises)
{
Ember.RSVP.all(exercises.getEach('exercise')).then(function()
{
console.log("RESOLVED");
resolve(sessions);
});
});
});
});
I think you can simple get away by chaining the promises.
var client = this.modelFor('workspace.client');
return client.get('sessions').then(function(sessions) {
return Ember.RSVP.all(sessions.getEach('exercises'));
}).then(function(exercises) {
return Ember.RSVP.all(exercises.getEach('exercise'));
}).then(function(allExercises) {
console.log("RESOLVED");
return allExercises;
});
note: not sure what you are trying to do when resolving with sessions, and not doing anything with the exercises