I've been looking at different tutorials and I know I'm close but I'm getting lost in implementation details because some of them are a little bit dated and a few things have changed since Firefox 3. I have already written the javascript for the firefox extension, now I need to make it into an XPCOM component.
This is the functionality that I need:
My Javascript file is simple, I have two functions startServer() and stopServer. I need to run startServer() when the browser starts and stopServer() when firefox quits.
Edit:
I've updated my code with a working solution (thanks to Neil). The following is in MyExtension/components/myextension.js.
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
const CI = Components.interfaces, CC = Components.classes, CR = Components.results;
// class declaration
function MyExtension() {}
MyExtension.prototype = {
classDescription: "My Firefox Extension",
classID: Components.ID("{xxxx-xxxx-xxx-xxxxx}"),
contractID: "#example.com/MyExtension;1",
QueryInterface: XPCOMUtils.generateQI([CI.nsIObserver]),
// add to category manager
_xpcom_categories: [{
category: "profile-after-change"
}],
// start socket server
startServer: function () { /* socket initialization code */ },
// stop socket server
stopServer: function () { /* stop server */ },
observe: function(aSubject, aTopic, aData)
{
var obs = CC["#mozilla.org/observer-service;1"].getService(CI.nsIObserverService);
switch (aTopic)
{
case "quit-application":
this.stopServer();
obs.removeObserver(this, "quit-application");
break;
case "profile-after-change":
this.startServer();
obs.addObserver(this, "quit-application", false);
break;
default:
throw Components.Exception("Unknown topic: " + aTopic);
}
}
};
var components = [MyExtension];
function NSGetModule(compMgr, fileSpec) {
return XPCOMUtils.generateModule(components);
}
As far as I can tell, all of your code goes into your component.
You need a JavaScript object that represents your component and register it with the component registrar. (It can be a new object or you can multitask an existing object.) The way this is done depends on whether you're targetting Firefox 3.x or Firefox 4.
You need to register for the profile-after-change notification using the category manager. The way this is done also depends on whether you're targetting Firefox 3, Firefox 3.5/6 or Firefox 4.
When the profile-after-change notification fires, your component is then created and the observe method is called. This is where you start your server and also ask to observe the quit-application notification. Note that this also calls the observe method, so it has to check which notification it's getting.
function myExt() {}
myExt.prototype = {
observe: function(aSubject, aTopic, aData) {
switch (aTopic) {
case "quit-application":
stopServer();
obs.removeObserver(this, "quit-application");
break;
case "profile-after-change":
startServer();
obs.addObserver(this, "quit-application", false);
break;
}
}
};
Related
I want to be able to add value and order parameters to before/after hooks. And values in before/after hooks should be triggered from tags in bdd scenarios.
To execute a specific set of steps before a scenario, I created the method below.
let preConditionTags = [];
Before((test) => {
preConditionTags = test.tags.filter(tags => tags.includes("PC_"))
console.log(preConditionTags)
for (let tag of preConditionTags) {
switch (tag) {
case "#PC_login":
console.log("PC_login executed");
break;
case "#PC_add_user":
console.log("PC_add_user executed");
break;
default:
break;
}
}
});
This can be done natively in cucumber:
#Before(order = 10, value = "#smth" )
public void doSomething(){
// Do something before each scenario
}
Scenario example:
#smth #PC_login #PC_add_user #TD_logout
Scenario: Update added user
Given A new user is created
When I click update user
Then User should be updated
CodeceptJS version: CodeceptJS v3.3.0
NodeJS Version: v12.18.3
Operating System: win 10
Helper: Playwright
I want to track how much time user is taking in completing a particular action (including server response time and render time(DOM related changes )) in website.
I have tried it in Angular framework. To do it, I am thinking of recording the time when user started the action and I want to note the time when the action is completed. As a developer, I will know when user started the activity and when user finish the action like search, filter, edit, add, delete etc. So, we can take the difference b/w them. But to note every action, we have to write code in every part of the app. Can we create a plugin so that we can use it everywhere instead of writing same code everywhere to track the time of user. Any approach to create it? Or is there any tool available to achieve this feature?
Would something like this help?
#Injectable({provideIn: 'root'})
export class TrackingService {
private cache: {[id: number]: {description: string, time: number}} = {};
private id: number = 0;
public startTracking(actionDescription: string): number{
const id = ++this.id;
this.cache[id] = { description: actionDescription, time: new Date().getTime() };
return id;
}
public stopTracking(actionId: number){
const data = this.cache[actionId];
if(data){
const elapsed = new Date().getTime() - data.time;
// ...
// Do something with your 'elapsed' and 'data.description'
// ...
delete this.cache[id];
return {...data, elapsed: elapsed};
}
throw `No action with id [${actionId}] running! `;
}
}
Ad then anywhere you need to track an action:
private actionId: number;
constructor(private trackingService: TrackingService){}
startAction(){
this.actionId = this.trackingService.startTracking('Description');
}
stopAction(){
const trackingResult = this.trackingService.stopTracking(this.actionId);
}
You can automate the tracking in some places, for example for routing:
// app.module.ts
private routeChangeSubscription: Subscription;
private configLoadActionId: number;
private navigationActionId: number;
constructor(private router: Router, private trackingService: TrackingService){
this.routeChangeSubscription = router.events.subscribe((event: Event) => {
if (event instanceof RouteConfigLoadStart) {
this.configLoadActionId = this.trackingService.startTracking('configLoad');
}
else if (event instanceof RouteConfigLoadEnd) {
const result = this.trackingService.stopTracking(this.configLoadActionId);
// ... process the result if you wish
}
else if (event instanceof NavigationStart) {
this.navigationActionId = this.trackingService.startTracking('navigation');
}
else if (event instanceof NavigationEnd) {
const result = this.trackingService.stopTracking(this.navigationActionId);
// ... process the result if you wish
}
});
}
Or for HTTP requests:
// http-tracking.interceptor
export class HttpTrackingInterceptor implements HttpInterceptor {
constructor(private trackingService: TrackingService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const actionId = this.trackingService.startTracking('HTTP request');
return next.handle(req.clone()).pipe(
tap(r => this.trackingService.stopTracking(actionId))
);
}
}
// app.module.ts
#NgModule({
// ... other module stuff
providers: [
// ... other providers
{
provide: HTTP_INTERCEPTORS,
useClass: HttpTrackingInterceptor,
multi: true,
deps: [TrackingService]
}
]
})
export class AppModule { ... }
You can easily extend the TrackingService to return Promises or Observables or whatever else, in case you prefer that...
Hope this helps a little :-)
Can we create a plugin so that we can use it everywhere instead of
writing same code everywhere to track the time of user. Any approach
to create it? Or is there any tool available to achieve this feature?
It's a very important Feature Request by many. So, I write a detailed, working and simple solution on the subject here.
#himanshu-garg You are requesting a feature already created for this workflow. It's a plugin you can include in any website. It's none other than activity tracking in timeonsite.js
Look at the following code,
<head>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/timeonsite/1.2.0/timeonsitetracker.js"></script>
<script>
var config = {
// track page by seconds. Default tracking is by milliseconds
trackBy: 'seconds',
callback: function(data) { /* callback denotes your data tracking is real-time */
console.log(data);
var endPointUrl = 'http://example.com' //Replace with your actual backend API URL http://localhost/tos
if (data && data.trackingType) {
if (data.trackingType == 'tos') {
if (Tos.verifyData(data) != 'valid') {
console.log('Data abolished!');
return;
}
}
// make use of sendBeacon if this API is supported by your browser.
if (navigator && typeof navigator.sendBeacon === 'function') {
data.trasferredWith = 'sendBeacon';
var blob = new Blob([JSON.stringify(data)], {type : 'application/json'});
navigator.sendBeacon(endPointUrl, blob);
}
}
}
};
var Tos;
if (TimeOnSiteTracker) {
Tos = new TimeOnSiteTracker(config);
}
</script>
</head>
Then, when the user clicks on a specific action in the site, for example "edit the post" or "click on the create post",
You just initiate the Tos.startActivity() API like,
Tos.startActivity({actionPerfomed: 'Edit a post'});
Then when the user completes the edit or create post actions and when he finally clicks the "save/submit" button, you trigger the Tos.endActivity() API like,
Tos.endActivity({customData: 'custom data if any here...'});
You'll see following object directly saved into your table,
{
TOSId: 585872449448,
TOSSessionKey: "14802525481391382263",
TOSUserId: "anonymous",
title: "Test application - TimeOnSiteTracker",
URL: "http://example.com/post/nature-is-beautiful/edit.php",
activityStart: "2021-11-27 13:20:46.707",
activityEnd: "2021-11-27 13:20:50.213",
timeTaken:4,
timeTakenByDuration: "0d 00h 00m 04s"
timeTakenTrackedBy: "second",
trackingType: "activity",
actionPerfomed: "Edit a post", //optional fields
customData: "custom data if any here..." //optional fields
}
As you can see, the actions
"Edit/Create post" is captured
"timeTaken" is captured in seconds/milliseconds depending upon configuration
"type:activity" is captured
"activityStart" is captured
"activityEnd" is captured
"TOSUserId" // who does the action along with TOSSessionKey to uniquely identify the session.
What else you need? Since it's stored in SQL DB table, you can do analysis/reporting queries yourself and take it to top-level management for decisions. The same is the case for NoSQL as well. Timeonsite.js is supporting both RDBMS and NoSql DB types.
On top of it, 1.Minimize tab, 2.Inactive tab and 3.Switch tab's idle time are all computed and ignored automatically by the tracker itself.
This tracker can be plugged-in in any library Angular, React, Jquery etc. since it's plain vanilla JS library.
Let me know if you need more input on the subject. I can assist you on this.
You have to write a simple Event Tracker in your client code. Since I don't know which events you want to track, I'll provide the solution for a general case.
Also, you'll have to manually trigger the start and stop tracking.
EventTracker = {
trackedEvents: {},
start: function(key) {
var startTime = new Date();
this.trackedEvents[key] = {
start: startTime
}
},
stop: function(key) {
var endTime = new Date();
this.trackedEvents[key]['duration'] = (endTime - this.trackedEvents[key]['start']) / 1000 + 's';
this.trackedEvents[key]['end'] = endTime;
},
}
// Use EventTracker everywhere to track performance
// Example:
EventTracker.start('search_track'); // User searches, start tracking.
setTimeout(function() {
EventTracker.stop('search_track'); // Records fetched after 5 seconds. Stop tracking.
console.log(EventTracker.trackedEvents);
}, 5000);
You can track all events according to your need. For server response, use: EventTracker.start('search_ajax_track') when you make the request and stop the tracking when you get the response.
You can modify above code to measure other parameters according to your requirements.
I am going to recommend you use custom Google Analytics events. In particular User Timings. This allows you to log specific timings on your webpage, you can log with your own labels and categories.
To quote the documentation:
User timings allow developers to measure periods of time using the
analytics.js library. This is particularly useful for developers to
measure the latency, or time spent, making AJAX requests and loading
web resources.
I have some sample code below, this just hooks into clicks, and will get a descriptor from attribute data-name - if not available will just log as 'Anonymous Click' - you can customise this to not track unmarked items. You can also hook into ajax calls and other notable events, without knowing your specific requirements it's hard to give further examples.
Example markup helper to lock click events.
<button data-name="Foo"/>
The below code does the logging, note that it logs using window.performance.now() - which will return the time from when the page was loaded in milliseconds. This will allow you to generate a timeline of user interactions as opposed to getting raw time spent on a single task, which by the way Google Analytics reports can calculate for you.
(function($, Analytics) {
init_hooks();
function init_hooks() {
$('body').on('click', track);
}
function track(e) {
// Get a name to record this against
var name = e.target.data(name) || "Anonymous Click";
// Time since page loaded
var time = window.performance.now()
Analytics('send', {
hitType: 'timing',
timingCategory: 'Front End Intereactions',
timingVar: name,
timingValue: time
});
}
})(jQuery, ga)
Find out more look at the docs.
You could instrument your code with OpenTracing for Js.
You will need to add a request in your transaction start and end.
Also a OpenTracing server to receive request from the browser.
So currently in a windows 8 WinJS app I'm coding, I am trying to get the loading of an xml file to take place in the app startup sequence, while the splash screen is still showing, as this xmldoc element is needed for when the home page loads, and loading of the home page will fail without it.
This is my initiation sequence in default.js:
(function () {
"use strict";
var activation = Windows.ApplicationModel.Activation;
var app = WinJS.Application;
var nav = WinJS.Navigation;
var sched = WinJS.Utilities.Scheduler;
var ui = WinJS.UI;
app.addEventListener("activated", function (args) {
if (args.detail.kind === activation.ActivationKind.launch) {
if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
// TODO: This application has been newly launched. Initialize
// your application here.
console.log("Newly Launched!");
var localSettings = Windows.Storage.ApplicationData.current.localSettings;
WinJS.Namespace.define("MyGlobals", { localSettings: localSettings });
// APP RUN TYPE CHECK AND SEQUENCE (FIRST RUN / NOT FIRST RUN):
if (MyGlobals.localSettings.values['firstRunCompleted']) {
console.log("NOT FIRST RUN!");
// CACHE VERSION CHECK. IF APP HAS BEEN UPDATED, INITIATE NEWLY ADDED CACHE VALUES HERE:
} else {
console.log("FIRST RUN!")
MyGlobals.localSettings.values['firstRunCompleted'] = true;
};
//loadXML(); have tried many things with this. doesn't work.
} else {
// TODO: This application has been reactivated from suspension.
// Restore application state here.
var currentVolume = app.sessionState.currentVolume;
if (currentVolume) {
console.log("RESTORE FROM SUSPENSION");
console.log(currentVolume);
};
}
nav.history = app.sessionState.history || {};
nav.history.current.initialPlaceholder = true;
// Optimize the load of the application and while the splash screen is shown, execute high priority scheduled work.
ui.disableAnimations();
var p = ui.processAll().then(function () {
return nav.navigate(nav.location || Application.navigator.home, nav.state);
}).then(function () {
return sched.requestDrain(sched.Priority.aboveNormal + 1);
}).then(function () {
ui.enableAnimations();
});
args.setPromise(p);
args.setPromise(WinJS.UI.processAll().then(function completed() {
loadSavedColour();
// Populate Settings pane and tie commands to Settings flyouts.
WinJS.Application.onsettings = function (e) {
e.detail.applicationcommands = {
"helpDiv": { href: "html/Help.html", title: WinJS.Resources.getString("settings_help").value },
"aboutDiv": { href: "html/About.html", title: WinJS.Resources.getString("settings_about").value },
"settingsDiv": { href: "html/Settings.html", title: WinJS.Resources.getString("settings_settings").value },
};
WinJS.UI.SettingsFlyout.populateSettings(e);
}
As you can see where I have the commented line of "loadXML()", that is where I need the loadXML() function to take place.
Here is my loadXML() function:
function loadXML() {
Windows.ApplicationModel.Package.current.installedLocation.getFolderAsync("foldername").then(function (externalDtdFolder) {
externalDtdFolder.getFileAsync(MyGlobals.localSettings.values['currentBook']).done(function (file) {
Windows.Data.Xml.Dom.XmlDocument.loadFromFileAsync(file).then(function (doc) {
WinJS.Namespace.define("MyGlobals", {
xmlDoc: doc,
});
})
})
});
};
(loadXML is a working function and works in other scenarios)
However, the issue is that before the loadXML function finishes, the app splash screen goes away, and the next home.html home page loads, which starts the accompanying home.js, which has a function that requires the MyGlobals.xmlDoc object that loadXML should have made. This immediately crashes the app, as MyGlobals.xmlDoc is undefined/null.
I used to have this app working by running loadXML in home.js for the home.html page directly, but in that scenario the XML document is reloaded every time navigation is made to the page, wasting time and resources. As such, I'm trying to move the xmldocument loading into the app startup/initialization.
Thanks a lot!
loadXML has async functionality and you need to handle that.
You shouldn't expect that the loadFromFileAsync (or any of the other async functions) have completed before it returns to the caller. If your code doesn't wait, you'll find that the MyGlobals.xmlDoc value won't be set when you need it.
I've renamed it below to be more accurate as to its behavior. The big change is that it returns a Promise that can be used by the caller to properly wait for the Xml doc to be loaded. This Promise could be used with other Promises to wait on multiple conditions if you'd like (or in the case, other async work).
function loadXMLAsync() {
return new WinJS.Promise(function (complete, error, progress) {
var localSettings = MyGlobals.localSettings.values;
var installedLocation = Windows.ApplicationModel.Package.current.installedLocation;
installedLocation.getFolderAsync("foldername").then(function (externalDtdFolder) {
externalDtdFolder.getFileAsync(values['currentBook']).done(function (file) {
Windows.Data.Xml.Dom.XmlDocument.loadFromFileAsync(file).then(function (doc) {
complete(doc);
});
});
});
});
};
Then, in use:
loadXmlAsync().then(function(doc) {
WinJS.Namespace.define("MyGlobals", {
xmlDoc: doc,
});
// and any other code that should wait until this has completed
});
The code above does not handle errors.
I believe what you need to do is to extend the splash screen to give your app more time to initialize UI. For your scenario, it's loading the xml.
I will suggest you read How to extend the splash screen (HTML). The main idea is to display an extended splash screen(you can use the same image as the default one) in the activated event, then call your loadXML.
In addition to other comments, what you really need to do is include the promise from loadXml with what you pass to args.setPromise. As you know, setPromise is a way to tell the app loader to wait until that promise is fulfilled before removing the splash screen. However, in your code you're calling setPromise multiple times. What you should be doing is joining all the promises you care about (animations, loadXml, and setting loading) with WinJS.Promise.join, so that you get a single promise that's then waiting on all the other three, and when that one is fulfilled, then remove the splash screen.
Alan's suggestion for an extended splash screen is helpful if that whole loading process ends up taking too long, as doing an extended splash gives you total control over what's happening and when the transition happens to your main page.
Good day.
i have problem with porting chromium extension to firefox.
i need to detect all outgoing request and id's of tabs to which it belongs.
to detect requests i using system/events api, but i can't find a way how to detect id of tab from incomming events. As i understand this events is xpcom objects and i should use QueryInterface to get some interface to get some other interface to get some other interface to get some other interface ..... to get some other interface to get id of tab from it (just like in COM implementation in windows), but i can't find which interface i need, can't find documentation about this events at all...
code which i using in chromium:
chrome.webRequest.onBeforeRequest.addListener(
function(info) {
if(info.tabId)
//do stuff here
}
so it's what i want to achieve from firefox...
code which i currently write for firefox:
exports.main = function(options)
{
//stuf here ....
........
function listener(event)
{
var channel = event.subject.QueryInterface(Ci.nsIHttpChannel);
console.log(channel);
//TODO: get tab here somehow
}
events.on("http-on-opening-request", listener);
}
i have looked on xpcom docs few days, but still have not enough info to implement this simple thing... so if someone have success with this, please help.
I just found a code snippet for getting the browser that fires the http-on-modify-request notification. The code there seems to be broken but I used some of it to create this function to get a tab from the channel.
const getTabFromChannel = (aChannel) => {
try {
let notificationCallbacks = aChannel.notificationCallbacks || aChannel.loadGroup.notificationCallbacks;
if (!notificationCallbacks)
return null;
let domWin = notificationCallbacks.getInterface(Ci.nsIDOMWindow);
let chromeTab = tabsUtils.getTabForContentWindow(domWin);
return getSdkTabFromChromeTab(chromeTab);
}
catch (e) {
// some type errors happen here, not sure how to handle them
console.log(e);
return null;
}
}
This function converts the low-level tab to a high-level tab. Depending on which one you need you could skip this function of course. Again, in the latest SDK you probably can replace it with tabs.viewFor(chromeTab).
const tabs = require("sdk/tabs");
const tabsUtils = require("sdk/tabs/utils");
const getSdkTabFromChromeTab = (chromeTab) => {
const tabId = tabsUtils.getTabId(chromeTab);
for each (let sdkTab in tabs){
if (sdkTab.id === tabId) {
return sdkTab;
}
}
return null;
};
There seems to be a problem that the listener fails when switching between windows when using system/events. Use Services.obs.addObserver instead:
const httpRequestObserver = {
observe: function (subject, topic, data) {
var channel = subject.QueryInterface(Ci.nsIHttpChannel);
console.log("channel");
var tab = getTabFromChannel(channel);
if(tab) {
console.log("request by tab", tab.id);
}
}
}
exports.main = function() {
Cu.import('resource://gre/modules/Services.jsm');
Services.obs.addObserver(httpRequestObserver, 'http-on-opening-request', false);
}
I can only hope that it works for all the requests you need to detect. The documentation already mentions some cases where it won't work:
Note that some HTTP requests aren't associated with a tab; for example, RSS feed updates, extension manager requests, XHR requests from XPCOM components, etc.
The article Listening to events on all tabs describes how to set up web progress listeners for tabs. With this listener you can get requests and redirects.
const tabsUtils = require("sdk/tabs/utils");
const listener = {
QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener", "nsISupportsWeakReference"]),
onLocationChange: (browser, progress, request, uri) => {
let tab = tabsUtils.getTabForContentWindow(progress.DOMWindow);
// ...
},
onStateChange: (browser, progress, request, state) => {
let tab = tabsUtils.getTabForContentWindow(progress.DOMWindow);
// ...
}
// ...
};
getChromeWindow(sdkWindow).getBrowser().addTabsProgressListener(listener);
At some point you may need to convert between low- and high-level tabs or chrome/dom/sdk windows which is implemented really bad and confusing. An sdk window in this case is one you get with windows.browserWindows, the chrome window has a reference to the gBrowser. If you are using the latest sdk maybe this helps: https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_API/tabs#Converting_to_XUL_tabs and https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/windows#Converting_to_DOM_windows. I used this function to get the chrome window from a sdk window: https://bugzilla.mozilla.org/show_bug.cgi?id=695143#c15
const { BrowserWindow } = require('sdk/windows');
const { windows } = require('sdk/window/utils');
function getChromeWindow(sdkWindow) {
// to include private window use the as second argument
// { includePrivate: true }
for (let window of windows('navigator:browser'))
if (BrowserWindow({window: window}) === sdkWindow)
return window;
return null;
}
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);
}