Manifest v3 extension: asynchronous event listener does not keep the service worker alive [duplicate] - service-worker

I am trying to pass messages between content script and the extension
Here is what I have in content-script
chrome.runtime.sendMessage({type: "getUrls"}, function(response) {
console.log(response)
});
And in the background script I have
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.type == "getUrls"){
getUrls(request, sender, sendResponse)
}
});
function getUrls(request, sender, sendResponse){
var resp = sendResponse;
$.ajax({
url: "http://localhost:3000/urls",
method: 'GET',
success: function(d){
resp({urls: d})
}
});
}
Now if I send the response before the ajax call in the getUrls function, the response is sent successfully, but in the success method of the ajax call when I send the response it doesn't send it, when I go into debugging I can see that the port is null inside the code for sendResponse function.

From the documentation for chrome.runtime.onMessage.addListener:
This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until sendResponse is called).
So you just need to add return true; after the call to getUrls to indicate that you'll call the response function asynchronously.

The accepted answer is correct, I just wanted to add sample code that simplifies this.
The problem is that the API (in my view) is not well designed because it forces us developers to know if a particular message will be handled async or not. If you handle many different messages this becomes an impossible task because you never know if deep down some function a passed-in sendResponse will be called async or not.
Consider this:
chrome.extension.onMessage.addListener(function (request, sender, sendResponseParam) {
if (request.method == "method1") {
handleMethod1(sendResponse);
}
How can I know if deep down handleMethod1 the call will be async or not? How can someone that modifies handleMethod1 knows that it will break a caller by introducing something async?
My solution is this:
chrome.extension.onMessage.addListener(function (request, sender, sendResponseParam) {
var responseStatus = { bCalled: false };
function sendResponse(obj) { //dummy wrapper to deal with exceptions and detect async
try {
sendResponseParam(obj);
} catch (e) {
//error handling
}
responseStatus.bCalled= true;
}
if (request.method == "method1") {
handleMethod1(sendResponse);
}
else if (request.method == "method2") {
handleMethod2(sendResponse);
}
...
if (!responseStatus.bCalled) { //if its set, the call wasn't async, else it is.
return true;
}
});
This automatically handles the return value, regardless of how you choose to handle the message. Note that this assumes that you never forget to call the response function. Also note that chromium could have automated this for us, I don't see why they didn't.

You can use my library https://github.com/lawlietmester/webextension to make this work in both Chrome and FF with Firefox way without callbacks.
Your code will look like:
Browser.runtime.onMessage.addListener( request => new Promise( resolve => {
if( !request || typeof request !== 'object' || request.type !== "getUrls" ) return;
$.ajax({
'url': "http://localhost:3000/urls",
'method': 'GET'
}).then( urls => { resolve({ urls }); });
}) );

Related

How can the electron framework enable communication like an Ajax request?

ipcRenderer.sendSync may block the whole renderer process. ipcRenderer.send need use ipcRenderer.on to listen for the asynchronous return of events.
So is there a way of communicating that data can be returned as a callback directly where it was requested?
It might look something like this: ipcRenderer.sendAsync('eventName', args, callback), Or by other means.
ipcRenderer.on("onMessage", (e, {cbName, data}) => {
switch (cbName) {
case 'foo1':
foo1(data)
break
case 'foo2':
foo2(data)
break
case 'foo3':
foo2(data)
break
// more
default:
break
}
})
ipcRenderer.send("message", { cbName, /* other args */ })
what you need is ipcMain.handle() and ipcRenderer.invoke()
this will return a promise back to renderer.
// Main process
ipcMain.handle('my-invokable-ipc', async (event, ...args) => {
const result = await somePromise(...args)
return result
})
// Renderer process
async () => {
const result = await ipcRenderer.invoke('my-invokable-ipc', arg1, arg2)
// ...
}
For further info
https://www.electronjs.org/docs/api/ipc-main#ipcmainhandlechannel-listener
What you're asking for, if I understand it correctly, is to pass a callback from the renderer process to the main process and have the handler in main call into it with the response rather than sending it back.
so rather than doing this:
main.js:
ipcMain.on("message", (e, arg) => {
e.sender.send("onMessage", "response");
});
renderer.js:
ipcRenderer.send("message", 1);
ipcRenderer.on("onMessage", (e, response) => { });
you want to do this:
main.js:
ipcMain.on("message", (e, arg, callback) => {
callback("response");
});
renderer.js:
function callback(response) { }
ipcRenderer.send("message", 1, callback);
Then no, you can't do that because you can't pass functions across processes like this. Even if you .toString() the function and then recreate the function in the main process via new Function(...), you would be executing it in the context of the main process, not the renderer, and presumably you'd want to execute it in the renderer process.
Using e.sender.send(...) is the idiomatic way of sending messages back to the other process, and you shouldn't shy away from it.

Amazon Lex- slot elicitation not working inside callback

I have a call back function which is getting data from an external API and depends on a data check I have tried for a slot elicitation inside callback but looks like elicitation is not working inside the callback. Please find the code snippet below,
GetCustomerDetails().then(response => {
var serializedcustomerDetails = convert.xml2json(response.data, {
compact: true,
spaces: 2
});
var customerDetails = JSON.parse(serializedcustomerDetails);
let filteredCustomerDetails = _.filter(customerDetails.CustomerInfo.CustomerDetails, function (o) {
return o.CustomerName._text.includes(customerName);
})
if (filteredCustomerDetails.length == 1) {
callback(elicitSlot(outputSessionAttributes, intentRequest.currentIntent.name,
intentRequest.currentIntent.slots, '​CustomerCode', {
contentType: 'PlainText',
content: `Do you mean ${filteredCustomerDetails[0].CustomerName._text} of ${filteredCustomerDetails[0].SpecialityName._text} department?`
}));
return;
}
}).catch(error => {
console.log(`${error}`)
})
This is my first Awnser on stack so please bear with me.
I have come accross the same problem in a recent project and there are a few things that you can check.
How long does the API call take?
If your API call takes a long time it will be worth checking the timeout settings on your Lambda function. AWS Console -> Lambda -> Your Function -> Basic settings -> Timeout.
Does your Lambda function finish before the API call is done?
I fixed this issue by building a node module to handle my business logic, the module has a function called getNextSlot it returns as a Promise. Inside this function I check the incoming event and figure out which slot I need to elicit next, part of my flow is to call an API endpoint that takes around 10 seconds to complete.
I use the request-promise package to make the api call, this node module makes sure that the lambda function keeps running while the call is running.
exports.getData = function (url, data) {
var pr = require("request-promise");
var options = {
method: 'POST',
url: 'api.example',
qs: {},
headers:
{
'Content-Type': 'application/json'
},
body: {
"example": data
},
json: true,
timeout: 60000
};
return pr(options);
}
In my main code I call this function as:
apiModule.getData("test", "data")
.then(function (data) {
//Execute callback
})
.catch(function (error) {
console.log(error);
reject(error);
});
This solved the issue for me anyways.
Thanks,

Pass custom data to service worker sync?

I need to make a POST request and send some data. I'm using the service worker sync to handle offline situation.
But is there a way to pass the POST data to the service worker, so it makes the same request again?
Cause apparently the current solution is to store requests in some client side storage and after client gets connection - get the requests info from the storage and then send them.
Any more elegant way?
PS: I thought about just making the service worker send message to the application code so it does the request again ... but unfortunately it doesn't know the exact client that registered the service worker :(
You can use fetch-sync
or i use postmessage to fix this problem, which i agree that indexedDB looks trouble.
first of all, i send the message from html.
// send message to serviceWorker
function sync (url, options) {
navigator.serviceWorker.controller.postMessage({type: 'sync', url, options})
}
i got this message in serviceworker, and then i store it.
const syncStore = {}
self.addEventListener('message', event => {
if(event.data.type === 'sync') {
// get a unique id to save the data
const id = uuid()
syncStore[id] = event.data
// register a sync and pass the id as tag for it to get the data
self.registration.sync.register(id)
}
console.log(event.data)
})
in the sync event, i got the data and fetch
self.addEventListener('sync', event => {
// get the data by tag
const {url, options} = syncStore[event.tag]
event.waitUntil(fetch(url, options))
})
it works well in my test, what's more you can delete the memory store after the fetch
what's more, you may want to send back the result to the page. i will do this in the same way by postmessage.
as now i have to communicate between each other, i will change the fucnction sync into this way
// use messagechannel to communicate
sendMessageToSw (msg) {
return new Promise((resolve, reject) => {
// Create a Message Channel
const msg_chan = new MessageChannel()
// Handler for recieving message reply from service worker
msg_chan.port1.onmessage = event => {
if(event.data.error) {
reject(event.data.error)
} else {
resolve(event.data)
}
}
navigator.serviceWorker.controller.postMessage(msg, [msg_chan.port2])
})
}
// send message to serviceWorker
// you can see that i add a parse argument
// this is use to tell the serviceworker how to parse our data
function sync (url, options, parse) {
return sendMessageToSw({type: 'sync', url, options, parse})
}
i also have to change the message event, so that i can pass the port to sync event
self.addEventListener('message', event => {
if(isObject(event.data)) {
if(event.data.type === 'sync') {
// in this way, you can decide your tag
const id = event.data.id || uuid()
// pass the port into the memory stor
syncStore[id] = Object.assign({port: event.ports[0]}, event.data)
self.registration.sync.register(id)
}
}
})
up to now, we can handle the sync event
self.addEventListener('sync', event => {
const {url, options, port, parse} = syncStore[event.tag] || {}
// delete the memory
delete syncStore[event.tag]
event.waitUntil(fetch(url, options)
.then(response => {
// clone response because it will fail to parse if it parse again
const copy = response.clone()
if(response.ok) {
// parse it as you like
copy[parse]()
.then(data => {
// when success postmessage back
port.postMessage(data)
})
} else {
port.postMessage({error: response.status})
}
})
.catch(error => {
port.postMessage({error: error.message})
})
)
})
At the end. you cannot use postmessage to send response directly.Because it's illegal.So you need to parse it, such as text, json, blob, etc. i think that's enough.
As you have mention that, you may want to open the window.
i advice that you can use serviceworker to send a notification.
self.addEventListener('push', function (event) {
const title = 'i am a fucking test'
const options = {
body: 'Yay it works.',
}
event.waitUntil(self.registration.showNotification(title, options))
})
self.addEventListener('notificationclick', function (event) {
event.notification.close()
event.waitUntil(
clients.openWindow('https://yoursite.com')
)
})
when the client click we can open the window.
To comunicate with the serviceworker I use a trick:
in the fetch eventlistener I put this:
self.addEventListener('fetch', event => {
if (event.request.url.includes("sw_messages.js")) {
var zib = "some data";
event.respondWith(new Response("window.msg=" + JSON.stringify(zib) + ";", {
headers: {
'Content-Type': 'application/javascript'
}
}));
}
return;
});
then, in the main html I just add:
<script src="sw_messages.js"></script>
as the page loads, global variable msg will contain (in this example) "some data".

Ignore ajax requests in service worker

I have an app with a basic 'shell' of HTML, CSS and JS. The main content of the page is loaded via multiple ajax calls to an API that is at another URL to the one my app is running on. I have set up a service-worker to cache the main 'shell' of the application:
var urlsToCache = [
'/',
'styles/main.css',
'scripts/app.js',
'scripts/apiService.js',
'third_party/handlebars.min.js',
'third_party/handlebars-intl.min.js'
];
and to respond with the cached version when requested. The problem I am having is that the response of my ajax calls are also being cached. I'm pretty sure that I need to add some code to the fetch event of the service-worker that always get them from the network rather than looking in the cache.
Here is my fetch event:
self.addEventListener('fetch', function (event) {
// ignore anything other than GET requests
var request = event.request;
if (request.method !== 'GET') {
event.respondWith(fetch(request));
return;
}
// handle other requests
event.respondWith(
caches.open(CACHE_NAME).then(function (cache) {
return cache.match(event.request).then(function (response) {
return response || fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
I'm not sure how I can ignore the requests to the API. I've tried doing this:
if (request.url.indexOf(myAPIUrl !== -1) {
event.respondWith(fetch(request));
return;
}
but according to the network tab in Chrome Dev Tools, all of these responses are still coming from the service-worker.
You do not have to use event.respondWith(fetch(request)) to handle requests that you want to ignore. If you return without calling event.respondWith browser will fetch the resource for you.
You can do something like:
if (request.method !== 'GET') { return; }
if (request.url.indexOf(myAPIUrl) !== -1) { return; }
\\ handle all other requests
event.respondWith(/* return promise here */);
IOW as long as you can determine synchronously that you don't want to handle the request you can just return from the handler and let the default request processing to take over. Check out this example.

how to secure or set a rails style before_filter for all angular controllers?

I'm using angularjs for the front end and rails + devise for authentication on the backend.
On the front end I have added a responseInterceptor to redirect to the /#/sign_in page upon any 401 response from any xhr request and display a growl style pop-up message using toastr.
App.config(['$httpProvider', function ($httpProvider) {
$httpProvider.responseInterceptors.push('securityInterceptor');
}]);
App.factory('securityInterceptor', ['$injector', '$location', '$cookieStore', function ($injector,$location,$cookieStore) {
return function(promise) {
var $http = $injector.get('$http');
return promise.then(null, function(response){
if (response.status === 401) {
$cookieStore.remove('_angular_devise_user');
toastr.warning('You are logged out');
$location.path('/#/sign_in');
}
});
};
});
My problem is, when I click on a page that loads several xhr requests during the controllers initialization, for example:
var products = Product.query();
var categories = Category.query();
var variations = Variation.query();
These are needed for various navigation components and they all fire off in parallel, resulting in several duplicate growl-style messages.
Is there a way to make angular quit on the first 401 and stop execution of the rest of the controller from within the interceptor? In a traditional rails app, there would be a "before_filter" that stops regular execution, preventing the page and queries from loading... what's the best way to do this in angular?
I've been pondering about this problem for my own apps too. A sketch of my thoughts (NOT REAL IMPLEMENTATION, SO BEWARE):
A userData service keeps track of whether the user is logged in + other information (e.g. user name, real user name etc):
App.service("userData", function() {
var currentData = {
loggedIn: false
};
function getCurrent() {
return currentData;
}
// called when the user logs in with the user data
function loggedIn(userData) {
// the object is REPLACED to avoid race conditions, see explanation below
currentData = angular.extend({loggedIn: true}, userData);
}
return {
getCurrent: getCurrent,
loggedIn: loggedIn
};
});
The interceptors keep track of the currentData. If an interceptor receives HTTP 401 and the loggedIn flag is true, it changes the flag to false and redirects to the login view. If an interceptor receives HTTP 401 and the loggedIn flag is false, it does nothing besides rejecting the request, because another interceptor has done the view redirection.
When the user logs in, the currentData is replaced, so as to avoid situations with delayed responses (e.g. call1 and call2 are initiated, call1 responds 401; call2 also results in 401, but the delivery of the actual response is delayed; then the user logs in again; then call2 receives its 401; the second 401 should not overwrite the current state)
App.config(["$provide", "$httpProvider", function($provide, $httpProvider) {
$provide.factory("myHttpInterceptor", ["$q", "userData", "$cookieStore", "toastr", "$location",
function($q, userData, $cookieStore, toastr, $location) {
return {
request: function(config) {
config.currentUserData = userData.getCurrent();
return config;
},
responseError: function(rejection) {
if( rejection && rejection.status === 401 && rejection.config && rejection.config.currentUserData && rejection.config.currentUserData.loggedIn ) {
rejection.config.currentUserData.loggedIn = false;
$cookieStore.remove('_angular_devise_user');
toastr.warning('You are logged out');
$location.path('/#/sign_in');
}
return $q.reject(rejection);
}
};
}
]);
$httpProvider.interceptors.push("myHttpInterceptor");
});
Also note I am using the newer way to register interceptors, as $httpProvider.responseInterceptors seems to be deprecated.

Resources