1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 // This file simulates a typical foreground process of an offline-capable 6 // authoring application. When in an "offline" state, simulated user actions 7 // are recorded for later playback in an IDB data store. When in an "online" 8 // state, the recorded actions are drained from the store (as if being sent 9 // to the server). 10 11 self.indexedDB = self.indexedDB || self.webkitIndexedDB || 12 self.mozIndexedDB; 13 self.IDBKeyRange = self.IDBKeyRange || self.webkitIDBKeyRange; 14 15 var $ = function(s) { 16 return document.querySelector(s); 17 }; 18 19 function status(message) { 20 var elem = $('#status'); 21 while (elem.firstChild) 22 elem.removeChild(elem.firstChild); 23 elem.appendChild(document.createTextNode(message)); 24 } 25 26 function log(message) { 27 status(message); 28 } 29 30 function error(message) { 31 status(message); 32 console.error(message); 33 } 34 35 function unexpectedErrorCallback(e) { 36 error("Unexpected error callback: (" + e.target.error.name + ") " + 37 e.target.webkitErrorMessage); 38 } 39 40 function unexpectedAbortCallback(e) { 41 error("Unexpected abort callback: (" + e.target.error.name + ") " + 42 e.target.webkitErrorMessage); 43 } 44 45 function unexpectedBlockedCallback(e) { 46 error("Unexpected blocked callback!"); 47 } 48 49 var DBNAME = 'endurance-db'; 50 var DBVERSION = 1; 51 var MAX_DOC_ID = 25; 52 53 var db; 54 55 function initdb() { 56 var request = indexedDB.deleteDatabase(DBNAME); 57 request.onerror = unexpectedErrorCallback; 58 request.onblocked = unexpectedBlockedCallback; 59 request.onsuccess = function () { 60 request = indexedDB.open(DBNAME, DBVERSION); 61 request.onerror = unexpectedErrorCallback; 62 request.onblocked = unexpectedBlockedCallback; 63 request.onupgradeneeded = function () { 64 db = request.result; 65 request.transaction.onabort = unexpectedAbortCallback; 66 67 var syncStore = db.createObjectStore( 68 'sync-chunks', {keyPath: 'sequence', autoIncrement: true}); 69 syncStore.createIndex('doc-index', 'docid'); 70 71 var docStore = db.createObjectStore( 72 'docs', {keyPath: 'docid'}); 73 docStore.createIndex( 74 'owner-index', 'owner', {multiEntry: true}); 75 76 var userEventStore = db.createObjectStore( 77 'user-events', {keyPath: 'sequence', autoIncrement: true}); 78 userEventStore.createIndex('doc-index', 'docid'); 79 }; 80 request.onsuccess = function () { 81 log('initialized'); 82 $('#offline').disabled = true; 83 $('#online').disabled = false; 84 }; 85 }; 86 } 87 88 var offline = true; 89 var worker = new Worker('indexeddb_app_worker.js?cachebust'); 90 worker.onmessage = function (event) { 91 var data = event.data; 92 switch (data.type) { 93 case 'ABORT': 94 unexpectedAbortCallback( 95 {target:{ 96 error: data.error, 97 webkitErrorMessage: data.webkitErrorMessage}}); 98 break; 99 case 'ERROR': 100 unexpectedErrorCallback( 101 {target:{ 102 error: data.error, 103 webkitErrorMessage: data.webkitErrorMessage}}); 104 break; 105 case 'BLOCKED': 106 unexpectedBlockedCallback( 107 {target:{ 108 error: data.error, 109 webkitErrorMessage: data.webkitErrorMessage}}); 110 break; 111 case 'LOG': 112 log('WORKER: ' + data.message); 113 break; 114 case 'ERROR': 115 error('WORKER: ' + data.message); 116 break; 117 } 118 }; 119 worker.onerror = function (event) { 120 error("Error in: " + event.filename + "(" + event.lineno + "): " + 121 event.message); 122 }; 123 124 $('#offline').addEventListener('click', goOffline); 125 $('#online').addEventListener('click', goOnline); 126 127 var EVENT_INTERVAL = 100; 128 var eventIntervalId = 0; 129 130 function goOffline() { 131 if (offline) 132 return; 133 offline = true; 134 $('#offline').disabled = offline; 135 $('#online').disabled = !offline; 136 $('#state').innerHTML = 'offline'; 137 log('offline'); 138 139 worker.postMessage({type: 'offline'}); 140 141 eventIntervalId = setInterval(recordEvent, EVENT_INTERVAL); 142 } 143 144 function goOnline() { 145 if (!offline) 146 return; 147 offline = false; 148 $('#offline').disabled = offline; 149 $('#online').disabled = !offline; 150 $('#state').innerHTML = 'online'; 151 log('online'); 152 153 worker.postMessage({type: 'online'}); 154 155 setTimeout(playbackEvents, 100); 156 clearInterval(eventIntervalId); 157 eventIntervalId = 0; 158 }; 159 160 function recordEvent() { 161 if (!db) { 162 error("Database not initialized"); 163 return; 164 } 165 166 var transaction = db.transaction(['user-events'], 'readwrite'); 167 var store = transaction.objectStore('user-events'); 168 var record = { 169 // 'sequence' key will be generated 170 docid: Math.floor(Math.random() * MAX_DOC_ID), 171 timestamp: new Date(), 172 data: randomString(256) 173 }; 174 175 log('putting user event'); 176 var request = store.put(record); 177 request.onerror = unexpectedErrorCallback; 178 transaction.onabort = unexpectedAbortCallback; 179 transaction.oncomplete = function () { 180 log('put user event'); 181 }; 182 } 183 184 function sendEvent(record, callback) { 185 setTimeout( 186 function () { 187 if (offline) 188 callback(false); 189 else { 190 var serialization = JSON.stringify(record); 191 callback(true); 192 } 193 }, 194 Math.random() * 200); // Simulate network jitter 195 } 196 197 var PLAYBACK_NONE = 0; 198 var PLAYBACK_SUCCESS = 1; 199 var PLAYBACK_FAILURE = 2; 200 201 function playbackEvent(callback) { 202 log('playbackEvent'); 203 var result = false; 204 var transaction = db.transaction(['user-events'], 'readonly'); 205 transaction.onabort = unexpectedAbortCallback; 206 var store = transaction.objectStore('user-events'); 207 var cursorRequest = store.openCursor(); 208 cursorRequest.onerror = unexpectedErrorCallback; 209 cursorRequest.onsuccess = function () { 210 var cursor = cursorRequest.result; 211 if (cursor) { 212 var record = cursor.value; 213 var key = cursor.key; 214 // NOTE: sendEvent is asynchronous so transaction should finish 215 sendEvent( 216 record, 217 function (success) { 218 if (success) { 219 // Use another transaction to delete event 220 var transaction = db.transaction(['user-events'], 'readwrite'); 221 transaction.onabort = unexpectedAbortCallback; 222 var store = transaction.objectStore('user-events'); 223 var deleteRequest = store.delete(key); 224 deleteRequest.onerror = unexpectedErrorCallback; 225 transaction.oncomplete = function () { 226 // successfully sent and deleted event 227 callback(PLAYBACK_SUCCESS); 228 }; 229 } else { 230 // No progress made 231 callback(PLAYBACK_FAILURE); 232 } 233 }); 234 } else { 235 callback(PLAYBACK_NONE); 236 } 237 }; 238 } 239 240 var playback = false; 241 242 function playbackEvents() { 243 log('playbackEvents'); 244 if (!db) { 245 error("Database not initialized"); 246 return; 247 } 248 249 if (playback) 250 return; 251 252 playback = true; 253 log("Playing back events"); 254 255 function nextEvent() { 256 playbackEvent( 257 function (result) { 258 switch (result) { 259 case PLAYBACK_NONE: 260 playback = false; 261 log("Done playing back events"); 262 return; 263 case PLAYBACK_SUCCESS: 264 setTimeout(nextEvent, 0); 265 return; 266 case PLAYBACK_FAILURE: 267 playback = false; 268 log("Failure during playback (dropped offline?)"); 269 return; 270 } 271 }); 272 } 273 274 nextEvent(); 275 } 276 277 function randomString(len) { 278 var s = ''; 279 while (len--) 280 s += Math.floor((Math.random() * 36)).toString(36); 281 return s; 282 } 283 284 window.onload = function () { 285 log("initializing..."); 286 initdb(); 287 };