├── .gitattributes ├── .gitignore ├── doWork.js ├── webworker.js ├── index.html ├── README.md └── app.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | -------------------------------------------------------------------------------- /doWork.js: -------------------------------------------------------------------------------- 1 | var RaananW; 2 | (function (RaananW) { 3 | var WorkedIDBTest; 4 | (function (WorkedIDBTest) { 5 | function doWork(data, onProgress, onSuccess) { 6 | function sleep(milliseconds) { 7 | var start = new Date().getTime(); 8 | for (var i = 0; i < 1e7; i++) { 9 | if ((new Date().getTime() - start) > milliseconds) { 10 | break; 11 | } 12 | } 13 | } 14 | var lastP = 0; 15 | data.forEach(function (d, idx) { 16 | sleep(d); 17 | var newP = ((idx / data.length) * 100) >> 0; 18 | if (newP > lastP) { 19 | lastP = newP; 20 | onProgress(lastP, d); 21 | } 22 | }); 23 | onSuccess(); 24 | } 25 | WorkedIDBTest.doWork = doWork; 26 | })(WorkedIDBTest = RaananW.WorkedIDBTest || (RaananW.WorkedIDBTest = {})); 27 | })(RaananW || (RaananW = {})); 28 | //# sourceMappingURL=doWork.js.map -------------------------------------------------------------------------------- /webworker.js: -------------------------------------------------------------------------------- 1 | importScripts("doWork.js"); 2 | onmessage = function (e) { 3 | var msg = e.data; 4 | var db; 5 | var request = indexedDB.open(msg.dbName, msg.dbVersion); 6 | request.onerror = function (event) { 7 | alert("Why didn't you allow my test app to use IndexedDB?!"); 8 | }; 9 | request.onsuccess = function (event) { 10 | db = event.target['result']; 11 | var trans = db.transaction("numbers", IDBTransaction.READ_ONLY); 12 | var store = trans.objectStore("numbers"); 13 | var items = []; 14 | trans.oncomplete = function (evt) { 15 | RaananW.WorkedIDBTest.doWork(items, function (progress) { 16 | postMessage(progress); 17 | }, function () { 18 | postMessage("finished"); 19 | }); 20 | }; 21 | var cursorRequest = store.openCursor(); 22 | cursorRequest.onerror = function (error) { 23 | console.log(error); 24 | }; 25 | cursorRequest.onsuccess = function (evt) { 26 | var cursor = evt.target['result']; 27 | if (cursor) { 28 | items.push(cursor.value); 29 | cursor.continue(); 30 | } 31 | }; 32 | }; 33 | }; 34 | //# sourceMappingURL=webworker.js.map 35 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IndexedDB and Webworkers 7 | 8 | 9 | 10 | 11 |
12 |

IndexedDB and Web-workers

13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 | 43 |
44 | 45 | 46 | 47 |
48 |
49 |
50 |
51 |

About

52 |

53 | This demo shows one of the benefits of using indexed db in a webworker. 54 |

55 |

56 | The generated data, which is an array of integers is used to simulate "work". 57 | The data is being sent to the WebWorker using an IndexedDB database, which is being populated on the main thread. The WebWorker then fetches the entire data set and processes it. 58 |

59 |

60 | Processing the data in the main thread will caused the browser to be "stuck" for a while, while the webworker doesn't halt the animation and actually updates the UI correctly. 61 |

62 |

63 | The lower progress bar will turn green after the data was completely processed. 64 |

65 |

66 | More @ GitHub. 67 |

68 |

Usage

69 |

70 | To generate a new set of data (delays) choose the properties and press "Generate data". 71 |

72 |

73 | To see how this data is being processed on the main thread or persisted in IndexedDB and processed in a webworker click the corresponding buttons. 74 |

75 |

76 | Try 10000 : 1 , 2500 : 10, 1000 : 25 . You will notice the overhead of storing the data and reading it entirely is not affecting the site's performance. 77 |

78 |
79 |
80 |
81 | 82 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Workers with IndexedDB 2 | 3 | ## About 4 | 5 | Running expensive processes on the main JS thread will cause the UI to flicker or the website be non-responsive. 6 | 7 | The solution that was introduced was web workers - JS code that is running concurrently to the main (UI) thread. Web workers have a very limited scope and very limits set of tools to interact with the browser and the DOM itself (https://developer.mozilla.org/en-US/docs/Web/API/Worker/Functions_and_classes_available_to_workers) 8 | 9 | Sending data to and from the web worker is done using JSON/String messages and ArrayBuffers. I will not be talking about the up and down sides of using both. 10 | 11 | Another option to send data to web workers is using IndexedDB. This option was not available at the beginning and is slowly being introduced to all of the major browsers (Chrome >= 37, IE >= 10, FF >= 37. Safari?... still waiting.). 12 | 13 | The idea came from basic worker patterns, for example: 14 | * http://www.infoq.com/news/2010/09/Patterns-Windows-Azure 15 | * AWS Worker in Elastic Beanstalk 16 | 17 | that suggest having a joined database between the main application and the worker, while only passing "tasks" or actions to 18 | the worker. The worker will then process the request using the shared database and will either alter the data there, or send a simple reply to the task. 19 | 20 | The same can work in a web worker combined with indexedDB. 21 | 22 | Using this method, spawning new workers won't require synchronizing data with them or sending each worker the entire data store - they are all synchronized using one single database. 23 | 24 | ## The Demo 25 | 26 | To introduce the combination of both technologies I have implemented a very quick demo available here - http://raananw.github.io/WebWorkers-IndexedDB/ 27 | 28 | The site generates a random array of integers and is using this data to generate delays (or simulate work). It has the option to execute the work on both the main thread and a web worker. 29 | 30 | The main thread simply executes the work. You will notice that the UI gets stuck until the process is over. The progress bar is being updated, but it doesn't show. 31 | 32 | The web worker option is executing the following tasks (I know, UML would be much better :-) ): 33 | 34 | 1. [Main Thread] Store the entire array (single items) in an indexed db store. 35 | ```javascript 36 | var added = 0; 37 | for (var i = 0; i < data.length; ++i) { 38 | var req = objectStore.add(data[i]); 39 | req.onsuccess = function () { 40 | if (++added == data.length) { 41 | successCallback(); 42 | } 43 | } 44 | } 45 | ``` 46 | 2. [Main Thread] Send a simple post message with the name of the db and the name of the store to execute 47 | ```javascript 48 | webworker.postMessage({ dbName: "webworkerTest", dbVersion:1, asArray: asArray }); 49 | ``` 50 | 3. [Web Worker] Open the database, fetch all records from the store (using a cursor) 51 | ```javascript 52 | //... 53 | var cursor = evt.target['result']; 54 | if (cursor) { 55 | items.push(cursor.value); 56 | cursor.continue(); 57 | } 58 | ``` 59 | 4. [Web Worker] Do the work (the exact same function that was used in the main thread's work). [Main Thread] Update the progress bar. 60 | 5. [Web Worker] Notify the main thread that we are finished. 61 | 6. [Main thread] Load the records from the db (to simulate an update that was done by the web worker). 62 | 7. ??? 63 | 8. Profit. 64 | 65 | ## Why? 66 | 67 | Because it can be super heplful. Having a persisted model in a DB and having an external thread process it and update it can benefit a lot of applications - from games to heavy canvas manipulations (WebGL comes to mind as well). 68 | 69 | ## Suggestions? Questions? 70 | 71 | Simply contact me! 72 | 73 | ## MIT License 74 | 75 | Copyright (c) 2014-2015 Raanan Weber (info@raananweber.com) 76 | 77 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 78 | 79 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 80 | 81 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 82 | 83 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var RaananW; 2 | (function (RaananW) { 3 | var WorkedIDBTest; 4 | (function (WorkedIDBTest) { 5 | function generateData(length, maxDelay) { 6 | if (maxDelay === void 0) { maxDelay = 25; } 7 | var data = []; 8 | for (var i = 0; i < length; ++i) { 9 | data.push((Math.random() * maxDelay) >> 0); 10 | } 11 | return data; 12 | } 13 | WorkedIDBTest.generateData = generateData; 14 | function openDatabase(dbName, dbVersion, deleteDatabase, successCallback) { 15 | if (deleteDatabase) { 16 | indexedDB.deleteDatabase(dbName); 17 | } 18 | var request = indexedDB.open(dbName, dbVersion); 19 | request.onupgradeneeded = function (event) { 20 | var db = event.target['result']; 21 | var objStore = db.createObjectStore("numbers", { autoIncrement: true }); 22 | successCallback(db, objStore); 23 | }; 24 | } 25 | function storeInDb(objectStore, data, asArray, successCallback) { 26 | var clearReq = objectStore.clear(); 27 | clearReq.onsuccess = function () { 28 | if (asArray) { 29 | var req = objectStore.add(data, "array"); 30 | req.onsuccess = function () { 31 | successCallback(); 32 | }; 33 | } 34 | else { 35 | var added = 0; 36 | for (var i = 0; i < data.length; ++i) { 37 | var req = objectStore.add(data[i]); 38 | req.onsuccess = function () { 39 | if (++added == data.length) { 40 | successCallback(); 41 | } 42 | }; 43 | } 44 | } 45 | }; 46 | } 47 | function getData(db, asArray, successCallback) { 48 | var trans = db.transaction("numbers", IDBTransaction.READ_ONLY); 49 | var store = trans.objectStore("numbers"); 50 | var items = []; 51 | trans.oncomplete = function (evt) { 52 | successCallback(items); 53 | }; 54 | if (asArray) { 55 | var req = store.get("array"); 56 | req.onsuccess = function (e) { 57 | successCallback(e.target['result']); 58 | }; 59 | } 60 | else { 61 | var cursorRequest = store.openCursor(); 62 | cursorRequest.onerror = function (error) { 63 | console.log(error); 64 | }; 65 | cursorRequest.onsuccess = function (evt) { 66 | var cursor = evt.target['result']; 67 | if (cursor) { 68 | items.push(cursor.value); 69 | cursor.continue(); 70 | } 71 | }; 72 | } 73 | } 74 | function runSingleTest(data, asArray, webworker, successCallback, progress) { 75 | if (!webworker) { 76 | WorkedIDBTest.doWork(data, progress, function () { 77 | successCallback(data); 78 | }); 79 | } 80 | else { 81 | var db = openDatabase("webworkerTest", 1, true, function (db, objStore) { 82 | console.log("db opened"); 83 | storeInDb(objStore, data, asArray, function () { 84 | webworker.onmessage = function (ev) { 85 | if (progress) 86 | progress(ev.data); 87 | if (ev.data === "finished") { 88 | getData(db, asArray, function (sortedData) { 89 | db.close(); 90 | successCallback(); 91 | webworker.onmessage = null; 92 | }); 93 | } 94 | }; 95 | webworker.postMessage({ dbName: "webworkerTest", dbVersion: 1, asArray: asArray }); 96 | }); 97 | }); 98 | } 99 | } 100 | WorkedIDBTest.runSingleTest = runSingleTest; 101 | })(WorkedIDBTest = RaananW.WorkedIDBTest || (RaananW.WorkedIDBTest = {})); 102 | })(RaananW || (RaananW = {})); 103 | $(function () { 104 | var data; 105 | var webworker = new Worker('webworker.js'); 106 | $("#generateData").click(function () { 107 | data = RaananW.WorkedIDBTest.generateData($("#numberOfItems").val(), $("#maxDelay").val()); 108 | }).click(); 109 | $(".runTest").click(function () { 110 | var worker = $(this).data("worker") == true ? webworker : null; 111 | $("#progressBar").width("0").removeClass("progress-bar-success"); 112 | RaananW.WorkedIDBTest.runSingleTest(data, false, worker, function () { 113 | $("#progressBar").width("100%").addClass("progress-bar-success"); 114 | ; 115 | }, function (progress) { 116 | $("#progressBar").width(progress + "%"); 117 | }); 118 | }); 119 | }); 120 | //# sourceMappingURL=app.js.map --------------------------------------------------------------------------------