| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // This file simulates a typical foreground process of an offline-capable |
| // authoring application. When in an "offline" state, simulated user actions |
| // are recorded for later playback in an IDB data store. When in an "online" |
| // state, the recorded actions are drained from the store (as if being sent |
| // to the server). |
| |
| var $ = function(s) { |
| return document.querySelector(s); |
| }; |
| |
| function status(message) { |
| var elem = $('#status'); |
| while (elem.firstChild) |
| elem.removeChild(elem.firstChild); |
| elem.appendChild(document.createTextNode(message)); |
| } |
| |
| function log(message) { |
| status(message); |
| } |
| |
| function error(message) { |
| status(message); |
| console.error(message); |
| } |
| |
| function unexpectedErrorCallback(e) { |
| error("Unexpected error callback: (" + e.target.error.name + ") " + |
| e.target.error.message); |
| } |
| |
| function unexpectedAbortCallback(e) { |
| error("Unexpected abort callback: (" + e.target.error.name + ") " + |
| e.target.error.message); |
| } |
| |
| function unexpectedBlockedCallback(e) { |
| error("Unexpected blocked callback!"); |
| } |
| |
| var DBNAME = 'endurance-db'; |
| var DBVERSION = 1; |
| var MAX_DOC_ID = 25; |
| |
| var db; |
| |
| function initdb() { |
| var request = indexedDB.deleteDatabase(DBNAME); |
| request.onerror = unexpectedErrorCallback; |
| request.onblocked = unexpectedBlockedCallback; |
| request.onsuccess = function () { |
| request = indexedDB.open(DBNAME, DBVERSION); |
| request.onerror = unexpectedErrorCallback; |
| request.onblocked = unexpectedBlockedCallback; |
| request.onupgradeneeded = function () { |
| db = request.result; |
| request.transaction.onabort = unexpectedAbortCallback; |
| |
| var syncStore = db.createObjectStore( |
| 'sync-chunks', {keyPath: 'sequence', autoIncrement: true}); |
| syncStore.createIndex('doc-index', 'docid'); |
| |
| var docStore = db.createObjectStore( |
| 'docs', {keyPath: 'docid'}); |
| docStore.createIndex( |
| 'owner-index', 'owner', {multiEntry: true}); |
| |
| var userEventStore = db.createObjectStore( |
| 'user-events', {keyPath: 'sequence', autoIncrement: true}); |
| userEventStore.createIndex('doc-index', 'docid'); |
| }; |
| request.onsuccess = function () { |
| log('initialized'); |
| $('#offline').disabled = true; |
| $('#online').disabled = false; |
| }; |
| }; |
| } |
| |
| var offline = true; |
| var worker = new Worker('indexeddb_app_worker.js?cachebust'); |
| worker.onmessage = function (event) { |
| var data = event.data; |
| switch (data.type) { |
| case 'ABORT': |
| unexpectedAbortCallback({target: {error: data.error}}); |
| break; |
| case 'ERROR': |
| unexpectedErrorCallback({target: {error: data.error}}); |
| break; |
| case 'BLOCKED': |
| unexpectedBlockedCallback({target: {error: data.error}}); |
| break; |
| case 'LOG': |
| log('WORKER: ' + data.message); |
| break; |
| case 'ERROR': |
| error('WORKER: ' + data.message); |
| break; |
| } |
| }; |
| worker.onerror = function (event) { |
| error("Error in: " + event.filename + "(" + event.lineno + "): " + |
| event.message); |
| }; |
| |
| $('#offline').addEventListener('click', goOffline); |
| $('#online').addEventListener('click', goOnline); |
| |
| var EVENT_INTERVAL = 100; |
| var eventIntervalId = 0; |
| |
| function goOffline() { |
| if (offline) |
| return; |
| offline = true; |
| $('#offline').disabled = offline; |
| $('#online').disabled = !offline; |
| $('#state').innerHTML = 'offline'; |
| log('offline'); |
| |
| worker.postMessage({type: 'offline'}); |
| |
| eventIntervalId = setInterval(recordEvent, EVENT_INTERVAL); |
| } |
| |
| function goOnline() { |
| if (!offline) |
| return; |
| offline = false; |
| $('#offline').disabled = offline; |
| $('#online').disabled = !offline; |
| $('#state').innerHTML = 'online'; |
| log('online'); |
| |
| worker.postMessage({type: 'online'}); |
| |
| setTimeout(playbackEvents, 100); |
| clearInterval(eventIntervalId); |
| eventIntervalId = 0; |
| }; |
| |
| function recordEvent() { |
| if (!db) { |
| error("Database not initialized"); |
| return; |
| } |
| |
| var transaction = db.transaction(['user-events'], 'readwrite'); |
| var store = transaction.objectStore('user-events'); |
| var record = { |
| // 'sequence' key will be generated |
| docid: Math.floor(Math.random() * MAX_DOC_ID), |
| timestamp: new Date(), |
| data: randomString(256) |
| }; |
| |
| log('putting user event'); |
| var request = store.put(record); |
| request.onerror = unexpectedErrorCallback; |
| transaction.onabort = unexpectedAbortCallback; |
| transaction.oncomplete = function () { |
| log('put user event'); |
| }; |
| } |
| |
| function sendEvent(record, callback) { |
| setTimeout( |
| function () { |
| if (offline) |
| callback(false); |
| else { |
| var serialization = JSON.stringify(record); |
| callback(true); |
| } |
| }, |
| Math.random() * 200); // Simulate network jitter |
| } |
| |
| var PLAYBACK_NONE = 0; |
| var PLAYBACK_SUCCESS = 1; |
| var PLAYBACK_FAILURE = 2; |
| |
| function playbackEvent(callback) { |
| log('playbackEvent'); |
| var result = false; |
| var transaction = db.transaction(['user-events'], 'readonly'); |
| transaction.onabort = unexpectedAbortCallback; |
| var store = transaction.objectStore('user-events'); |
| var cursorRequest = store.openCursor(); |
| cursorRequest.onerror = unexpectedErrorCallback; |
| cursorRequest.onsuccess = function () { |
| var cursor = cursorRequest.result; |
| if (cursor) { |
| var record = cursor.value; |
| var key = cursor.key; |
| // NOTE: sendEvent is asynchronous so transaction should finish |
| sendEvent( |
| record, |
| function (success) { |
| if (success) { |
| // Use another transaction to delete event |
| var transaction = db.transaction(['user-events'], 'readwrite'); |
| transaction.onabort = unexpectedAbortCallback; |
| var store = transaction.objectStore('user-events'); |
| var deleteRequest = store.delete(key); |
| deleteRequest.onerror = unexpectedErrorCallback; |
| transaction.oncomplete = function () { |
| // successfully sent and deleted event |
| callback(PLAYBACK_SUCCESS); |
| }; |
| } else { |
| // No progress made |
| callback(PLAYBACK_FAILURE); |
| } |
| }); |
| } else { |
| callback(PLAYBACK_NONE); |
| } |
| }; |
| } |
| |
| var playback = false; |
| |
| function playbackEvents() { |
| log('playbackEvents'); |
| if (!db) { |
| error("Database not initialized"); |
| return; |
| } |
| |
| if (playback) |
| return; |
| |
| playback = true; |
| log("Playing back events"); |
| |
| function nextEvent() { |
| playbackEvent( |
| function (result) { |
| switch (result) { |
| case PLAYBACK_NONE: |
| playback = false; |
| log("Done playing back events"); |
| return; |
| case PLAYBACK_SUCCESS: |
| setTimeout(nextEvent, 0); |
| return; |
| case PLAYBACK_FAILURE: |
| playback = false; |
| log("Failure during playback (dropped offline?)"); |
| return; |
| } |
| }); |
| } |
| |
| nextEvent(); |
| } |
| |
| function randomString(len) { |
| var s = ''; |
| while (len--) |
| s += Math.floor((Math.random() * 36)).toString(36); |
| return s; |
| } |
| |
| window.onload = function () { |
| log("initializing..."); |
| initdb(); |
| }; |