Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
386
gaia/apps/calendar/js/backend/store/abstract.js
Executable file
386
gaia/apps/calendar/js/backend/store/abstract.js
Executable file
|
|
@ -0,0 +1,386 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Responder = require('common/responder');
|
||||
var core = require('core');
|
||||
var denodeifyAll = require('common/promise').denodeifyAll;
|
||||
var nextTick = require('common/next_tick');
|
||||
|
||||
/**
|
||||
* Creates an abstract store instance.
|
||||
* Every store must contain a reference
|
||||
* to the database.
|
||||
*/
|
||||
function Abstract() {
|
||||
this._cached = Object.create(null);
|
||||
Responder.call(this);
|
||||
|
||||
denodeifyAll(this, [
|
||||
'persist',
|
||||
'all',
|
||||
'_allCached',
|
||||
'removeByIndex',
|
||||
'get',
|
||||
'remove',
|
||||
'count'
|
||||
]);
|
||||
}
|
||||
module.exports = Abstract;
|
||||
|
||||
Abstract.prototype = {
|
||||
__proto__: Responder.prototype,
|
||||
|
||||
_store: null,
|
||||
|
||||
/**
|
||||
* Stores that will need to be removed
|
||||
* when a record is removed from this store.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
_dependentStores: null,
|
||||
|
||||
_createModel: function(object, id) {
|
||||
if (typeof(id) !== 'undefined') {
|
||||
object._id = id;
|
||||
}
|
||||
|
||||
return object;
|
||||
},
|
||||
|
||||
_addToCache: function(object) {
|
||||
this._cached[object._id] = object;
|
||||
},
|
||||
|
||||
_clearCache: function() {
|
||||
this._cached = Object.create(null);
|
||||
},
|
||||
|
||||
_removeFromCache: function(id) {
|
||||
if (id in this._cached) {
|
||||
delete this._cached[id];
|
||||
}
|
||||
},
|
||||
|
||||
_transactionCallback: function(trans, callback) {
|
||||
if (callback) {
|
||||
trans.addEventListener('error', function(e) {
|
||||
callback(e);
|
||||
});
|
||||
|
||||
trans.addEventListener('complete', function() {
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds an account to the database.
|
||||
*
|
||||
* @param {Object} object reference to account object to store.
|
||||
* @param {IDBTransaction} trans transaction.
|
||||
* @param {Function} callback node style callback.
|
||||
*/
|
||||
persist: function(object, trans, callback) {
|
||||
if (typeof(trans) === 'function') {
|
||||
callback = trans;
|
||||
trans = undefined;
|
||||
}
|
||||
|
||||
if (!trans) {
|
||||
trans = core.db.transaction(
|
||||
this._dependentStores || this._store,
|
||||
'readwrite'
|
||||
);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var store = trans.objectStore(this._store);
|
||||
var data = this._objectData(object);
|
||||
var id;
|
||||
var model;
|
||||
|
||||
var putReq;
|
||||
var reqType = this._detectPersistType(object);
|
||||
|
||||
trans.addEventListener('error', function(event) {
|
||||
if (callback) {
|
||||
callback(event);
|
||||
}
|
||||
});
|
||||
|
||||
// determine type of event
|
||||
if (reqType === 'update') {
|
||||
putReq = store.put(data);
|
||||
} else {
|
||||
this._assignId(data);
|
||||
putReq = store.add(data);
|
||||
this._addDependents(object, trans);
|
||||
}
|
||||
|
||||
// when we have the id we can add the model to the cache.
|
||||
if (data._id) {
|
||||
id = data._id;
|
||||
model = self._createModel(object, id);
|
||||
self._addToCache(model);
|
||||
}
|
||||
|
||||
trans.addEventListener('complete', function(data) {
|
||||
if (!model) {
|
||||
id = putReq.result;
|
||||
model = self._createModel(object, id);
|
||||
self._addToCache(model);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(null, id, model);
|
||||
}
|
||||
|
||||
self.emit(reqType, id, model);
|
||||
self.emit('persist', id, model);
|
||||
});
|
||||
},
|
||||
|
||||
_allCached: function(callback) {
|
||||
var list = this._cached;
|
||||
nextTick(function() {
|
||||
callback(null, list);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads all records in the database
|
||||
* for this store.
|
||||
*
|
||||
* Using this function will fill
|
||||
* the cache with all records in the store.
|
||||
* As such this should only be used once
|
||||
* during the app life-cycle.
|
||||
*/
|
||||
all: function(callback) {
|
||||
if (this._allCallbacks) {
|
||||
this._allCallbacks.push(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
// list of pending callbacks
|
||||
this._allCallbacks = [callback];
|
||||
|
||||
var self = this;
|
||||
var trans = core.db.transaction(this._store);
|
||||
var store = trans.objectStore(this._store);
|
||||
|
||||
function process(data) {
|
||||
return self._addToCache(self._createModel(data));
|
||||
}
|
||||
|
||||
function fireQueue(err, value) {
|
||||
var callback;
|
||||
while ((callback = self._allCallbacks.shift())) {
|
||||
callback(err, value);
|
||||
}
|
||||
}
|
||||
|
||||
store.mozGetAll().onsuccess = function(event) {
|
||||
event.target.result.forEach(process);
|
||||
};
|
||||
|
||||
trans.onerror = function(event) {
|
||||
fireQueue(event.target.error.name);
|
||||
};
|
||||
|
||||
trans.oncomplete = function() {
|
||||
fireQueue(null, self._cached);
|
||||
self.all = self._allCached;
|
||||
};
|
||||
},
|
||||
|
||||
_addDependents: function() {},
|
||||
_removeDependents: function(trans) {},
|
||||
|
||||
_detectPersistType: function(object) {
|
||||
return ('_id' in object) ? 'update' : 'add';
|
||||
},
|
||||
|
||||
_parseId: function(id) {
|
||||
return id;
|
||||
},
|
||||
|
||||
_assignId: function(obj) {
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes all records a index value and removes
|
||||
* them from the cache. 'remove' events are *not* emitted
|
||||
* when removing in this manner for performance reasons.
|
||||
*
|
||||
* TODO: the test for this method still lives in the event store tests
|
||||
* where this code began should refactor those tests to be general
|
||||
* and live in the abstract tests.
|
||||
*
|
||||
* @param {String} indexName name of store index.
|
||||
* @param {Numeric} indexValue value in index.
|
||||
* @param {IDBTransation} [trans] optional transaction to reuse.
|
||||
* @param {Function} [callback] optional callback to use.
|
||||
* When called without a transaction chances
|
||||
* are you should pass a callback.
|
||||
*/
|
||||
removeByIndex: function(indexName, indexValue, trans, callback) {
|
||||
var self = this;
|
||||
if (typeof(trans) === 'function') {
|
||||
callback = trans;
|
||||
trans = undefined;
|
||||
}
|
||||
|
||||
if (!trans) {
|
||||
trans = core.db.transaction(
|
||||
this._dependentStores || this._store,
|
||||
'readwrite'
|
||||
);
|
||||
}
|
||||
if (callback) {
|
||||
|
||||
trans.addEventListener('complete', function() {
|
||||
callback(null);
|
||||
});
|
||||
|
||||
trans.addEventListener('error', function(event) {
|
||||
callback(event);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var index = trans.objectStore(this._store).index(indexName);
|
||||
var req = index.openCursor(
|
||||
IDBKeyRange.only(indexValue)
|
||||
);
|
||||
|
||||
req.onsuccess = function(event) {
|
||||
var cursor = event.target.result;
|
||||
if (cursor) {
|
||||
// remove deps first intentionally to mimic, removes normal behaviour
|
||||
self._removeDependents(cursor.primaryKey, trans);
|
||||
self._removeFromCache(cursor.primaryKey);
|
||||
cursor.delete();
|
||||
cursor.continue();
|
||||
}
|
||||
};
|
||||
|
||||
return req;
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds a single record.
|
||||
*
|
||||
* Does not go through any cache or emit any events.
|
||||
*
|
||||
* @param {String} id id of record.
|
||||
* @param {IDBTransaction} [trans] optional transaction.
|
||||
* @param {Function} callback node style [err, record].
|
||||
*/
|
||||
get: function(id, trans, callback) {
|
||||
var self = this;
|
||||
|
||||
if (typeof(trans) === 'function') {
|
||||
callback = trans;
|
||||
trans = null;
|
||||
}
|
||||
|
||||
if (!trans) {
|
||||
trans = core.db.transaction(this._store);
|
||||
}
|
||||
|
||||
var store = trans.objectStore(this._store);
|
||||
var req = store.get(this._parseId(id));
|
||||
|
||||
req.onsuccess = function() {
|
||||
var model;
|
||||
|
||||
if (req.result) {
|
||||
model = self._createModel(req.result);
|
||||
}
|
||||
|
||||
callback(null, model);
|
||||
};
|
||||
|
||||
req.onerror = function(event) {
|
||||
callback(event);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a object from the store.
|
||||
*
|
||||
* @param {String} id record reference.
|
||||
* @param {IDBTransaction} trans transaction.
|
||||
* @param {Function} callback node style callback.
|
||||
*/
|
||||
remove: function(id, trans, callback) {
|
||||
if (typeof(trans) === 'function') {
|
||||
callback = trans;
|
||||
trans = undefined;
|
||||
}
|
||||
|
||||
if (!trans) {
|
||||
trans = core.db.transaction(
|
||||
this._dependentStores || this._store,
|
||||
'readwrite'
|
||||
);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var store = trans.objectStore(this._store);
|
||||
id = this._parseId(id);
|
||||
|
||||
store.delete(id);
|
||||
|
||||
this._removeDependents(id, trans);
|
||||
self.emit('preRemove', id);
|
||||
|
||||
trans.addEventListener('error', function(event) {
|
||||
if (callback) {
|
||||
callback(event);
|
||||
}
|
||||
});
|
||||
|
||||
trans.addEventListener('complete', function() {
|
||||
if (callback) {
|
||||
callback(null, id);
|
||||
}
|
||||
|
||||
self.emit('remove', id);
|
||||
|
||||
// intentionally after the callbacks...
|
||||
self._removeFromCache(id);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Find number of records in store.
|
||||
*
|
||||
* @param {Function} callback node style err/count.
|
||||
*/
|
||||
count: function(callback) {
|
||||
var trans = core.db.transaction(this._store);
|
||||
var store = trans.objectStore(this._store);
|
||||
|
||||
var req = store.count();
|
||||
|
||||
req.onsuccess = function() {
|
||||
callback(null, req.result);
|
||||
};
|
||||
|
||||
req.onerror = function(e) {
|
||||
callback(e);
|
||||
};
|
||||
},
|
||||
|
||||
_objectData: function(data) {
|
||||
if ('toJSON' in data) {
|
||||
return data.toJSON();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
400
gaia/apps/calendar/js/backend/store/account.js
Executable file
400
gaia/apps/calendar/js/backend/store/account.js
Executable file
|
|
@ -0,0 +1,400 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var AccountModel = require('models/account');
|
||||
var Abstract = require('./abstract');
|
||||
var core = require('core');
|
||||
var debug = require('common/debug')('store/account');
|
||||
var denodeifyAll = require('common/promise').denodeifyAll;
|
||||
var extend = require('common/extend');
|
||||
var nextTick = require('common/next_tick');
|
||||
var probablyParseInt = require('common/probably_parse_int');
|
||||
|
||||
function Account() {
|
||||
Abstract.apply(this, arguments);
|
||||
|
||||
denodeifyAll(this, [
|
||||
'verifyAndPersist',
|
||||
'sync',
|
||||
'markWithError',
|
||||
'syncableAccounts',
|
||||
'availablePresets'
|
||||
]);
|
||||
}
|
||||
module.exports = Account;
|
||||
|
||||
Account.prototype = {
|
||||
__proto__: Abstract.prototype,
|
||||
|
||||
_store: 'accounts',
|
||||
|
||||
_parseId: probablyParseInt,
|
||||
|
||||
/**
|
||||
* Checks if a given account is a duplicate of another.
|
||||
*
|
||||
* @param {Calendar.Model.Account} model to check.
|
||||
* @param {Function} callback [err].
|
||||
*/
|
||||
_validateModel: function(model, callback) {
|
||||
this.all(function(err, allAccounts) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if this account is already registered
|
||||
for (var index in allAccounts) {
|
||||
if (
|
||||
allAccounts[index].user === model.user &&
|
||||
allAccounts[index].fullUrl === model.fullUrl &&
|
||||
allAccounts[index]._id !== model._id
|
||||
) {
|
||||
|
||||
var dupErr = new Error(
|
||||
'Cannot add two accounts with the same url / entry point'
|
||||
);
|
||||
|
||||
dupErr.name = 'account-exist';
|
||||
callback(dupErr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
verifyAndPersist: function(model, callback) {
|
||||
var self = this;
|
||||
var provider = core.providerFactory.get(
|
||||
model.providerType
|
||||
);
|
||||
|
||||
provider.getAccount(model.toJSON(), function(err, data) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
model.error = undefined;
|
||||
|
||||
// if this works we always will get a calendar home.
|
||||
// This is used to find calendars.
|
||||
model.calendarHome = data.calendarHome;
|
||||
|
||||
// server may override properties on demand.
|
||||
extend(model, data);
|
||||
|
||||
self._validateModel(model, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.persist(model, callback);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Because this is a top-level store
|
||||
* when we remove an account all records
|
||||
* related to it must be removed.
|
||||
*/
|
||||
_dependentStores: [
|
||||
'accounts', 'calendars', 'events',
|
||||
'busytimes', 'alarms', 'icalComponents'
|
||||
],
|
||||
|
||||
_removeDependents: function(id, trans) {
|
||||
var store = core.storeFactory.get('Calendar');
|
||||
store.remotesByAccount(id, trans, function(err, related) {
|
||||
if (err) {
|
||||
return console.error('Error removing deps for account: ', id);
|
||||
}
|
||||
|
||||
var key;
|
||||
for (key in related) {
|
||||
store.remove(related[key]._id, trans);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Syncs all calendars for account.
|
||||
*
|
||||
* TODO: Deprecate this method in favor of new provider API's.
|
||||
*
|
||||
* @param {Calendar.Models.Account} account sync target.
|
||||
* @param {Function} callback node style.
|
||||
*/
|
||||
sync: function(account, callback) {
|
||||
//TODO: We need to block removal when syncing
|
||||
//OR after removal ensure everything created here
|
||||
//is purged.
|
||||
|
||||
var self = this;
|
||||
var provider = core.providerFactory.get(account.providerType);
|
||||
var calendarStore = core.storeFactory.get('Calendar');
|
||||
|
||||
var persist = [];
|
||||
|
||||
// remotesByAccount return an object indexed by remote ids
|
||||
var calendars;
|
||||
|
||||
// these are remote ids not local ones
|
||||
var originalIds;
|
||||
|
||||
function fetchExistingCalendars(err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
calendars = results;
|
||||
originalIds = Object.keys(calendars);
|
||||
|
||||
provider.findCalendars(account, persistCalendars);
|
||||
}
|
||||
|
||||
function persistCalendars(err, remoteCals) {
|
||||
var key;
|
||||
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
for (key in remoteCals) {
|
||||
if (remoteCals.hasOwnProperty(key)) {
|
||||
var cal = remoteCals[key];
|
||||
var idx = originalIds.indexOf(key);
|
||||
|
||||
if (idx !== -1) {
|
||||
// update an existing calendar
|
||||
originalIds.splice(idx, 1);
|
||||
|
||||
var original = calendars[key];
|
||||
original.remote = cal;
|
||||
original.error = undefined;
|
||||
persist.push(original);
|
||||
} else {
|
||||
// create a new calendar
|
||||
persist.push(
|
||||
calendarStore._createModel({
|
||||
remote: new Object(cal),
|
||||
accountId: account._id
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// at this point whatever is left in originalIds
|
||||
// is considered a removed calendar.
|
||||
|
||||
// update / remove
|
||||
if (persist.length || originalIds.length) {
|
||||
var trans = core.db.transaction(
|
||||
self._dependentStores,
|
||||
'readwrite'
|
||||
);
|
||||
|
||||
originalIds.forEach(function(id) {
|
||||
calendarStore.remove(calendars[id]._id, trans);
|
||||
});
|
||||
|
||||
persist.forEach(function(object) {
|
||||
calendarStore.persist(object, trans);
|
||||
});
|
||||
|
||||
// event listeners must come at the end
|
||||
// because persist/remove also listen to
|
||||
// transaction complete events.
|
||||
trans.addEventListener('error', function(err) {
|
||||
callback(err);
|
||||
});
|
||||
|
||||
trans.addEventListener('complete', function() {
|
||||
callback(null);
|
||||
});
|
||||
} else {
|
||||
// invoke callback nothing to sync
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
|
||||
calendarStore.remotesByAccount(
|
||||
account._id,
|
||||
fetchExistingCalendars
|
||||
);
|
||||
},
|
||||
|
||||
_createModel: function(obj, id) {
|
||||
if (!(obj instanceof AccountModel)) {
|
||||
obj = new AccountModel(obj);
|
||||
}
|
||||
|
||||
if (typeof(id) !== 'undefined') {
|
||||
obj._id = id;
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
/**
|
||||
* Marks given model with an error and sends an error event with the given
|
||||
* model
|
||||
*
|
||||
* This will trigger an 'error' event immediately with the given model.
|
||||
* The callback fires _after_ the event. Its entirely possible (under rare
|
||||
* conditions) that this operation will fail but the event will fire.
|
||||
*
|
||||
*
|
||||
* @param {Object} account model.
|
||||
* @param {Calendar.Error} error to mark model with.
|
||||
* @param {IDBTransaction} [trans] optional transaction.
|
||||
* @param {Function} [callback] optional called with [err, model].
|
||||
*/
|
||||
markWithError: function(account, error, trans, callback) {
|
||||
if (typeof(trans) === 'function') {
|
||||
callback = trans;
|
||||
trans = null;
|
||||
}
|
||||
|
||||
if (!account._id) {
|
||||
throw new Error('given account must be persisted');
|
||||
}
|
||||
|
||||
if (!account.error) {
|
||||
account.error = {
|
||||
name: error.name,
|
||||
date: new Date(),
|
||||
count: 0
|
||||
};
|
||||
}
|
||||
|
||||
// increment the error count
|
||||
account.error.count++;
|
||||
|
||||
var calendarStore = core.storeFactory.get('Calendar');
|
||||
var self = this;
|
||||
function fetchedCalendars(err, calendars) {
|
||||
if (!trans) {
|
||||
trans = core.db.transaction(
|
||||
self._dependentStores,
|
||||
'readwrite'
|
||||
);
|
||||
}
|
||||
|
||||
if (err) {
|
||||
console.error('Cannot fetch all calendars', err);
|
||||
return self.persist(account, trans, callback);
|
||||
}
|
||||
|
||||
for (var id in calendars) {
|
||||
calendarStore.markWithError(calendars[id], error, trans);
|
||||
}
|
||||
|
||||
self.persist(account, trans);
|
||||
self._transactionCallback(trans, callback);
|
||||
|
||||
}
|
||||
|
||||
// find related calendars and mark those too
|
||||
calendarStore.remotesByAccount(
|
||||
account._id,
|
||||
fetchedCalendars
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds and returns all accounts that can sync (based on their provider).
|
||||
*
|
||||
* accountStore.syncableAccounts(function(err, list) {
|
||||
* if (list.length === 0)
|
||||
* // hide sync options
|
||||
* });
|
||||
*
|
||||
* @param {Function} callback [Error err, Array accountList].
|
||||
*/
|
||||
syncableAccounts: function(callback) {
|
||||
debug('Will find syncable accounts...');
|
||||
this.all((err, list) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var results = [];
|
||||
for (var key in list) {
|
||||
var account = list[key];
|
||||
var provider = core.providerFactory.get(account.providerType);
|
||||
if (provider.canSync) {
|
||||
results.push(account);
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a list of available presets filtered by
|
||||
* the currently used presets in the database.
|
||||
*
|
||||
* Expected structure of the presetList is as follows:
|
||||
*
|
||||
* {
|
||||
* 'presetType': {
|
||||
* // most important field when true if the preset
|
||||
* // is available in the database that preset type
|
||||
* // will be excluded.
|
||||
* singleUse: true
|
||||
* providerType: 'X',
|
||||
* options: {}
|
||||
* }
|
||||
*
|
||||
* }
|
||||
*
|
||||
* @param {Object} presetList see example ^^^.
|
||||
* @param {Function} callback [err, ['presetKey', ...]].
|
||||
*/
|
||||
availablePresets: function(presetList, callback) {
|
||||
var results = [];
|
||||
var singleUse = {};
|
||||
var hasSingleUses = false;
|
||||
|
||||
for (var preset in presetList) {
|
||||
if (presetList[preset].singleUse) {
|
||||
hasSingleUses = true;
|
||||
singleUse[preset] = true;
|
||||
} else {
|
||||
results.push(preset);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSingleUses) {
|
||||
return nextTick(function() {
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
this.all(function(err, list) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var id in list) {
|
||||
var preset = list[id].preset;
|
||||
if (singleUse[preset]) {
|
||||
delete singleUse[preset];
|
||||
}
|
||||
}
|
||||
|
||||
// add un-used presets to the list.
|
||||
callback(null, results.concat(Object.keys(singleUse)));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
285
gaia/apps/calendar/js/backend/store/alarm.js
Executable file
285
gaia/apps/calendar/js/backend/store/alarm.js
Executable file
|
|
@ -0,0 +1,285 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Abstract = require('./abstract');
|
||||
var Calc = require('common/calc');
|
||||
var createDOMPromise = require('common/create_dom_promise');
|
||||
var core = require('core');
|
||||
var debug = require('common/debug')('store/alarm');
|
||||
var denodeifyAll = require('common/promise').denodeifyAll;
|
||||
//var object = require('common/object');
|
||||
|
||||
/**
|
||||
* The alarm store can be thought of as a big queue.
|
||||
* Over time we add and remove alarm times related to
|
||||
* a specific busytime/event instance.
|
||||
* (and there could be multiple alarms per busytime/event).
|
||||
*
|
||||
* When `workQueue` is called records will be removed
|
||||
* from the queue (this object store) and added (via mozAlarms).
|
||||
*/
|
||||
function Alarm() {
|
||||
Abstract.apply(this, arguments);
|
||||
this._processQueue = this._processQueue.bind(this);
|
||||
|
||||
denodeifyAll(this, [
|
||||
'findAllByBusytimeId',
|
||||
'workQueue'
|
||||
]);
|
||||
}
|
||||
module.exports = Alarm;
|
||||
|
||||
Alarm.prototype = {
|
||||
__proto__: Abstract.prototype,
|
||||
|
||||
_store: 'alarms',
|
||||
|
||||
_dependentStores: ['alarms'],
|
||||
|
||||
/**
|
||||
* Number of hours ahead of current time to add new alarms.
|
||||
*
|
||||
* @type Numeric
|
||||
*/
|
||||
_alarmAddThresholdHours: 48,
|
||||
|
||||
/** disable caching */
|
||||
_addToCache: function() {},
|
||||
_removeFromCache: function() {},
|
||||
|
||||
/**
|
||||
* When false will not process queue automatically
|
||||
* (that is after each alarm transaction is complete).
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
autoQueue: false,
|
||||
|
||||
_processQueue: function() {
|
||||
this.workQueue();
|
||||
},
|
||||
|
||||
_objectData: function(object) {
|
||||
var data = Abstract.prototype._objectData.call(this, object);
|
||||
if (data.startDate) {
|
||||
// ensure the pending trigger is always in sync
|
||||
// with the current trigger whenever we update
|
||||
// the model.
|
||||
data.trigger = data.startDate;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Manage the queue when alarms are added.
|
||||
*/
|
||||
_addDependents: function(obj, trans) {
|
||||
if (!this.autoQueue) {
|
||||
return;
|
||||
}
|
||||
|
||||
// by using processQueue even if we added
|
||||
// 6000 alarms during a single transaction we only
|
||||
// receive the event once as addEventListener discards
|
||||
// duplicates.
|
||||
trans.addEventListener('complete', this._processQueue);
|
||||
},
|
||||
|
||||
/**
|
||||
* Move alarms over to the alarm api's database.
|
||||
*
|
||||
*
|
||||
* @param {Date} now date to use as current time.
|
||||
*
|
||||
* @param {Boolean} requiresAlarm attempts to ensure at
|
||||
* lest one alarm is added.
|
||||
*
|
||||
* @param {Function} callback node style callback.
|
||||
*/
|
||||
_moveAlarms: function(now, requiresAlarm, callback) {
|
||||
// use transport dates so we can handle timezones & floating time.
|
||||
var time = Calc.dateToTransport(now);
|
||||
var utc = time.utc;
|
||||
// keep adding events until we are beyond this time.
|
||||
var minimum = utc + (this._alarmAddThresholdHours * Calc.HOUR);
|
||||
|
||||
var request = core.db
|
||||
.transaction('alarms', 'readwrite')
|
||||
.objectStore('alarms')
|
||||
.index('trigger')
|
||||
.openCursor();
|
||||
|
||||
request.onerror = function() {
|
||||
callback(new Error('Alarm cursor failed to open.'));
|
||||
};
|
||||
|
||||
var past = []; // Alarms that should be fired immediately.
|
||||
var future = []; // Alarms that should fire in the future.
|
||||
request.onsuccess = function(event) {
|
||||
var cursor = event.target.result;
|
||||
if (!cursor ||
|
||||
(cursor.key >= minimum && (!requiresAlarm || future.length))) {
|
||||
// We've pulled all (or at least enough) alarms into memory.
|
||||
// Now we can send them to the notifications controller
|
||||
// or the alarms api.
|
||||
return dispatchAlarms(past, future)
|
||||
.then(callback)
|
||||
.catch(error => debug('Error dispatching alarms:', error));
|
||||
}
|
||||
|
||||
var record = cursor.value;
|
||||
var date = Calc.dateFromTransport(record.trigger);
|
||||
var bucket = date < Date.now() ? past : future;
|
||||
bucket.push(record);
|
||||
// We need to save the trigger time so that we can send the
|
||||
// appropriate time to the alarms api. However, we want to mark
|
||||
// that we've handled this alarm so delete the trigger prop.
|
||||
record.triggered = record.trigger;
|
||||
delete record.trigger;
|
||||
cursor.update(record);
|
||||
cursor.continue();
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds single alarm by busytime id.
|
||||
*
|
||||
* @param {Object} related busytime object.
|
||||
* @param {IDBTransaction} [trans] optional transaction.
|
||||
* @param {Function} callback node style [err, records].
|
||||
*/
|
||||
findAllByBusytimeId: function(busytimeId, trans, callback) {
|
||||
if (typeof(trans) === 'function') {
|
||||
callback = trans;
|
||||
trans = null;
|
||||
}
|
||||
|
||||
if (!trans) {
|
||||
trans = core.db.transaction(this._dependentStores);
|
||||
}
|
||||
|
||||
var store = trans.objectStore(this._store);
|
||||
var index = store.index('busytimeId');
|
||||
var key = IDBKeyRange.only(busytimeId);
|
||||
|
||||
index.mozGetAll(key).onsuccess = function(e) {
|
||||
callback(null, e.target.result);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Works queue putting alarms into the alarm api database where needed.
|
||||
*
|
||||
*/
|
||||
workQueue: function(now, callback) {
|
||||
if (typeof(now) === 'function') {
|
||||
callback = now;
|
||||
now = null;
|
||||
}
|
||||
|
||||
now = now || new Date();
|
||||
var alarms = navigator.mozAlarms;
|
||||
|
||||
if (!alarms) {
|
||||
if (callback) {
|
||||
callback(null);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var requiresAlarm = false;
|
||||
|
||||
/**
|
||||
* Why are we getting all alarms here?
|
||||
*
|
||||
* The alarms are designed to keep the total number
|
||||
* of entires (in mozAlarms) down but we should keep at
|
||||
* minimum one active at all times. For example if the user
|
||||
* has sync turned off and wants notifications we need
|
||||
* to have an alarm go off to trigger adding more alarms.
|
||||
*/
|
||||
var req = alarms.getAll();
|
||||
|
||||
//XXX: even with the good reasons above we need
|
||||
// to justify the perf cost here later.
|
||||
req.onsuccess = function(e) {
|
||||
var data = e.target.result;
|
||||
var len = data.length;
|
||||
var mozAlarm;
|
||||
|
||||
requiresAlarm = true;
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
mozAlarm = data[i].data;
|
||||
if (
|
||||
mozAlarm &&
|
||||
'eventId' in mozAlarm &&
|
||||
'trigger' in mozAlarm
|
||||
) {
|
||||
requiresAlarm = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
callback = callback || function() {};
|
||||
self._moveAlarms(
|
||||
now,
|
||||
requiresAlarm,
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
var msg = 'failed to get alarms';
|
||||
console.error('CALENDAR:', msg);
|
||||
|
||||
if (callback) {
|
||||
callback(new Error(msg));
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function dispatchAlarms(past, future) {
|
||||
// If the alarm was meant to be triggered in the past,
|
||||
// we want to immediately issue a notification.
|
||||
// However, in bug 857284 we add the stipulation that
|
||||
// we shouldn't issue duplicates, so handle that here also.
|
||||
var eventToAlarm = {};
|
||||
past.forEach(alarm => {
|
||||
var event = alarm.eventId;
|
||||
if (!event || event in eventToAlarm) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventToAlarm[event] = alarm;
|
||||
});
|
||||
|
||||
/* TODO(gareth): Should not talk to notifications controller from backend.
|
||||
object.forEach(eventToAlarm, (event, alarm) => {
|
||||
core.notificationsController.onAlarm(alarm);
|
||||
});
|
||||
*/
|
||||
|
||||
// If the alarm should be triggered in the future, then we can create an
|
||||
// entry in the alarms api to wake us up to issue a notification for it
|
||||
// at the appropriate time.
|
||||
var alarms = navigator.mozAlarms;
|
||||
return Promise.all(future.map(alarm => {
|
||||
var timezone = alarm.triggered.tzid === Calc.FLOATING ?
|
||||
'ignoreTimezone' :
|
||||
'honorTimezone';
|
||||
return createDOMPromise(
|
||||
alarms.add(
|
||||
Calc.dateFromTransport(alarm.triggered),
|
||||
timezone,
|
||||
alarm
|
||||
)
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
});
|
||||
162
gaia/apps/calendar/js/backend/store/busytime.js
Executable file
162
gaia/apps/calendar/js/backend/store/busytime.js
Executable file
|
|
@ -0,0 +1,162 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Abstract = require('./abstract');
|
||||
var Calc = require('common/calc');
|
||||
var binsearch = require('common/binsearch');
|
||||
var core = require('core');
|
||||
var compare = require('common/compare');
|
||||
var denodeifyAll = require('common/promise').denodeifyAll;
|
||||
|
||||
/**
|
||||
* Objects saved in the busytime store:
|
||||
*
|
||||
* {
|
||||
* _id: (uuid),
|
||||
* start: Calendar.Calc.dateToTransport(x),
|
||||
* end: Calendar.Calc.dateToTransport(x),
|
||||
* eventId: eventId,
|
||||
* calendarId: calendarId
|
||||
* }
|
||||
*
|
||||
*/
|
||||
function Busytime() {
|
||||
Abstract.apply(this, arguments);
|
||||
this._setupCache();
|
||||
|
||||
denodeifyAll(this, [
|
||||
'removeEvent',
|
||||
'loadSpan'
|
||||
]);
|
||||
}
|
||||
module.exports = Busytime;
|
||||
|
||||
Busytime.prototype = {
|
||||
__proto__: Abstract.prototype,
|
||||
|
||||
_store: 'busytimes',
|
||||
|
||||
_dependentStores: ['alarms', 'busytimes'],
|
||||
|
||||
_setupCache: function() {
|
||||
this._byEventId = Object.create(null);
|
||||
},
|
||||
|
||||
_createModel: function(input, id) {
|
||||
return this.initRecord(input, id);
|
||||
},
|
||||
|
||||
initRecord: function(input, id) {
|
||||
var _super = Abstract.prototype._createModel;
|
||||
var model = _super.apply(this, arguments);
|
||||
model.startDate = Calc.dateFromTransport(model.start);
|
||||
model.endDate = Calc.dateFromTransport(model.end);
|
||||
return model;
|
||||
},
|
||||
|
||||
_removeDependents: function(id, trans) {
|
||||
core.storeFactory.get('Alarm').removeByIndex('busytimeId', id, trans);
|
||||
},
|
||||
|
||||
removeEvent: function(id, trans, callback) {
|
||||
if (typeof(trans) === 'function') {
|
||||
callback = trans;
|
||||
trans = undefined;
|
||||
}
|
||||
|
||||
if (typeof(trans) === 'undefined') {
|
||||
trans = core.db.transaction(
|
||||
this._dependentStores,
|
||||
'readwrite'
|
||||
);
|
||||
}
|
||||
|
||||
// build the request using the inherited method
|
||||
var req = this.removeByIndex('eventId', id, trans);
|
||||
|
||||
// get the original method which handles the generic bit
|
||||
var success = req.onsuccess;
|
||||
|
||||
// override the default .onsuccess to get the ids
|
||||
// so we can emit remove events.
|
||||
var self = this;
|
||||
req.onsuccess = function(e) {
|
||||
var cursor = e.target.result;
|
||||
|
||||
if (cursor) {
|
||||
var id = cursor.primaryKey;
|
||||
self.emit('remove', id);
|
||||
}
|
||||
|
||||
success(e);
|
||||
};
|
||||
|
||||
this._transactionCallback(trans, callback);
|
||||
},
|
||||
|
||||
_startCompare: function(aObj, bObj) {
|
||||
var a = aObj.start.utc;
|
||||
var b = bObj.start.utc;
|
||||
return compare(a, b);
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads all busytimes in given timespan.
|
||||
*
|
||||
* @param {Calendar.Timespan} span timespan.
|
||||
* @param {Function} callback node style callback
|
||||
* where first argument is
|
||||
* an error (or null)
|
||||
* and the second argument
|
||||
* is a list of all loaded
|
||||
* busytimes in the timespan.
|
||||
*/
|
||||
loadSpan: function(span, callback) {
|
||||
var trans = core.db.transaction(this._store);
|
||||
var store = trans.objectStore(this._store);
|
||||
|
||||
var startPoint = Calc.dateToTransport(new Date(span.start));
|
||||
var endPoint = Calc.dateToTransport(new Date(span.end));
|
||||
|
||||
// XXX: we need to implement busytime chunking
|
||||
// to make this efficient.
|
||||
var keyRange = IDBKeyRange.lowerBound(startPoint.utc);
|
||||
|
||||
var index = store.index('end');
|
||||
var self = this;
|
||||
|
||||
index.mozGetAll(keyRange).onsuccess = function(e) {
|
||||
var data = e.target.result;
|
||||
|
||||
// sort data
|
||||
data = data.sort(self._startCompare);
|
||||
|
||||
// attempt to find a start time that occurs
|
||||
// after the end time of the span
|
||||
var idx = binsearch.insert(
|
||||
data,
|
||||
{ start: { utc: endPoint.utc + 1 } },
|
||||
self._startCompare
|
||||
);
|
||||
|
||||
// remove unrelated timespan...
|
||||
data = data.slice(0, idx);
|
||||
|
||||
// fire callback
|
||||
if (callback) {
|
||||
callback(null, data.map(function(item) {
|
||||
return self.initRecord(item);
|
||||
}));
|
||||
}
|
||||
|
||||
};
|
||||
},
|
||||
|
||||
/* we don't use id based caching for busytimes */
|
||||
|
||||
_addToCache: function() {},
|
||||
_removeFromCache: function() {}
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
329
gaia/apps/calendar/js/backend/store/calendar.js
Executable file
329
gaia/apps/calendar/js/backend/store/calendar.js
Executable file
|
|
@ -0,0 +1,329 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Abstract = require('./abstract');
|
||||
var CalendarModel = require('models/calendar');
|
||||
var core = require('core');
|
||||
var denodeifyAll = require('common/promise').denodeifyAll;
|
||||
var localCalendarId = require('common/constants').localCalendarId;
|
||||
var probablyParseInt = require('common/probably_parse_int');
|
||||
|
||||
function Store() {
|
||||
Abstract.apply(this, arguments);
|
||||
this._usedColors = [];
|
||||
|
||||
denodeifyAll(this, [
|
||||
'markWithError',
|
||||
'remotesByAccount',
|
||||
'sync',
|
||||
'providerFor',
|
||||
'ownersOf'
|
||||
]);
|
||||
}
|
||||
module.exports = Store;
|
||||
|
||||
/**
|
||||
* Remote calendar colors
|
||||
*/
|
||||
Store.REMOTE_COLORS = [
|
||||
'#00aacc', // light blue
|
||||
'#bad600', // light green
|
||||
'#df4784', // pink
|
||||
'#f9bc17', // yellow
|
||||
'#0766b7', // dark blue
|
||||
'#76a408', // dark green
|
||||
'#33a185' // teal
|
||||
];
|
||||
|
||||
/**
|
||||
* Local calendar color (orange)
|
||||
*/
|
||||
Store.LOCAL_COLOR = '#f97c17',
|
||||
|
||||
/**
|
||||
* List of possible calendar capabilities.
|
||||
*/
|
||||
Store.capabilities = {
|
||||
createEvent: 'canCreateEvent',
|
||||
updateEvent: 'canUpdateEvent',
|
||||
deleteEvent: 'canDeleteEvent'
|
||||
};
|
||||
|
||||
Store.prototype = {
|
||||
__proto__: Abstract.prototype,
|
||||
|
||||
_store: 'calendars',
|
||||
|
||||
_dependentStores: [
|
||||
'calendars', 'events', 'busytimes',
|
||||
'alarms', 'icalComponents'
|
||||
],
|
||||
|
||||
_parseId: probablyParseInt,
|
||||
|
||||
_createModel: function(obj, id) {
|
||||
if (!(obj instanceof CalendarModel)) {
|
||||
obj = new CalendarModel(obj);
|
||||
}
|
||||
|
||||
if (typeof(id) !== 'undefined') {
|
||||
obj._id = id;
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
_removeDependents: function(id, trans) {
|
||||
var store = core.storeFactory.get('Event');
|
||||
store.removeByIndex('calendarId', id, trans);
|
||||
},
|
||||
|
||||
/**
|
||||
* Marks a given calendar with an error.
|
||||
*
|
||||
* Emits a 'error' event immediately.. This method is typically
|
||||
* triggered by an account wide error.
|
||||
*
|
||||
*
|
||||
* @param {Object} calendar model.
|
||||
* @param {Calendar.Error} error for given calendar.
|
||||
* @param {IDBTransaction} transaction optional.
|
||||
* @param {Function} callback fired when model is saved [err, id, model].
|
||||
*/
|
||||
markWithError: function(calendar, error, trans, callback) {
|
||||
if (typeof(trans) === 'function') {
|
||||
callback = trans;
|
||||
trans = null;
|
||||
}
|
||||
|
||||
if (!calendar._id) {
|
||||
throw new Error('given calendar must be persisted.');
|
||||
}
|
||||
|
||||
calendar.error = {
|
||||
name: error.name,
|
||||
date: new Date()
|
||||
};
|
||||
|
||||
this.persist(calendar, trans, callback);
|
||||
},
|
||||
|
||||
persist: function(calendar, trans, callback) {
|
||||
if (typeof(trans) === 'function') {
|
||||
callback = trans;
|
||||
trans = undefined;
|
||||
}
|
||||
|
||||
this._updateCalendarColor(calendar);
|
||||
|
||||
var cb = callback;
|
||||
var cached = this._cached[calendar._id];
|
||||
|
||||
if (cached && cached.localDisplayed !== calendar.localDisplayed) {
|
||||
cb = function(err, id, model) {
|
||||
this.emit('calendarVisibilityChange', id, model);
|
||||
callback(err, id, model);
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
Abstract.prototype.persist.call(this, calendar, trans, cb);
|
||||
},
|
||||
|
||||
remove: function(id, trans, callback) {
|
||||
this._removeCalendarColorFromCache(id);
|
||||
Abstract.prototype.remove.apply(this, arguments);
|
||||
},
|
||||
|
||||
_updateCalendarColor: function(calendar) {
|
||||
// we avoid storing multiple colors for same calendar in case of an
|
||||
// "update" operation
|
||||
this._removeCalendarColorFromCache(calendar._id);
|
||||
this._setCalendarColor(calendar);
|
||||
// cache is built asynchronously, we need to store the color as soon as
|
||||
// possible to avoid adding same color multiple times in a row (eg.
|
||||
// account with multiple calendars will call persist multiple times)
|
||||
this._usedColors.push(calendar.color);
|
||||
},
|
||||
|
||||
_removeCalendarColorFromCache: function(id) {
|
||||
// we need to remove the color from index as soon as possible to avoid
|
||||
// race conditions (remove is async)
|
||||
var color = this.getColorByCalendarId(id);
|
||||
var index = this._usedColors.indexOf(color);
|
||||
if (index !== -1) {
|
||||
this._usedColors.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
getColorByCalendarId: function(id) {
|
||||
return this._cached[id] && this._cached[id].color;
|
||||
},
|
||||
|
||||
_setCalendarColor: function(calendar) {
|
||||
// local calendar should always use the same color
|
||||
if (calendar._id === localCalendarId) {
|
||||
calendar.color = Store.LOCAL_COLOR;
|
||||
return;
|
||||
}
|
||||
|
||||
// restore previous color only if it is part of the palette, otherwise we
|
||||
// get the next available color (or least used)
|
||||
var prevColor = this.getColorByCalendarId(calendar._id);
|
||||
if (prevColor && Store.REMOTE_COLORS.indexOf(prevColor) !== -1) {
|
||||
calendar.color = prevColor;
|
||||
} else {
|
||||
calendar.color = this._getNextColor();
|
||||
}
|
||||
},
|
||||
|
||||
_getNextColor: function() {
|
||||
var available = Store.REMOTE_COLORS.filter(function(color) {
|
||||
return this._usedColors.indexOf(color) === -1;
|
||||
}, this);
|
||||
|
||||
return available.length ? available[0] : this._getLeastUsedColor();
|
||||
},
|
||||
|
||||
_getLeastUsedColor: function() {
|
||||
var counter = {};
|
||||
this._usedColors.forEach(function(color) {
|
||||
counter[color] = (counter[color] || 0) + 1;
|
||||
});
|
||||
|
||||
var leastUsedColor;
|
||||
var leastUsedCount = Infinity;
|
||||
for (var color in counter) {
|
||||
if (counter[color] < leastUsedCount) {
|
||||
leastUsedCount = counter[color];
|
||||
leastUsedColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
return leastUsedColor;
|
||||
},
|
||||
|
||||
shouldDisplayCalendar: function(calendarId) {
|
||||
var calendar = this._cached[calendarId];
|
||||
return calendar && calendar.localDisplayed;
|
||||
},
|
||||
|
||||
/**
|
||||
* Find calendars in a specific account.
|
||||
* Results will be returned in an object where
|
||||
* the key is the remote.id and the value is the calendar.
|
||||
*
|
||||
* @param {String|Numeric} accountId id of account.
|
||||
* @param {Function} callback [err, object] see above.
|
||||
*/
|
||||
remotesByAccount: function(accountId, trans, callback) {
|
||||
if (typeof(trans) === 'function') {
|
||||
callback = trans;
|
||||
trans = null;
|
||||
}
|
||||
|
||||
if (!trans) {
|
||||
trans = core.db.transaction(this._store);
|
||||
}
|
||||
|
||||
var store = trans.objectStore(this._store);
|
||||
|
||||
var reqKey = IDBKeyRange.only(accountId);
|
||||
var req = store.index('accountId').mozGetAll(reqKey);
|
||||
|
||||
req.onerror = function remotesError(e) {
|
||||
callback(e.target.error);
|
||||
};
|
||||
|
||||
var self = this;
|
||||
req.onsuccess = function remotesSuccess(e) {
|
||||
var result = Object.create(null);
|
||||
e.target.result.forEach(function(calendar) {
|
||||
result[calendar.remote.id] = self._createModel(
|
||||
calendar,
|
||||
calendar._id
|
||||
);
|
||||
});
|
||||
|
||||
callback(null, result);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync remote and local events for a calendar.
|
||||
*
|
||||
* TODO: Deprecate use of this function in favor of a sync methods
|
||||
* inside of providers.
|
||||
*/
|
||||
sync: function(account, calendar, callback) {
|
||||
var provider = core.providerFactory.get(account.providerType);
|
||||
provider.syncEvents(account, calendar, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortcut to find provider for calendar.
|
||||
*
|
||||
* @param {Calendar.Models.Calendar} calendar input calendar.
|
||||
* @param {Function} callback [err, provider].
|
||||
*/
|
||||
providerFor: function(calendar, callback) {
|
||||
this.ownersOf(calendar, function(err, owners) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, core.providerFactory.get(owners.account.providerType));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds calendar/account for a given event.
|
||||
*
|
||||
* TODO: think about moving this function into its
|
||||
* own file as a mixin.
|
||||
*
|
||||
* @param {Object|String|Numeric} objectOrId must contain .calendarId.
|
||||
* @param {Function} callback [err, { ... }].
|
||||
*/
|
||||
ownersOf: function(objectOrId, callback) {
|
||||
var result = {};
|
||||
|
||||
var accountStore = core.storeFactory.get('Account');
|
||||
|
||||
// case 1. given a calendar
|
||||
if (objectOrId instanceof CalendarModel) {
|
||||
result.calendar = objectOrId;
|
||||
accountStore.get(objectOrId.accountId, fetchAccount);
|
||||
return;
|
||||
}
|
||||
|
||||
// case 2 given a calendar id or object
|
||||
|
||||
if (typeof(objectOrId) === 'object') {
|
||||
objectOrId = objectOrId.calendarId;
|
||||
}
|
||||
|
||||
// why??? because we use this method in event store too..
|
||||
var calendarStore = core.storeFactory.get('Calendar');
|
||||
calendarStore.get(objectOrId, fetchCalendar);
|
||||
|
||||
function fetchCalendar(err, calendar) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
result.calendar = calendar;
|
||||
accountStore.get(calendar.accountId, fetchAccount);
|
||||
}
|
||||
|
||||
function fetchAccount(err, account) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
result.account = account;
|
||||
callback(null, result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
156
gaia/apps/calendar/js/backend/store/event.js
Executable file
156
gaia/apps/calendar/js/backend/store/event.js
Executable file
|
|
@ -0,0 +1,156 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Abstract = require('./abstract');
|
||||
var Calc = require('common/calc');
|
||||
var Calendar = require('./calendar');
|
||||
var core = require('core');
|
||||
var denodeifyAll = require('common/promise').denodeifyAll;
|
||||
|
||||
function Events() {
|
||||
Abstract.apply(this, arguments);
|
||||
|
||||
denodeifyAll(this, [
|
||||
'providerFor',
|
||||
'findByIds',
|
||||
'ownersOf',
|
||||
'eventsForCalendar'
|
||||
]);
|
||||
}
|
||||
module.exports = Events;
|
||||
|
||||
Events.prototype = {
|
||||
__proto__: Abstract.prototype,
|
||||
_store: 'events',
|
||||
_dependentStores: ['events', 'busytimes', 'alarms', 'icalComponents'],
|
||||
|
||||
/** disable caching */
|
||||
_addToCache: function() {},
|
||||
_removeFromCache: function() {},
|
||||
|
||||
_createModel: function(input, id) {
|
||||
var _super = Abstract.prototype._createModel;
|
||||
var model = _super.apply(this, arguments);
|
||||
model.remote.startDate = Calc.dateFromTransport(model.remote.start);
|
||||
model.remote.endDate = Calc.dateFromTransport(model.remote.end);
|
||||
return model;
|
||||
},
|
||||
|
||||
/**
|
||||
* Link busytime dependants see _addDependents.
|
||||
*/
|
||||
_removeDependents: function(id, trans) {
|
||||
this.removeByIndex('parentId', id, trans);
|
||||
|
||||
var busy = core.storeFactory.get('Busytime');
|
||||
busy.removeEvent(id, trans);
|
||||
|
||||
var component = core.storeFactory.get('IcalComponent');
|
||||
component.remove(id, trans);
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate an id for a newly created record.
|
||||
* Based off of remote id (uuid) and calendar id.
|
||||
*/
|
||||
_assignId: function(obj) {
|
||||
var id = obj.calendarId + '-' + obj.remote.id;
|
||||
obj._id = id;
|
||||
return id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortcut finds provider for given event.
|
||||
*
|
||||
* @param {Object} event full event record from db.
|
||||
*/
|
||||
providerFor: function(event, callback) {
|
||||
this.ownersOf(event, function(err, owners) {
|
||||
callback(null, core.providerFactory.get(owners.account.providerType));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds a list of events by id.
|
||||
*
|
||||
* @param {Array} ids list of ids.
|
||||
* @param {Function} callback node style second argument
|
||||
* is an object of _id/event.
|
||||
*/
|
||||
findByIds: function(ids, callback) {
|
||||
var results = {};
|
||||
var pending = ids.length;
|
||||
var self = this;
|
||||
|
||||
if (!pending) {
|
||||
callback(null, results);
|
||||
}
|
||||
|
||||
function next() {
|
||||
if (!(--pending)) {
|
||||
// fatal errors should break
|
||||
// and so we are not handling them
|
||||
// here...
|
||||
callback(null, results);
|
||||
}
|
||||
}
|
||||
|
||||
function success(e) {
|
||||
var item = e.target.result;
|
||||
|
||||
if (item) {
|
||||
results[item._id] = self._createModel(item);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
function error() {
|
||||
// can't find it or something
|
||||
// skip!
|
||||
next();
|
||||
}
|
||||
|
||||
ids.forEach(function(id) {
|
||||
var trans = core.db.transaction('events');
|
||||
var store = trans.objectStore('events');
|
||||
var req = store.get(id);
|
||||
|
||||
req.onsuccess = success;
|
||||
req.onerror = error;
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds calendar/account for a given event.
|
||||
*
|
||||
* @param {Object} event must contain .calendarId.
|
||||
* @param {Function} callback [err, { ... }].
|
||||
*/
|
||||
ownersOf: Calendar.prototype.ownersOf,
|
||||
/**
|
||||
* Loads all events for given calendarId
|
||||
* and returns results. Does not cache.
|
||||
*
|
||||
* @param {String} calendarId calendar to find.
|
||||
* @param {Function} callback node style [err, array of events].
|
||||
*/
|
||||
eventsForCalendar: function(calendarId, callback) {
|
||||
var trans = core.db.transaction('events');
|
||||
var store = trans.objectStore('events');
|
||||
var index = store.index('calendarId');
|
||||
var key = IDBKeyRange.only(calendarId);
|
||||
|
||||
var req = index.mozGetAll(key);
|
||||
|
||||
req.onsuccess = function(e) {
|
||||
callback(null, e.target.result);
|
||||
};
|
||||
|
||||
req.onerror = function(e) {
|
||||
callback(e);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
31
gaia/apps/calendar/js/backend/store/factory.js
Executable file
31
gaia/apps/calendar/js/backend/store/factory.js
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
define(function(require, exports) {
|
||||
'use strict';
|
||||
|
||||
var debug = require('common/debug')('store/factory');
|
||||
|
||||
var ctors = {
|
||||
Account: require('./account'),
|
||||
Alarm: require('./alarm'),
|
||||
Busytime: require('./busytime'),
|
||||
Calendar: require('./calendar'),
|
||||
Event: require('./event'),
|
||||
IcalComponent: require('./ical_component'),
|
||||
Setting: require('./setting')
|
||||
};
|
||||
|
||||
exports._instances = Object.create(null);
|
||||
|
||||
exports.get = function(name) {
|
||||
if (!exports._instances[name]) {
|
||||
try {
|
||||
debug(`Initializing store ${name}`);
|
||||
exports._instances[name] = new ctors[name]();
|
||||
} catch (e) {
|
||||
console.error('Error', e.name, e.message);
|
||||
console.error('Failed to initialize store', name, e.stack);
|
||||
}
|
||||
}
|
||||
return exports._instances[name];
|
||||
};
|
||||
|
||||
});
|
||||
64
gaia/apps/calendar/js/backend/store/ical_component.js
Executable file
64
gaia/apps/calendar/js/backend/store/ical_component.js
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Abstract = require('./abstract');
|
||||
var Calc = require('common/calc');
|
||||
var core = require('core');
|
||||
var denodeifyAll = require('common/promise').denodeifyAll;
|
||||
|
||||
function IcalComponent() {
|
||||
Abstract.apply(this, arguments);
|
||||
|
||||
denodeifyAll(this, [
|
||||
'findRecurrencesBefore'
|
||||
]);
|
||||
}
|
||||
module.exports = IcalComponent;
|
||||
|
||||
IcalComponent.prototype = {
|
||||
__proto__: Abstract.prototype,
|
||||
|
||||
_store: 'icalComponents',
|
||||
|
||||
/** disable caching */
|
||||
_addToCache: function() {},
|
||||
_removeFromCache: function() {},
|
||||
|
||||
_createModel: function(object) {
|
||||
return object;
|
||||
},
|
||||
|
||||
_detectPersistType: function(object) {
|
||||
// always fire update.
|
||||
return 'update';
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds all components which have recurrences
|
||||
* that are not expanded beyond the given date.
|
||||
*
|
||||
* @param {Date} maxDate max date to find.
|
||||
* @param {Function} callback results of search [err, [icalComp, ...]].
|
||||
*/
|
||||
findRecurrencesBefore: function(maxDate, callback) {
|
||||
var trans = core.db.transaction(this._store, 'readwrite');
|
||||
|
||||
trans.onerror = function(event) {
|
||||
callback(event.target.error.name);
|
||||
};
|
||||
|
||||
var time = Calc.dateToTransport(maxDate);
|
||||
var utc = time.utc;
|
||||
var range = IDBKeyRange.bound(0, utc);
|
||||
var store = trans.objectStore(this._store);
|
||||
var idx = store.index('lastRecurrenceId');
|
||||
|
||||
var req = idx.mozGetAll(range);
|
||||
|
||||
req.onsuccess = function(event) {
|
||||
callback(null, event.target.result);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
139
gaia/apps/calendar/js/backend/store/setting.js
Executable file
139
gaia/apps/calendar/js/backend/store/setting.js
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Abstract = require('./abstract');
|
||||
var denodeifyAll = require('common/promise').denodeifyAll;
|
||||
var nextTick = require('common/next_tick');
|
||||
|
||||
function Setting() {
|
||||
Abstract.apply(this, arguments);
|
||||
denodeifyAll(this, [
|
||||
'getValue',
|
||||
'set'
|
||||
]);
|
||||
}
|
||||
module.exports = Setting;
|
||||
|
||||
Setting.prototype = {
|
||||
__proto__: Abstract.prototype,
|
||||
|
||||
_store: 'settings',
|
||||
|
||||
/**
|
||||
* Default option values.
|
||||
*/
|
||||
defaults: {
|
||||
standardAlarmDefault: -300,
|
||||
alldayAlarmDefault: 32400,
|
||||
syncFrequency: 15,
|
||||
syncAlarm: {
|
||||
alarmId: null,
|
||||
start: null,
|
||||
end: null
|
||||
}
|
||||
},
|
||||
|
||||
/** disable caching */
|
||||
_addToCache: function() {},
|
||||
_removeFromCache: function() {},
|
||||
|
||||
/**
|
||||
* This method also will use the internal cache to ensure
|
||||
* callers are in a consistent state and don't require round
|
||||
* trips to the database. When the value does not exist defaults
|
||||
* are used where possible...
|
||||
*
|
||||
*
|
||||
* settings.getValue('syncFrequency', function(err, value) {
|
||||
* // ...
|
||||
* });
|
||||
*
|
||||
*
|
||||
* @param {String} key name of setting.
|
||||
* @param {Function} callback usual [err, value] does not include metadata.
|
||||
*/
|
||||
getValue: function(key, callback) {
|
||||
var self = this;
|
||||
|
||||
if (key in this._cached) {
|
||||
nextTick(function handleCached() {
|
||||
callback(null, self._cached[key].value);
|
||||
});
|
||||
|
||||
// we have cached value exit...
|
||||
return;
|
||||
}
|
||||
|
||||
this.get(key, function handleStored(err, value) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (value === undefined && self.defaults[key] !== undefined) {
|
||||
value = { value: self.defaults[key] };
|
||||
}
|
||||
|
||||
self._cached[key] = value;
|
||||
callback(null, value.value);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Persist a setting change.
|
||||
*
|
||||
* In addition to updating the value of the setting
|
||||
* it will also update the updatedAt & createdAt properties
|
||||
* of the record.
|
||||
*
|
||||
* Calling this function will also emit a 'change' event
|
||||
* prior to fully persisting the record to the database.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* var settingStore;
|
||||
*
|
||||
* settingStore.set('syncFrequency', 15, function() {
|
||||
* // done
|
||||
* });
|
||||
*
|
||||
* // somewhere else in the app:
|
||||
*
|
||||
* settingStore.on('syncFrequencyChange', function(value) {
|
||||
* // value === 15
|
||||
* });
|
||||
*
|
||||
* @param {String} key name of setting.
|
||||
* @param {Object} value any object that IndexedDb can store.
|
||||
* @param {IDBTransaction} [trans] idb transaction optional.
|
||||
* @param {Function} [callback] optional callback.
|
||||
*/
|
||||
set: function(key, value, trans, callback) {
|
||||
if (typeof(trans) === 'function') {
|
||||
callback = trans;
|
||||
trans = null;
|
||||
}
|
||||
|
||||
var cached = this._cached[key];
|
||||
var record;
|
||||
|
||||
if (cached && cached._id) {
|
||||
cached.value = value;
|
||||
cached.updatedAt = new Date();
|
||||
record = cached;
|
||||
} else {
|
||||
var created = new Date();
|
||||
this._cached[key] = record = {
|
||||
_id: key,
|
||||
createdAt: created,
|
||||
updatedAt: created,
|
||||
value: value
|
||||
};
|
||||
}
|
||||
|
||||
this.emit(key + 'Change', value, record);
|
||||
this.persist(record, trans, callback);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
Loading…
Reference in a new issue