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 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