Service Worker: serviceWorker.ready + event.waitUntil not orchestrating as expected - service-worker

I have a service worker that intercepts calls to compile jsx if needed, and cache the result, and want my website to bootstrap only once the service worker got active.
Looking at the documentation I found the navigator.serviceWorker.ready property, which is a promise that resolves once the worker got active.
For some reason, the promise resolves before the activation should really be finished. Here is my code:
index.html
<!DOCTYPE html>
<html>
<head>
<script type="application/javascript">
navigator.serviceWorker.register('./sw.js', { scope: './' }).then((reg) => {
console.log('registered successfully')
}).catch(console.error)
navigator.serviceWorker.ready.then((registration) => {
console.log('worker active, bootstrapping')
// ... bootstrapping the app now (loading jsx scripts etc.)
})
</script>
...
</head>
....
</html>
sw.js
self.addEventListener('activate', (event) => {
console.log('activating')
event.waitUntil(self.clients.claim().then(() => console.log('active')))
})
Resulting log output:
registered successfully
activating
worker active, bootstrapping
active
Consequently, loading the app scripts as part of the bootstrapping will not be reliably intercepted by the service worker at the beginning, which will make the app fail.
I cleared the entire site data in Chrome including unregistering existing service workers, before I loaded the page.
Am I using the APIs wrong, e.g. misunderstanding the waitUntil?

Related

How do I pass the environment info to javascript code with importmaps

I’m using Rails 7 with importmaps and I’m trying to make JavaScript code conditionally depend on the environment specific config. At the very least, I’d need to pass what environment is it in the first place (dev, test, etc) and ideally I’d like to pass arbitrary env dependent configuration.
What I could do is serve a js file from a controller and create a global config var in it, serving whatever config info I want to be available to the js code.
But I’m wondering if I’m missing some obvious “official” way do so. Is there one?
Thought I'd post an architectural answer on how I've managed this in the past, to provide an easy to manage and secure solution for all types of user application.
It is technology agnostic but this design should work well for your Rails app. I would avoid Rails import maps if they didn't allow you to follow these principles.
BUILD BINARIES ONCE
There are quite a few advantages to building code once, whatever the technology, then promoting assets down a pipeline without changes. So you test exactly the same code everywhere. If there are errors, stack traces are identical and so on:
DEV --> STAGING --> PRODUCTION
WEB BINARIES
These days I like to build Javascript into bundles, then add subresource integrity tags to the index.html file, to tie resources together, as in this demo app of mine:
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
<base href='/spa/' />
<title>OAuth Demo App</title>
<link rel='stylesheet' href='bootstrap.min.css?t=1649189065161' integrity='sha256-YvdLHPgkqJ8DVUxjjnGVlMMJtNimJ6dYkowFFvp4kKs='>
<link rel='stylesheet' href='app.css?t=1649189065161' integrity='sha256-B7pu+gcFspulW4zXfgczVtPcEuZ81tZRFYeRciEzWro='>
<body>
<div id='root' class='container'></div>
<script type='module' src='vendor.bundle.js?t=1649189065161' integrity='sha256-p+HJCny/HKNJXb18K7GW6jKqw4bZN2AZnUWtXJCDLT8='></script>
<script type='module' src='app.bundle.js?t=1649189065161' integrity='sha256-WGayM6x6jt2rJ5PwBvuV+vytuDBdBOkHQoO08iCdWfM='></script>
</body>
</html>
CODE POINTS TO PRODUCTION
In code I have an environment object for the production configuration, as in the below example. This only contains public / non sensitive data and I wouldn't care if a hacker got hold of it.
export const productionConfiguration = {
app: {
webOrigin: 'https://web.authsamples.com',
apiBaseUrl: 'https://tokenhandler.authsamples.com/api'
},
oauth: {
oauthAgentBaseUrl: 'https://tokenhandler.authsamples.com/oauth-agent',
}
} as Configuration;
OTHER ENVIRONMENT OVERRIDES
At runtime, if running in a different origin, I download a static config.json file from the web origin, containing the environment data:
public async get(): Promise<Configuration> {
if (location.origin.toLowerCase() === productionConfiguration.app.webOrigin.toLowerCase()) {
return productionConfiguration;
}
return this._download();
}
This enables new test environments to be created without needing to rebuild code. I like to avoid this download for production users, so that all of the main web resources are tied to the index.html file.
MOBILE
The same principle of shipping with production configuration built into APK / IPA files can be used in mobile apps. Mobile environment data might include different info, as in this example app of mine:
{
"app": {
"apiBaseUrl": "https://api.authsamples.com/api"
},
"oauth": {
"authority": "https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_qqJgVeuTn",
"clientId": "3pj7bd0ff8h1klhok774303dq8",
"webBaseUrl": "https://authsamples.com",
"loginRedirectPath": "/apps/basicmobileapp/postlogin.html",
"postLogoutRedirectPath": "/apps/basicmobileapp/postlogout.html",
"scope": "openid profile email https://api.authsamples.com/api/transactions_read",
"deepLinkBaseUrl": "https://mobile.authsamples.com",
"customLogoutEndpoint": "https://login.authsamples.com/logout"
}
}
The usual technique is then to have a settings page where these values can be overridden, eg to point the app to test environments.
SECURE ENVIRONMENT DATA
Of course the UI only downloads any sensitive environment data after the user has authenticated, eg by sending a secure cookie or access token to an API.

are network requests due to code after a service worker is registered guaranteed to be served by the service worker fetch() handler?

Registering a service worker is done in index.html with (eg):
<script>
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
</script>
If that code is followed by something that requests a resource, eg:
<script src="a.js"></script>
is that request guaranteed to trigger the 'fetch' event handler in the service worker (and so, potentially, be served from a cache)?
Or, should any code that causes a network access in index.html be added dynamically in the then() callback of the register() function (and, is THAT then guaranteed to be served by the service worker's 'fetch' event handler)?
I would recommend reading through "The Service Worker Lifecycle" for more general information.
The answer to most of your questions is "no," since what you're talking about is the initial registration of the service worker. Registering a service worker kicks off an installation and activation process that's independent from the promise returned by register(). The only thing you could infer from that promise is whether starting the process succeeded or not.
What you're asking about—whether a fetch handler will be invoked or not—relies on a service worker being in control of the current page.
In terms of JavaScript, if you want to answer the question "is this page controlled by (any) service worker?", you can do that by checking whether or not navigator.serviceWorker.controller is undefined.
If you want to write code that will only execute once there's a service worker in control of the current page (with the caveat that it might never execute, if something prevented the service worker from properly activating), you could do that by creating a promise that will resolve immediately if there's already a controller, and will otherwise resolve once the controllerchange event fires:
const p = new Promise(r => {
if (navigator.serviceWorker.controller) return r();
navigator.serviceWorker.addEventListener('controllerchange', e => r());
});
// Later, if you want code to execute only if the page is controlled:
p.then(() => {
// There's a SW in control at this point.
});
Inside your service worker, you can add the following to your activate handler to ensure that as soon as a newly installed service worker activates, it takes control of any open pages (including the page that registered it for the first time):
self.addEventListener('activate', () => self.clients.claim());
If you don't include self.clients.claim() inside your service worker's activate handler, then the page that starts out uncontrolled will never start being controlled, even though it's registered a service worker that has activated.

ServiceWorker not intercepting calls immediately after installation

I am playing around with ServiceWorkers, and I noticed that even after a successful registration, the service worker is not intercepting calls
my sw.js:
self.addEventListener('install', (event) => {
console.log('install')
})
self.addEventListener('activate', (event) => {
console.log('activate')
})
self.addEventListener('fetch', (event) => {
console.log('fetch')
})
in my index.html:
...
<head>
...
<script>
navigator.serviceWorker.register('./sw.js', { scope: './' }).then(() => {
// just delay it so we're sure sw is active before we load that script
setTimeout(() => {
console.log('load jsx script to be intercepted')
const script = document.createElement('script')
script.setAttribute('src', 'test.js')
document.head.appendChild(script)
}, 2000)
})
</script>
...
</head>
...
Result:
My console output from the first page load, when service worker is not yet installed, is then (in that order):
install
activate
load jsx script to be intercepted
Uncaught SyntaxError: Unexpected token '<'
The syntax error is because I'm trying to load a JSX script, and I would like to intercept that loading in the service worker, to compile it first before it gets executed.
But on the initial load, the service worker seems to not intercept, as I don't see the log output 'fetch', although I clearly see, when I attempt to load the jsx script, by that time the service worker is already active.
When I now reload the page, I get following console output:
fetch (<-- that's the loading of the index.html itself)
load jsx script that should be intercepted
fetch (<-- that's the loading of the test.js)
So from then on, things work out as planned, but not on the initial load. Why is that the case?
After some research it seems I was able to solve the problem using the related post here, plus the documentation of the "claim" api.
So if I add the following to my 'active' handler, things seem to work:
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim())
I would appreciate any feedback on whether this is indeed the right way to go about it :-)

How do I use ServiceWorker without a separate JS file?

We create service workers by
navigator.serviceWorker.register('sw.js', { scope: '/' });
We can create new Workers without an external file like this,
var worker = function() { console.log('worker called'); };
var blob = new Blob( [ '(' , worker.toString() , ')()' ], {
type: 'application/javascript'
});
var bloburl = URL.createObjectURL( blob );
var w = new Worker(bloburl);
With the approach of using blob to create ServiceWorkers, we will get a Security Error as the bloburl would be blob:chrome-extension..., and the origin won't be supported by Service Workers.
Is it possible to create a service worker without external file and use the scope as / ?
I would strongly recommend not trying to find a way around the requirement that the service worker implementation code live in a standalone file. There's a very important of the service worker lifecycle, updates, that relies on your browser being able to fetch your registered service worker JavaScript resource periodically and do a byte-for-byte comparison to see if anything has changed.
If something has changed in your service worker code, then the new code will be considered the installing service worker, and the old service worker code will eventually be considered the redundant service worker as soon as all pages that have the old code registered and unloaded/closed.
While a bit difficult to wrap your head around at first, understanding and making use of the different service worker lifecycle states/events are important if you're concerned about cache management. If it weren't for this update logic, once you registered a service worker for a given scope once, it would never give up control, and you'd be stuck if you had a bug in your code/needed to add new functionality.
One hacky way is to use the the same javascript file understand the context and act as a ServiceWorker as well as the one calling it.
HTML
<script src="main.js"></script>
main.js
if(!this.document) {
self.addEventListener('install', function(e) {
console.log('service worker installation');
});
} else {
navigator.serviceWorker.register('main.js')
}
To prevent maintaining this as a big file main.js, we could use,
if(!this.document) {
//service worker js
importScripts('sw.js');
else {
//loadscript document.js by injecting a script tag
}
But it might come back to using a separate sw.js file for service worker to be a better solution. This would be helpful if one'd want a single entry point to the scripts.

Consume WCF Rest Service in ASP.net using jquery

I am trying to consume a wcf rest api in a asp.net project using jquery. for doing so i have done:
Created a WCF Rest service source code can be downloaded from here.
Created a ASP.Net project to consume that restAPI using jquery. source code here.
In ASP .Net project on the click of button I am trying to call a REST service. But every time I gets two issues:
calling var jsondata = JSON.stringify(request); in TestHTML5.js throws an error saying "Microsoft JScript runtime error: 'JSON' is undefined"
When I press ignore it continues towards WCF Rest API call but it always returns error (Not Found) function. Rest API never gets called.
Thanks for every one's help in advance.
ANSWER:
Solution and source link can be found on this link.
I have looked at the sample code you provided and the problem is that you are violating the same origin policy restriction. You cannot perform cross domain AJAX calls. In your example the service is hosted on http://localhost:35798 and the web application calling it on http://localhost:23590 which is not possible. You will have to host both the service and the calling application in the same ASP.NET project. You seem to have attempted to enable CORS on the client side using ($.support.cors = true;) but on your service doesn't support CORS.
Another issue saw with your calling page (TestHTML5.htm) is the fact that you have included jquery twice (once the minified and once the standard version) and you have included your script (TestHTML5.js) after jquery. You should fix your script references. And yet another issue is the following line <script type="text/javascript"/> which is invalid.
So start by fixing your markup (I have removed all the CSS noise you had in your markup in order to focus on the important parts):
<!DOCTYPE html>
<html dir="ltr" lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>SignUp Form</title>
<script type="text/javascript" src="../Scripts/jquery-1.7.2.min.js"></script>
<script type="text/javascript" src="../Scripts/TestHTML5.js"></script>
</head>
<body>
<button id="Send" onclick="testHTML5OnClick();">
Send Me ID!
</button>
</body>
</html>
and then in your TestHTML5.js you could also clean a little bit. For example your service is listening for the following url pattern json/{id} and accepting only GET verbs and you are attempting to use POST which is not possible. In addition to that you are attempting to use the JSON.stringify method which doesn't make any sense with the GET verb. You should simply send the id as part of the url portion as you defined in your service.
function testHTML5OnClick() {
var id = 5;
var url = "../RestServiceImpl.svc/json/" + id;
var type = 'GET';
callLoginService(url);
}
function callLoginService(url, type) {
$.ajax({
type: type,
url: url,
success: serviceSucceeded,
error: serviceFailed
});
}
function serviceSucceeded(result) {
alert(JSON.stringify(result));
}
function serviceFailed(result) {
alert('Service call failed: ' + result.status + '' + result.statusText);
}
Did u add this reference?
script type="text/javascript" src="../../json.js"></script>
I have same problem and search i get this and this result

Resources