Home | History | Annotate | Download | only in endure
      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 };