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