Skip to content

Commit f9cb631

Browse files
Issue #640 - migrate backup service to indexeddb
1 parent ed749a7 commit f9cb631

File tree

3 files changed

+183
-48
lines changed

3 files changed

+183
-48
lines changed

src/js/controller/settings/ImportController.js

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,19 @@
2929

3030
ns.ImportController.prototype.initRestoreSession_ = function () {
3131
var previousSessionContainer = document.querySelector('.previous-session');
32-
var previousInfo = pskl.app.backupService.getPreviousPiskelInfo();
33-
if (previousInfo) {
34-
var previousSessionTemplate_ = pskl.utils.Template.get('previous-session-info-template');
35-
var date = pskl.utils.DateUtils.format(previousInfo.date, '{{H}}:{{m}} - {{Y}}/{{M}}/{{D}}');
36-
previousSessionContainer.innerHTML = pskl.utils.Template.replace(previousSessionTemplate_, {
37-
name : previousInfo.name,
38-
date : date
39-
});
40-
this.addEventListener('.restore-session-button', 'click', this.onRestorePreviousSessionClick_);
41-
} else {
42-
previousSessionContainer.innerHTML = 'No piskel backup was found on this browser.';
43-
}
32+
pskl.app.backupService.getPreviousPiskelInfo().then(function (previousInfo) {
33+
if (previousInfo) {
34+
var previousSessionTemplate_ = pskl.utils.Template.get('previous-session-info-template');
35+
var date = pskl.utils.DateUtils.format(previousInfo.date, '{{H}}:{{m}} - {{Y}}/{{M}}/{{D}}');
36+
previousSessionContainer.innerHTML = pskl.utils.Template.replace(previousSessionTemplate_, {
37+
name : previousInfo.name,
38+
date : date
39+
});
40+
this.addEventListener('.restore-session-button', 'click', this.onRestorePreviousSessionClick_);
41+
} else {
42+
previousSessionContainer.innerHTML = 'No piskel backup was found on this browser.';
43+
}
44+
}.bind(this));
4445
};
4546

4647
ns.ImportController.prototype.closeDrawer_ = function () {

src/js/model/Piskel.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
this.descriptor = descriptor;
1717
this.savePath = null;
1818
this.fps = fps;
19+
// This id is used to keep track of sessions in the BackupService.
20+
this.sessionId = pskl.utils.Uuid.generate();
1921

2022
} else {
2123
throw 'Missing arguments in Piskel constructor : ' + Array.prototype.join.call(arguments, ',');

src/js/service/BackupService.js

Lines changed: 168 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,197 @@
11
(function () {
22
var ns = $.namespace('pskl.service');
33

4-
// 1 minute = 1000 * 60
5-
var BACKUP_INTERVAL = 1000 * 60;
4+
var DB_NAME = 'PiskelSessionsDatabase';
5+
var DB_VERSION = 1;
6+
7+
var ONE_SECOND = 1000;
8+
var ONE_MINUTE = 60 * ONE_SECOND;
9+
10+
// Save every minute = 1000 * 60
11+
var BACKUP_INTERVAL = ONE_MINUTE;
12+
// Store a new snapshot every 5 minutes.
13+
var SNAPSHOT_INTERVAL = ONE_MINUTE * 5;
14+
// Store up to 12 snapshots for a piskel session, min. 1 hour of work
15+
var MAX_SNAPSHOTS_PER_SESSION = 12;
16+
17+
var _requestPromise = function (req) {
18+
var deferred = Q.defer();
19+
req.onsuccess = deferred.resolve.bind(deferred);
20+
req.onerror = deferred.reject.bind(deferred);
21+
return deferred.promise;
22+
};
623

724
ns.BackupService = function (piskelController) {
825
this.piskelController = piskelController;
926
this.lastHash = null;
27+
this.nextSnapshotDate = -1;
1028
};
1129

1230
ns.BackupService.prototype.init = function () {
13-
var previousPiskel = window.localStorage.getItem('bkp.next.piskel');
14-
var previousInfo = window.localStorage.getItem('bkp.next.info');
15-
if (previousPiskel && previousInfo) {
16-
this.savePiskel_('prev', previousPiskel, previousInfo);
17-
}
31+
var request = window.indexedDB.open(DB_NAME, DB_VERSION);
32+
33+
request.onerror = this.onRequestError_.bind(this);
34+
request.onupgradeneeded = this.onUpgradeNeeded_.bind(this);
35+
request.onsuccess = this.onRequestSuccess_.bind(this);
36+
};
1837

38+
ns.BackupService.prototype.onRequestError_ = function (event) {
39+
console.log('Could not initialize the piskel backup database');
40+
};
41+
42+
ns.BackupService.prototype.onUpgradeNeeded_ = function (event) {
43+
// Set this.db early to allow migration scripts to access it in oncomplete.
44+
this.db = event.target.result;
45+
46+
// Create an object store "piskels" with the autoIncrement flag set as true.
47+
var objectStore = this.db.createObjectStore('snapshots', { keyPath: 'id', autoIncrement : true });
48+
49+
objectStore.createIndex('session_id', 'session_id', { unique: false });
50+
objectStore.createIndex('date', 'date', { unique: false });
51+
objectStore.createIndex('session_id, date', ['session_id', 'date'], { unique: false });
52+
53+
objectStore.transaction.oncomplete = function(event) {
54+
// TODO: Migrate existing data from local storage?
55+
};
56+
};
57+
58+
ns.BackupService.prototype.onRequestSuccess_ = function (event) {
59+
this.db = event.target.result;
1960
window.setInterval(this.backup.bind(this), BACKUP_INTERVAL);
2061
};
2162

63+
ns.BackupService.prototype.openObjectStore_ = function () {
64+
return this.db.transaction(['snapshots'], 'readwrite').objectStore('snapshots');
65+
};
66+
67+
ns.BackupService.prototype.createSnapshot = function (snapshot) {
68+
var objectStore = this.openObjectStore_();
69+
var request = objectStore.add(snapshot);
70+
return _requestPromise(request);
71+
};
72+
73+
ns.BackupService.prototype.replaceSnapshot = function (snapshot, replacedSnapshot) {
74+
snapshot.id = replacedSnapshot.id;
75+
76+
var objectStore = this.openObjectStore_();
77+
var request = objectStore.put(snapshot);
78+
return _requestPromise(request);
79+
};
80+
81+
ns.BackupService.prototype.deleteSnapshot = function (snapshot) {
82+
var objectStore = this.openObjectStore_();
83+
var request = objectStore.delete(snapshot.id);
84+
return _requestPromise(request);
85+
};
86+
87+
ns.BackupService.prototype.getSnapshotsBySessionId_ = function (sessionId) {
88+
// Create the backup promise.
89+
var deferred = Q.defer();
90+
91+
// Open a transaction to the snapshots object store.
92+
var objectStore = this.db.transaction(['snapshots']).objectStore('snapshots');
93+
94+
// Loop on all the saved snapshots for the provided piskel id
95+
var index = objectStore.index('session_id, date');
96+
var keyRange = IDBKeyRange.bound(
97+
[sessionId, 0],
98+
[sessionId, Infinity]
99+
);
100+
101+
var snapshots = [];
102+
// Ordered by date in descending order.
103+
index.openCursor(keyRange, 'prev').onsuccess = function(event) {
104+
var cursor = event.target.result;
105+
if (cursor) {
106+
snapshots.push(cursor.value);
107+
cursor.continue();
108+
} else {
109+
console.log('consumed all piskel snapshots');
110+
deferred.resolve(snapshots);
111+
}
112+
};
113+
114+
return deferred.promise;
115+
};
116+
22117
ns.BackupService.prototype.backup = function () {
23118
var piskel = this.piskelController.getPiskel();
24-
var descriptor = piskel.getDescriptor();
25119
var hash = piskel.getHash();
26-
var info = {
27-
name : descriptor.name,
28-
description : descriptor.info,
29-
date : Date.now(),
30-
hash : hash
31-
};
32120

33121
// Do not save an unchanged piskel
34-
if (hash !== this.lastHash) {
35-
this.lastHash = hash;
36-
var serializedPiskel = pskl.utils.serialization.Serializer.serialize(piskel);
37-
this.savePiskel_('next', serializedPiskel, JSON.stringify(info));
122+
if (hash === this.lastHash) {
123+
return;
38124
}
125+
126+
// Update the hash
127+
// TODO: should only be done after a successfull save.
128+
this.lastHash = hash;
129+
130+
// Prepare the backup snapshot.
131+
var descriptor = piskel.getDescriptor();
132+
var date = Date.now();
133+
var snapshot = {
134+
session_id: piskel.sessionId,
135+
date: date,
136+
name: descriptor.name,
137+
description: descriptor.description,
138+
serialized: pskl.utils.serialization.Serializer.serialize(piskel)
139+
};
140+
141+
this.getSnapshotsBySessionId_(piskel.sessionId).then(function (snapshots) {
142+
var latest = snapshots[0];
143+
144+
if (latest && date < this.nextSnapshotDate) {
145+
// update the latest snapshot
146+
return this.replaceSnapshot(snapshot, latest);
147+
} else {
148+
// add a new snapshot
149+
this.nextSnapshotDate = date + SNAPSHOT_INTERVAL;
150+
return this.createSnapshot(snapshot).then(function () {
151+
if (snapshots.length >= MAX_SNAPSHOTS_PER_SESSION) {
152+
// remove oldest snapshot
153+
return this.deleteSnapshot(snapshots[snapshots.length - 1]);
154+
}
155+
}.bind(this));
156+
}
157+
}.bind(this)).catch(function (e) {
158+
console.log('Backup failed');
159+
console.error(e);
160+
});
39161
};
40162

41163
ns.BackupService.prototype.getPreviousPiskelInfo = function () {
42-
var previousInfo = window.localStorage.getItem('bkp.prev.info');
43-
if (previousInfo) {
44-
return JSON.parse(previousInfo);
45-
}
46-
};
164+
// Create the backup promise.
165+
var deferred = Q.defer();
47166

48-
ns.BackupService.prototype.load = function() {
49-
var previousPiskel = window.localStorage.getItem('bkp.prev.piskel');
50-
previousPiskel = JSON.parse(previousPiskel);
167+
// Open a transaction to the snapshots object store.
168+
var objectStore = this.db.transaction(['snapshots']).objectStore('snapshots');
51169

52-
pskl.utils.serialization.Deserializer.deserialize(previousPiskel, function (piskel) {
53-
pskl.app.piskelController.setPiskel(piskel);
54-
});
170+
var sessionId = this.piskelController.getPiskel().sessionId;
171+
var index = objectStore.index('date');
172+
var range = IDBKeyRange.upperBound(Infinity);
173+
index.openCursor(range, 'prev').onsuccess = function(event) {
174+
var cursor = event.target.result;
175+
var snapshot = cursor && cursor.value;
176+
if (snapshot && snapshot.session_id === sessionId) {
177+
// Skip snapshots for the current session.
178+
cursor.continue();
179+
} else {
180+
deferred.resolve(snapshot);
181+
}
182+
};
183+
184+
return deferred.promise;
55185
};
56186

57-
ns.BackupService.prototype.savePiskel_ = function (type, piskel, info) {
58-
try {
59-
window.localStorage.setItem('bkp.' + type + '.piskel', piskel);
60-
window.localStorage.setItem('bkp.' + type + '.info', info);
61-
} catch (e) {
62-
console.error('Could not save piskel backup in localStorage.', e);
63-
}
187+
ns.BackupService.prototype.load = function() {
188+
this.getPreviousPiskelInfo().then(function (snapshot) {
189+
pskl.utils.serialization.Deserializer.deserialize(
190+
JSON.parse(snapshot.serialized),
191+
function (piskel) {
192+
pskl.app.piskelController.setPiskel(piskel);
193+
}
194+
);
195+
});
64196
};
65197
})();

0 commit comments

Comments
 (0)