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.
Related
I try to add some routes in cache using message event.
On every page, there are several on dynamic that I would like to keep in cache. For this, i send an array of URL to my Service Worker on document load :
window.addEventListener('load', () => {
if (serviceWorker.isServiceWorkerSupported()) {
serviceWorker.register();
if (typeof PRECACHE_ROUTES !== 'undefined') {
serviceWorker.sendPreCacheRoutesToSW(PRECACHE_ROUTES);
}
}
});
But, with this method, if user have no network, the StaleWhileRevalidate same not work, you can see an example :
registerRoute(
'/',
new StaleWhileRevalidate({
cacheName: 'routes', // Work on offline
plugins,
}),
);
self.addEventListener('message', event => {
if (event.data && event.data.type === 'PRECACHE_ROUTES') {
event.data.routes.forEach(route => {
registerRoute(
route,
new StaleWhileRevalidate({
cacheName: 'routes', // Not work on offline
}),
);
});
event.waitUntil(
caches.open('routes').then(cache => cache.addAll(event.data.routes)),
);
}
});
All urls are well cached, but do not seem to be taken into account offline.
Anyone can help me ?
I would suggest following this recipe using workbox-window and workbox-routing to accomplish that:
// From within your web page, using workbox-window:
const wb = new Workbox('/sw.js');
wb.addEventListener('activated', (event) => {
// Get the current page URL + all resources the page loaded.
// Replace with a list of URLs obtained elsewhere, as needed.
const urlsToCache = [
location.href,
...performance.getEntriesByType('resource').map((r) => r.name),
];
// Send that list of URLs to your router in the service worker.
wb.messageSW({
type: 'CACHE_URLS',
payload: {urlsToCache},
});
});
// Register the service worker after event listeners have been added.
wb.register();
This will automatically apply the routes defined in your service worker to the URLs you provide in the payload.
Dynamically setting up routes for those URLs inside of your message event isn't going to give you the behavior you're after, as you've found.
Using Workbox in a service worker in a javascript webapp.
Want to clear the entire workbox/application cache of all content... basically go back to a state as similar as possible to the state before first load of the app into a browser, potentially to be followed by refreshing via window.location.href = '/'.
Googling and looking on SO, I have found various recipes for clearing various things from the cache. I have not been able to figure out a simple way to just 'clear everything and start over'.
I tried this in server code in sw.js:
var logit = true;
if (logit) console.log('Service Worker Starting Up for Caching... Hello from sw.js');
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.6.1/workbox-sw.js');
if (workbox) {
if (logit) console.log(`Yay! Workbox is loaded 🎉`);
} else {
if (logit) console.log(`Boo! Workbox didn't load 😬`);
}
workbox.routing.registerRoute(
// Cache image files
/.*\.(?:mp3|flac|png|gif|jpg|jpeg|svg|mp4)/,
// Use the cache if it's available
workbox.strategies.cacheFirst({
// Use a custom cache name
cacheName: 'asset-cache',
plugins: [
new workbox.expiration.Plugin({
// Cache only 20 images
maxEntries: 20,
// Cache for a maximum of x days
maxAgeSeconds: 3 * 24 * 60 * 60,
})
],
})
);
self.addEventListener('message', function(event){
msg = event.data;
console.log("SW Received Message: " + msg);
if (msg==='clearCache') {
console.log('Clearing Workbox Cache.');
WorkBoxCache = new workbox.expiration.Plugin;
WorkBoxCache.expirationPlugin.deleteCacheAndMetadata();
//WorkBoxCacheServer.clear();
}
});
paired with this on the client:
navigator.serviceWorker.controller.postMessage("clearCache");
This didn't work, though the message was apparently passed. Also, this seems an inelegant solution and I presume there is a simpler one.
How can this be done?
How can it be initiated from the client side in client side js on the browser? What does this require in server side code (eg in sw.js).
Thank you
CacheStorage is accessible in the client code (where you register the SW) so you can delete it from there.
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
});
If we only delete the cache then it damage service worker ,service worker will not work properly, so we have to unregister service worker then have to delete cache and then reregister service worker.
refreshCacheAndReload = () => {
if ('serviceWorker' in navigator) {
serviceWorkerRegistration.unregister();
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
}).then(() => {
serviceWorkerRegistration.register();
})
}
setTimeout(function () { window.location.replace(""); }, 300)
}
Demo video: https://www.youtube.com/watch?v=UBfnvx6jC_A
I followed along with Udacity's Offline Web Applications course in order to get my app working offline. Here is my code:
main.js
// other stuff above
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/service-worker.js').catch(function() {
console.log('Service worker registration failed.');
});
}
service-worker.js
let currCacheName = 'premium-poker-tools-1';
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(currCacheName).then(function(cache) {
let promise = cache.addAll([
'/',
'app.js',
// 'c7d016677eb7e912bc40.worker.js',
// 'f328c7e2b379df12fa4c.worker.js',
'static/logo.png',
'static/favicon.png',
'static/loading.svg',
'static/cards/ace-of-clubs.png',
'static/cards/king-of-clubs.png',
'static/cards/queen-of-clubs.png',
'static/cards/jack-of-clubs.png',
'static/cards/ten-of-clubs.png',
'static/cards/nine-of-clubs.png',
'static/cards/eight-of-clubs.png',
'static/cards/seven-of-clubs.png',
'static/cards/six-of-clubs.png',
'static/cards/five-of-clubs.png',
'static/cards/four-of-clubs.png',
'static/cards/three-of-clubs.png',
'static/cards/two-of-clubs.png',
'static/cards/ace-of-spades.png',
'static/cards/king-of-spades.png',
'static/cards/queen-of-spades.png',
'static/cards/jack-of-spades.png',
'static/cards/ten-of-spades.png',
'static/cards/nine-of-spades.png',
'static/cards/eight-of-spades.png',
'static/cards/seven-of-spades.png',
'static/cards/six-of-spades.png',
'static/cards/five-of-spades.png',
'static/cards/four-of-spades.png',
'static/cards/three-of-spades.png',
'static/cards/two-of-spades.png',
'static/cards/ace-of-hearts.png',
'static/cards/king-of-hearts.png',
'static/cards/queen-of-hearts.png',
'static/cards/jack-of-hearts.png',
'static/cards/ten-of-hearts.png',
'static/cards/nine-of-hearts.png',
'static/cards/eight-of-hearts.png',
'static/cards/seven-of-hearts.png',
'static/cards/six-of-hearts.png',
'static/cards/five-of-hearts.png',
'static/cards/four-of-hearts.png',
'static/cards/three-of-hearts.png',
'static/cards/two-of-hearts.png',
'static/cards/ace-of-diamonds.png',
'static/cards/king-of-diamonds.png',
'static/cards/queen-of-diamonds.png',
'static/cards/jack-of-diamonds.png',
'static/cards/ten-of-diamonds.png',
'static/cards/nine-of-diamonds.png',
'static/cards/eight-of-diamonds.png',
'static/cards/seven-of-diamonds.png',
'static/cards/six-of-diamonds.png',
'static/cards/five-of-diamonds.png',
'static/cards/four-of-diamonds.png',
'static/cards/three-of-diamonds.png',
'static/cards/two-of-diamonds.png',
'static/feedback/1.png',
'static/feedback/2.png',
'static/feedback/3.png',
'static/feedback/4.png',
'static/feedback/flop-selector.png',
'static/feedback/green-grid-squares.png',
'static/feedback/user-set-range-to-simulate-to-street.png',
'static/guides/beginners-guide/1.png',
'static/guides/beginners-guide/2.png',
'static/guides/beginners-guide/3.png',
'static/guides/beginners-guide/4.png',
'static/guides/beginners-guide/5.png',
'static/guides/beginners-guide/6.png',
'static/guides/beginners-guide/7.png',
'static/guides/beginners-guide/8.png',
'static/guides/beginners-guide/9.png',
'static/guides/beginners-guide/10.png',
'static/guides/beginners-guide/11.png',
'static/guides/beginners-guide/12.png',
'static/guides/beginners-guide/13.png',
'static/guides/beginners-guide/14.png',
'static/guides/beginners-guide/15.png',
'static/guides/beginners-guide/16.png',
'static/guides/beginners-guide/17.png',
'static/guides/beginners-guide/18.png',
'static/guides/beginners-guide/19.png',
'static/guides/beginners-guide/20.png',
'static/guides/beginners-guide/21.png',
'static/guides/faq/double-counting/1.png',
'static/guides/faq/hit-percentage-calculation/1.png',
'static/guides/faq/hit-percentage-calculation/2.png',
'static/guides/faq/hit-percentage-calculation/3.png',
'static/guides/faq/insights/1.png',
'static/guides/faq/insights/2.png',
'static/guides/faq/insights/3.png',
'static/guides/faq/insights/4.png',
'static/guides/faq/insights/5.png',
'static/guides/faq/insights/6.png',
'static/guides/faq/insights/7.png',
'static/guides/faq/insights/8.png',
'static/guides/faq/set-checks-to-default/1.png',
'static/guides/quick-guide/1.png',
'static/guides/quick-guide/2.png',
'static/guides/quick-guide/3.png',
'static/guides/quick-guide/4.png',
'static/guides/quick-guide/5.png',
'static/guides/quick-guide/6.png',
'static/guides/quick-guide/7.png',
'static/guides/quick-guide/8.png',
'static/guides/quick-guide/save-load-scenario.png',
'static/home/1.png',
'static/home/2.png',
'static/home/3.png',
'static/settings/equity-calculator-insights-not-visible.png',
'static/settings/equity-calculator-insights-visible.png',
'static/settings/outcome-analyzer-checkboxes-collapsed-1.png',
'static/settings/outcome-analyzer-checkboxes-collapsed-2.png',
'static/settings/outcome-analyzer-checkboxes-included-1.png',
'static/settings/outcome-analyzer-checkboxes-included-2.png',
'static/settings/outcome-analyzer-hands.png',
'static/settings/outcome-analyzer-insights-not-visible.png',
'static/settings/outcome-analyzer-insights-visible.png',
'static/settings/saved-ranges-1.png',
'static/settings/saved-ranges-2.png',
'static/settings/saved-ranges-3.png',
'static/settings/saved-ranges-4.png',
'static/settings/included-selectors/double-slider-selector.png',
'static/settings/included-selectors/log-double-slider-selector.png',
'static/settings/included-selectors/saved-ranges-selector.png',
'static/settings/included-selectors/single-slider-selector.png',
'static/settings/included-selectors/tier-and-category-selector.png',
'static/settings/tiers/tiers.png',
'static/settings/visual/dont-show-num-combos-in-range.png',
'static/settings/visual/green-grid-squares.png',
'static/settings/visual/multicolored-grid-squares.png',
'static/settings/visual/show-num-combos-in-range.png',
]).then(function () {
console.log('Successfully cached everything.')
}).catch(function (error) {
console.log('Problem caching: ', error);
});
return promise;
}).catch(function () {
console.error('Error with caches.open or cache.addAll');
})
);
});
self.addEventListener('activate', function(event) {
console.log('activate');
event.waitUntil(
caches.keys()
.then(function getOldCachesThatBeginWithPremiumPokerToolsDash (cacheNames) {
console.log(cacheNames);
return cacheNames.filter(function (cacheName) {
return cacheName.startsWith('premium-poker-tools-') && (cacheName !== currCacheName);
});
})
.then(function removeOldCachesThatBeginWithPremiumPokerToolsDash (oldCachesThatBeginWithPremiumPokerToolsDash) {
console.log(oldCachesThatBeginWithPremiumPokerToolsDash)
let removeCachePromises = [];
oldCachesThatBeginWithPremiumPokerToolsDash.forEach(function (oldCacheThatBeginsWithPremiumPokerToolsDash) {
removeCachePromises.push(caches.delete(oldCacheThatBeginsWithPremiumPokerToolsDash));
});
console.log(removeCachePromises);
return Promise.all(removeCachePromises);
})
);
});
self.addEventListener('fetch', function(event) {
console.log('fetch');
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
}).catch(function () {
console.error('Error trying to match event request to cache.');
})
);
});
The issue comes when I am in a state where I have a service worker installed and active, and I have all of my stuff cached. When I have the dev tools open and have "update on reload" checked in Chrome, if I reload:
The page looks the same, but has the spinner indicating that it is still loading.
In the dev tools, it shows that a new service worker is "waiting to activate".
In the network tab, it shows that the request to http://localhost:8080/ is continually pending.
"Successfully cached everything." is the only thing that gets logged to the console. "activate" doesn't get logged, and neither does "fetch".
But if I press the "x" in Chrome to tell it to stop loading, and then refresh again, it loads perfectly.
I can't seem to figure out what is wrong. / is in the premium-poker-tools-1 cache, so shouldn't the request hit the service worker and return the cached HTML? And even if it doesn't find it there, shouldn't it be sending a request out to the server to get the response? How is it getting hung up?
Edit: I now understand that the service worker is replaced when "Update on reload" is checked even if the service worker hasn't changed.
This answer is moved from the bottom of the question, to make it clearer what the underlying issue is:
I now
understand
that the service worker is replaced when "Update on reload" is checked
even if the service worker hasn't changed.
Right now I am replicating my entire device database over to my remote database.
Once that is complete, I grab all my data that is not older than 1 month from my remote database, using a filter, and bring it to my device.
FILTER
{
_id: '_design/filters',
"filters": {
"device": function(doc, req) {
if(doc.type == "document" || doc.type == "signature") {
if(doc.created >= req.query.date) return true;
else return false;
}
else return true;
}
}
}
REPLICATION
device_db.replicate.to(remote_db)
.on('complete', function () {
device_db.replicate.from(remote_db, {
filter: "filters/device",
query_params: { "date": (Math.floor(Date.now() / 1000)-2419200) }
})
.on('complete', function () {
console.log("localtoRemoteSync replicate.to success");
callback(true);
});
});
My question:
I want to be able to periodically delete data from my device that is older than 3 months (old enough data where I already know it's been sync'd)
But just because I delete it from my device, when I replicate the data back to my remote_db, I don't want it to be deleted on there too.
How can I delete specific data on my device but not have that deletion translated when I replicate?
FILTERS
Here, we have 2 filters:
noDeleted : This filter doesn't push _deleted documents.
device : Filter to get the latest data only.
{
_id: '_design/filters',
"filters": {
"device": function(doc, req) {
if (doc.type == "document" || doc.type == "signature") {
if (doc.created >= req.query.date) return true;
else return false;
}
return true;
},
"noDeleted": function(doc, req) {
//Document _deleted won't pass through this filter.
//If we delete the document locally, the delete won't be replicated to the remote DB
return !doc._deleted;
}
}
}
REPLICATION
device_db.replicate.to(remote_db, {
filter: "filters/noDeleted"
})
.on('complete', function() {
device_db.replicate.from(remote_db, {
filter: "filters/device",
query_params: { "date": (Math.floor(Date.now() / 1000) - 2419200) }
})
.on('complete', function() {
console.log("localtoRemoteSync replicate.to success");
callback(true);
});
});
Workflow
You push all your documents without pushing the deleted document.
You get all the updates for the latest data
You delete your old documents
You could either query the remote DB to get the ids of the documents that are too old and delete them locally. Note that the documents will still be there as _deleted. To completely remove them, a compaction will be required.
You could also totally destroy your local database after step1 and start from scratch.
callback(true);
Add a one-way filtered replication. However anything you need back on the server you will need to use a put request with the server's _rev.
For example
Replicate from server to client, then add a filter mechanism, like transfer:true to the docs you want to replicate. replication
db.replicate.from(remoteDB, {
live: true,
retry: true,
selector: {transfer:true}// or any other type of selector
});
To delete a doc on the client, set transfer to false, then delete it on the client. it won't meet your filter criteria so it won't replicate.
Anything you want to put back to the server use a put request instead of replicate.
If you want the document back on the client just set transfer to true in the doc.
I have a simple web app based on this project ( https://github.com/arthurkao/angular-drywall ), running with NodeJS and AngularJS as the front-end.
I'm trying to set up a simple page that displays a list of all connected users on a map (using Google Maps, Geolocation and PubNub).
Here's how I'm actually doing it:
angular.module('base').controller('TravelCtrl',
function($rootScope, $scope, NgMap, security, $geolocation, PubNub){
$rootScope.extusers = []; //remote users
$scope.initTravel = function() { //declare the init function
PubNub.init({
subscribe_key: $rootScope.security.keys.psk,
publish_key: $rootScope.security.keys.ppk,
uuid: $rootScope.security.currentUser.username,
ssl: true
});
PubNub.ngSubscribe({
channel: "travel",
state: {
position: {},
}
});
console.log("Loaded Travel");
$geolocation.getCurrentPosition({
timeout: 60000
}).then(function(position) { //when location is retreived
$scope.position = position;
PubNub.ngSubscribe({
channel: "travel",
state: {
position: {
lat: Math.floor($scope.position.coords.latitude*1000)/1000, //decrease accuracy
long: Math.floor($scope.position.coords.longitude*1000)/1000,
},
}
});
$rootScope.$on(PubNub.ngPrsEv("travel"), function(event, payload) {
$scope.$apply(function() {
$scope.extusers = PubNub.ngPresenceData("travel");
});
});
PubNub.ngHereNow({ channel: "travel" });
$scope.showInfo = function(evt, marker) { //show user window on map
$scope.extuser = marker;
$scope.showInfoWindow('infoWindow');
};
});
};
if ($rootScope.hasLoaded()) { //if username and keys are already loaded, then init module
$scope.initTravel();
} else { //else, wait for username and keys to be loaded
$rootScope.$on('info-loaded', function(event, args) {
$scope.initTravel();
});
}
}
);
Although it works, it seems like it's very buggy and only loads sometimes. Occasionally, I get this:
Result screenshot
I really don't know what I'm doing wrong, as I simply followed the tutorials on PubNub's AngularJS SDK.
I think this has to do with how I'm initialising the application.
angular.module('app').run(['$location', '$rootScope', 'security', function($location, $rootScope, security) {
// Get the current user when the application starts
// (in case they are still logged in from a previous session)
$rootScope.hasLoaded = function() {
return (security.keys && security.info && security.currentUser); //check if everything is loaded correctly
};
$rootScope.checkLoading = function() {
if ($rootScope.hasLoaded()) {
$rootScope.$broadcast('info-loaded'); //broadcast event to "TravelCtrl" in order to init the module
}
};
security.requestKeys().then($rootScope.checkLoading); //request secret keys
security.requestSiteInfo().then($rootScope.checkLoading); //then templating info (site title, copyright, etc.)
security.requestCurrentUser().then($rootScope.checkLoading); //and finally, current user (name, id, etc.)
$rootScope.security = security;
// add a listener to $routeChangeSuccess
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
$rootScope.title = current.$$route && current.$$route.title? current.$$route.title: 'Default title';
});
}]);
1- Request secret keys, site info and current user with JSON API.
2- Wait until everything's loaded then init the application with the appropriate keys (PubNub, Google Maps)
--
My question is:
How do you instantiate an AngularJS app after retrieving useful information via a RESTful API?
I'm pretty new to AngularJS, and I wouldn't be surprised if my approach is totally ridiculous, but I really need to get some advice on this.
Thanks in advance for your help,
Ulysse
You don't have to wait that the AJAX Query ended to initate the angular APPs.
you can use the $http promise ( details her )
In the controller :
// Simple GET request example:
$http({
method: 'GET',
url: '/someUrl'
}).then(function successCallback(response) {
// this callback will be called asynchronously
// when the response is available
// data is now accessible in the html
$scope.data = response ;
// you can call a function to add markers on your maps with the received data
addMarkerOnMap(response);
}, function errorCallback(response) {
// called asynchronously if an error occurs
// or server returns response with an error status.
});
You can also add a watch on some variable to wait modification on them :
// you should have $scope.yourVarName declared.
$scope.$watch('yourVarName', function(newValue, oldValue) {
console.log(newValue);
});
Or watch a list/object
$scope.$watchCollection('[var1,var2]', function () {
},true);