Feature detection for Firefox 57+ shared worker bug - javascript

As discussed in this question and this bug, shared workers have been badly broken in Firefox since version 57. Basically, the "shared" part is missing. The way it's supposed to work is that creating a shared worker with the same worker script in two tabs will only create one instance of the worker, but in Firefox it creates two instances.
Anyone using shared workers is familiar with this type of problem, because shared workers are not supported in IE, Edge, or Safari. So you have to make due with web workers. That's fine, I could do the same thing with buggy Firefox versions, except... how do I do feature detection for this, so I can send the non-shared version of my worker in this case?
I figured I could use a worker like this and see if the counter resets when it shouldn't:
var count = 0; onconnect = function (e) { var port = e.ports[0]; port.postMessage(count); count += 1; }
My first idea was to just create two shared workers in the same page and see if the counter is 0 for both of them. But it's not. This:
function createWorker(url, workerNum) {
var worker = new SharedWorker(url);
worker.port.onmessage = function (e) {
console.log("Worker " + workerNum + ": " + e.data);
};
}
for (var i = 0; i < 2; i++) {
createWorker("worker.js", i);
}
produces the correct output:
Worker 0: 0
Worker 1: 1
Then I thought maybe I could run that script in two iframes within the same page, but no dice, this is also correct:
Worker 0: 0
Worker 1: 1
Worker 0: 2
Worker 1: 3
If I open the page in two tabs, I can see the problem - the counter resets to 0 in each tab. But if it requires users to open a new tab, it's not very useful feature detection. Even worse, the bug seems to be intermittent. Sometimes the counter does correctly persist across tabs! So this approach is not going to work...
This seems like a very nasty bug to do feature detection for. Does anyone have any ideas? Or am I stuck parsing user agent strings?

Related

How to get browsers to release memory used by OfflineAudioContext?

I'm using OfflineAudioContext to render WebAudio signals into a buffer so I can analyze the results. When I do this repeatedly, it seems that the associated memory is never released, eventually causing the browser tab to crash.
Here's a minimal reproduction:
// render 10 minutes of audio into a big buffer
var ctx = new OfflineAudioContext(1, 44100 * 600, 44100)
var osc = ctx.createOscillator()
osc.start()
ctx.startRendering().then(buffer => {
// attempt to clean up
osc.stop()
osc = null
buffer = null
ctx = null
})
Running that in a JS console will render a ~100MB buffer that never gets released. Running it repeatedly will chew up memory until the tab eventually crashes (tested in mac chrome/mozilla, windows chrome/mozilla/edge).
How can I get browsers to free the memory associated with an OfflineAudioContext?
This has been confirmed as a bug, with no workarounds. Until it's fixed, it looks like this is a fact of life.
This one was a major headache, but I finally found a workaround: create an iframe in which you run the audio rendering code, return the result to your main window using postMessage, and immediately remove the iframe from the DOM when you receive the result. That clears all resources associated with it, including the offlineAudioContext.
Of course, that's only practical if your use case is to do a relatively small number of relatively long renders.
Note that to transfer the data back in an efficient manner, you should send ArrayBuffer objects, which are Transferable:
context.oncomplete = function (e) {
var channels = [];
for (var c = 0; c < e.renderedBuffer.numberOfChannels; c++) {
channels.push(e.renderedBuffer.getChannelData(c));
}
channels = channels.map( function(c) {return c.buffer;} );
window.parent.postMessage({
action:'renderResult',
data: channels
}, "*", channels);
}
and recreate Float32Arrays from them on the receiving end.

iPod/iPhone 4, iOS 6.1.6, Safari unexpectedly closes during HTML5/CSS3/JavaScript game initialization

In my HTML5/CSS3/JavaScript application I create instances of the main classes of the game. For example:
function initialize (iconsParamArray) {
for (var i = 0; i < iconsParamArray.length; i++) {
this._iconsArray[i] = new Icon(iconsParamArray[i]);
}
}
The length of the iconsParamArray can be more than one hundred. And sometimes in the middle of this process Safari unexpectedly closes.
If I perform browser cache clearing in Safari settings panel, Safari works normally in around 70% of cases. It looks like GC can't collect all garbage from the memory in time. So memory amount exceeded the limit and browser closes.
Has anyone run into the same problem? How can I avoid this unexpected closings? Maybe I have to use setTimeout function for dividing initialization process on few parts?

Are cookie read/write atomic in browser

I am trying to implement a cross tab mutex for my needs. I found a implementation here. which seems quite promising. Basically, it implements Leslie Lamport's algorithm with needs atomic read/writes for creating a mutex.
However it relies on localStorage providing atomic read/writes. This works well in most browsers except for Chrome.
So my question is, can I use cookie read/write instead? Are cookie reads/writes atomic in all mainstream browsers (IE, Chrome, Safari, Firefox)?
Neither cookies, nor localStorage provide atomic transactions.
I think you might have misunderstood that blog post, it doesn't say that his implementation doesn't work in Chrome, it does not rely on localStorage providing atomic read/writes. He says that normal localStorage access is more volatile in Chrome. I'm assuming this is related to the fact that Chrome uses a separate process for each tab, whereas most other browsers tend to use a single process for all tabs. His code implements a locking system on top of localStorage which should protect against things getting overwritten.
Another solution would be to use IndexedDB. IndexedDB does provide atomic transactions. Being a new standard it is not supported in as many browsers as localStorage, but it does have good support in recent versions of Firefox, Chrome and IE10.
No. Even if the browsers probably implement a read and a write lock on the cookie it won't protect you from changes that happens between a read and a consequent write. This is easy to see by looking at the javascript API for cookies, there is no mutex functionality there...
I ran into this concurrency issue using localStorage today (two years alter..)
Scenario: Multiple tabs of a browser (e.g. Chrome) have identical script code that gets executed, basically at the same time (called by e.g. SignalR). The code reads and writes to localStorage. Since the tabs run in different processes but access the shared local storage collectively, reading and writing leads to undefined results since a locking mechanism is missing here. In my case I wanted to make sure that only one of the tabs actually works with the local storage and not all of them..
I tried the locking mechanism of Benjamin Dumke-von der Ehe metioned in the question above but got undesired results. So I decided to roll my own experimental code:
localStorage lock:
Object.getPrototypeOf(localStorage).lockRndId = new Date().getTime() + '.' + Math.random();
Object.getPrototypeOf(localStorage).lock = function (lockName, maxHold, callback) {
var that = this;
var value = this.getItem(lockName);
var start = new Date().getTime();
var wait = setInterval(function() {
if ((value == null) || (parseInt(value.split('_')[1]) + maxHold < start)) {
that.setItem(lockName, that.lockRndId + '_' + start);
setTimeout(function () {
if (that.getItem(lockName) == (that.lockRndId + '_' + start)) {
clearInterval(wait);
try { callback(); }
catch (e) { throw 'exeption in user callback'; }
finally { localStorage.removeItem(lockName); }
}
}, 100);
}
}, 200);
};
usage:
localStorage.lock(lockName, maxHold, callback);
lockName - a global scope unique name for the lock - string
maxHold - the maximum time to protect the script in milliseconds - integer
callback - the function containing the script that gets protected
example: "only play a sound in one tab"
//var msgSound = new Audio('/sounds/message.mp3');
localStorage.lock('lock1', 5000, function(){
// only one of the tabs / browser processes gets here at a time
console.log('lock aquired:' + new Date().getTime());
// work here with local storage using getItem, setItem
// e.g. only one of the tabs is supposed to play a sound and only if none played it within 3 seconds
var tm = new Date().getTime();
if ((localStorage.lastMsgBeep == null)||(localStorage.lastMsgBeep <tm-3000)) {
localStorage.lastMsgBeep = tm;
//msgSound.play();
console.log('beep');
}
});

Persistent unique ID for Chrome tabs that lasts between browser sessions

I'm trying to ascertain some way to establish a unique ID for Chrome tabs that meets the following conditions:
Uniquely identifies each tab
Stays the same for a given tab between browser restarts (session-restored tabs)
Stays the same if a tab is closed and then reopened with Undo Closed Tab (Ctrl+Shift+T)
Stays distinct if a tab is duplicated
I've done some rather aggressive research to find a comprehensive solution, but nothing seems to quite do the trick. Here are the methods I have tried, in increasing order of efficacy:
Use Chrome's provided tab.id: does not persist between browser sessions or close/undo-close
Put a GUID in cookies: is not unique per tab, only per domain/URL
Put a GUID in localStorage: persists between browser sessions and close/undo-close, but is not unique per tab, only per domain
Put a GUID in sessionStorage: unique per tab, persists across close/undo-close, unique for duplicated tabs, but is wiped out between browser sessions
Use identifiable webpage document attributes as a unique key: this is the best approach I've found so far. A key can be constructed via a content script from the following values: [location.href, document.referrer, history.length].
Regarding this last approach, the constructed key is unique across all tabs which share a common URL, referrer, and history length. Those values will remain the same for a given tab between browser restarts/session-restores and close/undo-closes. While this key is "pretty" unique, there are cases where it is ambiguous: for example, 3 new tabs opened to http://www.google.com would all have the same key in common (and this kind of thing happens pretty often in practice).
The "put GUID in sessionStorage" method can additionally be used to disambiguate between multiple tabs with the same constructed key for the close/undo-close and duplicated-tab cases during the current browser session. But this does not solve the ambiguity problem between browser restarts.
This last ambiguity can be partially mitigated during session restore by observing which tabs Chrome opens together in which windows, and extrapolating for a given ambiguous key which tab belongs to which window based on the presence of expected 'sibling' tabs (recorded during the previous browser session). As you might imagine, implementing this solution is quite involved and rather dodgy. And it can only disambiguate between same-keyed tabs that Chrome restores into different windows. That leaves same-keyed tabs that restore into the same window as irreconcilably ambiguous.
Is there a better way? A guaranteed unique, browser-generated, per-tab GUID that persists between browser restarts (session restores) and close/undo-close would be ideal but so far I haven't found anything like this.
The question here does most of the discovery work, and the accepted answer basically completes it, but there's a big implementation gap still for people looking to implement something which requires persistent tab IDs. I've attempted to distill this into an actual implementation.
To recap: Tabs can be (almost) uniquely and consistently identified as required by the question by maintaining a register of tabs which stores the following combination of variables in local persistent storage:
Tab.id
Tab.index
A 'fingerprint' of the document open in the tab - [location.href, document.referrer, history.length]
These variables can be tracked and stored in the registry using listeners on a combination of the following events:
onUpdated
onCreated
onMoved
onDetached
onAttached
onRemoved
onReplaced
There are still ways to fool this method, but in practice they are probably pretty rare - mostly edge cases.
Since it looks like I'm not the only one who has needed to solve this problem, I built my implementation as a library with the intention that it could be used in any Chrome extension. It's MIT licensed and available on GitHub for forking and pull requests (in fact, any feedback would be welcome - there are definitely possible improvements).
If I correctly understand your problem, your 5th method should do the trick, but along with these two criteria:
chrome.tabs.windowId (The ID of the window the tab is contained within)
chrome.tabs.index (The zero-based index of the tab within its window)
All these values need to be stored inside your extension. Besides that, you will also have to hook up your extension to chrome.tabs.onUpdated() and updated accordingly, when tabs are being dragged around, moved across owner windows, etc.
Put this as a persistent background script in manifest.json:
"background": {
"scripts": [ "background.js" ],
"persistent": true
},
Here is background.js.
Hopefully the code is self explanatory.
var tabs_hashes = {};
var tabs_hashes_save_queued = false;
function Start(){
chrome.tabs.query({windowType: "normal"}, function(querytabs){
querytabs.forEach(function(tab){
tabs_hashes[tab.id] = GetHash(tab.url);
});
if (localStorage.getItem("tabs_hashes") !== null){
var ref_load = JSON.parse(localStorage["tabs_hashes"]);
var ref_tabId = {};
querytabs.forEach(function(tab){
for (var t = 0; t < ref_load.length; t++){
if (ref_load[t][1] === tabs_hashes[tab.id]){
ref_tabId[ref_load[t][0]] = tab.id;
ref_load.splice(t, 1);
break;
}
}
});
// do what you have to do to convert previous tabId to the new one
// just use ref_tabId[your_previous_tabId] to get the current corresponding new tabId
console.log(ref_tabId);
}
});
}
function SaveHashes(){
if (!tabs_hashes_save_queued && Object.keys(tabs_hashes).length > 0){
tabs_hashes_save_queued = true;
chrome.tabs.query({windowType: "normal"}, function(querytabs){
var data = [];
querytabs.forEach(function(tab){
if (tabs_hashes[tab.id]){
data.push([tab.id, tabs_hashes[tab.id]]);
} else {
data.push([tab.id, GetHash(tab.url)]);
}
});
localStorage["tabs_hashes"] = JSON.stringify(data);
setTimeout(function(){ tabs_hashes_save_queued = false; }, 1000);
});
}
}
function GetHash(s){
var hash = 0;
if (s.length === 0){
return 0;
}
for (var i = 0; i < s.length; i++){
hash = (hash << 5)-hash;
hash = hash+s.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
chrome.tabs.onCreated.addListener(function(tab){
SaveHashes();
});
chrome.tabs.onAttached.addListener(function(tabId){
SaveHashes();
});
chrome.tabs.onRemoved.addListener(function(tabId){
delete tabs_hashes[tabId];
SaveHashes();
});
chrome.tabs.onDetached.addListener(function(tabId){
SaveHashes();
});
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo){
if (changeInfo.pinned != undefined || changeInfo.url != undefined){
delete tabs_hashes[tabId];
SaveHashes();
}
});
chrome.tabs.onMoved.addListener(function(tabId){
SaveHashes();
});
chrome.tabs.onReplaced.addListener(function(addedTabId, removedTabId){
delete tabs_hashes[removedTabId];
SaveHashes();
});
Start();
I use array to save data, because in this way I can preserve tabs order, which is unlikely if data would be saved in the object. When loading data after browser's restart, even if url is not unique, I can trust that it will be under some "close enough" index. I would do it more complex, for example reverse check if tab was not found, but this works ok so far.

Bring spell check window to foreground with JavaScript/JScript in Windows 7

I have some JScript code (converted from some old VBScript) that starts like this:
var Word = new ActiveXObject("Word.Basic");
Word.FileNew(); // opens new Word document
Word.Insert(IncorrectText);
Word.ToolsSpelling(); // opens spell check behind IE
The idea is to utilize the MS Word spell check for browser use, and it works well in XP, but the spell check box opens in the background in Windows 7 / IE 8 (this question tells me that the problem started in Vista and is probably an OS issue, not a browser or Office issue).
So my question is, how can I bring this window to the foreground? One important note is that the last line, Word.ToolsSpelling();, locks up my script, so anything I do will need to be before that.
I've tried
var wshShell = new ActiveXObject("WScript.Shell");
wshShell.AppActivate("Document1 - Microsoft Word"); // or some other text
before the ToolsSpelling call, but this does not do anything (maybe because the Word document is not actually revealed at this point?). Of course, this will only work if no "Document1" is already opened, so this is a questionable thought to begin with.
Per this answer, I tried using window.blur(); in order to blur IE, but this will only work if the IE window is the only one opened. Maybe there's some way I can loop through all opened windows and apply this?
SetForegroundWindow looked promising, but I don't know how to use it in JSript.
Any ideas?
Note: Browser permissions will be completely open for this site.
Update: Turns out if you use Word.Application, the spell check comes up in front as it should. Only the Word.Basic method has this problem (I don't expect to know why this side of eternity):
var wordApp = new ActiveXObject("Word.Application");
wordApp.Documents.Add();
wordDoc = wordApp.ActiveDocument;
... // safety checks before insertion
wordSelection.TypeText(IncorrectText);
wordDoc.CheckSpelling();
wordApp.Visible = false; // CheckSpelling makes the document visible
You might be able to jigger the window state. When the window is maximized after having been minimized, Windows will stack that in front (zIndex to top).
Something like:
var WIN_MAX = 2;
var WIN_MIN = 1;
var Word = new ActiveXObject("Word.Application");
Word.Visible = true;
// minimize the app
Word.WindowState = WIN_MIN ;
// in 500ms, maximize
setTimeout(function () {
Word.WindowState = WIN_MAX;
}, 500);
The setTimeout call seeks to work around a timing issue; Windows will sometimes "get confused" when a programmatic minimize/maximize happens in immediate succession. You might have to extend that delay a little, test it repeatedly and see what kind of results you get.

Categories