I have a problem where my MessageChannel does not send a message from a service worker to a client. Why MessageChannel blocks forever and the client never gets message?
In the sw on fetch event I create a MessageChannel then I do postMessage, however the client never gets addEventListener('message' fired why ?
For service worker I created method like that:
self.addEventListener("fetch", async (event) => {
// Exit early if we don't have access to the client.
// Eg, if it's cross-origin.
if (!event.clientId) return null;
// eslint-disable-next-line
const client = await clients.get(event.clientId);
const response = await sendMessageToClient(client, { url: event.request.url }, 5000);
})
async function sendMessageToClient(client, message, timeoutAfter) {
// Exit early if we don't get the client.
// Eg, if it closed.
if (!client) return null;
// This wraps the message posting/response in a promise, which will resolve if the response doesn't
// contain an error, and reject with the error if it does.
return await new Promise((resolve, reject) => {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function (event) {
if (event.data && event.data.error) {
reject(event.data.error);
} else {
resolve(event.data);
}
};
// This sends the message data as well as transferring messageChannel.port2 to the client.
// The client 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
client.postMessage(message, [messageChannel.port2]);
// Set up the timeout
setTimeout(() => {
messageChannel.port1.close();
messageChannel.port2.close();
reject('Promise timed out after ' + timeoutAfter + ' ms');
}, timeoutAfter);
});
}
On the client I expect to get message here:
// Set up a listener for messages posted from the service worker.
// The service worker is set to post a message to specific client only
// so you should see this message event fire once.
// You can force it to fire again by visiting this page in an Incognito window.
navigator.serviceWorker.addEventListener('message', function (event) {
console.log(event.data);
});
But I never get the message on client and if there is no time out it will w8 forever.
Update
what i have tried:
window.addEventListener('message', function (event) { console.log('window.addEventListener', event.data) });
window.onmessage = function (event) { console.log('window.onmessage', event.data) };
navigator.serviceWorker.addEventListener('message', function (event) { console.log('navigator.serviceWorker.addEventListener', event.data) });
navigator.serviceWorker.onmessage = function (event) { console.log('navigator.serviceWorker.onmessage', event.data) };
// navigator.serviceWorker.controller.addEventListener('message', function (event) { console.log('navigator.serviceWorker.contoller.addEventListener', event.data) });
// navigator.serviceWorker.controller.onmessage = function (event) { console.log('navigator.serviceWorker.contoller.onmessage', event.data) };
navigator.serviceWorker.ready.then(function (registration) {
console.log('navigator.serviceWorker.ready');
registration.active.addEventListener('message', function (event) { console.log('registration.active.addEventListener', event.data) });
registration.active.onmessage = function (event) { console.log('registration.active.onmessage', event.data) };
});
navigator.serviceWorker.register("sw.js")
.then(function (registration) {
// Registration was successful
console.log("ServiceWorker registration successful with scope: ", registration.scope);
})
.catch(function (error) {
console.error("Service Worker Error", error);
});
}
and the only logs I got were:
Navigated to http://localhost:3000/ navigator.serviceWorker.ready
ServiceWorker registration successful with scope:
http://localhost:3000/
Related
I have this endpoint. This api takes long time to get the response.
app.get('/api/execute_script',(req,res) =>{
//code here
}
I have following endpoint which will kill the process
app.get('/api/kill-process',(req,res) => {
//code here
}
but unless first api gets response second api doesnt get executed. How to cancel previous api request and execute the second request?
You can use an EventEmitter to kill the other process, all you'll need is a session/user/process identifier.
const EventEmitter = require('events');
const emitter = new EventEmitter();
app.get('/api/execute_script', async(req,res,next) => {
const eventName = `kill-${req.user.id}`; // User/session identifier
const proc = someProcess();
const listener = () => {
// or whatever you have to kill/destroy/abort the process
proc.abort()
}
try {
emitter.once(eventName, listener);
await proc
// only respond if the process was not aborted
res.send('process done')
} catch(e) {
// Process should reject if aborted
if(e.code !== 'aborted') {
// Or whatever status code
return res.status(504).send('Timeout');
}
// process error
next(e);
} finally {
// cleanup
emitter.removeListener(eventName, listener)
}
})
app.get('/api/kill-process',(req,res) => {
//code here
const eventName = `kill-${req.user.id}`;
// .emit returns true if the emitter has listener attached
const killed = emitter.emit(eventName);
res.send({ killed })
})
I want to connect to a Unix Domain Socket server in a Node application. If the connection succeeds and was opened, a loop (that may take some time) shall be executed. If an error occurs during the execution of this loop, it should receive some kind of notification. If a connection to the client is not possible at all, the loop should not be executed in the first place (that seems to work with the Promise). To me this sounds like the most simple thing in the world, but I just can't get it to work... This is what I have until now:
new Promise(function(resolve, reject) {
let connection = net.createConnection('/tmp/socket.s', () => {resolve(connection);})
.on('data', function(data) {
// Do something (during loop execution)
})
.on('error', reject); // If this callback is executed, the while loop should terminate (by receiving some kind of signal within the loop)
}).then(function(connection) {
for(...) {
// Do stuff that takes some time, executes other callbacks, sends data to the socket
}
connection.end();
}, function(error) {
// Error handling
});
What am I missing?
Try to listen to the data event in the resolve section of the promise. The following code should do it:
const net = require('net');
/**
* Client
* --------------------------------------------
*/
new Promise((resolve, reject) => {
let client = net.createConnection({ path: '/tmp/socket.s'}, () => {
console.log('Client: connected ')
resolve(client);
});
// Reject on error
client.on('error', err => reject(err) );
client.on('end', () => {
console.log('Client: disconnected from server #1');
});
}).then( connection => {
connection.on('data', data => {
// Do stuff with the data
console.log(`Client: the server says: ${data.toString()}\n`);
if(data != 'Data recieved'){
// Just to ensure that the following loop runs only once
for (let i = 0; i <= 10; i++) {
setTimeout(() => {
// Send data to the server
connection.write(`Client Data ${i}`);
if (i == 10) {
// Close the connection after everything is done
connection.end();
}
}, i*2000);
}
}
});
}, error => {
console.log('Client: promise rejection error', error );
});
My test server looks like this
const net = require('net');
/**
* Server
* --------------------------------------------
*/
const server = net.createServer( connectionListener => {
console.log(`#${process.pid} Server: client connected`);
connectionListener.on('end', () => {
console.log(`#${process.pid} Server: client disconnected`);
});
connectionListener.write('Hello\r\n');
connectionListener.on('data', data => {
console.log(`#${process.pid} Server: client sends: ${data.toString()}`);
connectionListener.write('Data recieved');
});
});
server.on('error', (err) => {
console.log(err);
server.close();
});
server.listen('/tmp/socket.s', () => {
console.log(`#${process.pid} Server: server bound`);
});
process.on('exit', code => {
console.log(code);
server.close();
});
process.on('SIGTERM', () => server.close() );
process.on('SIGINT', () => server.close() );
In this example the client sends data to server and the server replies each time. The client then closes the connection after having sent data 10 times.
P.S. There is no need to use a Promise unless you do need to return a promise at some point in your code.
I want to use a service worker to cache responses that can be used when the user is either offline or the my app's backend is down. For user experience reasons, I'd like to show a notification to the user that the app's backend currently can't be reached and that cached content is being served instead. What's the best way to do that? I can add a header to the response in the service worker but I'm not sure that's the "right way"... it seems like there should be a more straight-forward pattern. This is my service worker code:
self.addEventListener('fetch', event => {
console.log(`fetch event`, event);
event.respondWith(doFetch(event.request));
});
// fetch from network, fallback to cache
function doFetch(request) {
return fetch(request)
.then(response => {
return caches.open(CACHE)
.then(cache => {
cache.put(request, response.clone());
return response;
})
})
.catch(error => {
console.warn(`fetch to ${request.url} failed`, error);
return fromCache(request);
});
}
function fromCache(request) {
return caches.open(CACHE)
.then(cache => cache.match(request))
.then(match => {
if (match) {
// response.clone doesn't work here because I need to modify it
return cloneResponse(match);
} else {
throw new Error(`no match for ${request.url}`);
}
});
}
// this clones a response in a way that let's me modify it
function cloneResponse(response) {
let init = {
status: response.status,
statusText: response.statusText,
headers: { 'X-From-SW-Cache': 'true' }
};
response.headers.forEach((v, k) => {
init.headers[k] = v;
});
return response.text().then((body) => new Response(body, init));
}
Adding a header to the response is definitely one option.
Another option to consider is to use postMessage() to send a message to the client page letting it know that a cached response is being used. You could extend that logic to also send a message to the client letting it know when a network response is used, if you want to toggle some UI element once you're back to using the network.
Here's the code to accomplish that in your service worker:
async function postToClient(clientId, message) {
const client = await clients.get(clientId);
client.postMessage(message);
}
async function responseLogic(event) {
const cache = await caches.open('cache-name');
try {
const response = await fetch(event.request);
await cache.put(event.request, response.clone());
event.waitUntil(postToClient(event.request.clientId, 'FROM_NETWORK'));
return response;
} catch (error) {
const cachedResponse = await cache.match(event.request);
if (cachedResponse) {
event.waitUntil(postToClient(event.request.clientId, 'FROM_CACHE'));
return cachedResponse;
}
throw error;
}
}
self.addEventListener('fetch', (event) => {
// Check to see if this is a request for your API:
if (event.request.url === 'https://example.com/api') {
event.respondWith(responseLogic(event));
}
});
And then on your client page, you can register a message listener to respond to those messages from the service worker by showing or hiding some UI element, depending on whether the last response came from the network or cache:
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data === 'FROM_NETWORK') {
// Do something in your UI.
} else if (event.data === 'FROM_CACHE') {
// Do something in your UI.
}
});
I am writing web app which controls hardware. I have a server communicated with the device through the serial port. Everything works except the interaction with a user. The device has registers which I repeatedly ask waiting for some values. If some values come, I emit an event to the client and confirmation box appears. The user selects resume or abort. After that client emit the response (true or false) and I would like to resolve this response in my promise function. I need to catch response from the user exactly in the function because I have a sequence of actions I need to proceed. Promise after promise. It seems that my function ends before the user answers. How to solve this problem?
this is my code on the server:
waitUserResponse(message) {
return new Promise(async (resolve) => {
const handler = function(data) {
console.log('userAnswer = ', data);
resolve(data);
return;
}
this.io.sockets.emit('alerts', message);
this.io.sockets.once('userAnswer', handler);
})
}
this is my code on the client:
componentDidMount() {
const confirmDialog = (msg) => {
return new Promise((resolve) => {
let confirmed = window.confirm(msg);
resolve(confirmed);
return;
})
}
socket.on('alerts', data => {
confirmDialog(data).then(data => {
console.log(data);
socket.emit('userAnswer', data);
});
});
}
I should use socket.id - id for my connection
io.sockets.connected[socket.id].once('userResponce', handler);
I'm having trouble to wrap my head around the Clients.claim API of the ServiceWorker. From what I understand (here and here) I can call claim() on the service worker activate event to prevent having to refresh the page to initialize the ServiceWorker. I can't get it to work though and always end up having to refresh. Here's my code:
Inside the service worker:
self.addEventListener('install', function (event) {
self.skipWaiting();
event.waitUntil(caches.open(CURRENT_CACHE_DICT.prefetch)
.then(function(cache) {
var cachePromises = PREFETCH_URL_LIST.map(function(prefetch_url) {
var url = new URL(prefetch_url, location.href),
request = new Request(url, {mode: 'no-cors'});
return fetch(request).then(function(response) {
if (response.status >= 400) {
throw new Error('request for ' + prefetch_url +
' failed with status ' + response.statusText);
}
return cache.put(prefetch_url, response);
}).catch(function(error) {
console.error('Not caching ' + prefetch_url + ' due to ' + error);
});
});
return Promise.all(cachePromises).then(function() {
console.log('Pre-fetching complete.');
});
}).catch(function(error) {
console.error('Pre-fetching failed:', error);
})
);
});
self.addEventListener('activate', function (event) {
// claim the scope immediately
// XXX does not work?
//self.clients.claim();
event.waitUntil(self.clients.claim()
.then(caches.keys)
.then(function(cache_name_list) {
return Promise.all(
cache_name_list.map(function() {...}
);
})
);
});
The above runs but I'm ending up having to refresh and found anIllegal invocation error in the Chrome ServiceWorker internals. If I remove the clients.claim from the waitUntil handler and uncomment the previous one, I get no errors, but I still have to refresh. The debugger shows:
Console: {"lineNumber":128,"message":"Pre-fetching complete.","message_level":1,"sourceIdentifier":3,"sourceURL":""}
Console: {"lineNumber":0,"message":"Uncaught (in promise) TypeError: Illegal invocation","message_level":3,"sourceIdentifier":1,"sourceURL":""}
The refresh is triggered like this:
function waitForInstallation(registration) {
return new RSVP.Promise(function(resolve, reject) {
if (registration.installing) {
registration.installing.addEventListener('statechange', function(e) {
if (e.target.state == 'installed') {
resolve();
} else if (e.target.state == 'redundant') {
reject(e);
}
});
} else {
resolve();
}
});
}
// refreshing should not be necessary if scope is claimed on activate
function claimScope(installation) {
return new RSVP.Promise(function (resolve, reject) {
if (navigator.serviceWorker.controller) {
resolve();
} else {
reject(new Error("Please refresh to initialize serviceworker."));
}
});
}
rJS(window)
.declareMethod('render', function (my_option_dict) {
var gadget = this;
if ('serviceWorker' in navigator) {
return new RSVP.Queue()
.push(function () {
return navigator.serviceWorker.register(
my_option_dict.serviceworker_url,
{scope: my_option_dict.scope}
);
})
.push(function (registration) {
return waitForInstallation(registration);
})
.push(function (installation) {
return claimScope(installation);
})
.push(null, function (my_error) {
console.log(my_error);
throw my_error;
});
} else {
throw new Error("Browser does not support serviceworker.");
}
});
Question:
How do I correctly prevent the page from having to be refreshed to activate the ServiceWorker using claim? None of the links I found mentioned having to explicitly check for controller but I assume if a ServiceWorker is active it would have a controller accessible.
Thanks for shedding some info.
EDIT:
Figured it out with help from below. This made it work for me:
// runs while an existing worker runs or nothing controls the page (update here)
self.addEventListener('install', function (event) {
event.waitUntil(caches.open(CURRENT_CACHE_DICT.dictionary)
.then(function(cache) {
var cache_promise_list = DICTIONARY_URL_LIST.map(function(prefetch_url) {...});
return Promise.all(cache_promise_list).then(function() {
console.log('Pre-fetching complete.');
});
})
.then(function () {
// force waiting worker to become active worker (claim)
self.skipWaiting();
}).catch(function(error) {
console.error('Pre-fetching failed:', error);
})
);
});
// runs active page, changes here (like deleting old cache) breaks page
self.addEventListener('activate', function (event) {
event.waitUntil(caches.keys()
.then(function(cache_name_list) {
return Promise.all(
cache_name_list.map(function(cache_name) { ... })
);
})
.then(function () {
return self.clients.claim();
})
);
});
Triggering script:
var SW = navigator.serviceWorker;
function installServiceWorker(my_option_dict) {
return new RSVP.Queue()
.push(function () {
return SW.getRegistration();
})
.push(function (is_registered_worker) {
// XXX What if this isn't mine?
if (!is_registered_worker) {
return SW.register(
my_option_dict.serviceworker_url, {
"scope": my_option_dict.scope
}
);
}
return is_registered_worker;
});
}
function waitForInstallation(registration) {
return new RSVP.Promise(function(resolve, reject) {
if (registration.installing) {
// If the current registration represents the "installing" service
// worker, then wait until the installation step completes (during
// which any defined resources are pre-fetched) to continue.
registration.installing.addEventListener('statechange', function(e) {
if (e.target.state == 'installed') {
resolve(registration);
} else if (e.target.state == 'redundant') {
reject(e);
}
});
} else {
// Otherwise, if this isn't the "installing" service worker, then
// installation must have beencompleted during a previous visit to this
// page, and the any resources will already have benn pre-fetched So
// we can proceed right away.
resolve(registration);
}
});
}
// refreshing should not be necessary if scope is claimed on activate
function claimScope(registration) {
return new RSVP.Promise(function (resolve, reject) {
if (registration.active.state === 'activated') {
resolve();
} else {
reject(new Error("Please refresh to initialize serviceworker."));
}
});
}
rJS(window)
.ready(function (my_gadget) {
my_gadget.property_dict = {};
})
.declareMethod('render', function (my_option_dict) {
var gadget = this;
if (!SW) {
throw new Error("Browser does not support serviceworker.");
}
return new RSVP.Queue()
.push(function () {
return installServiceWorker(my_option_dict),
})
.push(function (my_promise) {
return waitForInstallation(my_promise);
})
.push(function (my_installation) {
return claimScope(my_installation);
})
.push(function () {
return gadget;
})
.push(null, function (my_error) {
console.log(my_error);
throw my_error;
});
});
Firstly, you are seem to be getting the error because of a typo in your code. See notes about it at the bottom.
Besides, skipWaiting() and Clients.claim() does both install and activate new SW with a single request. But quite naturally you will only get static assets like css after you reload.
So, even when equipped with skipWaiting() and Clients.claim(), you need two page reloads to see updated static content like new html or styles;
Page load #1
Request to sw.js is made, and since SW contents is changed install event is fired on it.
Also activate event is fired, since you have self.skipWaiting() in your install handler.
Consequently, your activate handler run and there is your self.clients.claim() call. Which will order the SW to take over the control of all the clients which under control of it's predecessor.
At this point, assets in cache are updated and your pages are all controlled by new service worker. Any Ajax request in range of service worker will return newly cached responses, for example.
Page load #2
Your app loads, and your SW responds from cache by hijacking the requests as usual. But now caches are up-to-date, and user gets to use app completely with new assets.
The error you are getting
Uncaught (in promise) TypeError: Illegal invocation error must be due to a missing a parenthesis in your activate handler;
event.waitUntil(self.clients.claim()
.then(caches.keys)
.then(function(cache_name_list) {
return Promise.all(
cache_name_list.map(function() {...}
); <-- Here is our very lonely single parenthesis.
})
);
That error should go away if you fix it.