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
11var $ = function(s) {
12  return document.querySelector(s);
13};
14
15function status(message) {
16  var elem = $('#status');
17  while (elem.firstChild)
18    elem.removeChild(elem.firstChild);
19  elem.appendChild(document.createTextNode(message));
20}
21
22function log(message) {
23  status(message);
24}
25
26function error(message) {
27  status(message);
28  console.error(message);
29}
30
31function unexpectedErrorCallback(e) {
32  error("Unexpected error callback: (" + e.target.error.name + ") " +
33        e.target.error.message);
34}
35
36function unexpectedAbortCallback(e) {
37  error("Unexpected abort callback: (" + e.target.error.name + ") " +
38        e.target.error.message);
39}
40
41function unexpectedBlockedCallback(e) {
42  error("Unexpected blocked callback!");
43}
44
45var DBNAME = 'endurance-db';
46var DBVERSION = 1;
47var MAX_DOC_ID = 25;
48
49var db;
50
51function 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
84var offline = true;
85var worker = new Worker('indexeddb_app_worker.js?cachebust');
86worker.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};
106worker.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
114var EVENT_INTERVAL = 100;
115var eventIntervalId = 0;
116
117function 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
131function 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
147function 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
171function 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
184var PLAYBACK_NONE = 0;
185var PLAYBACK_SUCCESS = 1;
186var PLAYBACK_FAILURE = 2;
187
188function 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
227var playback = false;
228
229function 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
264function randomString(len) {
265  var s = '';
266  while (len--)
267    s += Math.floor((Math.random() * 36)).toString(36);
268  return s;
269}
270
271window.onload = function () {
272  log("initializing...");
273  initdb();
274};
275