I want to sort an array, using Web Workers. But this array might receive new values over time, while the worker is still performing the sort function.
So my question is, how can I "stop" the sorting computation on the worker after receiving the new item, so it can perform the sort on the array with that item, while still keeping the sorting that was already made?
Example:
let worker = new Worker('worker.js');
let list = [10,1,5,2,14,3];
worker.postMessage({ list });
setInterval(() => worker.postMessage({ num: SOME_RANDOM_NUM, list }), 100);
worker.onmessage = event => {
list = event.data.list;
}
So lets say that, I've passed 50, the worker made some progress in the sorting before that and now I have something like this:
[1, 2, 3, 10, 5, 14, 50]. Which means the sorting stopped at index 3. So I pass this new array back to the worker, so it can continue the sorting from position 3.
How can I accomplish that, since there is no way to pause/resume a web worker?
Even though the Worker works on an other thread than the one of your main page, and can thus run continuously without blocking the UI, it still runs on a single thread.
This means that until your sort algorithm has finished, the Worker will delay the execution of the message event handler; it is as blocked as would be the main thread.
Even if you made use of an other Worker from inside this worker, the problem would be the same.
The only solution would be to use a kind of generator function as the sorter, and to yield it every now and then so that the events can get executed.
But doing this will drastically slow down your sorting algorithm.
To make it better, you could try to hook to each Event Loop, thanks to a MessageChannel object: you talk in one port and receive the message in the next Event loop. If you talk again to the other port, then you have your own hook to each Event loop.
Now, the best would be to run a good batch in every of these Event loop, but for demo, I'll call only one instance of our generator function (that I borrowed from this Q/A)
const worker = new Worker(getWorkerURL());
worker.onmessage = draw;
onclick = e => worker.postMessage(0x0000FF/0xFFFFFF); // add a red pixel
// every frame we request the current state from Worker
function requestFrame() {
worker.postMessage('gimme a frame');
requestAnimationFrame(requestFrame);
}
requestFrame();
// drawing part
const ctx = canvas.getContext('2d');
const img = ctx.createImageData(50, 50);
const data = new Uint32Array(img.data.buffer);
ctx.imageSmoothingEnabled = false;
function draw(evt) {
// converts 0&1 to black and white pixels
const list = evt.data;
list.forEach((bool, i) =>
data[i] = (bool * 0xFFFFFF) + 0xFF000000
);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.putImageData(img,0,0);
// draw bigger
ctx.scale(5,5);
ctx.drawImage(canvas, 0,0);
}
function getWorkerURL() {
const script = document.querySelector('[type="worker-script"]');
const blob = new Blob([script.textContent]);
return URL.createObjectURL(blob);
}
body{
background: ivory;
}
<script type="worker-script">
// our list
const list = Array.from({length: 2500}).map(_=>+(Math.random()>.5));
// our sorter generator
let sorter = bubbleSort(list);
let done = false;
/* inner messaging channel */
const msg_channel = new MessageChannel();
// Hook to every Event loop
msg_channel.port2.onmessage = e => {
// procede next step in sorting algo
// could be a few thousands in a loop
const state = sorter.next();
// while running
if(!state.done) {
msg_channel.port1.postMessage('');
done = false;
}
else {
done = true;
}
}
msg_channel.port1.postMessage("");
/* outer messaging channel (from main) */
self.onmessage = e => {
if(e.data === "gimme a frame") {
self.postMessage(list);
}
else {
list.push(e.data);
if(done) { // restart the sorter
sorter = bubbleSort(list);
msg_channel.port1.postMessage('');
}
}
};
function* bubbleSort(a) { // * is magic
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
var temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
yield swapped; // pause here
}
}
} while (swapped);
}
</script>
<pre> click to add red pixels</pre>
<canvas id="canvas" width="250" height="250"></canvas>
Note that the same can be achieved with an async function, which may be more practical in some cases:
const worker = new Worker(getWorkerURL());
worker.onmessage = draw;
onclick = e => worker.postMessage(0x0000FF/0xFFFFFF); // add a red pixel
// every frame we request the current state from Worker
function requestFrame() {
worker.postMessage('gimme a frame');
requestAnimationFrame(requestFrame);
}
requestFrame();
// drawing part
const ctx = canvas.getContext('2d');
const img = ctx.createImageData(50, 50);
const data = new Uint32Array(img.data.buffer);
ctx.imageSmoothingEnabled = false;
function draw(evt) {
// converts 0&1 to black and white pixels
const list = evt.data;
list.forEach((bool, i) =>
data[i] = (bool * 0xFFFFFF) + 0xFF000000
);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.putImageData(img,0,0);
// draw bigger
ctx.scale(5,5);
ctx.drawImage(canvas, 0,0);
}
function getWorkerURL() {
const script = document.querySelector('[type="worker-script"]');
const blob = new Blob([script.textContent]);
return URL.createObjectURL(blob);
}
body{
background: ivory;
}
<script type="worker-script">
// our list
const list = Array.from({length: 2500}).map(_=>+(Math.random()>.5));
// our sorter generator
let done = false;
/* outer messaging channel (from main) */
self.onmessage = e => {
if(e.data === "gimme a frame") {
self.postMessage(list);
}
else {
list.push(e.data);
if(done) { // restart the sorter
bubbleSort(list);
}
}
};
async function bubbleSort(a) { // async is magic
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
const temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
}
if( i % 50 === 0 ) { // by batches of 50?
await waitNextTask(); // pause here
}
}
} while (swapped);
done = true;
}
function waitNextTask() {
return new Promise( (resolve) => {
const channel = waitNextTask.channel ||= new MessageChannel();
channel.port1.addEventListener("message", (evt) => resolve(), { once: true });
channel.port2.postMessage("");
channel.port1.start();
});
}
bubbleSort(list);
</script>
<pre> click to add red pixels</pre>
<canvas id="canvas" width="250" height="250"></canvas>
There are two decent options.
Option 1: Worker.terminate()
The first is just to kill your existing web worker and start a new one. For that you can use Worker.terminate().
The terminate() method of the Worker interface immediately terminates the Worker. This does not offer the worker an opportunity to finish its operations; it is simply stopped at once.
The only downsides of this approach are:
You lose all worker state. If you had to copy a load of data into it for the request you have to do it all again.
It involves thread creation and destruction, which isn't as slow as most people think but if you terminate web workers a lot it might cause issues.
If neither of those are an issue it is probably the easiest option.
In my case I have lots of state. My worker is rendering part of an image, and when the user pans to a different area I want it to stop what it is doing and start rendering the new area. But the data needed to render the image is pretty huge.
In your case you have the state of your (presumably huge) list that you don't want to use.
Option 2: Yielding
The second option is basically to do cooperative multitasking. You run your computation as normal, but every now and then you pause (yield) and say "should I stop?", like this (this is for some nonsense calculation, not sorting).
let requestId = 0;
onmessage = event => {
++requestId;
sortAndSendData(requestId, event.data);
}
function sortAndSendData(thisRequestId, data) {
let isSorted = false;
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
// Check if we are still the current request ID.
if (thisRequestId !== requestId) {
// Data was changed. Cancel this sort.
return;
}
}
postMessage(total);
}
This won't work though because sortAndSendData() runs to completion and blocks the web worker's event loop. We need some way to yield just before thisRequestId !== requestId. Unfortunately Javascript doesn't quite have a yield method. It does have async/await so we might try this:
let requestId = 0;
onmessage = event => {
console.log("Got event", event);
++requestId;
sortAndSendData(requestId, event.data);
}
async function sortAndSendData(thisRequestId, data) {
let isSorted = false;
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
await Promise.resolve();
// Check if we are still the current request ID.
if (thisRequestId !== requestId) {
console.log("Cancelled!");
// Data was changed. Cancel this sort.
return;
}
}
postMessage(total);
}
Unfortunately it doesn't work. I think it's because async/await executes things eagerly using "microtasks", which get executed before pending "macrotasks" (our web worker message) if possible.
We need to force our await to become a macrotask, which you can do using setTimeout(0):
let requestId = 0;
onmessage = event => {
console.log("Got event", event);
++requestId;
sortAndSendData(requestId, event.data);
}
function yieldToMacrotasks() {
return new Promise((resolve) => setTimeout(resolve));
}
async function sortAndSendData(thisRequestId, data) {
let isSorted = false;
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
await yieldToMacrotasks();
// Check if we are still the current request ID.
if (thisRequestId !== requestId) {
console.log("Cancelled!");
// Data was changed. Cancel this sort.
return;
}
}
postMessage(total);
}
This works! However it is extremely slow. await yieldToMacrotasks() takes approximately 4 ms on my machine with Chrome! This is because browsers set a minimum timeout on setTimeout(0) of something like 1 or 4 ms (the actual minimum seems to be complicated).
Fortunately another user pointed me to a quicker way. Basically sending a message on another MessageChannel also yields to the event loop, but isn't subject to the minimum delay like setTimeout(0) is. This code works and each loop only takes ~0.04 ms which should be fine.
let currentTask = {
cancelled: false,
}
onmessage = event => {
currentTask.cancelled = true;
currentTask = {
cancelled: false,
};
performComputation(currentTask, event.data);
}
async function performComputation(task, data) {
let total = 0;
let promiseResolver;
const channel = new MessageChannel();
channel.port2.onmessage = event => {
promiseResolver();
};
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
// Yield to the event loop.
const promise = new Promise(resolve => {
promiseResolver = resolve;
});
channel.port1.postMessage(null);
await promise;
// Check if this task has been superceded by another one.
if (task.cancelled) {
return;
}
}
// Return the result.
postMessage(total);
}
I'm not totally happy about it - it relies on postMessage() events being processed in FIFO order, which I doubt is guaranteed. I suspect you could rewrite the code to make it work even if that isn't true.
You can do it with some trick – with the help of setTimeout function interrupting. For example it is not possible without an addition thread to execute 2 functions parallel, but with setTimeout function interrupting trick we can do it like follows:
Example of parallel execution of functions
var count_0 = 0,
count_1 = 0;
function func_0()
{
if(count_0 < 3)
setTimeout(func_0, 0);//the same: setTimeout(func_0);
console.log('count_0 = '+count_0);
count_0++
}
function func_1()
{
if(count_1 < 3)
setTimeout(func_1, 0);
console.log('count_1 = '+count_1)
count_1++
}
func_0();
func_1();
You will get this output:
count_0 = 0
count_1 = 0
count_0 = 1
count_1 = 1
count_0 = 2
count_1 = 2
count_0 = 3
count_1 = 3
Why is it possible? Because the setTimeout function needs some time to be executed. And this time is even enought for the execution of some part from your following code.
Solution for you
For this case you have to write your own array sort function (or you can also use the following function from me) because we can not interrupt the native sort function. And in this your own function you have to use this setTimeout function interrupting trick. And you can receive your message event notification.
In the following example I have the interrupting in the half length of my array, and you can change it if you want.
Example with custom sort function interrupting
var numbers = [4, 2, 1, 3, 5];
// this is my bubble sort function with interruption
/**
* Sorting an array. You will get the same, but sorted array.
* #param {array[]} arr – array to sort
* #param {number} dir – if dir = -1 you will get an array like [5,4,3,2,1]
* and if dir = 1 in opposite direction like [1,2,3,4,5]
* #param {number} passCount – it is used only for setTimeout interrupting trick.
*/
function sortNumbersWithInterruption(arr, dir, passCount)
{
var passes = passCount || arr.length,
halfOfArrayLength = (arr.length / 2) | 0; // for ex. 2.5 | 0 = 2
// Why we need while loop: some values are on
// the end of array and we have to change their
// positions until they move to the first place of array.
while(passes--)
{
if(!passCount && passes == halfOfArrayLength)
{
// if you want you can also not write the following line for full break of sorting
setTimeout(function(){sortNumbersWithInterruption(arr, dir, passes)}, 0);
/*
You can do here all what you want. Place 1
*/
break
}
for(var i = 0; i < arr.length - 1; i++)
{
var a = arr[i],
b = arr[i+1];
if((a - b) * dir > 0)
{
arr[i] = b;
arr[i+1] = a;
}
}
console.log('array is: ' + arr.join());
}
if(passCount)
console.log('END sring is: ' + arr.join());
}
sortNumbersWithInterruption(numbers, -1); //without passCount parameter
/*
You can do here all what you want. Place 2
*/
console.log('The execution is here now!');
You will get this output:
array is: 4,2,3,5,1
array is: 4,3,5,2,1
The execution is here now!
array is: 4,5,3,2,1
array is: 5,4,3,2,1
END sring is: 5,4,3,2,1
You can do it with insertion sort (kind of).
Here is the idea:
Start your worker with an internal empty array (empty array is sorted obviously)
Your worker receives only elements not the entire array
Your worker insert any received element right in correct position into the array
Every n seconds, the worker raises a message with the current array if it has changed after the last event. (If you prefer, you can send the array on every insertion, but is more efficient to buffer somehow)
Eventually, you get the entire array, if any item is added, you will receive the updated array to.
NOTE: Because your array is always sorted, you can insert in correct position using binary search. This is very efficient.
I think the case comes down to careful management of postMessage calls and amount of data passed to be processed at a time. Was dealing with problem of this kind - think about not sending all new data into the function at once but rather creating your own queue and when small enough portion of the task has been acomplished by webworker thread send a message back to the main thread and decide to send the next portion, wait or quit.
In Your case, e.g. one time You get 9000 new items, next 100k - maybe create a queue/buffer that adds next 10k new elements each time webworker is done processing last data change.
const someWorker = new Worker('abc.js');
var processingLock = false;
var queue = [];
function newDataAction(arr = null) {
if (arr != null) {
queue = queue.concat(arr);
}
if (!processingLock) {
processingLock = true;
var data = [];
for (let i = 0; i < 10000 && queue.length > 0; i++) {
data.push(queue.pop());
}
worker.postMessage(data);
}
}
someWorker.addEventListener('message', function(e) {
if (e.data == 'finished-last-task') {
processingLock = false;
if (queue.length > 0) {
newDataAction();
}
}
});
Worked through many sorting algorithms and I don't see how sending new data into an sorting algorithm with partially sorted array makes much difference in terms of compuation time from sorting them both sequentially and performing a merge.
I need to perform a huge calculation. So, I refer here to create a simple loading screen. Unfortunately, the loading screen is shown after the calculation complete.
Here is my code
class RosterScheduler
{
.........................
autoAssign()
{
var startDate=parseInt($("#autoPlannStartDate").val());
var endDate=parseInt($("#autoPlanEndDate").val());
$("body").addClass("loading");
if (startDate>endDate)
alert("Invalid start date or end date selection");
else
{
if (this.rosterTable.haveInvalidPreferredShift(startDate,endDate))
alert("Invalid shift requirement detected");
else
{
var self=this;
var finalRoster;
var roster,tempAverageSD,lowestAverageSD=100.0;
this.theLowestSDRosters=[];
this.theLowestMissingShiftRosters=[];
for (var i=0;i<100;i++)
{
this._genRoster(startDate,endDate);
}
console.log("Done");
}
}
}
}
I found that if I remark the for loop, the loading screen working properly.
If I uncomment the for loop, the loading screen is shown after the "Done" is shown in the console. How can I fix the problem? I have tried to convert the function _genRoster into a Promise function, however, it does not work.
It looks like _genRoster is blocking the browser, and not giving it any resources to re-render/repaint until the loop is completed. One possibility would be to run the loop after giving the browser a few ms to render the .loading:
this.theLowestSDRosters = [];
this.theLowestMissingShiftRosters = [];
setTimeout(() => {
for (var i = 0; i < 100; i++) {
this._genRoster(startDate, endDate);
}
console.log("Done");
}, 50);
It might be more elegant to call a function with 0 timeout after a single requestAnimationFrame, thus giving the browser time to repaint and then running the heavy loop immediately afterwards:
(warning: the following code will block your browser for a bit)
document.body.style.backgroundColor = 'green';
window.requestAnimationFrame(() => {
console.log('start, setting timeout');
setTimeout(() => {
for (let i = 0; i < 1000000000; i++) {
}
console.log('done');
});
});
I have function:
function addModel() {
var values = new Array();
var $input = $('input[type=\'text\']');
var error = 0;
$input.each(function() {
$(this).removeClass('double-error');
var that = this;
if (that.value!='') {
values[that.value] = 0;
$('input[type=\'text\']').each(function() {
if (this.value == that.value) {
values[that.value]++;
}
});
}
});
$input.each(function(key) {
if (values[this.value]>1) {
//error++;
var name = this.value;
var product_model = $(this).parent().parent().find('.product-model').text();
var m = product_model.toLowerCase().areplace(search,replace);
$(this).parent().find('input[type=\'text\']').val(name + '-' + m);
}
});
return error <= 0; //return error > 0 ? false : true;
}
where are a lot of inputs to recheck... up to 50000. Usually are about 5000 to 20000 inputs. Of course browsers are freezing... How to move this function to web-worker and call it to get data back and fill form type="text"
thank you in advance.
Web workers don't have direct access to the DOM. So to do this, you'd need to gather the values of all 5-50k inputs into an array or similar, and then send that to the web worker (via postMessage) for processing, and have the web worker do the relevant processing and post the result back, and then use that result to update the inputs. See any web worker tutorial for the details of launching the worker and passing messages back and forth (and/or see my answer here).
Even just gathering up the values of the inputs and posting them to the web worker is going to take significant time on the main UI thread, though, as is accepting the result from the worker and updating the inputs; even 5k inputs is just far, far too many for a web page.
If maintaining browser responsiveness is the issue, then releasing the main thread periodically will allow the browser to process user input and DOM events. The key to this approach is to find ways to process the inputs in smaller batches. For example:
var INTERVAL = 10; // ms
var intervalId = setInterval((function () {
var values = [];
var $input = $('input[type=\'text\']');
var index;
return function () {
var $i = $input[index];
var el = $i.get();
$i.removeClass('double-error');
if (el.value != '') {
values[el.value] = 0;
$input.each(function() {
if (this.value == el.value) {
values[el.value]++;
}
});
}
if (index++ > $input.length) {
clearInterval(intervalId);
index = 0;
// Now set elements
intervalId = setInterval(function () {
var $i = $input[index];
var el = $i.get();
if (values[el.value] > 1) {
var name = el.value;
var product_model = $i.parent().parent().find('.product-model').text();
var m = product_model.toLowerCase().areplace(search,replace);
$i.parent().find('input[type=\'text\']').val(name + '-' + m);
}
if (index++ > $input.length) {
clearInterval(intervalId);
}
}, INTERVAL);
}
};
}()), INTERVAL);
Here, we do just a little bit of work, then use setInterval to release the main thread so that other work can be performed. After INTERVAL we will do some more work, until we finish and call clearInterval
I have a 100 or so Word Open XML (.xml, not .docx, saved as "Word XML Document")documents (components) stored on SharePoint.
I use AJAX to load these by selection, as xml, 1 to many into an array, in which I also manage the selection sequence.
Once the user has selected the "components" they can then insert them into Word, the insertion is done via an array traversal (there is probably a better way to do this - but for now it does work),
wordBuild does the loading
function writeDocSync(){
// run through nameXMLArray to find the right sequence
var x = 0;
var countXMLAdds = 0;
//debugger;
toggleWriteButton("disable");
$('.progress-button').progressInitialize("Building Word");
toggleProgressBar(true);
// only run if we have data present
if(nameXMLArray.length > 0){
// increment through sequentially until we have all values
while (countXMLAdds <= checkedList.length){
// repeatedly traverse the array to get the next in sequence
while (x < nameXMLArray.length){
if (Number(nameXMLArray[x].position) === countXMLAdds && nameXMLArray[x].useStatus === true){
progHold = countXMLAdds;
wordBuild(nameXMLArray[x].xml, nameXMLArray[x].filename, countXMLAdds);
}
x++;
}
x=0;
countXMLAdds ++;
}
document.getElementById("showCheck").className = "results";
writeSelections("<b>You just built your proposal using<br/>the following components:</b><br/>");
toggleWriteButton("enable");
}
}
xxxxxxxxx
function wordBuild(xmlBody, nameDoc, progress){
var aryLN = checkedList.length;
var progPCT = (progress/aryLN)*100;
progressMeter.progressSet(progPCT);
Word.run(function (context) {
var currentDoc = context.document;
var body = currentDoc.body;
body.insertOoxml(xmlBody, Word.InsertLocation.end);
body.insertBreak(Word.BreakType.page, Word.InsertLocation.end);
return context.sync().then(function () {
showNotification("Written " + nameDoc);
});
})
.catch(function (error) {
showNotification('Error: ' + nameDoc + ' :' + JSON.stringify(error));
if (error instanceof OfficeExtension.Error) {
showNotification('Debug info: ' + JSON.stringify(error.debugInfo));
}
});
}
All the documents will load singly, and all will load in batches of say 10 - 30 or more.
The problem comes when I load the entire set (I have a "check all" option).
Sometimes 50 will build before I get an exception, sometimes 60, rarely more than 60, but very occasionally I get a gap where the exception doesn't occur, then it continues later.
The exception (which is repeated for each file) is:
Debug info: {}
Error: componentABC.xml :{"name":"OfficeExtension.Error","code":"GeneralException","message":"An internal error has occurred.","traceMessages":[],"debugInfo":{},"stack":"GeneralException: An internal error has occurred.\n at Anonymous function (https://customerportal.sharepoint.com/sites/components/Shared%20Documents/componentAssembler/Scripts/Office/1/word-win32-16.00.js:19:150094)\n at yi (https://customerportal.sharepoint.com/sites/components/Shared%20Documents/componentAssembler/Scripts/Office/1/word-win32-16.00.js:19:163912)\n at st (https://customerportal.sharepoint.com/sites/components/Shared%20Documents/componentAssembler/Scripts/Office/1/word-win32-16.00.js:19:163999)\n at d (https://customerportal.sharepoint.com/sites/components/Shared%20Documents/componentAssembler/Scripts/Office/1/word-win32-16.00.js:19:163819)\n at c (https://customerportal.sharepoint.com/sites/components/Shared%20Documents/componentAssembler/Scripts/Office/1/word-win32-16.00.js:19:162405)"}
Any help with what might cause this would be hugely appreciated.
Oh I should also say, the files where the exception is raised don't get inserted into Word. But in smaller batches - they work without issue.
Word.run() is an asynchronous call, and there's a limit to the number of concurrent Word.run() calls you can make. Since you're executing Word.run() inside a while loop, all of them get kicked off at the same time and run simultaneously.
There are a few ways to work around this.
Put everything inside one Word.run() call. This puts everything in one giant batch, avoiding multiple roundtrip calls to Word.
if (nameXMLArray.length > 0 {
Word.run(function(context) {
//...
while(...) {
wordBuild(context, nameXMLArray[x].xml, nameXMLArray[x].filename, countXMLAdds);
//...
}
return context.sync();
});
}
function wordBuild(context, xmlBoxy, nameDoc, progress) {
//everything as it currently is, except without the Word.run and the context.sync
}
Implement wordBuild as a promise, and use AngularJS’s $q service to chain the promises, something vaguely like this:
function wordBuild(...) {
var deferred = $q.defer();
Word.run( function(context) {
// current code
return context.sync().then(function() {
deferred.resolve();
});
});
return deferred.promise;
}
//Somewhere else
for (var x…)
{
promises.add(wordBuild);
}
$q.all(promises);
https://docs.angularjs.org/api/ng/service/$q
Angularjs $q.all
Chain the wordBuild calls yourself, as something like this:
var x = 0;
var context;
function (wordBuild() {
if (x >= nameXMLArray.length)
return;
else {
context.document.body.insertOoxml(ooxml, Word.InsertLocation.end);
x++;
return context.sync().then(wordBuild);
}
});
Word.run(function (ctx) {
context = ctx;
return wordBuild();
}
This sort of approach is difficult to maintain, but it could work.
Incidentally, the progress meter in your original code only updates when the call to Word starts, not when it actually returns. You might want to move the progress meter update code into the callback.
I ended up using jQuery deferreds, I was already using jQuery for treeview and checkboxes etc. so it made sense.
This is a mix of Geoffrey's suggestions and my own! I cannot claim it to be good code, only that is does work. (If it is good code or not will take me more time to understand!)
I run batches of 49 xml doc inserts, at 51 the Async call "Word.run" failed in tests, and inserts of 80 or so documents in one Word.run caused Word to freeze, so although not proven 49 inserts within 1 Word.run seems like a good starter for 10! 50 inserts of 49 pieces allows for 2450 inserts, which is way beyond anything I can see being needed, and would probably break Word!
To get the deferreds and sent variables to keep their values once launched as asynch deferreds I had to create a variable to transfer both new deferreds, and values, so I could use the "bind" command.
As Word async returns context.sync() I check the count of the batch, when the batch is completed, I then call the next batch - inside the context.sync()
A sort of recursive call, still a combination of Geoffrey's suggestion, and batches. This has a theoretical limit of 50 batches of 49 document sections. So far this has worked in all tests.
The progress meter exists in its own timed call, but as JavaScript prioritises code over UI it does hop. For example 120 documents it will hop just below half way fairly quickly, then a while later jump to almost complete, then complete (effectively 3 hops of a massively fast sequential percentage increases, various tricks suggested have zero effect (forceRepaint() is the latest experiment!).
function startUILock(){
// batch up in groups of 49 documents (51 and more were shown to fail, 49 gives manouvre room)
toggleProgressBar(true);
$('.progress-button').progressInitialize("Building Word");
progressMeter.progressSet(1);
$.blockUI({message: "Building word..."});
setTimeout(forceRepaint, 3000);
}
function forceRepaint(){
var el = document.getElementById('progDiv');
el.style.cssText += ';-webkit-transform:rotateZ(0deg)';
el.offsetHeight;
el.style.cssText += ';-webkit-transform:none';
}
function UIUnlock(insertedCount){
debugger;
var pct = (insertedCount/checkedList.length)*100
//showNotification('Progress percent is: ' + pct);
if (insertedCount !== checkedList.length ){
progressMeter.progressSet(pct);
forceRepaint();
} else {
$.unblockUI();
progressMeter.progressSet(100);
}
}
function writeDocDeffered(){
insertedCounter = 0;
var lastBatch = 0;
var x = 49;
var z = checkedList.length + 1;
if(x > z){
x=z;
}
deferreds = buildDeferredBatch(x, lastBatch);
$.when(deferreds).done(function () {
return;
})
.fail(function () {
//showNotification('One of our promises failed');
});
}
function buildDeferredBatch(batch, lastBatch) {
// this ensures the variables remain as issued - allows use of "bind"
var deferredsa = [];
var docSender = {
defr : $.Deferred(),
POSITION: batch,
LASTPOSITION: lastBatch,
runMe : function(){
this.defr.resolve(writeDocBatchedDeferred(this.POSITION, this.LASTPOSITION, this.defr));
}
}
// small timeout might not be required
deferredsa.push(setTimeout(docSender.runMe.bind(docSender), 10));
return deferredsa;
}
function writeDocBatchedDeferred(batch, lastBatch, defr){
// write the batches using deferred and promises
var x;
var countXMLAdds = lastBatch;
x = 0;
var fileName;
debugger;
// only run if we have data present
if(nameXMLArray.length > 0){
var aryLN = checkedList.length;
// increment through sequentially until we have all values
Word.run(function (context) {
var currentDoc = context.document;
var body = currentDoc.body;
while (countXMLAdds <= batch){
// repeatedly traverse the array to get the next in sequence
while (x < nameXMLArray.length){
if (Number(nameXMLArray[x].position) === countXMLAdds && nameXMLArray[x].useStatus === true){
fileName = nameXMLArray[x].filename;
body.insertOoxml(nameXMLArray[x].xml, Word.InsertLocation.end);
body.insertBreak(Word.BreakType.page, Word.InsertLocation.end);
insertedCounter = countXMLAdds;
var latest = insertedCounter;
var timerIt = {
LATEST: latest,
runMe : function(){
UIUnlock(this.LATEST);
}
}
setTimeout(timerIt.runMe.bind(timerIt),1000);
}
x++;
}
x=0;
countXMLAdds ++;
}
return context.sync().then(function () {
if(countXMLAdds = batch){
var lastBatch = batch + 1;
// set for next batch
var nextBatch = batch + 50;
var totalBatch = checkedList.length + 1;
// do not exceed the total batch
if(nextBatch > totalBatch){
nextBatch=totalBatch;
}
// any left to process keep going
if (nextBatch <= totalBatch && lastBatch < nextBatch){
deferreds = deferreds.concat(buildDeferredBatch(nextBatch, lastBatch));
}
// this batch done
defr.done();
}
});
})
.catch(function (error) {
showNotification('Error: ' + nameXMLArray[x].filename + " " + JSON.stringify(error));
if (error instanceof OfficeExtension.Error) {
showNotification('Debug info: ' + JSON.stringify(error.debugInfo));
}
});
document.getElementById("showCheck").className = "results";
writeSelections("<b>You just built your document using<br/>the following components:</b><br/>");
}
return defr.promise;
}
I am trying to collect results of FB API asynchronous requests which are called in loop. I use next code:
function getPicturesByUserIds(friendsIdList) {
var userPictures = [];
for (var i = 0; i < friendsIdList.length; i++) {
(function () {
var userId = friendsIdList[i];
var j = i;
var friendsIdListLength = friendsIdList.length - 1;
FB.api('/'+userId+'/picture?type=large', function (responce) {
if (responce && !responce.error) {
userPictures[userId] = responce.data.url;
}
if (j >= friendsIdListLength) {
console.log(userPictures);
sendPicturesAndGetResponse(userPictures);
}
});
})();
}
}
This code works in Chrome, but in Firefox array userPictures is empty.
You can solve those kind of things with "recursive functions", or - better - just use one API call for it:
/me/friends?fields=name,picture.type(large)
I would count recursive functions to the basics of programming though, you should get familiar with those. For example: Calling a javascript function recursively