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
11self.indexedDB = self.indexedDB || self.webkitIndexedDB ||
12  self.mozIndexedDB;
13self.IDBKeyRange = self.IDBKeyRange || self.webkitIDBKeyRange;
14
15var $ = function(s) {
16  return document.querySelector(s);
17};
18
19function status(message) {
20  var elem = $('#status');
21  while (elem.firstChild)
22    elem.removeChild(elem.firstChild);
23  elem.appendChild(document.createTextNode(message));
24}
25
26function log(message) {
27  status(message);
28}
29
30function error(message) {
31  status(message);
32  console.error(message);
33}
34
35function unexpectedErrorCallback(e) {
36  error("Unexpected error callback: (" + e.target.error.name + ") " +
37        e.target.webkitErrorMessage);
38}
39
40function unexpectedAbortCallback(e) {
41  error("Unexpected abort callback: (" + e.target.error.name + ") " +
42        e.target.webkitErrorMessage);
43}
44
45function unexpectedBlockedCallback(e) {
46  error("Unexpected blocked callback!");
47}
48
49var DBNAME = 'endurance-db';
50var DBVERSION = 1;
51var MAX_DOC_ID = 25;
52
53var db;
54
55function 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
88var offline = true;
89var worker = new Worker('indexeddb_app_worker.js?cachebust');
90worker.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};
119worker.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
127var EVENT_INTERVAL = 100;
128var eventIntervalId = 0;
129
130function 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
144function 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
160function 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
184function 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
197var PLAYBACK_NONE = 0;
198var PLAYBACK_SUCCESS = 1;
199var PLAYBACK_FAILURE = 2;
200
201function 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
240var playback = false;
241
242function 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
277function randomString(len) {
278  var s = '';
279  while (len--)
280    s += Math.floor((Math.random() * 36)).toString(36);
281  return s;
282}
283
284window.onload = function () {
285  log("initializing...");
286  initdb();
287};