Can I have both Workbox registerroute and service worker fetch event handler at the same time? - progressive-web-apps

I'm building a PWA application, and I have used workbox registerroute for a few api endpoints, as well as an explicit service worker fetch event listener. During the debugging on some caching issues, I've noticed that these two seems to interfere with each other. Specifically sometimes the fetch handler is not triggered - which causes me trouble on debugging - I'm assuming this is due to the registerroute caching policy I have set via workbox.
My question is that, can I only pick one or the other, instead of having both fetch handler and registerroute? In my case, I needed fetch handler to deal with some advanced caching related to POST requests. So I think if I can only pick one, I'll have to stick with the fetch handler.

First, here's some background information about what happens when there's multiple fetch event handlers in the active service worker.
With that background info in mind, there are a few approaches for accomplishing what you're describing.
Option 1a: Register your own fetch event handler first
As long as you register your own fetch handler first, before any calls to Workbox's registerRoute(), it's guaranteed to have the "first shot" at responding the incoming fetch event.
The thing to keep in mind is that your own fetch handler needs to make a synchronous decision about whether or not to call event.respondWith(), and when you do call event.respondWith(), then Workbox's routes will not get used to respond to a given request.
So, you could do the following:
self.addEventListener('fetch', (event) => {
// Alternatively, check event.request.headers,
// or some other synchronous criteria.
if (event.request.url.endsWith('.json')) {
event.respondWith(customResponseLogic(event));
}
});
// Then, include any Workbox-specific routes you want.
registerRoute(
({request}) => request.destination === 'image',
new CacheFirst()
);
// The default handler will only apply if your own
// fetch handler didn't respond.
registerDefaultHandler(new StaleWhileRevalidate());
Option 1b: Ensure Workbox routes won't match
This is similar to 1a, but the main thing is to make sure that you don't have a "catch-all" route that will match all requests, and that you don't use registerDefaultHandler().
Assuming your Workbox routes just match a specific set of well-defined criteria, and don't match any of the requests that you want to respond to in your own handler, it shouldn't matter how you order them:
// Because this will only match image requests, it doesn't
// matter if it's listed first.
registerRoute(
({request}) => request.destination === 'image',
new CacheFirst()
);
self.addEventListener('fetch', (event) => {
// Alternatively, check event.request.headers,
// or some other synchronous criteria.
if (event.request.url.endsWith('.json')) {
event.respondWith(customResponseLogic(event));
}
});
(What's going on "under the hood" is that if there isn't a Route whose synchronous matchHandler returns a truthy value, Workbox's Router won't call event.respondWith().)
Option 2: Use custom handler logic
It should be viable to use Workbox to handle all your routing, and run your custom response generation code in either a handlerCallback (more straightforward) or a custom subclass of the Strategy base class (more reusable, but overkill for simple use cases).
The one thing to keep in mind is that if you're dealing with POST requests, you need to explicitly tell registerRoute() to respond to them, by passing in 'POST' as the (optional) third parameter.
Here's an example of how you could do this, assuming as before that you custom logic is defined in a customResponseLogic() function:
registerRoute(
({request}) => request.destination === 'image',
new CacheFirst()
);
registerRoute(
// Swap this out for whatever criteria you need.
({url}) => url.pathname.endsWith('.json'),
// As before, this assumes that customResponseLogic()
// takes a FetchEvent and returns a Promise for a Response.
({event}) => customResponseLogic(event),
// Make sure you include 'POST' here!
'POST'
);

Related

scalajs-react router. How to perform ajax request inside conditional route

I am trying to make some conditional routes. The condition resolves on the serverside.
Route rule example:
| (dynamicRouteCT("#user" / long.caseClass[User]) ~> dynRender((page: User) => <.div("Hello, " + page.id.toString)))
.addCondition((page: User) => checkPermissions(page.id))(_ => Some(redirectToPage(Page403)(Redirect.Push)))
checkpermissions body:
def checkPermissions(id: Long) = CallbackTo.future{
/*Ajax.get(s"http://some.uri/?id=$id") map (res =>
* if (something) true
* else false
* )
*/
//the request before returns Future[XMLHttprequest] witch maps to Future[Boolean]
Future(false)
}
I got type missmatch here: (page: User) => checkPermissions(page.id)
Is it possible to perform ajax request inside conditional routes?
If we look at def addCondition(cond: Page => CallbackTo[Boolean])(condUnmet: Page => Option[Action[Page]]): Rule[Page] we can see that it requires a CallbackTo[Boolean]. Because of the nature of the JS env, there is now way to go from Future[A] to A. Although it's not a limitation from scalajs-react itself, it is an inherited reality that will affect your scalajs-react code; as this table in the doc shows, there's no way to go from a CallbackTo[Future[Boolean]] to a CallbackTo[Boolean].
This type-level restriction is actually a really good thing for user experience. The router is synchronous, it must determine how to render routes and route changes immediately. If it were allowed to be async and somehow supported Futures, then the user would experience noticable (and potentially huge) delays without any kind of visual feedback or means of interruption.
The "right way" to solve this problem is to use a model that covers the async state. This is what I would do:
Create an AsyncState[E, A] ADT with cases: Empty, AwaitingResponse, Loaded(value: A), Failed(error: E).(You can enrich these further if desired, eg. loadTime on Loaded, retry callback on Failed, timeStarted on AwaitingResponse, etc.)
Have an instance of AsyncState[Boolean] in your (local/client-side) state.
Optionally kick-off an async load on page startup.
Have the router pass its value to a component and/or check the value of this.(The router won't know the value because it's dynamic, use Callback in a for-comprehension to wire things up and satisfy the types.)
Depending on the value of AsyncState[Boolean], render something meaningful to the user. If it's AwaitingResponse, display a little spinner; if it's failed display an error and probably a retry button.
(It should also be noted that AsyncState[Boolean] shouldn't actually be Boolean as that's not very descriptive or true to the domain. It would probably be something more meaningful like AsyncState[UserAccess] or something like that.)
Hope that helps! Good luck!

How do I call a method on my ServiceWorker from within my page?

I have a ServiceWorker registered on my page and want to pass some data to it so it can be stored in an IndexedDB and used later for network requests (it's an access token).
Is the correct thing just to use network requests and catch them on the SW side using fetch, or is there something more clever?
Note for future readers wondering similar things to me:
Setting properties on the SW registration object, e.g. setting self.registration.foo to a function within the service worker and doing the following in the page:
navigator.serviceWorker.getRegistration().then(function(reg) { reg.foo; })
Results in TypeError: reg.foo is not a function. I presume this is something to do with the lifecycle of a ServiceWorker meaning you can't modify it and expect those modification to be accessible in the future, so any interface with a SW likely has to be postMessage style, so perhaps just using fetch is the best way to go...?
So it turns out that you can't actually call a method within a SW from your app (due to lifecycle issues), so you have to use a postMessage API to pass serialized JSON messages around (so no passing callbacks etc).
You can send a message to the controlling SW with the following app code:
navigator.serviceWorker.controller.postMessage({'hello': 'world'})
Combined with the following in the SW code:
self.addEventListener('message', function (evt) {
console.log('postMessage received', evt.data);
})
Which results in the following in my SW's console:
postMessage received Object {hello: "world"}
So by passing in a message (JS object) which indicates the function and arguments I want to call my event listener can receive it and call the right function in the SW. To return a result to the app code you will need to also pass a port of a MessageChannel in to the SW and then respond via postMessage, for example in the app you'd create and send over a MessageChannel with the data:
var messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
console.log(event.data);
};
// This sends the message data as well as transferring messageChannel.port2 to the service worker.
// The service worker can then use the transferred port to reply via postMessage(), which
// will in turn trigger the onmessage handler on messageChannel.port1.
// See https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage
navigator.serviceWorker.controller.postMessage(message, [messageChannel.port2]);
and then you can respond via it in your Service Worker within the message handler:
evt.ports[0].postMessage({'hello': 'world'});
To pass data to your service worker, the above mentioned is a good way. But in case, if someone is still having a hard time implementing that, there is an other hack around for that,
1 - append your data to get parameter while you load service-worker (for eg., from sw.js -> sw.js?a=x&b=y&c=z)
2- Now in service worker, fetch those data using self.self.location.search.
Note, this will be beneficial only if the data you pass do not change for a particular client very often, other wise it will keep changing the loading url of service worker for that particular client and every time the client reloads or revisits, new service worker is installed.

Restangular - how to cancel/implement my own request

I found a few examples of using fullRequestInterceptor and httpConfig.timeout to allow canceling requests in restangular.
example 1 | example 2
this is how I'm adding the interceptor:
app.run(function (Restangular, $q) {
Restangular.addFullRequestInterceptor(function (element, operation, what, url, headers, params, httpConfig) {
I managed to abort the request by putting a resolved promise in timeout (results in an error being logged and the request goes out but is canceled), which is not what I want.
What I'm trying to do - I want to make the AJAX request myself with my own requests and pass the result back to whatever component that used Restangular. Is this possible?
I've been looking a restangular way to solve it, but I should have been looking for an angular way :)
Overriding dependency at runtime in AngularJS
Looks like you can extend $http before it ever gets to Restangular. I haven't tried it yet, but it looks like it would fit my needs 100%.
I'm using requestInterceptor a lot, but only to change parameters and headers of my request.
Basically addFullRequestInterceptor is helping you making change on your request before sending it. So why not changing the url you want to call ?
There is the httpConfig object that you can modify and return, and if it's close to the config of $http (and I bet it is) you can change the url and even method, and so change the original request to another one, entirely knew.
After that you don't need timeout only returning an httpConfig customise to your need.
RestangularConfigurer.addFullRequestInterceptor(function (element, operation, route, url, headers, params, httpConfig) {
httpConfig.url = "http://google.com";
httpConfig.method = "GET";
httpConfig.params = "";
return {
httpConfig: httpConfig
};
});
It will be pass on and your service or controller won't know that something change, that's the principle of interceptor, it allow you to change stuff and returning to be use by the next process a bit like a middleware. And so it will be transparent to the one making the call but the call will be made to what you want.

Routing based on query parameter in Play framework

My web application will be triggered from an external system. It will call one request path of my app, but uses different query parameters for different kinds of requests.
One of the parameters is the "action" that defines what is to be done. The rest of the params depend on the "action".
So I can get request params like these:
action=sayHello&user=Joe
action=newUser&name=Joe&address=xxx
action=resetPassword
...
I would like to be able to encode it similarly in the routes file for play so it does the query param based routing and as much of the validation of other parameters as possible.
What I have instead is one routing for all of these possibilities with plenty of optional parameters. The action processing it starts with a big pattern match to do dispatch and parameter validation.
Googling and checking SO just popped up plenty of samples where the params are encoded in the request path somehow, so multiple paths are routed to the same action, but I would like the opposite: one path routed to different actions.
One of my colleagues said we could have one "dispatcher" action that would just redirect based on the "action" parameter. It would be a bit more structured then the current solution, but it would not eliminate the long list of optional parameters which should be selectively passed to the next action, so I hope one knows an even better solution :-)
BTW the external system that calls my app is developed by another company and I have no influence on this design, so it's not an option to change the way how my app is triggered.
The single dispatcher action is probably the way to go, and you don't need to specify all of your optional parameters in the route. If action is always there then that's the only one you really need.
GET /someRoute controller.dispatcher(action: String)
Then in your action method you can access request.queryString to get any of the other optional parameters.
Note: I am NOT experienced Scala developer, so maybe presented snippets can be optimized... What's important for you they are valid and working.
So...
You don't need to declare every optional param in the routes file. It is great shortcut for type param's validation and best choice would be convince 'other company' to use API prepared by you... Anyway if you haven't such possibility you can also handle their requests as required.
In general: the dispatcher approach seems to be right in this place, fortunately you don't need to declare all optional params in the routes and pass it between actions/methods as they can be fetched directly from request. In PHP it can be compared to $_GET['action'] and in Java version of Play 2 controller - DynamicForm class - form().bindFromRequest.get("action").
Let's say that you have a route:
GET /dispatcher controllers.Application.dispatcher
In that case your dispatcher action (and additional methods) can look like:
def dispatcher = Action { implicit request =>
request.queryString.get("action").flatMap(_.headOption).getOrElse("invalid") match {
case "sayHello" => sayHelloMethod
case "newUser" => newUserMethod
case _ => BadRequest("Action not allowed!")
}
}
// http://localhost:9000/dispatcher?action=sayHello&name=John
def sayHelloMethod(implicit request: RequestHeader) = {
val name = request.queryString.get("name").flatMap(_.headOption).getOrElse("")
Ok("Hello " + name )
}
// http://localhost:9000/dispatcher?action=newUser&name=John+Doe&address=john#doe.com
def newUserMethod(implicit request: RequestHeader) = {
val name = request.queryString.get("name").flatMap(_.headOption).getOrElse("")
val address = request.queryString.get("address").flatMap(_.headOption).getOrElse("")
Ok("We are creating new user " + name + " with address " + address)
}
Of course you will need to validate incoming types and values 'manually', especially when actions will be operating on the DataBase, anyway biggest part of your problem you have resolved now.

ASP.NET MVC2 AsyncController: Does performing multiple async operations in series cause a possible race condition?

The preamble
We're implementing a MVC2 site that needs to consume an external API via https (We cannot use WCF or even old-style SOAP WebServices, I'm afraid). We're using AsyncController wherever we need to communicate with the API, and everything is running fine so far.
Some scenarios have come up where we need to make multiple API calls in series, using results from one step to perform the next.
The general pattern (simplified for demonstration purposes) so far is as follows:
public class WhateverController : AsyncController
{
public void DoStuffAsync(DoStuffModel data)
{
AsyncManager.OutstandingOperations.Increment();
var apiUri = API.getCorrectServiceUri();
var req = new WebClient();
req.DownloadStringCompleted += (sender, e) =>
{
AsyncManager.Parameters["result"] = e.Result;
AsyncManager.OutstandingOperations.Decrement();
};
req.DownloadStringAsync(apiUri);
}
public ActionResult DoStuffCompleted(string result)
{
return View(result);
}
}
We have several Actions that need to perform API calls in parallel working just fine already; we just perform multiple requests, and ensure that we increment AsyncManager.OutstandingOperations correctly.
The scenario
To perform multiple API service requests in series, we presently are calling the next step within the event handler for the first request's DownloadStringCompleted. eg,
req.DownloadStringCompleted += (sender, e) =>
{
AsyncManager.Parameters["step1"] = e.Result;
OtherActionAsync(e.Result);
AsyncManager.OutstandingOperations.Decrement();
}
where OtherActionAsync is another action defined in this same controller following the same pattern as defined above.
The question
Can calling other async actions from within the event handler cause a possible race when accessing values within AsyncManager?
I tried looking around MSDN but all of the commentary about AsyncManager.Sync() was regarding the BeginMethod/EndMethod pattern with IAsyncCallback. In that scenario, the documentation warns about potential race conditions.
We don't need to actually call another action within the controller, if that is off-putting to you. The code to build another WebClient and call .DownloadStringAsync() on that could just as easily be placed within the event handler of the first request. I have just shown it like that here to make it slightly easier to read.
Hopefully that makes sense! If not, please leave a comment and I'll attempt to clarify anything you like.
Thanks!
It turns out the answer is "No".
(for future reference incase anyone comes across this question via a search)