Issue #640 - extract database code to dedicated package

This commit is contained in:
Julian Descottes 2017-06-13 00:10:02 +02:00
parent f9cb631acb
commit f9570ea3c5
5 changed files with 363 additions and 240 deletions

View file

@ -0,0 +1,179 @@
(function () {
var ns = $.namespace('pskl.database');
var DB_NAME = 'PiskelSessionsDatabase';
var DB_VERSION = 1;
// Simple wrapper to promisify a request.
var _requestPromise = function (req) {
var deferred = Q.defer();
req.onsuccess = deferred.resolve.bind(deferred);
req.onerror = deferred.reject.bind(deferred);
return deferred.promise;
};
/**
* The BackupDatabase handles all the database interactions related
* to piskel snapshots continuously saved while during the usage of
* Piskel.
*
* @param {Object} options
* - onUpgrade {Function} optional callback called when a DB
* upgrade is performed.
*/
ns.BackupDatabase = function (options) {
options = options || {};
this.db = null;
this.onUpgrade = options.onUpgrade;
};
/**
* Open and initialize the database.
*/
ns.BackupDatabase.prototype.init = function () {
this.initDeferred_ = Q.defer();
var request = window.indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = this.onRequestError_.bind(this);
request.onsuccess = this.onRequestSuccess_.bind(this);
request.onupgradeneeded = this.onUpgradeNeeded_.bind(this);
return this.initDeferred_.promise;
};
ns.BackupDatabase.prototype.onRequestError_ = function (event) {
console.log('Could not initialize the piskel backup database');
this.initDeferred_.reject();
};
ns.BackupDatabase.prototype.onRequestSuccess_ = function (event) {
this.db = event.target.result;
this.initDeferred_.resolve(this.db);
};
ns.BackupDatabase.prototype.onUpgradeNeeded_ = function (event) {
// Set this.db early to allow migration scripts to access it in oncomplete.
this.db = event.target.result;
// Create an object store "piskels" with the autoIncrement flag set as true.
var objectStore = this.db.createObjectStore('snapshots', { keyPath: 'id', autoIncrement : true });
objectStore.createIndex('session_id', 'session_id', { unique: false });
objectStore.createIndex('date', 'date', { unique: false });
objectStore.createIndex('session_id, date', ['session_id', 'date'], { unique: false });
objectStore.transaction.oncomplete = function(event) {
if (typeof this.onUpgrade == 'function') {
this.onUpgrade(this.db);
}
}.bind(this);
};
ns.BackupDatabase.prototype.openObjectStore_ = function () {
return this.db.transaction(['snapshots'], 'readwrite').objectStore('snapshots');
};
/**
* Send an add request for the provided snapshot.
* Returns a promise that resolves the request event.
*/
ns.BackupDatabase.prototype.createSnapshot = function (snapshot) {
var objectStore = this.openObjectStore_();
var request = objectStore.add(snapshot);
return _requestPromise(request);
};
/**
* Send a put request for the provided snapshot.
* Returns a promise that resolves the request event.
*/
ns.BackupDatabase.prototype.updateSnapshot = function (snapshot) {
var objectStore = this.openObjectStore_();
var request = objectStore.put(snapshot);
return _requestPromise(request);
};
/**
* Send a delete request for the provided snapshot.
* Returns a promise that resolves the request event.
*/
ns.BackupDatabase.prototype.deleteSnapshot = function (snapshot) {
var objectStore = this.openObjectStore_();
var request = objectStore.delete(snapshot.id);
return _requestPromise(request);
};
/**
* Get the last (most recent) snapshot that satisfies the accept filter provided.
* Returns a promise that will resolve with the first matching snapshot (or null
* if no valid snapshot is found).
*
* @param {Function} accept:
* Filter method that takes a snapshot as argument and should return true
* if the snapshot is valid.
*/
ns.BackupDatabase.prototype.findLastSnapshot = function (accept) {
// Create the backup promise.
var deferred = Q.defer();
// Open a transaction to the snapshots object store.
var objectStore = this.db.transaction(['snapshots']).objectStore('snapshots');
var index = objectStore.index('date');
var range = IDBKeyRange.upperBound(Infinity);
index.openCursor(range, 'prev').onsuccess = function(event) {
var cursor = event.target.result;
var snapshot = cursor && cursor.value;
// Resolve null if we couldn't find a matching snapshot.
if (!snapshot) {
deferred.resolve(null);
} else if (accept(snapshot)) {
deferred.resolve(snapshot);
} else {
cursor.continue();
}
};
return deferred.promise;
};
/**
* Retrieve all the snapshots for a given session id, sorted by descending date order.
* Returns a promise that resolves with an array of snapshots.
*
* @param {String} sessionId
* The session id
*/
ns.BackupDatabase.prototype.getSnapshotsBySessionId = function (sessionId) {
// Create the backup promise.
var deferred = Q.defer();
// Open a transaction to the snapshots object store.
var objectStore = this.db.transaction(['snapshots']).objectStore('snapshots');
// Loop on all the saved snapshots for the provided piskel id
var index = objectStore.index('session_id, date');
var keyRange = IDBKeyRange.bound(
[sessionId, 0],
[sessionId, Infinity]
);
var snapshots = [];
// Ordered by date in descending order.
index.openCursor(keyRange, 'prev').onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
snapshots.push(cursor.value);
cursor.continue();
} else {
console.log('consumed all piskel snapshots');
deferred.resolve(snapshots);
}
};
return deferred.promise;
};
})();

View file

@ -0,0 +1,157 @@
(function () {
var ns = $.namespace('pskl.database');
var DB_NAME = 'PiskelDatabase';
var DB_VERSION = 1;
// Simple wrapper to promisify a request.
var _requestPromise = function (req) {
var deferred = Q.defer();
req.onsuccess = deferred.resolve.bind(deferred);
req.onerror = deferred.reject.bind(deferred);
return deferred.promise;
};
/**
* The PiskelDatabase handles all the database interactions related
* to the local piskel saved that can be performed in-browser.
*
* @param {Object} options
* - onUpgrade {Function} optional callback called when a DB
* upgrade is performed.
*/
ns.PiskelDatabase = function (options) {
options = options || {};
this.db = null;
this.onUpgrade = options.onUpgrade;
};
ns.PiskelDatabase.prototype.init = function () {
this.initDeferred_ = Q.defer();
var request = window.indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = this.onRequestError_.bind(this);
request.onsuccess = this.onRequestSuccess_.bind(this);
request.onupgradeneeded = this.onUpgradeNeeded_.bind(this);
return this.initDeferred_.promise;
};
ns.PiskelDatabase.prototype.onRequestError_ = function (event) {
console.log('Failed to initialize IndexedDB, local browser saves will be unavailable.');
this.initDeferred_.reject();
};
ns.PiskelDatabase.prototype.onRequestSuccess_ = function (event) {
console.log('Successfully initialized IndexedDB, local browser saves are available.');
this.db = event.target.result;
this.initDeferred_.resolve(this.db);
};
ns.PiskelDatabase.prototype.onUpgradeNeeded_ = function (event) {
// Set this.db early to allow migration scripts to access it in oncomplete.
this.db = event.target.result;
// Create an object store "piskels" with the autoIncrement flag set as true.
var objectStore = this.db.createObjectStore('piskels', { keyPath : 'name' });
objectStore.transaction.oncomplete = function(event) {
if (typeof this.onUpgrade == 'function') {
this.onUpgrade(this.db);
}
}.bind(this);
};
ns.PiskelDatabase.prototype.openObjectStore_ = function () {
return this.db.transaction(['piskels'], 'readwrite').objectStore('piskels');
};
/**
* Send a get request for the provided name.
* Returns a promise that resolves the request event.
*/
ns.PiskelDatabase.prototype.get = function (name) {
var objectStore = this.openObjectStore_();
return _requestPromise(objectStore.get(name));
};
/**
* List all locally saved piskels.
* Returns a promise that resolves an array of objects:
* - name: name of the piskel
* - description: description of the piskel
* - date: save date
*
* The sprite content is not contained in the object and
* needs to be retrieved with a separate get.
*/
ns.PiskelDatabase.prototype.list = function () {
var deferred = Q.defer();
var piskels = [];
var objectStore = this.openObjectStore_();
var cursor = objectStore.openCursor();
cursor.onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
piskels.push({
name: cursor.value.name,
date: cursor.value.date,
description: cursor.value.description
});
cursor.continue();
} else {
// Cursor consumed all availabled piskels
deferred.resolve(piskels);
}
};
cursor.onerror = function () {
deferred.reject();
};
return deferred.promise;
};
/**
* Send an put request for the provided args.
* Returns a promise that resolves the request event.
*/
ns.PiskelDatabase.prototype.update = function (name, description, date, serialized) {
var data = {};
data.name = name;
data.serialized = serialized;
data.date = date;
data.description = description;
var objectStore = this.openObjectStore_();
return _requestPromise(objectStore.put(data));
};
/**
* Send an add request for the provided args.
* Returns a promise that resolves the request event.
*/
ns.PiskelDatabase.prototype.create = function (name, description, date, serialized) {
var data = {};
data.name = name;
data.serialized = serialized;
data.date = date;
data.description = description;
var objectStore = this.openObjectStore_();
return _requestPromise(objectStore.add(data));
};
/**
* Delete a saved piskel for the provided name.
* Returns a promise that resolves the request event.
*/
ns.PiskelDatabase.prototype.delete = function (name) {
var objectStore = this.openObjectStore_();
return _requestPromise(objectStore.delete(name));
};
})();

View file

@ -1,9 +1,6 @@
(function () {
var ns = $.namespace('pskl.service');
var DB_NAME = 'PiskelSessionsDatabase';
var DB_VERSION = 1;
var ONE_SECOND = 1000;
var ONE_MINUTE = 60 * ONE_SECOND;
@ -14,104 +11,18 @@
// Store up to 12 snapshots for a piskel session, min. 1 hour of work
var MAX_SNAPSHOTS_PER_SESSION = 12;
var _requestPromise = function (req) {
var deferred = Q.defer();
req.onsuccess = deferred.resolve.bind(deferred);
req.onerror = deferred.reject.bind(deferred);
return deferred.promise;
};
ns.BackupService = function (piskelController) {
this.piskelController = piskelController;
this.lastHash = null;
this.nextSnapshotDate = -1;
this.backupDatabase = new pskl.database.BackupDatabase();
};
ns.BackupService.prototype.init = function () {
var request = window.indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = this.onRequestError_.bind(this);
request.onupgradeneeded = this.onUpgradeNeeded_.bind(this);
request.onsuccess = this.onRequestSuccess_.bind(this);
};
ns.BackupService.prototype.onRequestError_ = function (event) {
console.log('Could not initialize the piskel backup database');
};
ns.BackupService.prototype.onUpgradeNeeded_ = function (event) {
// Set this.db early to allow migration scripts to access it in oncomplete.
this.db = event.target.result;
// Create an object store "piskels" with the autoIncrement flag set as true.
var objectStore = this.db.createObjectStore('snapshots', { keyPath: 'id', autoIncrement : true });
objectStore.createIndex('session_id', 'session_id', { unique: false });
objectStore.createIndex('date', 'date', { unique: false });
objectStore.createIndex('session_id, date', ['session_id', 'date'], { unique: false });
objectStore.transaction.oncomplete = function(event) {
// TODO: Migrate existing data from local storage?
};
};
ns.BackupService.prototype.onRequestSuccess_ = function (event) {
this.db = event.target.result;
window.setInterval(this.backup.bind(this), BACKUP_INTERVAL);
};
ns.BackupService.prototype.openObjectStore_ = function () {
return this.db.transaction(['snapshots'], 'readwrite').objectStore('snapshots');
};
ns.BackupService.prototype.createSnapshot = function (snapshot) {
var objectStore = this.openObjectStore_();
var request = objectStore.add(snapshot);
return _requestPromise(request);
};
ns.BackupService.prototype.replaceSnapshot = function (snapshot, replacedSnapshot) {
snapshot.id = replacedSnapshot.id;
var objectStore = this.openObjectStore_();
var request = objectStore.put(snapshot);
return _requestPromise(request);
};
ns.BackupService.prototype.deleteSnapshot = function (snapshot) {
var objectStore = this.openObjectStore_();
var request = objectStore.delete(snapshot.id);
return _requestPromise(request);
};
ns.BackupService.prototype.getSnapshotsBySessionId_ = function (sessionId) {
// Create the backup promise.
var deferred = Q.defer();
// Open a transaction to the snapshots object store.
var objectStore = this.db.transaction(['snapshots']).objectStore('snapshots');
// Loop on all the saved snapshots for the provided piskel id
var index = objectStore.index('session_id, date');
var keyRange = IDBKeyRange.bound(
[sessionId, 0],
[sessionId, Infinity]
);
var snapshots = [];
// Ordered by date in descending order.
index.openCursor(keyRange, 'prev').onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
snapshots.push(cursor.value);
cursor.continue();
} else {
console.log('consumed all piskel snapshots');
deferred.resolve(snapshots);
}
};
return deferred.promise;
this.backupDatabase.init().then(function () {
window.setInterval(this.backup.bind(this), BACKUP_INTERVAL);
}.bind(this));
};
ns.BackupService.prototype.backup = function () {
@ -138,19 +49,20 @@
serialized: pskl.utils.serialization.Serializer.serialize(piskel)
};
this.getSnapshotsBySessionId_(piskel.sessionId).then(function (snapshots) {
this.backupDatabase.getSnapshotsBySessionId(piskel.sessionId).then(function (snapshots) {
var latest = snapshots[0];
if (latest && date < this.nextSnapshotDate) {
// update the latest snapshot
return this.replaceSnapshot(snapshot, latest);
snapshot.id = latest.id;
return this.backupDatabase.updateSnapshot(snapshot);
} else {
// add a new snapshot
this.nextSnapshotDate = date + SNAPSHOT_INTERVAL;
return this.createSnapshot(snapshot).then(function () {
return this.backupDatabase.createSnapshot(snapshot).then(function () {
if (snapshots.length >= MAX_SNAPSHOTS_PER_SESSION) {
// remove oldest snapshot
return this.deleteSnapshot(snapshots[snapshots.length - 1]);
return this.backupDatabase.deleteSnapshot(snapshots[snapshots.length - 1]);
}
}.bind(this));
}
@ -161,27 +73,10 @@
};
ns.BackupService.prototype.getPreviousPiskelInfo = function () {
// Create the backup promise.
var deferred = Q.defer();
// Open a transaction to the snapshots object store.
var objectStore = this.db.transaction(['snapshots']).objectStore('snapshots');
var sessionId = this.piskelController.getPiskel().sessionId;
var index = objectStore.index('date');
var range = IDBKeyRange.upperBound(Infinity);
index.openCursor(range, 'prev').onsuccess = function(event) {
var cursor = event.target.result;
var snapshot = cursor && cursor.value;
if (snapshot && snapshot.session_id === sessionId) {
// Skip snapshots for the current session.
cursor.continue();
} else {
deferred.resolve(snapshot);
}
};
return deferred.promise;
return this.backupDatabase.findLastSnapshot(function (snapshot) {
return snapshot.session_id !== sessionId;
});
};
ns.BackupService.prototype.load = function() {

View file

@ -5,14 +5,11 @@
ns.IndexedDbStorageService = function (piskelController) {
this.piskelController = piskelController;
this.piskelDatabase = new pskl.database.PiskelDatabase();
};
ns.IndexedDbStorageService.prototype.init = function () {
var request = window.indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = this.onRequestError_.bind(this);
request.onupgradeneeded = this.onUpgradeNeeded_.bind(this);
request.onsuccess = this.onRequestSuccess_.bind(this);
this.piskelDatabase.init();
};
ns.IndexedDbStorageService.prototype.save = function (piskel) {
@ -25,61 +22,18 @@
};
ns.IndexedDbStorageService.prototype.save_ = function (name, description, date, serialized) {
var deferred = Q.defer();
var objectStore = this.db.transaction(['piskels'], 'readwrite').objectStore('piskels');
var getRequest = objectStore.get(name);
getRequest.onsuccess = function (event) {
console.log('get request successful for name: ' + name);
return this.piskelDatabase.get(name).then(function (event) {
var data = event.target.result;
if (typeof data !== 'undefined') {
data.serialized = serialized;
data.date = date;
data.description = description;
var putRequest = objectStore.put(data);
putRequest.onerror = function(event) {
console.log('put request failed for name: ' + name);
deferred.reject();
};
putRequest.onsuccess = function(event) {
console.log('put request successful for name: ' + name);
deferred.resolve();
};
return this.piskelDatabase.update(name, description, date, serialized);
} else {
var request = objectStore.add({
name: name,
description: description,
serialized: serialized,
date: date
});
request.onerror = function(event) {
console.log('Failed to save a piskel');
deferred.reject();
};
request.onsuccess = function(event) {
console.log('Successfully saved a piskel');
deferred.resolve();
};
return this.piskelDatabase.create(name, description, date, serialized);
}
};
getRequest.onerror = function () {
console.log('get request failed for name: ' + name);
deferred.reject();
};
return deferred.promise;
}.bind(this));
};
ns.IndexedDbStorageService.prototype.load = function (name) {
var objectStore = this.db.transaction(['piskels'], 'readwrite').objectStore('piskels');
var getRequest = objectStore.get(name);
getRequest.onsuccess = function (event) {
console.log('get request successful for name: ' + name);
this.piskelDatabase.get(name).then(function (event) {
var data = event.target.result;
if (typeof data !== 'undefined') {
var serialized = data.serialized;
@ -92,80 +46,14 @@
} else {
console.log('no local browser save found for name: ' + name);
}
};
getRequest.onerror = function () {
console.log('get request failed for name: ' + name);
};
});
};
ns.IndexedDbStorageService.prototype.remove = function (name) {
var objectStore = this.db.transaction(['piskels'], 'readwrite').objectStore('piskels');
var deleteRequest = objectStore.delete(name);
deleteRequest.onsuccess = function (event) {
console.log('successfully deleted local browser save for name: ' + name);
};
deleteRequest.onerror = function (event) {
console.log('failed to delete local browser save for name: ' + name);
};
};
ns.IndexedDbStorageService.prototype.list = function () {
var deferred = Q.defer();
var piskels = [];
var objectStore = this.db.transaction(['piskels']).objectStore('piskels');
var cursor = objectStore.openCursor();
cursor.onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
piskels.push({
name: cursor.value.name,
date: cursor.value.date,
description: cursor.value.description
});
cursor.continue();
} else {
console.log('Cursor consumed all availabled piskels');
deferred.resolve(piskels);
}
};
cursor.onerror = function () {
deferred.reject();
};
return deferred.promise;
this.piskelDatabase.delete(name);
};
ns.IndexedDbStorageService.prototype.getKeys = function () {
return this.list();
};
ns.IndexedDbStorageService.prototype.onRequestError_ = function (event) {
console.log('Failed to initialize IndexedDB, local browser saves will be unavailable.');
};
ns.IndexedDbStorageService.prototype.onRequestSuccess_ = function (event) {
this.db = event.target.result;
console.log('Successfully initialized IndexedDB, local browser saves are available.');
};
ns.IndexedDbStorageService.prototype.onUpgradeNeeded_ = function (event) {
// Set this.db early to allow migration scripts to access it in oncomplete.
this.db = event.target.result;
// Create an object store "piskels" with the autoIncrement flag set as true.
var objectStore = this.db.createObjectStore('piskels', { keyPath : 'name' });
objectStore.transaction.oncomplete = function(event) {
// Migrate existing sprites from LocalStorage
pskl.service.storage.migrate.MigrateLocalStorageToIndexedDb.migrate().then(function () {
console.log('Successfully migrated local storage data to indexed db');
}).catch(function (e) {
console.log('Failed to migrate local storage data to indexed db');
console.error(e);
});
};
return this.piskelDatabase.list();
};
})();

View file

@ -79,6 +79,10 @@
"js/model/Palette.js",
"js/model/Piskel.js",
// Database (IndexedDB)
"js/database/BackupDatabase.js",
"js/database/PiskelDatabase.js",
// Selection
"js/selection/SelectionManager.js",
"js/selection/BaseSelection.js",