Javascript - websocket message stuck in switch case [duplicate] - javascript

I'm trying to implement a WebSocket with a fallback to polling. If the WebSocket connection succeeds, readyState becomes 1, but if it fails, readyState is 3, and I should begin polling.
I tried something like this:
var socket = new WebSocket(url);
socket.onmessage = onmsg;
while (socket.readyState == 0)
{
}
if (socket.readyState != 1)
{
// fall back to polling
setInterval(poll, interval);
}
I was expecting socket.readyState to update asynchronously, and allow me to read it immediately. However, when I run this, my browser freezes (I left it open for about half a minute before giving up).
I thought perhaps there was an onreadyStateChanged event, but I didn't see one in the MDN reference.
How should I be implementing this? Apparently an empty loop won't work, and there is no event for this.

This is simple and it work perfectly... you can add condition about maximal time, or number of try to make it more robust...
function sendMessage(msg){
// Wait until the state of the socket is not ready and send the message when it is...
waitForSocketConnection(ws, function(){
console.log("message sent!!!");
ws.send(msg);
});
}
// Make the function wait until the connection is made...
function waitForSocketConnection(socket, callback){
setTimeout(
function () {
if (socket.readyState === 1) {
console.log("Connection is made")
if (callback != null){
callback();
}
} else {
console.log("wait for connection...")
waitForSocketConnection(socket, callback);
}
}, 5); // wait 5 milisecond for the connection...
}

Here is a more elaborate explanation. First off, check the specific browser API, as not all browsers will be on the latest RFC. You can consult the
You don't want to run a loop to constantly check the readystate, it's extra overhead you don't need. A better approach is to understand all of the events relevant to a readystate change, and then wire them up appropriately. They are as follows:
onclose An event listener to be called when the WebSocket connection's readyState changes to CLOSED. The listener receives a CloseEvent named "close".
onerror An event listener to be called when an error occurs. This is a simple event named "error".
onmessage An event listener to be called when a message is received from the server. The listener receives a MessageEvent named "message".
onopen An event listener to be called when the WebSocket connection's readyState changes to OPEN; this indicates that the connection is ready to send and receive data. The event is a simple one with the name "open".
JS is entirely event driven, so you need to just wire up all of these events and check for the readystate, this way you can switch from WS to polling accordingly.
I recommend you look at the Mozilla reference, it's easier to read than the RFC document and it will give you a good overview of the API and how it works (link).
Don't forget to do a callback for a retry if you have a failure and poll until the callback for a successful reconnect is fired.

I am not using pooling at all. Instead, I use queuing.
First I create new send function and a queue:
var msgs = []
function send (msg) {
if (ws.readyState !== 1) {
msgs.push(msg)
} else {
ws.send(msg)
}
}
Then I need to read and send when the connection is first established:
function my_element_click () {
if (ws == null){
ws = new WebSocket(websocket_url)
ws.onopen = function () {
while (msgs.length > 0) {
ws.send(msgs.pop())
}
}
ws.onerror = function(error) {
// do sth on error
}
}
msg = {type: 'mymessage', data: my_element.value}
send(JSON.stringify(msg))
}
WebSocket connection in this example is created only on the first click. Usually, on second messages start to be sent directly.

Look on http://dev.w3.org/html5/websockets/
Search for "Event handler" and find the Table.
onopen -> open
onmessage -> message
onerror ->error
onclose ->close
function update(e){ /*Do Something*/};
var ws = new WebSocket("ws://localhost:9999/");
ws.onmessage = update;

If you use async/await and you just want to wait until the connection is available I would suggest this function :
async connection (socket, timeout = 10000) {
const isOpened = () => (socket.readyState === WebSocket.OPEN)
if (socket.readyState !== WebSocket.CONNECTING) {
return isOpened()
}
else {
const intrasleep = 100
const ttl = timeout / intrasleep // time to loop
let loop = 0
while (socket.readyState === WebSocket.CONNECTING && loop < ttl) {
await new Promise(resolve => setTimeout(resolve, intrasleep))
loop++
}
return isOpened()
}
}
Usage (in async function) :
const websocket = new WebSocket('...')
const opened = await connection(websocket)
if (opened) {
websocket.send('hello')
}
else {
console.log("the socket is closed OR couldn't have the socket in time, program crashed");
return
}

tl;dr
A simple proxy wrapper to add state event to WebSocket which will be emitted when its readyState changes:
const WebSocketProxy = new Proxy(WebSocket, {
construct: function(target, args) {
// create WebSocket instance
const instance = new target(...args);
//internal function to dispatch 'state' event when readyState changed
function _dispatchStateChangedEvent() {
instance.dispatchEvent(new Event('state'));
if (instance.onstate && typeof instance.onstate === 'function') {
instance.onstate();
}
}
//dispatch event immediately after websocket was initiated
//obviously it will be CONNECTING event
setTimeout(function () {
_dispatchStateChangedEvent();
}, 0);
// WebSocket "onopen" handler
const openHandler = () => {
_dispatchStateChangedEvent();
};
// WebSocket "onclose" handler
const closeHandler = () => {
_dispatchStateChangedEvent();
instance.removeEventListener('open', openHandler);
instance.removeEventListener('close', closeHandler);
};
// add event listeners
instance.addEventListener('open', openHandler);
instance.addEventListener('close', closeHandler);
return instance;
}
});
A long explanation:
You can use a Proxy object to monitor inner WebSocket state.
This is a good article which explains how to do it Debugging WebSockets using JS Proxy Object
And here is an example of code snippet from the article above in case the site won't be available in the future:
// proxy the window.WebSocket object
var WebSocketProxy = new Proxy(window.WebSocket, {
construct: function(target, args) {
// create WebSocket instance
const instance = new target(...args);
// WebSocket "onopen" handler
const openHandler = (event) => {
console.log('Open', event);
};
// WebSocket "onmessage" handler
const messageHandler = (event) => {
console.log('Message', event);
};
// WebSocket "onclose" handler
const closeHandler = (event) => {
console.log('Close', event);
// remove event listeners
instance.removeEventListener('open', openHandler);
instance.removeEventListener('message', messageHandler);
instance.removeEventListener('close', closeHandler);
};
// add event listeners
instance.addEventListener('open', openHandler);
instance.addEventListener('message', messageHandler);
instance.addEventListener('close', closeHandler);
// proxy the WebSocket.send() function
const sendProxy = new Proxy(instance.send, {
apply: function(target, thisArg, args) {
console.log('Send', args);
target.apply(thisArg, args);
}
});
// replace the native send function with the proxy
instance.send = sendProxy;
// return the WebSocket instance
return instance;
}
});
// replace the native WebSocket with the proxy
window.WebSocket = WebSocketProxy;

Just like you defined an onmessage handler, you can also define an onerror handler. This one will be called when the connection fails.
var socket = new WebSocket(url);
socket.onmessage = onmsg;
socket.onerror = function(error) {
// connection failed - try polling
}

Your while loop is probably locking up your thread. Try using:
setTimeout(function(){
if(socket.readyState === 0) {
//do nothing
} else if (socket.readyState !=1) {
//fallback
setInterval(poll, interval);
}
}, 50);

In my use case, I wanted to show an error on screen if the connection fails.
let $connectionError = document.getElementById("connection-error");
setTimeout( () => {
if (ws.readyState !== 1) {
$connectionError.classList.add( "show" );
}
}, 100 ); // ms
Note that in Safari (9.1.2) no error event gets fired - otherwise I would have placed this in the error handler.

Related

Event timing in JavaScript

I am connecting to a web socket server from my web page, and sending click events to the server through this.
In my click routine, I am doing
if (socket.readyState === WebSocket.OPEN) {
//send
}else {
msgsArray.push(messages)
}
Then in socket.onOpen, I post all the messages from msgsArray using shift();
But there seems to be a possible race condition, if the socket is opened after I've check that the web socket isn't open but before I've added the message to the array.
Is there actually no race condition because JavaScript is single-threaded? If not, is there any way for me to make this "thread"-safe? Is it guaranteed that onOpen will only be called after my click event processing is finished?
--- Update ---
The race condition I am referring to is where socket.readyState is not OPEN where I'm checking it, but it becomes OPEN and socket.onopen is called before I add the message to the array. But actually I don't think that can happen, can it? onopen can't be called before the click routine finishes up, right? But what about the opposite, where socket.readyState is OPEN, but closes right as I am writing to it? I will add try catch around that and add it to the array in the catch block. I think this should handle all possible situations. If not, can you advise?
After reading the documentation on WebSocket.send, I updated my code to be completely "race-safe"
//Obj.socketMessagesToSend is a JS array ([])
if (Obj.socket && Obj.socket.readyState === WebSocket.OPEN) {
try {
Obj.socket.send(JSON.stringify(report));
} catch (e) {
Obj.socketMessagesToSend.push(JSON.stringify(report));
Obj.connectToWebSocket();
}
} else {
Obj.socketMessagesToSend.push(JSON.stringify(report));
Obj.connectToWebSocket();
}
And
Obj.connectToWebSocket() = function() {
if (Obj.socket && (Obj.socket.readyState === WebSocket.CONNECTING || Obj.socket.readyState === WebSocket.OPEN)) return;
if (typeof Config.WebSocketInfo != 'object') return;
if (!Config.WebSocketInfo.enabled) return;
try {
Obj.socket = new WebSocket(Config.WebSocketInfo.url);
Obj.socket.onmessage = Obj.socketOnMessage;
Obj.socket.onopen = function(e) {
// console.log("Web Socket Connection established!");
while (Obj.socketMessagesToSend.length != 0) {
Obj.socket.send(Obj.socketMessagesToSend.shift());
}
}
} catch (e) {
console.log('Could not connect. Is connection blocked by your content-security-policy?');
}
}

IndexedDB's callbacks not being executed inside the 'fetch' event of a Service Worker

I'm trying to do a couple of things in the IndexedDB database inside the 'fetch' event of a service worker, when the aplication asks the server for a new page. Here's what I'm going for:
Create a new object store (they need to be created dynamically, according to the data that 'fetch' picks up);
Store an element on the store.
Or, if the store already exists:
Get an element from the store;
Update the element and store it back on the store.
The problem is that the callbacks (onupgradeneeded, onsuccess, etc) never get executed.
I've been trying with the callbacks inside of each other, though I know that may not be the best approach. I've also tried placing an event.waitUntil() on 'fetch' but it didn't help.
The 'fetch' event, where the function registerPageAccess is called:
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
event.waitUntil(function () {
const nextPageURL = new URL(event.request.url);
if (event.request.destination == 'document') {
if (currentURL) {
registerPageAccess(currentURL, nextPageURL);
}
currentURL = nextPageURL;
}
}());
/*
* some other operations
*/
return response || fetch(event.request);
})
);
});
registerPageAccess, the function with the callbacks.
I know it's plenty of code, but just look at secondRequest.onupgradeneeded in the 5th line. It is never executed, let alone the following ones.
function registerPageAccess(currentPageURL, nextPageURL) {
var newVersion = parseInt(db.version) + 1;
var secondRequest = indexedDB.open(DB_NAME, newVersion);
secondRequest.onupgradeneeded = function (e) {
db = e.target.result;
db.createObjectStore(currentPageURL, { keyPath: "pageURL" });
var transaction = request.result.transaction([currentPageURL], 'readwrite');
var store = transaction.objectStore(currentPageURL);
var getRequest = store.get(nextPageURL);
getRequest.onsuccess = function (event) {
var obj = getRequest.result;
if (!obj) {
// Insert element into the database
console.debug('ServiceWorker: No matching object in the database');
const addRes = putInObjectStore(nextPageURL, 1, store);
addRes.onsuccess = function (event) {
console.debug('ServiceWorker: Element was successfully added in the Object Store');
}
addRes.onerror = function (event) {
console.error('ServiceWorker error adding element to the Object Store: ' + addRes.error);
}
}
else {
// Updating database element
const updRes = putInObjectStore(obj.pageURL, obj.nVisits + 1, store);
updRes.onsuccess = function (event) {
console.debug('ServiceWorker: Element was successfully updated in the Object Store');
}
updRes.onerror = function (event) {
console.error('ServiceWorker error updating element of the Object Store: ' + putRes.error);
}
}
};
};
secondRequest.onsuccess = function (e) {
console.log('ServiceWorker: secondRequest onsuccess');
};
secondRequest.onerror = function (e) {
console.error('ServiceWorker: error on the secondRequest.open: ' + secondRequest.error);
};
}
I need a way to perform the operations in registerPageAccess, which involve executing a couple of callbacks, but the browser seems to kill the Service Worker before they get to occur.
All asynchronous logic inside of a service worker needs to be promise-based. Because IndexedDB is callback-based, you're going to find yourself needing to wrap the relevant callbacks in a promise.
I'd strongly recommend not attempting to do this on your own, and instead using one of the following libraries, which are well-tested, efficient, and lightweight:
idb-keyval, if you're okay with a simple key-value store.
idb if you're need the full IndexedDB API.
I'd also recommend that you consider using the async/await syntax inside of your service worker's fetch handler, as it tends to make promise-based code more readable.
Put together, this would look roughly like:
self.addEventListener('fetch', (event) => {
event.waitUntil((async () => {
// Your IDB cleanup logic here.
// Basically, anything that can execute separately
// from response generation.
})());
event.respondWith((async () => {
// Your response generation logic here.
// Return a Response object at the end of the function.
})());
});

Reassigning a variable after already in use?

I've got an app I'm writing in React Native. It's socketed and I have a file that controls all socket information.
import {Alert, AppState} from 'react-native';
import store from '../store/store';
import {updateNotifications} from '../reducers/notifications';
import {setError, clearError} from '../reducers/error';
import {updateCurrentEvent, updateEventStatus, setCurrentEvent} from '../reducers/event_details';
import {setAlert} from '../reducers/alert';
import {ws_url} from '../api/urls'
let conn = new WebSocket(ws_url);
/*
handleSocketConnections handles any actions that require rerouting. The rest are passed off to handleOnMessage
This is being called from authLogin on componentDidMount. It would be ideal to only initialize a socket conn
when a user logs in somehow, but this package gets ran when a user opens the app, meaning there are socket
connections that don't need to exist yet.
*/
function setAppStateHandler() {
AppState.addEventListener('change', cstate => {
if(cstate === 'active') {
reconnect()
}
})
}
export const handleSocketConnections = (navigator, route) => {
setAppStateHandler();
conn.onmessage = e => {
const state = store.getState();
const msg = JSON.parse(e.data);
const { type, payload, event_id } = msg;
const { event } = state.event_details.event_details;
if (type == "SET_EVENT_STATUS" && payload == "CLOSED" && event_id == event.event_id) {
navigator.push(route)
// store.dispatch(setAlert({
// message:"Event is closed, click to navigate to checkout."
// , scene: null
// }))
store.dispatch(updateEventStatus(payload));
} else {
handleOnMessage(msg, state)
}
}
}
export function reconnect() {
//TODO: Fatal errors should redirect the mainNav to a fatal error screen. Not dismount the nav entirely, as it does now
//and this should pop the error screen when it's fixed.
let state = store.getState();
conn = new WebSocket(ws_url);
setTimeout(function () {
if (conn.readyState == 1) {
if (typeof state.event_details.event_details != 'undefined') {
setSocketedEventInfo(state.event_details.event_details.event.event_id);
}
store.dispatch(clearError());
} else {
store.dispatch(setError('fatal',`Socket readyState should be 1 but it's ${conn.readyState}`))
}
}, 1000);
}
//Preform function on ES close.
conn.onclose = e => {
console.log("Closing wsbidder, ", `${e.code} -- ${e.reason}`);
//TODO: Set error here saying they need to restart the app. Maybe a 'reconnect' somehow?
//Maybe set a store variable to socketErr and if null, all is good. Else, panic the app?
//Use Case: Server is not started and user tries to connect to the app. String of e.message contains "Connection refused"
store.dispatch(setError("fatal", `Socket onclose: ${e.code} -- ${e.reason}`))
};
conn.onerror = e => {
console.log("Error at socket, ", e);
store.dispatch(setError("fatal", `Socket onerror: ${e.message}`))
};
//Initialization function for websocket.
// conn.onopen = e => console.log("Opening wsbidder, ", e)
function handleOnMessage(msg, state) {
switch (msg.type) {
//These types come from the SocketWrappers on the server.
//updateCurrentEvent should be filtering the event by event_id.
case "EVENT_ITEMS":
store.dispatch(updateCurrentEvent(
msg.payload
, state.user_info.uid
, state.event_details.event_details.event.event_id));
break;
case "NOTIFICATIONS":
//bug: this needs to filter notifications per event on the client-side.
store.dispatch(updateNotifications(
msg.payload
, state.event_details.event_details.event.event_id
, state.user_info.uid)
);
break;
case "NOT_BIDDABLE":
if (msg.event_id == state.event_details.event_details.event.event_id) {
store.dispatch(updateEventStatus("CLOSED"));
}
break;
case "PUSH_NOTIFICATION":
const {title, message} = msg.payload;
Alert.alert(title, message);
break;
default:
console.warn(`Unrecognized socket action type: ${msg.type}`);
}
}
//closes the socket connection and sends a reason to the server.
export const closeConn = reason => conn.close(null, reason);
export const setSocketedEventInfo = event_id => {
//Gives the event ID to the socketed connection, which pulls end dates.
const msg = {
type: "UPDATE_EVENT_DETAILS"
, payload: { event_id }
}
conn.send(JSON.stringify(msg));
}
export const createBid = (bid, cb) => {
/*
Expects:
const new_bid = {
item_id: item.item_id,
bid: amount, //Storage keeps storing it as a string
uid: 0, //Not needed here, but can't be null since the server wants an int.
event_id, key, bidder
};
*/
const new_bid = {
type: 'BID'
, payload: bid
};
// Send this to the server socket
conn.send(JSON.stringify(new_bid));
//Returning the callback so the front-end knows to flip the card back over.
return cb()
};
Some of the code is crap, I know. Unless you're giving true advice, which I'm always glad to follow, no need to bash it :-)
The issue I'm having is that when the socket dies (the conn variable), I can't re-initialize the socket and assign it to that conn variable. What I think is happening is all functions using the conn variable aren't using the 'new' one, still stuck to the 'old' one.
Line 9 -- Creating the original one.
Line 28 -- Creating an onMessage function for the conn object, within the handleSocketConnections function that gets called elsewhere at the start of the program
Line 57 -- Trying to re-assign a new connection to the conn variable in the reconnect function, that gets run whenever the app goes on standby (killing the socket connections).
Line 131 -- This gets called correctly from the reconnect function, connecting the socket to the server again
The reconnect() function runs correctly - the server registers the new connection with all the right info, but the app seems to still be in a weird state where there's no conn error (possibly looking at the new one??) but no actions are formed on the conn (possibly looking at the old one?).
Any ideas?
If you have to start a replacement webSocket connection, then you will need to rerun all the code that hooks up to the webSocket (installs event handlers, etc...). Because it's a new object, the old event listeners aren't associated with the new webSocket object.
The simplest way to do that is usually to create a single webSocketInit() function that you call both when you first create your webSocket connection and then call again any time you have to replace it with a new one. You can pass the latest webSocket object to webSocketInit() so any other code can see the new object. Individual blocks of code can register for onclose themselves if they want to know when the old one closes.
There are also more event-driven ways to do this by creating an EventEmitter that gets notified whenever the webSocket has been replaced and individual blocks of code can subscribe to that event if they want to get notified of that occurrence.

RxJs Dynamically add events from another EventEmitter

I have an Observable coming from an EventEmitter which is really just a http connection, streaming events.
Occasionally I have to disconnect from the underlying stream and reconnect. I am not sure how to handle this with rxjs.
I am not sure if i can complete a source and then dynamically add other "source" to the source, or if I have to do something like i have at the very bottom.
var Rx = require('rx'),
EventEmitter = require('events').EventEmitter;
var eventEmitter = new EventEmitter();
var eventEmitter2 = new EventEmitter();
var source = Rx.Observable.fromEvent(eventEmitter, 'data')
var subscription = source.subscribe(function (data) {
console.log('data: ' + data);
});
setInterval(function() {
eventEmitter.emit('data', 'foo');
}, 500);
// eventEmitter stop emitting data, underlying connection closed
// now attach seconds eventemitter (new connection)
// something like this but obvouisly doesn't work
source
.fromEvent(eventEmitter2, 'data')
Puesdo code that is more of what i am doing, I am creating a second stream connection before I close the first, so i don't "lose" any data. Here i am not sure how to stop the Observable without "losing" records due to onNext not being called due to the buffer.
var streams = [], notifiers = [];
// create initial stream
createNewStream();
setInterval(function() {
if (params of stream have changed) createNewStream();
}, $1minutes / 3);
function createNewStream() {
var stream = new eventEmitterStream();
stream.once('connected', function() {
stopOthers();
streams.push(stream);
createSource(stream, 'name', 'id');
});
}
function stopOthers() {
while(streams.length > 0) {
streams.pop().stop(); // stop the old stream
}
while(notifiers.length > 0) {
// if i call this, the buffer may lose records, before onNext() called
//notifiers.pop()(Rx.Notification.createOnCompleted());
}
}
function createObserver(tag) {
return Rx.Observer.create(
function (x) {
console.log('Next: ', tag, x.length, x[0], x[x.length-1]);
},
function (err) {
console.log('Error: ', tag, err);
},
function () {
console.log('Completed', tag);
});
}
function createSource(stream, event, id) {
var source = Rx.Observable
.fromEvent(stream, event)
.bufferWithTimeOrCount(time, max);
var subscription = source.subscribe(createObserver(id));
var notifier = subscription.toNotifier();
notifiers.push(notifier);
}
First and formost, you need to make sure you can remove all listeners from your previously "dead" emitter. Otherwise you'll create a leaky application.
It seems like the only way you'll know that an EventEmitter has died is to watch frequency, unless you have an event that fires on error or completion (for disconnections). The latter is much, much more preferrable.
Regardless, The secret sauce of Rx is making sure to wrap your data stream creation and teardown in your observable. If wrap the creation of the emitter in your observable, as well as a means to tear it down, you'll be able to use awesome things like the retry operator to recreate that observable.
So if you have no way of knowing if it died, and you want to reconnect it, you can use something like this:
// I'll presume you have some function to get an EventEmitter that
// is already set up
function getEmitter() {
var emitter = new EventEmitter();
setInterval(function(){
emitter.emit('data', 'foo');
}, 500)
return emitter;
}
var emitterObservable = Observable.create(function(observer) {
// setup the data stream
var emitter = getEmitter();
var handler = function(d) {
observer.onNext(d);
};
emitter.on('data', handler);
return function() {
// tear down the data stream in your disposal function
emitter.removeListener('on', handler);
};
});
// Now you can do Rx magic!
emitterObservable
// if it doesn't emit in 700ms, throw a timeout error
.timeout(700)
// catch all* errors and retry
// this means the emitter will be torn down and recreated
// if it times out!
.retry()
// do something with the values
.subscribe(function(x) { console.log(x); });
* NOTE: retry catches all errors, so you may want to add a catch above it to handle non-timeout errors. Up to you.

Message Manager API sendAsyncMessage callback

I can return a value if I send a sync message:
// frame script
var chromeBtnText = sendSyncMessage("getChromeToolbarButtonText");
if (chromeBtnText == 'blah') {
alert('tool is blah');
}
// chrome script
messageManager.addMessageListener("getChromeToolbarButtonText", listener);
function listener(message) {
return document.getElementById('myChromeToolbarButton').label.value;
}
How do I achieve this with a callback with sendAsyncMessage?
I was hoping to do something like:
// frame script
function myCallback(val) {
var chromeBtnText = val;
if (chromeBtnText == 'blah') {
alert('tool is blah');
}
}
var chromeBtnText = sendAsyncMessage("getChromeToolbarButtonText", null, myCallback);
There is no callback for replies. In fact, there is no reply at all. The return value from the chrome message listener is simply ignored for async messages.
To do fully async communication, you'd have to send another message containing the reply.
Frame script
addMessageListener("getChromeToolbarButtonTextReply", function(message) {
alert(message.data.btnText);
});
sendAsyncMessage("getChromeToolbarButtonText");
Chrome
messageManager.addMessageListener("getChromeToolbarButtonText", function(message) {
var btnText = document.getElementById('myChromeToolbarButton').label.value;
// Only send message to the frame script/message manager
// that actually asked for it.
message.target.messageManager.sendAsyncMessage(
"getChromeToolbarButtonTextReply",
{btnText: btnText}
);
});
PS: All messages share a namespace. So to avoid conflicts when another piece of code wants to use the same name getChromeToolbarButtonText, you better choose a more unique name in the first place, like prefixing your messages with your add-on name my-unique-addoon:getChromeToolbarButtonText or something like that. ;)
I was also hoping to do something similar:
messageManager.sendAsyncMessage("my-addon-framescript-message", null, myCallback);
I'm going the other direction so the myCallback would be in chrome but it's exactly the same principle.
I'd used similar approaches to #Noitidart and #nmaier before but in this new case I wanted to bind to some local data so myCallback can behave differently based on the application state at the time the first message was sent rather than at the time the callback is executed, all while allowing for the possibility of multiple message round-trips being in progress concurrently.
Chrome:
let someLocalState = { "hello": "world" };
let callbackName = "my-addon-somethingUnique"; // based on current state or maybe generate a UUID
let myCallback = function(message) {
messageManager.removeMessageListener(callbackName, myCallback);
//message.data.foo == "bar"
//someLocalState.hello == "world"
}.bind(this); // .bind(this) is optional but useful if the local state is attached to the current object
messageManager.addMessageListener(callbackName, myCallback);
messageManager.sendAsyncMessage("my-addon-framescript-message", { callbackName: callbackName } );
Framescript:
let messageHandler = function(message) {
let responseData = { foo: "bar" };
sendAsyncMessage(message.data.callbackName, responseData);
};
addMessageListener("my-addon-framescript-message", messageHandler);
There's a real-world example here: https://github.com/luckyrat/KeeFox/commit/c50f99033d2d07068140438816f8cc5e5e290da9
It should be possible for Firefox to be improved to encapsulate this functionality in the built-in messageManager one day but in the mean-time this approach works well and with a surprisingly small amount of boiler-plate code.
in this snippet below. i add the callback before sendAsyncMessage('my-addon-id#jetpack:getChromeToolbarbuttonText'... as i know it will send back. Then I remove it after callback executes. I know I don't have to but just to kind of make it act like real callback, just to kind of show people, maybe it helps someone understand.
Frame:
/////// frame script
function CALLBACK_getChromeToolbarButtonText(val) {
removeMessageListner('my-addon-id#jetpack:getChromeToolbarButtonTextCallbackMessage', CALLBACK_getChromeToolbarButtonText); //remove the callback
var chromeBtnText = val;
if (chromeBtnText == 'blah') {
alert('tool is blah');
}
}
addMessageListener('my-addon-id#jetpack:getChromeToolbarButtonTextCallbackMessage', CALLBACK_getChromeToolbarButtonText); //add the callback
var chromeBtnText = sendAsyncMessage("my-addon-id#jetpack:getChromeToolbarButtonText", null);
Chrome:
////// chrome script
messageManager.addMessageListener("my-addon-id#jetpack:getChromeToolbarButtonText", listener);
function listener() {
var val = document.getElementById('myChromeToolbarButton').label.value;
sendAsyncMessage('my-addon-id#jetpack:getChromeToolbarButtonTextCallbackMessage',val);
}

Categories