How to create a Web Worker from a string - javascript

How can I use create a Web worker from a string (which is supplied via a POST request)?
One way I can think of, but I'm not sure how to implement it, is by creating a data-URI from the server response, and passing that to the Worker constructor, but I've heard that some browsers don't allow this, because of the same origin policy.
MDN states the uncertainty about the origin policy around data URI's:
Note: The URI passed as parameter of the Worker constructor must obey the same-origin policy. There is currently disagreement among browsers vendors on whether data URIs are of the same-origin or not; Gecko 10.0 (Firefox 10.0 / Thunderbird 10.0) and later do allow data URIs as a valid script for workers. Other browsers may disagree.
Here's also a post discussing it on the whatwg.

Summary
blob: for Chrome 8+, Firefox 6+, Safari 6.0+, Opera 15+
data:application/javascript for Opera 10.60 - 12
eval otherwise (IE 10+)
URL.createObjectURL(<Blob blob>) can be used to create a Web worker from a string. The blob can be created using the BlobBuilder API deprecated or the Blob constructor.
Demo: http://jsfiddle.net/uqcFM/49/
// URL.createObjectURL
window.URL = window.URL || window.webkitURL;
// "Server response", used in all examples
var response = "self.onmessage=function(e){postMessage('Worker: '+e.data);}";
var blob;
try {
blob = new Blob([response], {type: 'application/javascript'});
} catch (e) { // Backwards-compatibility
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
blob = new BlobBuilder();
blob.append(response);
blob = blob.getBlob();
}
var worker = new Worker(URL.createObjectURL(blob));
// Test, used in all examples:
worker.onmessage = function(e) {
alert('Response: ' + e.data);
};
worker.postMessage('Test');
Compatibility
Web workers are supported in the following browsers source:
Chrome 3
Firefox 3.5
IE 10
Opera 10.60
Safari 4
This method's support is based on the support of the Blob API and the URL.createObjectUrl method. Blob compatibility:
Chrome 8+ (WebKitBlobBuilder), 20+ (Blob constructor)
Firefox 6+ (MozBlobBuilder), 13+ (Blob constructor)
Safari 6+ (Blob constructor)
IE10 supports MSBlobBuilder and URL.createObjectURL. However, trying to create a Web Worker from a blob:-URL throws a SecurityError.
Opera 12 does not support URL API. Some users may have a fake version of the URL object, thanks to this hack in browser.js.
Fallback 1: data-URI
Opera supports data-URIs as an argument to the Worker constructor. Note: Do not forget to escape special characters (Such as # and %).
// response as defined in the first example
var worker = new Worker('data:application/javascript,' +
encodeURIComponent(response) );
// ... Test as defined in the first example
Demo: http://jsfiddle.net/uqcFM/37/
Fallback 2: Eval
eval can be used as a fallback for Safari (<6) and IE 10.
// Worker-helper.js
self.onmessage = function(e) {
self.onmessage = null; // Clean-up
eval(e.data);
};
// Usage:
var worker = new Worker('Worker-helper.js');
// `response` as defined in the first example
worker.postMessage(response);
// .. Test as defined in the first example

I agree with the current accepted answer but often editing and managing the worker code will be hectic as its in the form of a string.
So optionally we can use the below approach where we can keep the worker as a function, and then covert to string->blob:
// function to be your worker
function workerFunction() {
var self = this;
self.onmessage = function(e) {
console.log('Received input: ', e.data); // message received from main thread
self.postMessage("Response back to main thread");
}
}
///////////////////////////////
var dataObj = '(' + workerFunction + ')();'; // here is the trick to convert the above fucntion to string
var blob = new Blob([dataObj.replace('"use strict";', '')]); // firefox adds "use strict"; to any function which might block worker execution so knock it off
var blobURL = (window.URL ? URL : webkitURL).createObjectURL(blob, {
type: 'application/javascript; charset=utf-8'
});
var worker = new Worker(blobURL); // spawn new worker
worker.onmessage = function(e) {
console.log('Worker said: ', e.data); // message received from worker
};
worker.postMessage("some input to worker"); // Send data to our worker.
This is tested in IE11+ and FF and Chrome

The accepted answer is a bit complex, due to supporting backwards compatibility, so I wanted to post the same thing but simplified. Try this in your (modern) browser console:
const code = "console.log('Hello from web worker!')"
const blob = new Blob([code], {type: 'application/javascript'})
const worker = new Worker(URL.createObjectURL(blob))
// See the output in your console.

I've made an approach with most of your ideas and adding some of mine. The only thing my code needs on worker is to use 'this' to refer 'self' scope. I'm pretty sure that this is very improvable:
// Sample code
var code = function() {
this.onmessage = function(e) {
this.postMessage('Worker: '+e.data);
this.postMessage('Worker2: '+e.data);
};
};
// New thread worker code
FakeWorkerCode = function(code, worker) {
code.call(this);
this.worker = worker;
}
FakeWorkerCode.prototype.postMessage = function(e) {
this.worker.onmessage({data: e});
}
// Main thread worker side
FakeWorker = function(code) {
this.code = new FakeWorkerCode(code, this);
}
FakeWorker.prototype.postMessage = function(e) {
this.code.onmessage({data: e});
}
// Utilities for generating workers
Utils = {
stringifyFunction: function(func) {
// Stringify the code
return '(' + func + ').call(self);';
},
generateWorker: function(code) {
// URL.createObjectURL
windowURL = window.URL || window.webkitURL;
var blob, worker;
var stringified = Utils.stringifyFunction(code);
try {
blob = new Blob([stringified], {type: 'application/javascript'});
} catch (e) { // Backwards-compatibility
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
blob = new BlobBuilder();
blob.append(stringified);
blob = blob.getBlob();
}
if ("Worker" in window) {
worker = new Worker(windowURL.createObjectURL(blob));
} else {
worker = new FakeWorker(code);
}
return worker;
}
};
// Generate worker
var worker = Utils.generateWorker(code);
// Test, used in all examples:
worker.onmessage = function(e) {
alert('Response: ' + e.data);
};
function runWorker() {
worker.postMessage('working fine');
}
Demo: http://jsfiddle.net/8N6aR/

Nice answer - I've been working on a similar problem today when trying to create Web Workers with fallback capabilities when they're not available (i.e. run worker script in main thread). As this thread is pertains to the topic, I thought I'd provide my solution here:
<script type="javascript/worker">
//WORKER FUNCTIONS
self.onmessage = function(event) {
postMessage('Hello, ' + event.data.name + '!');
}
</script>
<script type="text/javascript">
function inlineWorker(parts, params, callback) {
var URL = (window.URL || window.webkitURL);
if (!URL && window.Worker) {
var worker = new window.Worker(URL.createObjectURL(new Blob([parts], { "type" : "text/javascript" })));
worker.onmessage = function(event) {
callback(event.data);
};
worker.postMessage(params);
} else {
var postMessage = function(result) {
callback(result);
};
var self = {}; //'self' in scope of inlineWorker.
eval(parts); //Converts self.onmessage function string to function on self via nearest scope (previous line) - please email chrisgwgreen.site#gmail.com if this could be tidier.
self.onmessage({
data: params
});
}
}
inlineWorker(
document.querySelector('[type="javascript/worker"]').textContent,
{
name: 'Chaps!!'
},
function(result) {
document.body.innerHTML = result;
}
);
</script>
</body>

Depending on your use case you can use something like
task.js Simplified interface for getting CPU intensive code to run on all cores (node.js, and web)
A example would be
// turn blocking pure function into a worker task
const functionFromPostRequest = task.wrap('function (exampleArgument) {}');
// run task on a autoscaling worker pool
functionFromPostRequest('exampleArgumentValue').then(result => {
// do something with result
});

Expanding on #Chanu_Sukarno's code, you can simply pass in a worker function (or string) to this function and it will execute it inside a web worker:
async function doWorkerTask(workerFunction, input, buffers) {
// Create worker
let fnString = '(' + workerFunction.toString().replace('"use strict";', '') + ')();';
let workerBlob = new Blob([fnString]);
let workerBlobURL = window.URL.createObjectURL(workerBlob, { type: 'application/javascript; charset=utf-8' });
let worker = new Worker(workerBlobURL);
// Run worker
return await new Promise(function(resolve, reject) {
worker.onmessage = function(e) { resolve(e.data); };
worker.postMessage(input, buffers);
});
}
Here's an example of how to use it:
function myTask() {
self.onmessage = function(e) {
// do stuff with `e.data`, then:
self.postMessage("my response");
self.close();
}
}
let output = await doWorkerTask(myTask, input, inputBuffers);
// now you can do something with `output` (which will be equal to "my response")
In nodejs, doWorkerTask looks like this:
async function doWorkerTask(workerFunction, input, buffers) {
let Worker = require('webworker-threads').Worker;
let worker = new Worker(workerFunction);
// Run worker
return await new Promise(function(resolve, reject) {
worker.onmessage = function(e) { resolve(e.data); };
worker.postMessage(input, buffers);
});
}

Here's an alternative approach that lets you define a worker using a function with optional setup arguments.
note - you can only use setup arguments that can be stringified - use worker.postMessage to send anything else
// define our worker function
function workerFunc(arg1,arg2) {
console.log("worker is running:"+arg1+" "+arg2);
postMessage(arg1+" "+arg2);
setInterval(sendTime,1000);
function sendTime(){
postMessage(new Date().toUTCString() );
}
}
// (optionally) define an element to inspect the script that worker will use
workerFunc.debug = document.getElementById("worker_script");
// start the worker and get replies from it
startWorker(workerFunc,"hello","world").addEventListener("message",function(e){
document.getElementById("worker_reply").innerHTML = e.data;
});
function startWorker (fn) {
const src = fn.toString();
const args = src.substring(src.indexOf("(")+1,src.indexOf(")"));
const code = ( args ? "let ["+args+"]="+JSON.stringify([].slice.call(arguments,1))+";\n" : "" )+ src.substring(src.indexOf("{")+1,src.length-1);
const blob = new Blob([code], {type: 'application/javascript'})
if (fn.debug) fn.debug.innerHTML=code;
return new Worker(URL.createObjectURL(blob))
}
<textarea id="worker_reply"></textarea>
<h1>Worker Script</h1>
<pre id="worker_script">
</pre>

Use my tiny plugin https://github.com/zevero/worker-create
var worker_url = Worker.create("self.postMessage('Example post from Worker');");
var worker = new Worker(worker_url);
But you may also give it a function.

My solution (can be "promise"d easily...)
function makeWorker(workerFunction, cb) {
// Create worker
var tplFun = "onmessage = function(e){console.log(e); var args = Array.prototype.slice.call(e.data); var res=___.apply(this,args);postMessage(res);}"
var fnTxt = workerFunction.toString().replace('"use strict";', '');
var final = tplFun.replace("___", fnTxt);
let workerBlob = new Blob([final]);
let workerBlobURL = window.URL.createObjectURL(workerBlob, { type: 'application/javascript; charset=utf-8' });
let worker = new Worker(workerBlobURL);
return function () {
var args = Array.prototype.slice.call(arguments);
console.log(args)
worker.postMessage(args);
worker.onmessage = function (e) {
console.log(e.data);
cb(e.data);
}
}
}
function myTask(a, b, c) {
return a * b * c;
}
function onresult(e) {
alert(e);
}
var fn = makeWorker(myTask, onresult)
fn(1, 2, 3);
Can be nice with "pure and slow" function !

Based on accepted answer, class to worker, interesting topic
// worker class
class SimpleWorker {
constructor() {
console.log("simple worker init");
}
onMessage(event) {
console.log("main to worker", event.data);
postMessage({
type: "answer",
data: "data from worker"
});
}
}
// class to worker
const workerFromClass = workerClassRef => {
console.log(workerClassRef.name, "to worker");
// factory method, converted to string, used to instanciate worker
let workerFactory = (self, workerClass) => {
let worker = new workerClass();
self["onmessage"] = worker.onMessage.bind(worker);
};
// compute worker code string
// worker class & factory function
let str = workerClassRef.toString() + "\n"
+ "(" + workerFactory.toString() + ")"
+ "(this," + workerClassRef.name + ");"
// worker code to blob
let blob = new Blob(
[str],
{type: "application/javascript"}
);
// return worker instance
return new Worker(
URL.createObjectURL(blob)
);
};
// main
// create worker
let worker = workerFromClass(SimpleWorker);
// handle messages from worker
worker.addEventListener(
"message",
event => console.log("worker to main", event.data)
);
// send message to worker
let message = {
type: "question",
data: "data from main"
};
console.log("main to worker", message);
worker.postMessage(message);

You can get real-data from the objectURL and not just blob by changing the responseType to either "text" or "arraybuffer".
Here is a back-and-forth conversion of text/javascript to blob to objectURL back to blob or text/javascript.
if you are wondering, I'm using it to generate a web-worker with no external files
you may use it to return binary content, for example a YouTube video ;) (from the <video> tag resource attribute)
var blob = new Blob(['self.onmessage=function(e){postMessage(e)}'],{type: 'text/javascript'}); //->console: (object) Blob {size: 42, type: "text/javascript", slice: function}
var obju = URL.createObjectURL(js_blob); //->console: "blob:http%3A//stackoverflow.com/02e79c2b-025a-4293-be0f-f121dd57ccf7"
var xhr = new XMLHttpRequest();
xhr.open('GET', 'blob:http%3A//stackoverflow.com/02e79c2b-025a-4293-be0f-f121dd57ccf7', true);
xhr.responseType = 'text'; /* or "blob" */
xhr.onreadystatechange = function(){
if(xhr.DONE !== xhr.readyState) return;
console.log(xhr.response);
}
xhr.send();
/*
responseType "blob" ->console: (object) Blob {size: 42, type: "text/javascript", slice: function}
responseType "text" ->console: (text) 'self.onmessage=function(e){postMessage(e)}'
*/

Related

download a PDF with Javascript/Node.Js after generating data and storing the file into a path

I created a generator for PDF files that creates the file and sends to a specific path automatically after creation. Still I want to download it right after but didn't know how to do that. Any help would be appreciated. This is my generatorPdf.js :
module.exports={
pdfGenerator:function(data,pathfile){
var fonts = {
Roboto: {
normal: 'server/pdfgenerator/fonts/Roboto-Regular.ttf',
bold: 'server/pdfgenerator/fonts/Roboto-Medium.ttf',
italics: 'server/pdfgenerator/fonts/Roboto-Italic.ttf',
bolditalics: 'server/pdfgenerator/fonts/Roboto-MediumItalic.ttf'
}
};
var datePaiements='';
var dateFinPaiements='';
if(data.abonnement[0].datePaiement!=null)
datePaiements= new Date( data.abonnement[0].datePaiement.toString());
if(datePaiements!=='')
{
dateFinPaiements= ('0'+datePaiements.getDate()).slice(-2).toString()+'/'+('0'+(datePaiements.getMonth()+1)).slice(-2).toString()+'/'+(datePaiements.getFullYear()+1).toString();
datePaiements=('0'+datePaiements.getDate()).slice(-2).toString()+'/'+('0'+(datePaiements.getMonth()+1)).slice(-2).toString()+'/'+datePaiements.getFullYear().toString();
}
var dateFacture= new Date(data.abonnement[0].timestampCreation.toString());
dateFacture= ('0'+dateFacture.getDate()).slice(-2).toString()+'/'+('0'+(dateFacture.getMonth()+1)).slice(-2).toString()+'/'+dateFacture.getFullYear().toString();
var PdfPrinter = require('pdfmake/src/printer');
var printer = new PdfPrinter(fonts);
var fs = require('fs');
var dd = {
content: [ ..............],
footer:{.............}
}
try{
var pdfDoc = printer.createPdfKitDocument(dd);
if (fs.existsSync(pathfile)) {//server/pdfgenerator/documentpdf/basics21.pdf
fs.unlink(pathfile, (err) => {//server/pdfgenerator/documentpdf/basics21.pdf
if (err) {
console.error(err)
return
}
})
}
pdfDoc.pipe(fs.createWriteStream(pathfile)).on('finish',function(){//server/pdfgenerator/documentpdf/basics21.pdf
});
}
catch(e){
console.log(e);
return null;
}
}
}
and this is my remote method in Loopback to send the pdf to a path and where probably I have to do the download of the file:
cm_abonnements.getAbonnementById= async (options,req,res)=>{
const token = options && options.accessToken;
const userId = token && token.userId;
try{
if(userId!==null){
let dataComedien= await app.models.cm_comediens.getComedienByUser(userId);
let argAbn={};
const form = new formidable.IncomingForm();
var formPromise = await new Promise(function(resolve,reject){
form.parse(req,function(err,fields,files){
if(err)
{
reject(err);
return-1
}
console.log(fields.key)
argAbn.idAbonnement=fields.key;
resolve();
})
})
let dataFac=await cm_abonnements.find({where :{and :[{idAbonnement:argAbn.idAbonnement},{idComedien : dataComedien.idComedien}]}});
var data={abonnement:[]};
data.abonnement=dataFac;
var str_date= new Date(dataFac[0].timestampCreation.toString());
var nameFile= 'Fac_'+dataFac[0].idFacture+'_'+str_date.getFullYear().toString()+'-'+('0'+str_date.getMonth()+1).slice(-2).toString()+'-'+('0'+str_date.getDate()).slice(-2).toString()+'.pdf';
var path='public/upload/Comediens/'+dataComedien.idComedien.toString()+'/factures/'+nameFile;
createPdf.pdfGenerator(data,path);
return dataFac;
}
return null;
}
catch(e){
console.log(e);
return null;
}
}
cm_abonnements.remoteMethod(
'getAbonnementById',{
http:{
verb:'POST'
},
description:'Get detail facture by number facture',
accepts:[
{arg:"options", "type":"object","http":"optionsFromRequest"},
{ arg: 'req', type: 'object', 'http': {source: 'req'}},
{arg: 'res', type: 'object', 'http': {source: 'res'}}
],
returns:{arg:'data',root:true}
}
);
Any help would be appreciated. Thank you
You need to send the following HTTP headers:
Content-Type: application/pdf
Content-Disposition: inline; filename="download.pdf"
After data is generated and pdf file is stored, there are 2 steps left to implement the "download" feature:
Return HTTP response to browser, with Content-Type header as application/pdf, and Content-Disposition header as attachment; filename="yourname.pdf". Normally, this would be handled automatically by web framework. I'm not familiar with loopback, so take Express for example:
In generatorPdf.js, add a callback to listen the finish event:
pdfGenerator:function(data, pathfile, callback){
...
pdfDoc.pipe(fs.createWriteStream(pathfile)).on('finish', callback);
...
}
When pdfGenerator function is used, pass a callback function parameter. If the pdf work is "finished", return response to browser using res.download() (It's Express API, but I believe loopback has similar API as loopback is built on top of Express):
var nameFile=...
var path=...
createPdf.pdfGenerator(data, path, function() {
res.download(path, nameFile);
});
In browser side, if it's an AJAX request (I guess so, as you mentioned it is a POST request), you need to handle the request with some blob operation. Here is an example snippet, with explanation comment:
var req = new XMLHttpRequest();
req.open('POST', '/download', true); // Open an async AJAX request.
req.setRequestHeader('Content-Type', 'application/json'); // Send JSON data
req.responseType = 'blob'; // Define the expected data as blob
req.onreadystatechange = function () {
if (req.readyState === 4) {
if (req.status === 200) { // When data is received successfully
var data = req.response;
var defaultFilename = 'default.pdf';
// Or, you can get filename sent from backend through req.getResponseHeader('Content-Disposition')
if (typeof window.navigator.msSaveBlob === 'function') {
// If it is IE that support download blob directly.
window.navigator.msSaveBlob(data, defaultFilename);
} else {
var blob = data;
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = defaultFilename;
document.body.appendChild(link);
link.click(); // create an <a> element and simulate the click operation.
}
}
}
};
req.send(JSON.stringify({test: 'test'}));

AudioContext issue with safari

I use AudioContext to play some audios in my site.
It works fine on Chrome and Firefox, but not on Safari. On Safari stop function does not work and I get the following:
[Error] InvalidStateError: DOM Exception 11: An attempt was made to use an object that is not, or is no longer, usable.
noteOff (preload.js, line 85)
Does anyone know , how to fix this, and why this error occurrs?
function _initWebAudio(AudioContext, format, audios, callback) {
var context = new AudioContext();
preloader(audios, _preload, callback);
function _preload(asset, doneCallback) {
var request = new XMLHttpRequest();
request.open('GET', 'audio/' + asset.id + '.' + format, true);
request.responseType = 'arraybuffer';
request.onload = function () {
context.decodeAudioData(request.response, function (buffer) {
var source;
// default volume
//// support both webkitAudioContext or standard AudioContext
asset.gain = context.createGain ? context.createGain() : context.createGainNode();
asset.play = function () {
source = context.createBufferSource(); // creates a sound source
source.buffer = buffer; // tell the source which sound to play
source.connect(asset.gain); // connect the source to the context's destination (the speakers)
asset.gain.connect(context.destination);
// play the source now
// support both webkitAudioContext or standard AudioContext
source.noteOn ? source.noteOn(0) : source.start(0);
};
asset.stop = function () {
console.log(source);
source = context.createBufferSource(); // creates a sound source
source.noteOff ? source.noteOff(0) : source.stop(0);
}
asset.toggleVolume = function (muteSound) {
if (muteSound) {
asset.gain.gain.value = 0;
} else {
asset.gain.gain.value = 1;
}
}
doneCallback();
}, function (err) {
asset.play = function () {
};
doneCallback(err, asset.id);
});
};
request.onerror = function (err) {
console.log(err);
asset.play = function () {
};
doneCallback(err, asset.id);
};
// kick off load
request.send();
}
}
I'm not sure why this happened, but I solved this by using disconnect() method.
asset.stop = function () {
asset.gain.disconnect();
//source.noteOff ? source.noteOff(0) : source.stop(0);
}

How do I write FileReader test in Jasmine?

I'm trying to make this test work, but I couldn't get my head around how to write a test with FileReader. This is my code
function Uploader(file) {
this.file = file;
}
Uploader.prototype = (function() {
function upload_file(file, file_contents) {
var file_data = new FormData()
file_data.append('filename', file.name)
file_data.append('mimetype', file.type)
file_data.append('data', file_contents)
file_data.append('size', file.size)
$.ajax({
url: "/upload/file",
type: "POST",
data: file_contents,
contentType: file.type,
success: function(){
// $("#thumbnail").attr("src", "/upload/thumbnail");
},
error: function(){
alert("Failed");
},
xhr: function() {
myXhr = $.ajaxSettings.xhr();
if(myXhr.upload){
myXhr.upload.addEventListener('progress',showProgress, false);
} else {
console.log("Upload progress is not supported.");
}
return myXhr;
}
});
}
return {
upload : function() {
var self = this,
reader = new FileReader(),
file_content = {};
reader.onload = function(e) {
file_content = e.target.result.split(',')[1];
upload_file(self.file, file_content);
}
}
};
})();
And this is my test
describe("Uploader", function() {
it("should upload a file successfully", function() {
spyOn($, "ajax");
var fakeFile = {};
var uploader = new Uploader(fakeFile);
uploader.upload();
expect($.ajax.mostRecentCall.args[0]["url"]).toEqual("/upload/file");
})
});
But it never gets to reader.onload.
The problem here is the use of reader.onload which is hard to test. You could use reader.addEventListener instead so you can spy on the global FileReader object and return a mock:
eventListener = jasmine.createSpy();
spyOn(window, "FileReader").andReturn({
addEventListener: eventListener
})
then you can fire the onload callback by yourself:
expect(eventListener.mostRecentCall.args[0]).toEqual('load');
eventListener.mostRecentCall.args[1]({
target:{
result:'the result you wanna test'
}
})
This syntax changed in 2.0. Code below gives an example based on Andreas Köberle's answer but using the new syntax
// create a mock object, its a function with some inspection methods attached
var eventListener = jasmine.createSpy();
// this is going to be returned when FileReader is instantiated
var dummyFileReader = { addEventListener: eventListener };
// pipe the dummy FileReader to the application when FileReader is called on window
// this works because window.FileReader() is equivalent to new FileReader()
spyOn(window, "FileReader").and.returnValue(dummyFileReader)
// your application will do something like this ..
var reader = new FileReader();
// .. and attach the onload event handler
reader.addEventListener('load', function(e) {
// obviously this wouldnt be in your app - but it demonstrates that this is the
// function called by the last line - onloadHandler(event);
expect(e.target.result).toEqual('url');
// jasmine async callback
done();
});
// if addEventListener was called on the spy then mostRecent() will be an object.
// if not it will be null so careful with that. the args array contains the
// arguments that addEventListener was called with. in our case arg[0] is the event name ..
expect(eventListener.calls.mostRecent().args[0]).toEqual('load');
// .. and arg[1] is the event handler function
var onloadHandler = eventListener.calls.mostRecent().args[1];
// which means we can make a dummy event object ..
var event = { target : { result : 'url' } };
// .. and call the applications event handler with our test data as if the user had
// chosen a file via the picker
onloadHandler(event);
I also faced similar problem and was able to achieve it without use of addeventlistener. I had used onloadend, so below is what I did.
My ts file had below code:-
let reader = new FileReader();
reader.onloadend = function() {
let dataUrl = reader.result;
// Some working here
};
reader.readAsDataURL(blob);
My spec file (test) case code :-
let mockFileReader = {
result:'',
readAsDataURL:(blobInput)=> {
console.log('readAsDataURL');
},
onloadend:()=> {
console.log('onloadend');
}
};
spyOn<any>(window, 'FileReader').and.returnValue(mockFileReader);
spyOn<any>(mockFileReader, 'readAsDataURL').and.callFake((blobInput)=> {
// debug your running application and assign to "encodedString" whatever
//value comes actually after using readAsDataURL for e.g.
//"data:*/*;base64,XoteIKsldk......"
mockFileReader.result = encodedString;
mockFileReader.onloadend();
});
This way you have mocked the FileReader object and returned a fake call to your own "readAsDataURL". And thus now when your actual code calls "reasAsDataURL" your fake function is called in which you are assigning an encoded string in "result" and calling "onloadend" function which you had already assigned a functionality in your code (.ts) file. And hence it gets called with expected result.
Hope it helps.
I think the best way is to use the real FileReader (don't mock it), and pass in a real File or Blob. This improves your test coverage and makes your tests less brittle.
If your tests don't run in IE, you can use the File constructor, e.g.
const fakeFile = new File(["some contents"], "file.txt", {type: "text/plain"});
If you need to be compatible with IE, you can construct a Blob and make it look like a file:
const fakeFile = new Blob(["some contents"]);
fakeFile.name = "file.txt";
fakeFile.type = "text/plain";
The FileReader can read either of these objects so there is no need to mock it.
i found easiest for myself to do next.
mock blob file
run reader.onload while in test environment.
as result - i do not mock Filereader
// CONTROLLER
$scope.handleFile = function (e) {
var f = e[0];
$scope.myFile = {
name: "",
size: "",
base64: ""
};
var reader = new FileReader();
reader.onload = function (e) {
try {
var buffer = e.target.result;
$scope.myFile = {
name: f.name,
size: f.size,
base64: XLSX.arrayBufferToBase64(buffer)
};
$scope.$apply();
} catch (error) {
$scope.error = "ERROR!";
$scope.$apply();
}
};
reader.readAsArrayBuffer(f);
//run in test env
if ( typeof jasmine == 'object') {reader.onload(e)}
}
//JASMINE TEST
it('handleFile 0', function () {
var fileContentsEncodedInHex = ["\x45\x6e\x63\x6f\x64\x65\x49\x6e\x48\x65\x78\x42\x65\x63\x61\x75\x73\x65\x42\x69\x6e\x61\x72\x79\x46\x69\x6c\x65\x73\x43\x6f\x6e\x74\x61\x69\x6e\x55\x6e\x70\x72\x69\x6e\x74\x61\x62\x6c\x65\x43\x68\x61\x72\x61\x63\x74\x65\x72\x73"];
var blob = new Blob(fileContentsEncodedInHex);
blob.type = 'application/zip';
blob.name = 'name';
blob.size = 11111;
var e = {0: blob, target: {result: {}}};
$scope.handleFile(e);
expect($scope.error ).toEqual("");
});
I struggled to figure out how to test onloadend when it gets called from readAsDataURL.
Here is a dump of what I ended up with.
Production code:
loadFileDataIntoChargeback(tempFileList) {
var fileNamesAndData = [];
for (var i = 0, f; f = tempFileList[i]; i++) {
let theFile = tempFileList[i];
var reader = new FileReader();
reader.onloadend = ((theFile) => {
return (fileData) => {
var insertionIndex = this.chargeback.fileList.length;
this.chargeback.fileList.push({ FileName: theFile.name, Data: fileData.target.result, FileType: theFile.type });
this.loadFilePreviews(theFile, insertionIndex);
}
})(f);
reader.readAsDataURL(f);
}
this.fileInputPath = "";
}
Test code:
describe('when the files are loaded into the chargeback', () => {
it('loads file previews', () => {
let mockFileReader = {
target: { result: '' },
readAsDataURL: (blobInput) => {},
onloadend: () => {}
};
spyOn(chargeback, "loadFilePreviews");
spyOn(window, 'FileReader').and.returnValue(mockFileReader);
spyOn(mockFileReader, 'readAsDataURL').and.callFake((blobInput) => {
mockFileReader.onloadend({ target: { result: "data:image/jpeg;base64,/9j/4QAYRXh" } });
});
var readFileList = chargeback.getArrayFromFileInput([getImageFile1()]);
chargeback.loadFileDataIntoChargeback(readFileList);
expect(chargeback.loadFilePreviews).toHaveBeenCalled();
});
});

How to get a file or blob from an object URL?

I am allowing the user to load images into a page via drag&drop and other methods. When an image is dropped, I'm using URL.createObjectURL to convert to an object URL to display the image. I am not revoking the url, as I do reuse it.
So, when it comes time to create a FormData object so I can allow them to upload a form with one of those images in it, is there some way I can then reverse that Object URL back into a Blob or File so I can then append it to a FormData object?
Modern solution:
let blob = await fetch(url).then(r => r.blob());
The url can be an object url or a normal url.
As gengkev alludes to in his comment above, it looks like the best/only way to do this is with an async xhr2 call:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'blob:http%3A//your.blob.url.here', true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
if (this.status == 200) {
var myBlob = this.response;
// myBlob is now the blob that the object URL pointed to.
}
};
xhr.send();
Update (2018): For situations where ES5 can safely be used, Joe has a simpler ES5-based answer below.
Maybe someone finds this useful when working with React/Node/Axios. I used this for my Cloudinary image upload feature with react-dropzone on the UI.
axios({
method: 'get',
url: file[0].preview, // blob url eg. blob:http://127.0.0.1:8000/e89c5d87-a634-4540-974c-30dc476825cc
responseType: 'blob'
}).then(function(response){
var reader = new FileReader();
reader.readAsDataURL(response.data);
reader.onloadend = function() {
var base64data = reader.result;
self.props.onMainImageDrop(base64data)
}
})
The problem with fetching the blob URL again is that this will create a full copy of the Blob's data, and so instead of having it only once in memory, you'll have it twice. With big Blobs this can blow your memory usage quite quickly.
It's rather unfortunate that the File API doesn't give us access to the currently linked Blobs, certainly they thought web-authors should store that Blob themselves at creation time anyway, which is true:
The best here is to store the object you used when creating the blob:// URL.
If you are afraid this would prevent the Blob from being Garbage Collected, you're right, but so does the blob:// URL in the first place, until you revoke it. So holding yourself a pointer to that Blob won't change a thing.
But for those who aren't responsible for the creation of the blob:// URI (e.g because a library made it), we can still fill that API hole ourselves by overriding the default URL.createObjectURL and URL.revokeObjectURL methods so that they do store references to the object passed.
Be sure to call this function before the code that does generate the blob:// URI is called.
// Adds an URL.getFromObjectURL( <blob:// URI> ) method
// returns the original object (<Blob> or <MediaSource>) the URI points to or null
(() => {
// overrides URL methods to be able to retrieve the original blobs later on
const old_create = URL.createObjectURL;
const old_revoke = URL.revokeObjectURL;
Object.defineProperty(URL, 'createObjectURL', {
get: () => storeAndCreate
});
Object.defineProperty(URL, 'revokeObjectURL', {
get: () => forgetAndRevoke
});
Object.defineProperty(URL, 'getFromObjectURL', {
get: () => getBlob
});
const dict = {};
function storeAndCreate(blob) {
const url = old_create(blob); // let it throw if it has to
dict[url] = blob;
return url
}
function forgetAndRevoke(url) {
old_revoke(url);
try {
if(new URL(url).protocol === 'blob:') {
delete dict[url];
}
} catch(e){}
}
function getBlob(url) {
return dict[url] || null;
}
})();
// Usage:
const blob = new Blob( ["foo"] );
const url = URL.createObjectURL( blob );
console.log( url );
const retrieved = URL.getFromObjectURL( url );
console.log( "retrieved Blob is Same Object?", retrieved === blob );
fetch( url ).then( (resp) => resp.blob() )
.then( (fetched) => console.log( "fetched Blob is Same Object?", fetched === blob ) );
And an other advantage is that it can even retrieve MediaSource objects, while the fetching solutions would just err in that case.
Using fetch for example like below:
fetch(<"yoururl">, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + <your access token if need>
},
})
.then((response) => response.blob())
.then((blob) => {
// 2. Create blob link to download
const url = window.URL.createObjectURL(new Blob([blob]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `sample.xlsx`);
// 3. Append to html page
document.body.appendChild(link);
// 4. Force download
link.click();
// 5. Clean up and remove the link
link.parentNode.removeChild(link);
})
You can paste in on Chrome console to test. the file with download with 'sample.xlsx' Hope it can help!
See Getting BLOB data from XHR request which points out that BlobBuilder doesn't work in Chrome so you need to use:
xhr.responseType = 'arraybuffer';
Unfortunately #BrianFreud's answer doesn't fit my needs, I had a little different need, and I know that is not the answer for #BrianFreud's question, but I am leaving it here because a lot of persons got here with my same need. I needed something like 'How to get a file or blob from an URL?', and the current correct answer does not fit my needs because its not cross-domain.
I have a website that consumes images from an Amazon S3/Azure Storage, and there I store objects named with uniqueidentifiers:
sample: http://****.blob.core.windows.net/systemimages/bf142dc9-0185-4aee-a3f4-1e5e95a09bcf
Some of this images should be download from our system interface.
To avoid passing this traffic through my HTTP server, since this objects does not require any security to be accessed (except by domain filtering), I decided to make a direct request on user's browser and use local processing to give the file a real name and extension.
To accomplish that I have used this great article from Henry Algus:
http://www.henryalgus.com/reading-binary-files-using-jquery-ajax/
1. First step: Add binary support to jquery
/**
*
* jquery.binarytransport.js
*
* #description. jQuery ajax transport for making binary data type requests.
* #version 1.0
* #author Henry Algus <henryalgus#gmail.com>
*
*/
// use this transport for "binary" data type
$.ajaxTransport("+binary", function (options, originalOptions, jqXHR) {
// check for conditions and support for blob / arraybuffer response type
if (window.FormData && ((options.dataType && (options.dataType == 'binary')) || (options.data && ((window.ArrayBuffer && options.data instanceof ArrayBuffer) || (window.Blob && options.data instanceof Blob))))) {
return {
// create new XMLHttpRequest
send: function (headers, callback) {
// setup all variables
var xhr = new XMLHttpRequest(),
url = options.url,
type = options.type,
async = options.async || true,
// blob or arraybuffer. Default is blob
dataType = options.responseType || "blob",
data = options.data || null,
username = options.username || null,
password = options.password || null;
xhr.addEventListener('load', function () {
var data = {};
data[options.dataType] = xhr.response;
// make callback and send data
callback(xhr.status, xhr.statusText, data, xhr.getAllResponseHeaders());
});
xhr.open(type, url, async, username, password);
// setup custom headers
for (var i in headers) {
xhr.setRequestHeader(i, headers[i]);
}
xhr.responseType = dataType;
xhr.send(data);
},
abort: function () {
jqXHR.abort();
}
};
}
});
2. Second step: Make a request using this transport type.
function downloadArt(url)
{
$.ajax(url, {
dataType: "binary",
processData: false
}).done(function (data) {
// just my logic to name/create files
var filename = url.substr(url.lastIndexOf('/') + 1) + '.png';
var blob = new Blob([data], { type: 'image/png' });
saveAs(blob, filename);
});
}
Now you can use the Blob created as you want to, in my case I want to save it to disk.
3. Optional: Save file on user's computer using FileSaver
I have used FileSaver.js to save to disk the downloaded file, if you need to accomplish that, please use this javascript library:
https://github.com/eligrey/FileSaver.js/
I expect this to help others with more specific needs.
If you show the file in a canvas anyway you can also convert the canvas content to a blob object.
canvas.toBlob(function(my_file){
//.toBlob is only implemented in > FF18 but there is a polyfill
//for other browsers https://github.com/blueimp/JavaScript-Canvas-to-Blob
var myBlob = (my_file);
})
Following #Kaiido answer, another way to overload URL without messing with URL is to extend the URL class like this:
export class URLwithStore extends URL {
static createObjectURL(blob) {
const url = super.createObjectURL(blob);
URLwithStore.store = { ...(URLwithStore.store ?? {}), [url]: blob };
return url;
}
static getFromObjectURL(url) {
return (URLwithStore.store ?? {})[url] ?? null;
}
static revokeObjectURL(url) {
super.revokeObjectURL(url);
if (
new URL(url).protocol === "blob:" &&
URLwithStore.store &&
url in URLwithStore.store
)
delete URLwithStore.store[url];
}
}
Usage
const blob = new Blob( ["foo"] );
const url = URLwithStore.createObjectURL( blob );
const retrieved = URLwithStore.getFromObjectURL( url );
console.log( "retrieved Blob is Same Object?", retrieved === blob );

javascript web workers - how do I pass arguments?

Found the answer some some of my problems, html5 web workers!!!
How do I pass an argument to a web worker though using this basic example?
contents of worker.js:
function doSomething() {
postMessage( ' done');
}
setTimeout ( "doSomething()", 3000 );
js code:
var worker = new Worker('worker.js');
worker.onmessage = function (event) {
alert(event.data);
};
As you can see you have the same mechanism for both worker-to-main and main-to-worker messages.
the postMessage method for sending messages
the onmessage member for defining the handler that receives the messages
In the main script:
worker.postMessage(data);
In the worker script:
self.addEventListener("message", function(e) {
// the passed-in data is available via e.data
}, false);
... or just...
onmessage = function(e) {
// the passed-in data is available via e.data
};
It may be that data has to be a string... (Firefox 3.5+ supports passing in JSON-compatible objects)
var worker = new Worker(window.App.baseUrl + '/Scripts/signature/GetCurrenProductWorker.js');
worker.postMessage(window.App.baseUrl)
var _base_url = ''
var xhr = new XMLHttpRequest();
onmessage = function (e) {
_base_url = e.data
xhr.open("GET", _base_url + "/api/product/Get");
xhr.onload = function () {
postMessage(xhr.responseText);
};
xhr.send();
};
this work for me

Categories