Using a service worker to get list of files to cache from server - asp.net-mvc

I´m trying to use a service worker in an existing asp mvc app. Generally, it´s working fine: I can cache files and so on. Problem is, that there are many files to be cached and I´m trying to return an array of paths to the service worker, so that the files can be added to cache without adding them manually.
Here´s what I have so far:
Controller:
public ActionResult GetFilesToCache()
{
string[] filePaths = Directory.GetFiles(Server.MapPath(#"~\Content"), "*", SearchOption.AllDirectories);
string[] cuttedFiles = new string[filePaths.Length];
int i = 0;
foreach (var path in filePaths)
{
cuttedFiles[i] = path.Substring(path.IndexOf("Content"));
i++;
}
return Json(new { filesToCache = cuttedFiles }, JsonRequestBehavior.AllowGet);
}
This gives me a string array with entries like "Content\image1.png" etc.
Service worker:
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then(function (cache) {
console.log('[ServiceWorker] Caching app shell');
return fetch('Home/GetFilesToCache').then(function (response) {
return response;
}).then(function (files) {
return cache.addAll(files);
});
})
);
});
The error I get is:
Uncaught (in promise) TypeError: Failed to execute 'addAll' on 'Cache': Iterator getter is not callable.
Calling the action works just fine, data is received by the service worker, but not added to cache.

In the following code:
return fetch('Home/GetFilesToCache').then(function (response) {
return response;
}).then(function (files) {
return cache.addAll(files);
});
the value of the files parameter is going to be a Response object. You want it to be the JSON deserialization of the Response object's body. You can get this by changing return response with return response.json(), leading to:
return fetch('Home/GetFilesToCache').then(function(response) {
return response.json();
}).then(function(files) {
return cache.addAll(files);
});

Got it working with this code:
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then(function (cache) {
console.log('[ServiceWorker] Caching app shell');
return fetch('Home/GetFilesToCache').then(function (response) {
return response.json();
}).then(function (files) {
var array = files.filesToCache;
return cache.addAll(array);
});
})
);
});
Notice:
Chrome only lists a part of files stored in cache, so you just have to click that little arrow to show the next page:

return fetch('Home/GetFilesToCache').then(function(response) {
return response.json();
}).then(function(files) {
return cache.addAll(files);
});
The returned file has "filesToCache" property on "files" and also use "add" instead of "addAll".
So you need to write the following way.
return fetch('Home/GetFilesToCache').then(function(response) {
return response.json();
}).then(function(files) {
return cache.add(files.filesToCache);
});

Related

Is there a way to have my service worker intercept fetch requests coming from a client-side SvelteKit load function? (+page.ts)

I'm trying to have a service worker intercept fetch requests coming from a client-side SvelteKit load function. The network requests are being made, but the fetch event is not being triggered.
The fetch request from the load function is going to /api/allTeams, which is cached as reported by chrome devtools, but like I said, it's not getting intercepted. All the function does it fetch the data, and return it in a prop.
Also, every couple minutes I run invalidateAll(), to reload the data, and even those requests aren't being picked up by the SW.
Thanks!
--reese
src/service-worker.js:
import { build, version } from '$service-worker';
self.addEventListener('fetch', function (event) {
console.log("fetch")
event.respondWith(
fetch(event.request).catch(function () {
return caches.match(event.request);
}),
);
});
self.addEventListener('install', async function (event) {
event.waitUntil(
caches.open("ccs-" + version).then(function (cache) {
cache.add("/api/allTeams")
cache.addAll(build)
return;
}),
);
});
src/app.html:
<script>
const registerServiceWorker = async () => {
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register("/service-worker.js", {
scope: "*",
});
if (registration.installing) {
console.log("Service worker installing");
} else if (registration.waiting) {
console.log("Service worker installed");
} else if (registration.active) {
console.log("Service worker active");
}
} catch (error) {
console.error(`Registration failed with ${error}`);
}
}
};
registerServiceWorker()
</script>
src/+page.ts:
export async function load(request: Request) {
const searchQuery = new URL(request.url).searchParams.get("q")
const apiUrl = new URL(request.url)
apiUrl.pathname = "/api/allTeams"
const req = await fetch(apiUrl)
const data = await req.json()
return {data, searchQuery};
}

Service Worker Caches to much

I'mm working on a service worker, who should just cache some specific files.
But after implementation and refresh (yes I enabled "dumb sw after reload") there are much more files "loaded via the sw" then I added in the "files to cache" list.
I'm new with the service worker and have no idea what is wrong. I just followed some tutorials about it.
Here the code. Perhaps I am on the complete wrong way.
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/worker.js').then(function(registration) {
}, function(err) {
console.log('ServiceWorker registration failed: ', err);
});
});
}
var debugMode = false;
//Variables to cache
var CACHE_NAME = 'companyname-cache-1511794947915';
var urlsToCache = [
'/typo3conf/ext/companyname/Resources/Public/css/animate.css',
'/typo3conf/ext/companyname/Resources/Public/css/bootstrap.css',
'/typo3conf/ext/companyname/Resources/Public/css/bootstrap.min.css',
'/typo3conf/ext/companyname/Resources/Public/css/bootstrap-theme.css',
'/typo3conf/ext/companyname/Resources/Public/css/bootstrap-theme.min.css',
'/typo3conf/ext/companyname/Resources/Public/css/main.css',
'/typo3conf/ext/companyname/Resources/Public/css/et/override.css',
'/typo3conf/ext/companyname/Resources/Public/css/et/screen-full.css',
'/typo3conf/ext/companyname/Resources/Public/css/et/screen-full.min.css',
'/typo3conf/ext/companyname/Resources/Public/Icons/favicon.ico',
'/typo3conf/ext/companyname/Resources/Public/Icons/FaviconTouch/android-chrome-512x512.png',
];
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener("activate", function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if(debugMode) {
console.log('actual cache name: %o', CACHE_NAME);
console.log('name inside the cache: %o', cacheName);
}
if ((cacheName.indexOf('companyname-cache-') !== -1) && (cacheName !== CACHE_NAME)) {
if(debugMode) {
console.log("old cache deleted");
}
return caches.delete(cacheName);
}
})
);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
if(debugMode) {
console.log("fetch 1");
}
return response;
}
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
function(response) {
if(!response || response.status !== 200 || response.type !== 'basic') {
if(debugMode) {
console.log("fetch 2");
}
return response;
}
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
if(debugMode) {
console.log("fetch 3");
}
return response;
}
);
})
);
});
The code in your SW works like this:
On installation, add all items in the URL LIST to the cache
On activation, check that the cache has items only for the CACHE NAME
On a fetch event: 1st check if the requested resource is in the cache and return if if found; 2nd if the requested resource was NOT in the cache, request it from the network AND put it in the cache
The last part of #3 causes the problem in your SW code – it is adding everything requested to the cache so in the future they're available from the cache.
You can see the problem in your code if you look for the last occurrence of this:
caches.open(CACHE_NAME)
That's the part of your code when something WASN'T found from the cache AND was requested from the network. The code is then putting it into the cache – that's not what you wanted.

ASP.Net MVC, WebAPI, AngularJS - Check if value exists in database

I am using webapi and angularjs in my ASP.net MVC app. Everything is working good but in my insert (post) method I need to check if a value exists in the db before doing an insert. I am not sure where that should go. I ruled out the webapi because a void does not return a value. The logical place seems to be the controller, but I cannot find the right way to call my angular getEmployee(id) method from within the insert controller method. Any help is appreciated.
Angular controller (post):
$scope.insertEmployee = function (employee) {
employeeFactory.insertEmployee(employee)
.success(function (emp) {
$scope.employee = emp;
})
.error(function (error) {
$scope.status = 'Unable to load employee data: ' + error.message;
});
};
Angular factory (post):
factory.insertEmployee = function (employee) {
url = baseAddress + "employee/insert/";
$http.post(url, employee).success(function (data) {
alert("Saved Successfully!!");
}).error(function (data) {
$scope.error = "An Error has occured while saving employee! " + data;
});
};
webapi controller post method:
[Route("api/employee/insert/")]
public void Post(Employee employee)
{
if (ModelState.IsValid)
{
context.Employee.Add(employee);
context.SaveChanges();
}
}
Angular controller (get):
$scope.getEmployees = function (term) {
employeeFactory.getEmployees(term)
.success(function (data) {
$scope.employees = data;
})
.error(function (error) {
$scope.status = 'Unable to load employee data: ' + error.message;
});
};
I would suggest doing this in your web-api server side. You can construct an exception and throw it. Some pseudo-code:
[Route("api/employee/insert/")]
public void Post(Employee employee)
{
if (ModelState.IsValid)
{
// verify doesn't already exist
if(...item-already-exists...) {
var resp = new HttpResponseMessage(HttpStatusCode.Conflict)
{
Content = new StringContent("Employee already exists!")),
ReasonPhrase = "Employee already exists!"
}
throw new HttpResponseException(resp);
}
context.Employee.Add(employee);
context.SaveChanges();
}
}
Your factory doesn't match your controller. If you want to use success and error from employeeFactory.insertEmployee, it needs to return the promise:
factory.insertEmployee = function (employee) {
url = baseAddress + "employee/insert/";
return $http.post(url, employee);
};
So then you can do
employeeFactory.insertEmployee(employee).success( ... )
Now to answer your question you could either do a database read in insertEmployee to check if the value exists before you insert it. Or save a server call and do the check during the insert request: if the employee exists, return an error or a specific message to tell the client.
With the help of floribon and Nicholas Smith I managed to come up with a solution that bubbles the error up to my view, which I display in a div element. This is only bare bones, but is a start.
My angular controller:
$scope.insertEmployee = function (employee) {
employeeFactory.insertEmployee(employee)
.success(function (data) {
$scope.employee = data;
$scope.status = 'The item was saved';
})
.error(function (error) {
$scope.status = 'Unable to save the employee data: ' + error;
});
};
My angular factory:
factory.insertEmployee = function (employee) {
url = baseAddress + "employee/insert/";
return $http.post(url, employee);
};
my webapi controller:
[Route("api/employee/insert/")]
public HttpResponseMessage Post(Employee employee)
{
HttpResponseMessage response;
// check for the employee
var employeeCheck = context.Employee
.Where(b => b.EmployeeNumber == employee.EmployeeNumber)
.FirstOrDefault();
if (employeeCheck == null)
{
// save the item here
response = Request.CreateResponse(HttpStatusCode.OK);
}
else
{
response = Request.CreateResponse(HttpStatusCode.Conflict, "The item already exists");
}
return response;
}
The result is the message is "Unable to save the employee data: The item already exists".

MVC return json info when file not found

i have application where user can download excel report using this button:
my method looks following:
[HttpPost]
public ActionResult GetYearlyReport(int plantId)
{
string fileName = Reports.GenerateYearlyReport();
if (!String.IsNullOrEmpty(fileName))
{
byte[] fileBytes = GetFile(fileName);
return File(fileBytes, MediaTypeNames.Application.Octet, fileName);
}
return Json(new { Result = "ERROR", Message = "Missing some parameters." });
}
Now , wheren filename isn't empty then i got the file, but when it is then i am redirected to non existed page GetYearlyReport, while I would like to just say message from json, is that possbile?
Why not just add another if() statement to handle the scenarios where file names are empty, and return an error and handle it client side?
$.ajax({
url: 'xxx/GetYearlyReport',
data: { plantId: plantId},
type: 'POST',
error: function (xhr, textStatus, exceptionThrown) {
if (xhr.status == xxx) {
alert(xhr.responseText);
}
},
success: function (data) {
if(data.Result = 'ERROR'){
//do something
alert(data.Message);
}
}
});
Or better define a common error handler for your ajax calls?
$(document).ajaxError(function (e, xhr, settings) {
if (xhr.status == 401)
{
alert("unauthorized");
}
else if (xhr.status == 0) {
alert(' Check Your Network.');
} else if (xhr.status == 404) {
alert('The resource you are looking for can not be found.');
} else if (xhr.status == 500) {
alert('Internel Server Error.');
} else {
alert('Unknow Error.\n' + x.responseText);
}
});
Your code seems fine, I believe redirection is occurred in the Reports.GenerateYearlyReport method, there must be a way to check the result of the method before invoke it.

Get one database record, display it, update it, and save it back to the database

SOLVED! It was a Knockout issue (wrong binding). But maybe someone likes to argue or comment about the code in general (dataservice, viewmodel, etc).
I tried to build a Breeze sample, where I get one database record (with fetchEntityByKey), display it for updating, then with a save button, write the changes back to the database. I could not figure out how to get it to work.
I was trying to have a dataservice ('class') and a viewmodel ('class'), binding the viewmodel with Knockout to the view.
I very much appreciated if someone could provide a sample or provide some hints.
Thankx, Harry
var dataservice = (function () {
var serviceName = "/api/amms/";
breeze.NamingConvention.camelCase.setAsDefault();
var entityManager = new breeze.EntityManager(serviceName);
var dataservice = {
serviceName: serviceName,
entityManager: entityManager,
init: init,
saveChanges: saveChanges,
getLocation: getLocation
};
return dataservice;
function init() {
return getMetadataStore();
}
function getMetadataStore() {
return entityManager.fetchMetadata()
.then(function (result) { return dataservice; })
.fail(function () { window.alert("fetchMetadata:fail"); })
.fin(function () { });
}
function saveChanges() {
return entityManager.saveChanges()
.then(function (result) { return result; })
.fail(function () { window.alert("fetchEntityByKey:fail"); })
.fin(function () { });
}
function getLocation() {
return entityManager.fetchEntityByKey("LgtLocation", 1001, false)
.then(function (result) { return result.entity; })
.fail(function () { window.alert("fetchEntityByKey:fail"); })
.fin(function () { });
}
})();
var viewmodel = (function () {
var viewmodel = {
location: null,
error: ko.observable(""),
init: init,
saveChanges: null
};
return viewmodel;
function init() {
return dataservice.init().then(function () {
viewmodel.saveChanges = dataservice.saveChanges;
return getLocation();
})
}
function getLocation() {
return dataservice.getLocation().then(function (result) {
return viewmodel.location = result;
})
}
})();
viewmodel.init().then(function () {
ko.applyBindings(viewmodel);
});
Glad you solved it. Can't help noticing that you added a great number of do-nothing callbacks. I can't think of a reason to do that. You also asked for metadata explicitly. But your call to fetchEntityByKey will do that implicitly for you because, as you called it, it will always go to the server.
Also, it is a good idea to re-throw the error in the fail callback within a dataservice so that a caller (e.g., the ViewModel) can add its own fail handler. Without re-throw, the caller's fail callback would not hear it (Q promise machinery acts as if the first fail handler "solved" the problem).
Therefore, your dataservice could be reduced to:
var dataservice = (function () {
breeze.NamingConvention.camelCase.setAsDefault();
var serviceName = "/api/amms/";
var entityManager = new breeze.EntityManager(serviceName);
var dataservice = {
serviceName: serviceName, // why are you exporting this?
entityManager: entityManager,
saveChanges: saveChanges,
getLocation: getLocation
};
return dataservice;
function saveChanges() {
return entityManager.saveChanges()
.fail(function () {
window.alert("saveChanges failed: " + error.message);
throw error; // re-throw so caller can hear it
})
}
function getLocation() {
return entityManager.fetchEntityByKey("LgtLocation", 1001, false)
.then(function (result) { return result.entity; })
.fail(function () {
window.alert("fetchEntityByKey failed: " + error.message);
throw error; // re-throw so caller can hear it
})
}
})();
I don't want to make too much of this. Maybe you're giving us the stripped down version of something more substantial. But, in case you (or a reader) think those methods are always necessary, I wanted to make clear that they are not.

Resources